-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
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 중립성 보장
-
✅ 필드명 중립화:
dbInstanceIdentifier→instanceName(모든 CSP 공통)instanceClass→instanceSize(VM 패턴과 일치)vpcSecurityGroupId→networkSecurityId(VM 패턴과 일치)availabilityZone→zone(VM 패턴과 일치)multiAz→highAvailability(모든 CSP 공통 개념)
-
✅ CSP 특화 필드 제거:
subnetGroupName(AWS 전용) →providerSpecificConfig로 이동skipFinalSnapshot(AWS 전용) →skipSnapshot으로 일반화하거나providerSpecificConfig로 이동
-
✅ 확장성:
providerSpecificConfig로 CSP별 특화 설정 지원- AWS:
subnetGroupName,parameterGroupName,optionGroupName등 - Azure:
resourceGroupName,sku,storageProfile등 - GCP:
databaseFlags,backupConfiguration,ipConfiguration등
- AWS:
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
-
RdbmsCreateRequestDTO 생성 -
RdbmsUpdateRequestDTO 생성 -
RdbmsDeleteRequestDTO 생성 -
RdbmsQueryRequestDTO 생성 -
RdbmsResponseDTO 생성 -
RdbmsController구현 - API 문서화 (Swagger)
Phase 5: Capability 및 설정
- CapabilityRegistry에 RDS 등록
- 설정 파일 업데이트
- Bean 등록 확인
Phase 6: 테스트
- 단위 테스트 작성
- 통합 테스트 작성
- API 테스트