-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
AWS Serverless Function(Lambda) 자원 관리 기능 구현 계획
개요
AWS Lambda, Azure Functions, GCP Cloud Functions를 헥사고날 아키텍처로 구현합니다.
도메인 추상화명은 FUNCTION(Serverless Function)을 사용합니다.
추상화명
- FUNCTION
- Serverless Function의 일반적인 용어
- AWS Lambda, Azure Functions, GCP Cloud Functions 모두를 포괄하는 추상화명
- 다른 CSP에서도 자연스럽게 매핑 가능
아키텍처 구조
헥사고날 아키텍처를 준수하여 다음과 같이 구성합니다:
domain/cloud/
├── adapter/outbound/aws/function/ # AWS Lambda 전용 어댑터
│ ├── AwsFunctionManagementAdapter.java # 함수 생명주기 관리
│ ├── AwsFunctionDiscoveryAdapter.java # 함수 조회
│ ├── AwsFunctionInvocationAdapter.java # 함수 실행
│ ├── AwsFunctionMapper.java # AWS SDK ↔ CloudResource 변환
│ └── ...
├── adapter/outbound/aws/config/
│ └── AwsFunctionConfig.java # Lambda 클라이언트 설정
├── controller/
│ └── FunctionController.java # REST API (CSP 독립적)
├── dto/
│ ├── FunctionCreateRequest.java
│ ├── FunctionUpdateRequest.java
│ ├── FunctionDeleteRequest.java
│ ├── FunctionQueryRequest.java
│ ├── FunctionInvokeRequest.java
│ └── FunctionResponse.java
├── port/model/function/ # Command 객체
│ ├── FunctionCreateCommand.java
│ ├── FunctionUpdateCommand.java
│ ├── FunctionDeleteCommand.java
│ ├── FunctionQuery.java
│ ├── FunctionInvokeCommand.java
│ └── GetFunctionCommand.java
├── port/outbound/function/ # 포트 인터페이스
│ ├── FunctionManagementPort.java # CRUD 작업
│ ├── FunctionDiscoveryPort.java # 조회 작업
│ └── FunctionInvocationPort.java # 함수 실행
├── service/function/
│ ├── FunctionUseCaseService.java # 유스케이스 서비스 (CSP 독립적)
│ └── FunctionPortRouter.java # 포트 라우터
└── repository/
└── CloudResourceRepository.java # 기존 리포지토리 활용
구현 단계
Phase 1: 포트 및 모델 정의
1.1 Command 모델 정의 (port/model/function/)
FunctionCreateCommand.java
/**
* Serverless Function 생성 도메인 커맨드 (CSP 중립적)
*
* UseCase Service에서 Adapter로 전달되는 내부 명령 모델입니다.
* CSP 중립적인 필드를 사용하며, 각 CSP Adapter의 Mapper에서 CSP 특화 요청으로 변환합니다.
*
* 필드 매핑 예시:
* - functionName: AWS(FunctionName), Azure(FunctionName), GCP(name)
* - runtime: AWS(Runtime), Azure(runtime), GCP(runtime)
* - handler: AWS(Handler), Azure(scriptFile.entryPoint), GCP(entryPoint)
* - memorySize: AWS(MemorySize), Azure(functionAppConfig), GCP(availableMemoryMb)
* - timeout: AWS(Timeout), Azure(functionTimeout), GCP(timeout)
*/
@Builder
public record FunctionCreateCommand(
ProviderType providerType,
String accountScope,
String region,
String serviceKey, // "LAMBDA", "AZURE_FUNCTIONS", "CLOUD_FUNCTIONS"
String resourceType, // "FUNCTION"
String functionName, // CSP 중립적: AWS(FunctionName), Azure(FunctionName), GCP(name)
String runtime, // CSP 중립적: "nodejs18.x", "python3.11", "java17", "go1.x"
String handler, // CSP 중립적: AWS(Handler), Azure(scriptFile.entryPoint), GCP(entryPoint)
Integer memorySize, // MB (모든 CSP 공통)
Integer timeout, // 초 (모든 CSP 공통, 최대값 CSP별로 다를 수 있음)
String roleArn, // CSP 중립적: AWS(Role ARN), Azure(identity), GCP(serviceAccountEmail)
Map<String, String> environmentVariables, // 환경 변수
String description, // 함수 설명
String vpcId, // VPC 연결 (선택적, CSP별 구현 다를 수 있음)
String codeUri, // 코드 URI (S3, Blob Storage, GCS 등)
byte[] codeZip, // 코드 ZIP 바이너리 (선택적, URI 대신 사용)
Map<String, String> tags,
String tenantKey,
Map<String, Object> providerSpecificConfig, // CSP별 특화 설정
CloudSessionCredential session
) {}FunctionUpdateCommand.java
/**
* Serverless Function 수정 도메인 커맨드 (CSP 중립적)
*/
@Builder
public record FunctionUpdateCommand(
ProviderType providerType,
String accountScope,
String region,
String providerResourceId, // 함수 ARN/ID (CSP별 형식 다를 수 있음)
String runtime, // 런타임 변경
String handler, // 핸들러 변경
Integer memorySize, // 메모리 크기 변경 (MB)
Integer timeout, // 타임아웃 변경 (초)
String roleArn, // 실행 역할 변경
Map<String, String> environmentVariables, // 환경 변수 변경
String description, // 설명 변경
String codeUri, // 코드 업데이트 URI
byte[] codeZip, // 코드 ZIP 바이너리
Map<String, String> tags,
String tenantKey,
Map<String, Object> providerSpecificConfig, // CSP별 특화 설정
CloudSessionCredential session
) {}FunctionDeleteCommand.java
/**
* Serverless Function 삭제 도메인 커맨드 (CSP 중립적)
*/
@Builder
public record FunctionDeleteCommand(
ProviderType providerType,
String accountScope,
String region,
String providerResourceId,
String tenantKey,
Map<String, Object> providerSpecificConfig, // CSP별 특화 삭제 옵션
CloudSessionCredential session
) {}FunctionQuery.java
/**
* Serverless Function 조회 쿼리 (CSP 중립적)
*/
@Builder
public record FunctionQuery(
ProviderType providerType,
String accountScope,
Set<String> regions,
String functionName, // 함수 이름으로 필터링 (CSP 중립적)
String runtime, // 런타임으로 필터링
String vpcId, // VPC ID로 필터링
Map<String, String> tagsEquals, // 태그로 필터링
int page,
int size
) {}FunctionInvokeCommand.java
/**
* Serverless Function 실행 커맨드 (CSP 중립적)
*/
@Builder
public record FunctionInvokeCommand(
ProviderType providerType,
String accountScope,
String region,
String providerResourceId, // 함수 ARN/ID
String invocationType, // "RequestResponse", "Event", "DryRun"
String payload, // JSON 문자열 페이로드
String qualifier, // 함수 버전/별칭 (선택적)
Map<String, String> context, // 추가 컨텍스트 정보
String tenantKey,
CloudSessionCredential session
) {}GetFunctionCommand.java
@Builder
public record GetFunctionCommand(
ProviderType providerType,
String accountScope,
String region,
String providerResourceId,
String serviceKey,
String resourceType,
CloudSessionCredential session
) {}1.2 포트 인터페이스 정의 (port/outbound/function/)
FunctionManagementPort.java
public interface FunctionManagementPort {
/**
* Serverless Function 생성
*/
CloudResource createFunction(FunctionCreateCommand command);
/**
* Serverless Function 수정
*/
CloudResource updateFunction(FunctionUpdateCommand command);
/**
* Serverless Function 삭제
*/
void deleteFunction(FunctionDeleteCommand command);
}FunctionDiscoveryPort.java
public interface FunctionDiscoveryPort {
/**
* Serverless Function 목록 조회
*/
Page<CloudResource> listFunctions(FunctionQuery query, CloudSessionCredential session);
/**
* 특정 Serverless Function 조회
*/
Optional<CloudResource> getFunction(String functionId, CloudSessionCredential session);
/**
* Serverless Function 상태 조회
*/
String getFunctionStatus(String functionId, CloudSessionCredential session);
/**
* Serverless Function 코드 조회
*/
byte[] getFunctionCode(String functionId, CloudSessionCredential session);
}FunctionInvocationPort.java
public interface FunctionInvocationPort {
/**
* Serverless Function 실행
*
* @param command 실행 커맨드
* @return 실행 결과 (JSON 문자열)
*/
String invokeFunction(FunctionInvokeCommand command);
/**
* Serverless Function 비동기 실행
*
* @param command 실행 커맨드
* @return 요청 ID (추적용)
*/
String invokeFunctionAsync(FunctionInvokeCommand command);
}Phase 2: AWS 어댑터 구현
2.1 Config 클래스 (adapter/outbound/aws/config/AwsFunctionConfig.java)
@Slf4j
@Configuration
@ConditionalOnProperty(name = "aws.enabled", havingValue = "true", matchIfMissing = true)
public class AwsFunctionConfig {
/**
* 세션 자격증명으로 Lambda Client를 생성합니다.
*
* @param session AWS 세션 자격증명
* @param region AWS 리전 (Lambda는 리전별로 관리)
* @return LambdaClient 인스턴스
*/
public LambdaClient createLambdaClient(CloudSessionCredential session, String region) {
AwsSessionCredential awsSession = validateAndCastSession(session);
String targetRegion = region != null ? region : awsSession.getRegion();
return LambdaClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(toSdkCredentials(awsSession)))
.region(Region.of(resolveRegion(targetRegion)))
.build();
}
// 기타 메서드들은 AwsRdsConfig와 유사한 패턴
}2.2 Mapper 클래스 (adapter/outbound/aws/function/AwsFunctionMapper.java)
@Component
public class AwsFunctionMapper {
/**
* AWS FunctionConfiguration → CloudResource 변환
*/
public CloudResource toCloudResource(FunctionConfiguration functionConfig, CloudProvider provider, CloudRegion region, CloudService service) {
return CloudResource.builder()
.resourceId(functionConfig.functionArn())
.resourceName(functionConfig.functionName())
.displayName(functionConfig.functionName())
.provider(provider)
.region(region)
.service(service)
.resourceType(CloudResource.ResourceType.FUNCTION)
.lifecycleState(mapLifecycleState(functionConfig.state()))
.memoryGb(functionConfig.memorySize() != null ? functionConfig.memorySize() / 1024 : null) // MB → GB
.instanceType(functionConfig.runtime()) // 런타임을 instanceType에 저장
.tags(convertTagsToJson(functionConfig.tags()))
.configuration(convertConfigurationToJson(functionConfig))
.status(mapStatus(functionConfig.state()))
.createdInCloud(functionConfig.lastModified() != null ?
Instant.from(functionConfig.lastModified()) : null)
.lastSync(LocalDateTime.now())
.build();
}
/**
* FunctionCreateCommand → CreateFunctionRequest 변환
* CSP 중립적 Command를 AWS SDK 요청으로 변환
*/
public CreateFunctionRequest toCreateRequest(FunctionCreateCommand command) {
var builder = CreateFunctionRequest.builder()
.functionName(command.functionName())
.runtime(command.runtime())
.handler(command.handler())
.memorySize(command.memorySize())
.timeout(command.timeout())
.role(command.roleArn())
.description(command.description());
// 환경 변수 설정
if (command.environmentVariables() != null && !command.environmentVariables().isEmpty()) {
builder.environment(Environment.builder()
.variables(command.environmentVariables())
.build());
}
// VPC 설정
if (command.vpcId() != null) {
// providerSpecificConfig에서 VPC 세부 정보 추출
VpcConfig vpcConfig = getVpcConfig(command);
builder.vpcConfig(vpcConfig);
}
// 코드 설정
if (command.codeZip() != null) {
builder.code(FunctionCode.builder()
.zipFile(SdkBytes.fromByteArray(command.codeZip()))
.build());
} else if (command.codeUri() != null) {
// S3 URI 파싱
S3CodeLocation s3Location = parseS3Uri(command.codeUri());
builder.code(FunctionCode.builder()
.s3Bucket(s3Location.bucket())
.s3Key(s3Location.key())
.build());
}
// 태그 설정
if (command.tags() != null && !command.tags().isEmpty()) {
builder.tags(command.tags());
}
return builder.build();
}
private VpcConfig getVpcConfig(FunctionCreateCommand command) {
// providerSpecificConfig에서 VPC 설정 추출
// AWS의 경우: subnetIds, securityGroupIds 필요
if (command.providerSpecificConfig() != null) {
@SuppressWarnings("unchecked")
List<String> subnetIds = (List<String>) command.providerSpecificConfig().get("subnetIds");
@SuppressWarnings("unchecked")
List<String> securityGroupIds = (List<String>) command.providerSpecificConfig().get("securityGroupIds");
if (subnetIds != null || securityGroupIds != null) {
return VpcConfig.builder()
.subnetIds(subnetIds != null ? subnetIds : List.of())
.securityGroupIds(securityGroupIds != null ? securityGroupIds : List.of())
.build();
}
}
return null;
}
// 기타 변환 메서드들...
}2.3 Management Adapter (adapter/outbound/aws/function/AwsFunctionManagementAdapter.java)
@Component
@RequiredArgsConstructor
public class AwsFunctionManagementAdapter implements FunctionManagementPort, ProviderScoped {
private final AwsFunctionConfig functionConfig;
private final AwsFunctionMapper mapper;
private final CloudErrorTranslator errorTranslator;
@Override
public CloudResource createFunction(FunctionCreateCommand command) {
LambdaClient lambdaClient = null;
try {
lambdaClient = functionConfig.createLambdaClient(command.session(), command.region());
CreateFunctionRequest request = mapper.toCreateRequest(command);
CreateFunctionResponse response = lambdaClient.createFunction(request);
// 생성된 함수 조회
FunctionConfiguration functionConfig = response.configuration();
return mapper.toCloudResource(functionConfig, ...);
} catch (Throwable t) {
throw errorTranslator.translate(t);
} finally {
if (lambdaClient != null) {
lambdaClient.close();
}
}
}
@Override
public ProviderType getProviderType() {
return ProviderType.AWS;
}
}2.4 Discovery Adapter (adapter/outbound/aws/function/AwsFunctionDiscoveryAdapter.java)
@Component
@RequiredArgsConstructor
public class AwsFunctionDiscoveryAdapter implements FunctionDiscoveryPort, ProviderScoped {
private final AwsFunctionConfig functionConfig;
private final AwsFunctionMapper mapper;
private final CloudErrorTranslator errorTranslator;
@Override
public Page<CloudResource> listFunctions(FunctionQuery query, CloudSessionCredential session) {
LambdaClient lambdaClient = null;
try {
lambdaClient = functionConfig.createLambdaClient(session, query.regions().iterator().next());
// AWS Lambda ListFunctions 호출
ListFunctionsRequest.Builder requestBuilder = ListFunctionsRequest.builder();
// 필터링 로직
if (query.functionName() != null) {
// AWS는 prefix 기반 필터링만 지원
requestBuilder.functionVersion("ALL");
}
ListFunctionsResponse response = lambdaClient.listFunctions(requestBuilder.build());
// 페이징 처리
List<CloudResource> resources = response.functions().stream()
.filter(function -> matchesQuery(function, query))
.map(function -> mapper.toCloudResource(function, ...))
.collect(Collectors.toList());
return new PageImpl<>(resources, PageRequest.of(query.page(), query.size()), resources.size());
} catch (Throwable t) {
throw errorTranslator.translate(t);
} finally {
if (lambdaClient != null) {
lambdaClient.close();
}
}
}
private boolean matchesQuery(FunctionConfiguration function, FunctionQuery query) {
// 쿼리 조건에 맞는지 필터링
if (query.functionName() != null && !function.functionName().startsWith(query.functionName())) {
return false;
}
if (query.runtime() != null && !query.runtime().equals(function.runtime())) {
return false;
}
return true;
}
@Override
public ProviderType getProviderType() {
return ProviderType.AWS;
}
}2.5 Invocation Adapter (adapter/outbound/aws/function/AwsFunctionInvocationAdapter.java)
@Component
@RequiredArgsConstructor
public class AwsFunctionInvocationAdapter implements FunctionInvocationPort, ProviderScoped {
private final AwsFunctionConfig functionConfig;
private final CloudErrorTranslator errorTranslator;
@Override
public String invokeFunction(FunctionInvokeCommand command) {
LambdaClient lambdaClient = null;
try {
lambdaClient = functionConfig.createLambdaClient(command.session(), command.region());
InvokeRequest.Builder requestBuilder = InvokeRequest.builder()
.functionName(command.providerResourceId())
.payload(SdkBytes.fromUtf8String(command.payload()));
// InvocationType 설정
if ("Event".equals(command.invocationType())) {
requestBuilder.invocationType(InvocationType.EVENT);
} else if ("DryRun".equals(command.invocationType())) {
requestBuilder.invocationType(InvocationType.DRY_RUN);
} else {
requestBuilder.invocationType(InvocationType.REQUEST_RESPONSE);
}
// Qualifier 설정 (버전/별칭)
if (command.qualifier() != null) {
requestBuilder.qualifier(command.qualifier());
}
InvokeResponse response = lambdaClient.invoke(requestBuilder.build());
return response.payload().asUtf8String();
} catch (Throwable t) {
throw errorTranslator.translate(t);
} finally {
if (lambdaClient != null) {
lambdaClient.close();
}
}
}
@Override
public ProviderType getProviderType() {
return ProviderType.AWS;
}
}Phase 3: Service 계층 구현
3.1 Port Router (service/function/FunctionPortRouter.java)
@Component
@RequiredArgsConstructor
public class FunctionPortRouter {
private final Map<ProviderType, FunctionManagementPort> managementMap;
private final Map<ProviderType, FunctionDiscoveryPort> discoveryMap;
private final Map<ProviderType, FunctionInvocationPort> invocationMap;
public FunctionManagementPort management(ProviderType type) {
return require(managementMap, type);
}
public FunctionDiscoveryPort discovery(ProviderType type) {
return require(discoveryMap, type);
}
public FunctionInvocationPort invocation(ProviderType type) {
return require(invocationMap, type);
}
private <T> T require(Map<ProviderType, T> map, ProviderType type) {
T port = map.get(type);
if (port == null) {
throw new BusinessException(CloudErrorCode.CLOUD_PROVIDER_NOT_SUPPORTED,
"지원하지 않는 프로바이더입니다: " + type);
}
return port;
}
}3.2 UseCase Service (service/function/FunctionUseCaseService.java)
@Slf4j
@Service
@RequiredArgsConstructor
public class FunctionUseCaseService {
private static final String RESOURCE_TYPE = "FUNCTION";
private final FunctionPortRouter portRouter;
private final CapabilityGuard capabilityGuard;
private final AccountCredentialManagementPort credentialPort;
private final CloudResourceRepository resourceRepository;
private final CloudResourceManagementHelper resourceHelper;
/**
* Serverless Function 생성
*/
@Transactional
public CloudResource createFunction(FunctionCreateRequest request) {
ProviderType providerType = request.getProviderType();
String accountScope = request.getAccountScope();
// Capability 검증
String serviceKey = getServiceKeyForProvider(providerType);
capabilityGuard.ensureSupported(providerType, serviceKey, RESOURCE_TYPE, Operation.CREATE);
// 세션 획득
CloudSessionCredential session = getSession(providerType, accountScope);
// Command 변환
FunctionCreateCommand command = toCreateCommand(request, session);
// 어댑터 호출
CloudResource resource = portRouter.management(providerType).createFunction(command);
// DB에 CloudResource 저장
CloudResource savedResource = resourceHelper.registerResource(
ResourceRegistrationRequest.builder()
.resource(resource)
.tenantKey(TenantContextHolder.getCurrentTenantKeyOrThrow())
.accountScope(accountScope)
.build()
);
return savedResource;
}
/**
* Serverless Function 목록 조회
*/
@Transactional(readOnly = true)
public Page<CloudResource> listFunctions(ProviderType providerType, String accountScope, FunctionQueryRequest request) {
CloudSessionCredential session = getSession(providerType, accountScope);
FunctionQuery query = toQuery(providerType, accountScope, request);
return portRouter.discovery(providerType).listFunctions(query, session);
}
/**
* Serverless Function 실행
*/
@Transactional
public String invokeFunction(ProviderType providerType, String accountScope, FunctionInvokeRequest request) {
CloudSessionCredential session = getSession(providerType, accountScope);
FunctionInvokeCommand command = toInvokeCommand(providerType, accountScope, request, session);
return portRouter.invocation(providerType).invokeFunction(command);
}
// 기타 메서드들...
private CloudSessionCredential getSession(ProviderType providerType, String accountScope) {
String tenantKey = TenantContextHolder.getCurrentTenantKeyOrThrow();
return credentialPort.getSession(tenantKey, accountScope, providerType);
}
private String getServiceKeyForProvider(ProviderType providerType) {
return switch (providerType) {
case AWS -> "LAMBDA";
case AZURE -> "AZURE_FUNCTIONS";
case GCP -> "CLOUD_FUNCTIONS";
default -> throw new BusinessException(CloudErrorCode.CLOUD_PROVIDER_NOT_SUPPORTED,
"지원하지 않는 프로바이더입니다: " + providerType);
};
}
}Phase 4: Controller 및 DTO 구현
4.1 DTO 클래스 (dto/)
FunctionCreateRequest.java
/**
* Serverless Function 생성 요청 DTO (CSP 중립적)
*
* Controller에서 받는 요청 객체로, CSP 중립적인 필드만 포함합니다.
* CSP 특화 설정은 providerSpecificConfig에 포함됩니다.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionCreateRequest {
@NotNull
private ProviderType providerType;
@NotBlank
private String accountScope;
@NotBlank
private String region;
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9-_]{1,64}$", message = "함수 이름은 1-64자의 영문, 숫자, 하이픈, 언더스코어만 사용 가능합니다")
private String functionName; // CSP 중립적
@NotBlank
private String runtime; // CSP 중립적: "nodejs18.x", "python3.11", "java17", "go1.x"
@NotBlank
private String handler; // CSP 중립적: AWS(Handler), Azure(scriptFile.entryPoint), GCP(entryPoint)
@Min(128)
@Max(10240)
private Integer memorySize; // MB
@Min(1)
@Max(900) // AWS 최대값, Azure/GCP는 다를 수 있음
private Integer timeout; // 초
@NotBlank
private String roleArn; // CSP 중립적: 실행 역할
private Map<String, String> environmentVariables; // 환경 변수
private String description; // 함수 설명
private String vpcId; // VPC 연결 (선택적)
@NotBlank
private String codeUri; // 코드 URI (S3, Blob Storage, GCS 등)
private Map<String, String> tags;
/**
* CSP별 특화 설정
*
* AWS 예시:
* - subnetIds: ["subnet-12345", "subnet-67890"] (VPC 연결 시)
* - securityGroupIds: ["sg-12345"] (VPC 연결 시)
* - layers: ["arn:aws:lambda:region:account:layer:layer-name:1"]
* - reservedConcurrentExecutions: 10
* - deadLetterQueueTargetArn: "arn:aws:sqs:region:account:dlq"
*
* Azure 예시:
* - hostingPlan: "Consumption" | "Premium" | "Dedicated"
* - appServicePlanId: "/subscriptions/.../resourceGroups/.../providers/..."
* - bindings: [{"type": "httpTrigger", "direction": "in", "authLevel": "function"}]
*
* GCP 예시:
* - serviceAccountEmail: "[email protected]"
* - vpcConnector: "projects/project/locations/region/connectors/connector"
* - maxInstances: 10
* - minInstances: 0
* - ingressSettings: "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB"
*/
private Map<String, Object> providerSpecificConfig;
}FunctionUpdateRequest.java
/**
* Serverless Function 수정 요청 DTO (CSP 중립적)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionUpdateRequest {
@NotNull
private ProviderType providerType;
@NotBlank
private String accountScope;
@NotBlank
private String functionId; // 수정할 함수 ID/ARN
private String runtime; // 런타임 변경
private String handler; // 핸들러 변경
@Min(128)
@Max(10240)
private Integer memorySize; // 메모리 크기 변경 (MB)
@Min(1)
@Max(900)
private Integer timeout; // 타임아웃 변경 (초)
private String roleArn; // 실행 역할 변경
private Map<String, String> environmentVariables; // 환경 변수 변경
private String description; // 설명 변경
private String codeUri; // 코드 업데이트 URI
private Map<String, String> tagsToAdd; // 추가할 태그
private Map<String, String> tagsToRemove; // 제거할 태그
/**
* CSP별 특화 설정
*/
private Map<String, Object> providerSpecificConfig;
}FunctionDeleteRequest.java
/**
* Serverless Function 삭제 요청 DTO (CSP 중립적)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionDeleteRequest {
@NotNull
private ProviderType providerType;
@NotBlank
private String accountScope;
@NotBlank
private String functionId; // 삭제할 함수 ID/ARN
private String reason; // 삭제 이유 (감사 로그용)
/**
* CSP별 특화 삭제 옵션
*/
private Map<String, Object> providerSpecificConfig;
/**
* 기본 삭제 요청 생성
*/
public static FunctionDeleteRequest basic(String functionId) {
return FunctionDeleteRequest.builder()
.functionId(functionId)
.build();
}
}FunctionQueryRequest.java
/**
* Serverless Function 조회 요청 DTO (CSP 중립적)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionQueryRequest {
@NotNull
private ProviderType providerType;
@NotBlank
private String accountScope;
private Set<String> regions; // 조회할 리전 목록
private String functionName; // 함수 이름으로 필터링
private String runtime; // 런타임으로 필터링
private String vpcId; // VPC ID로 필터링
private Map<String, String> tags; // 태그로 필터링
@Min(0)
@Builder.Default
private int page = 0;
@Min(1)
@Max(100)
@Builder.Default
private int size = 20;
}FunctionInvokeRequest.java
/**
* Serverless Function 실행 요청 DTO (CSP 중립적)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionInvokeRequest {
@NotNull
private ProviderType providerType;
@NotBlank
private String accountScope;
@NotBlank
private String functionId; // 실행할 함수 ID/ARN
@NotBlank
private String region;
@Pattern(regexp = "RequestResponse|Event|DryRun", message = "RequestResponse, Event, DryRun 중 하나여야 합니다")
@Builder.Default
private String invocationType = "RequestResponse"; // 기본값: 동기 실행
@NotBlank
private String payload; // JSON 문자열 페이로드
private String qualifier; // 함수 버전/별칭 (선택적)
private Map<String, String> context; // 추가 컨텍스트 정보
}FunctionResponse.java
/**
* Serverless Function 응답 DTO (CSP 중립적)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FunctionResponse {
private Long id;
private String resourceId;
private String resourceName; // function name
private ProviderType providerType;
private String region;
private String runtime; // 런타임
private String handler; // 핸들러
private Integer memorySize; // MB
private Integer timeout; // 초
private String roleArn; // 실행 역할
private Map<String, String> environmentVariables; // 환경 변수
private String description; // 함수 설명
private String vpcId; // VPC ID
private String status; // 상태
private String lifecycleState; // 생명주기 상태
private String lastModified; // 마지막 수정 시간
private Map<String, String> tags;
private LocalDateTime createdAt;
private LocalDateTime lastSync;
/**
* CloudResource → FunctionResponse 변환
*/
public static FunctionResponse from(CloudResource resource) {
// CloudResource의 configuration JSON에서 추가 정보 추출
return FunctionResponse.builder()
.id(resource.getId())
.resourceId(resource.getResourceId())
.resourceName(resource.getResourceName())
.providerType(resource.getProvider().getProviderType())
.region(resource.getRegion() != null ? resource.getRegion().getRegionKey() : null)
.status(resource.getStatus() != null ? resource.getStatus().name() : null)
.lifecycleState(resource.getLifecycleState() != null ? resource.getLifecycleState().name() : null)
.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/FunctionController.java)
@Slf4j
@RestController
@RequestMapping("/api/v1/cloud/providers/{provider}/accounts/{accountScope}/functions")
@RequiredArgsConstructor
@Tag(name = "Serverless Function Management", description = "Serverless Function 관리 API (멀티 클라우드 지원)")
public class FunctionController {
private final FunctionUseCaseService functionUseCaseService;
@PostMapping
@Operation(summary = "Serverless Function 생성")
public ResponseEntity<ApiResponse<FunctionResponse>> createFunction(
@PathVariable ProviderType provider,
@PathVariable String accountScope,
@Valid @RequestBody FunctionCreateRequest request) {
request.setProviderType(provider);
request.setAccountScope(accountScope);
CloudResource resource = functionUseCaseService.createFunction(request);
FunctionResponse response = FunctionResponse.from(resource);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(response, "Serverless Function 생성에 성공했습니다."));
}
@GetMapping
@Operation(summary = "Serverless Function 목록 조회")
public ResponseEntity<ApiResponse<Page<FunctionResponse>>> listFunctions(
@PathVariable ProviderType provider,
@PathVariable String accountScope,
@Valid FunctionQueryRequest request) {
// 구현...
}
@PostMapping("/{functionId}/invoke")
@Operation(summary = "Serverless Function 실행")
public ResponseEntity<ApiResponse<String>> invokeFunction(
@PathVariable ProviderType provider,
@PathVariable String accountScope,
@PathVariable String functionId,
@Valid @RequestBody FunctionInvokeRequest request) {
request.setProviderType(provider);
request.setAccountScope(accountScope);
request.setFunctionId(functionId);
String result = functionUseCaseService.invokeFunction(provider, accountScope, request);
return ResponseEntity.ok(ApiResponse.success(result, "Serverless Function 실행에 성공했습니다."));
}
// 기타 엔드포인트들...
}Phase 5: Capability 등록
CapabilityRegistry에 Function 기능 등록
// AwsCapabilityConfig.java에 추가
@Bean
public CspCapability awsFunctionCapability() {
return CspCapability.builder()
.providerType(ProviderType.AWS)
.serviceKey("LAMBDA")
.resourceType("FUNCTION")
.supportsCreate(true)
.supportsRead(true)
.supportsUpdate(true)
.supportsDelete(true)
.build();
}고려사항
1. 헥사고날 아키텍처 준수
- ✅ 포트 인터페이스: CSP 독립적인 비즈니스 계약 정의
- ✅ 어댑터 분리: AWS 전용 로직은
adapter/outbound/aws/function/에만 존재 - ✅ 의존성 방향: Service → Port → Adapter (단방향)
- ✅ 도메인 모델:
CloudResource엔티티 활용
2. CSP 독립성 보장
- ✅ Controller: 특정 CSP에 종속되지 않는 공통 API만 제공
- ✅ Service: CSP별 차이는 Port 인터페이스로 추상화
- ✅ Command: CSP 중립적인 필드만 포함,
providerSpecificConfig로 확장 가능
3. 보안 및 자격증명 관리
- ✅ JIT 세션 관리: Service 레벨에서 세션 획득 및 Port에 전달
- ✅ ThreadLocal 자격증명: 멀티 테넌트 환경 지원
- ✅ 코드 업로드: 민감한 코드는 암호화하여 전달
4. 리소스 동기화
- ✅ CloudResource 저장: 생성/수정 시
CloudResourceRepository에 저장 - ✅ 동기화 전략:
- 생성/수정 시 즉시 저장
- 조회 시 최신 상태 반영 (
lastSync필드 활용)
5. 에러 처리
- ✅ CloudErrorTranslator: AWS SDK 예외를 도메인 예외로 변환
- ✅ BusinessException: 비즈니스 로직 예외 처리
- ✅ 에러 코드:
CloudErrorCode에 Function 관련 에러 코드 추가
6. 트랜잭션 관리
- ✅ @transactional: Service 메서드에 적절한 트랜잭션 설정
- ✅ 읽기 전용: 조회 메서드는
@Transactional(readOnly = true)
7. Capability 검증
- ✅ CapabilityGuard: 작업 전 CSP 지원 여부 검증
- ✅ 동적 검증: 런타임에 Capability 확인
8. Serverless Function 특화 고려사항
8-1. 코드 배포 방식
- URI 방식: S3, Blob Storage, GCS 등에서 코드 다운로드
- 직접 업로드: ZIP 바이너리를 직접 전달 (작은 코드에 적합)
- 코드 크기 제한: CSP별로 최대 크기 제한 다름
- AWS Lambda: 50MB (직접 업로드), 250MB (압축), S3 무제한
- Azure Functions: 100MB (Consumption Plan)
- GCP Cloud Functions: 100MB (압축), 500MB (S3)
8-2. 런타임 지원
- CSP별로 지원하는 런타임과 버전이 다름
- 런타임 추상화 전략:
- 공통 런타임 식별자 사용:
nodejs18.x,python3.11,java17,go1.x - 매퍼에서 CSP별 실제 런타임 값으로 변환
- 공통 런타임 식별자 사용:
8-3. 핸들러/엔트리포인트
- AWS Lambda:
package.Class::method또는filename.handler - Azure Functions:
scriptFile.entryPoint(예:index.handler) - GCP Cloud Functions:
entryPoint함수 이름 - CSP 중립적으로
handler필드로 통일
8-4. 메모리 및 타임아웃
- 메모리: MB 단위 (모든 CSP 공통)
- 타임아웃: 초 단위 (최대값 CSP별로 다름)
- AWS Lambda: 최대 900초 (15분)
- Azure Functions: 최대 600초 (10분, Consumption Plan)
- GCP Cloud Functions: 최대 540초 (9분, 1세대), 3600초 (1시간, 2세대)
8-5. 실행 역할(Role)
- AWS Lambda: IAM Role ARN
- Azure Functions: Managed Identity 또는 App Service Identity
- GCP Cloud Functions: Service Account Email
- CSP 중립적으로
roleArn필드로 통일 (실제 값 형식은 CSP별로 다름)
8-6. VPC 연결
- AWS Lambda: VPC Config (Subnet IDs, Security Group IDs)
- Azure Functions: Virtual Network Integration
- GCP Cloud Functions: VPC Connector
- CSP 중립적으로
vpcId필드로 시작하되, 세부 설정은providerSpecificConfig로
8-7. 환경 변수
- 모든 CSP에서 키-값 쌍으로 환경 변수 지원
- 민감한 정보는 CSP별 Secrets Manager와 연동 권장
8-8. 함수 실행
- 동기 실행 (RequestResponse): 결과를 즉시 반환
- 비동기 실행 (Event): 이벤트로 큐에 넣고 즉시 반환
- Dry Run: 실제 실행 없이 검증만 수행
9. CSP 중립성 보장
- ✅ 필드명 중립화:
FunctionName→functionName(모든 CSP 공통)Runtime→runtime(모든 CSP 공통)Handler→handler(모든 CSP 공통)MemorySize→memorySize(MB, 모든 CSP 공통)Timeout→timeout(초, 모든 CSP 공통)Role→roleArn(실행 역할, 모든 CSP 공통)
- ✅ CSP 특화 필드 제거:
subnetIds,securityGroupIds(AWS 전용) →providerSpecificConfig로 이동hostingPlan(Azure 전용) →providerSpecificConfig로 이동vpcConnector(GCP 전용) →providerSpecificConfig로 이동
- ✅ 확장성:
providerSpecificConfig로 CSP별 특화 설정 지원- AWS:
layers,reservedConcurrentExecutions,deadLetterQueueTargetArn등 - Azure:
hostingPlan,appServicePlanId,bindings등 - GCP:
serviceAccountEmail,vpcConnector,maxInstances,minInstances등
- AWS:
10. 코드 업로드 및 배포
10-1. 코드 배포 전략
- Phase 1: URI 기반 배포만 지원 (S3, Blob Storage, GCS)
- Phase 2: 직접 ZIP 업로드 지원 고려
- 코드 크기 제한 검증 필요
10-2. 코드 업데이트
- 함수 코드만 업데이트 (코드 업데이트)
- 함수 설정만 업데이트 (설정 업데이트)
- 코드와 설정 모두 업데이트
11. 트리거/이벤트 소스 (Phase 2)
Phase 1에서는 함수 생성/수정/삭제/조회에 집중하고, 트리거 관리는 Phase 2에서 구현합니다.
Phase 2 예정 기능:
- HTTP 트리거
- 이벤트 소스 연결 (S3, SQS, EventBridge 등)
- 스케줄 트리거 (Cron)
12. 테스트 전략
- ✅ 단위 테스트: 각 Adapter, Service, Controller 테스트
- ✅ 통합 테스트: LocalStack을 활용한 AWS 통합 테스트
- ✅ Mock 테스트: Port 인터페이스 Mock으로 Service 테스트
- ✅ 코드 실행 테스트: 실제 Lambda 함수 실행 테스트
13. 성능 고려사항
- ✅ 페이징: 대량 조회 시 페이징 처리
- ✅ 비동기 실행: 장시간 작업은 비동기 실행 지원
- ✅ 코드 캐싱: 자주 사용되는 함수 코드 캐싱 고려
14. 모니터링 및 로깅
- ✅ 구조화된 로깅: 모든 레이어에서 일관된 로깅
- ✅ 실행 로그: 함수 실행 결과 로깅
- ✅ 메트릭: 함수 실행 횟수, 성공/실패, 실행 시간 등 메트릭 수집
- ✅ 감사 로그: 함수 생성/삭제 등 중요 작업 감사 로깅
15. 보안 고려사항
- ✅ 역할 기반 접근: 함수 실행 역할 검증
- ✅ VPC 격리: Private 함수의 경우 VPC 연결 검증
- ✅ 환경 변수 암호화: 민감한 환경 변수는 암호화하여 저장
- ✅ 코드 검증: 업로드된 코드의 보안 검증 (선택적)
구현 체크리스트
Phase 1: 포트 및 모델
FunctionCreateCommand정의FunctionUpdateCommand정의FunctionDeleteCommand정의FunctionQuery정의FunctionInvokeCommand정의GetFunctionCommand정의FunctionManagementPort인터페이스 정의FunctionDiscoveryPort인터페이스 정의FunctionInvocationPort인터페이스 정의
Phase 2: AWS 어댑터
AwsFunctionConfig클래스 생성AwsFunctionMapper클래스 생성AwsFunctionManagementAdapter구현AwsFunctionDiscoveryAdapter구현AwsFunctionInvocationAdapter구현ProviderScoped인터페이스 구현
Phase 3: Service 계층
FunctionPortRouter생성FunctionUseCaseService구현- Capability 검증 로직 추가
- JIT 세션 관리 구현
- 리소스 저장 로직 구현
Phase 4: Controller 및 DTO
FunctionCreateRequestDTO 생성FunctionUpdateRequestDTO 생성FunctionDeleteRequestDTO 생성FunctionQueryRequestDTO 생성FunctionInvokeRequestDTO 생성FunctionResponseDTO 생성FunctionController구현- API 문서화 (Swagger)
Phase 5: Capability 및 설정
- CapabilityRegistry에 Function 등록
- 설정 파일 업데이트
- Bean 등록 확인
Phase 6: 테스트
- 단위 테스트 작성
- 통합 테스트 작성
- API 테스트
- 함수 실행 테스트
참고 자료
- 헥사고날 아키텍처 가이드
- RDS 구현 예시 (Issue [FEATURE] AWS RDS 자원 관리 기능 구현 #179)
- DNS 구현 예시
- VM 구현 예시
- VPC 구현 예시
- AWS SDK Lambda 문서
- Lambda API 참조
Phase 2 예정: 트리거 및 이벤트 소스 관리
Serverless Function 기본 기능 구현 후, 트리거 및 이벤트 소스 관리 기능을 Phase 2로 구현합니다.
Phase 2 주요 기능
-
HTTP 트리거
- API Gateway 연동 (AWS)
- Function App HTTP 트리거 (Azure)
- Cloud Functions HTTP 트리거 (GCP)
-
이벤트 소스 연결
- S3, SQS, EventBridge (AWS)
- Event Grid, Service Bus (Azure)
- Pub/Sub, Cloud Storage (GCP)
-
스케줄 트리거
- EventBridge Rules (AWS)
- Timer Trigger (Azure)
- Cloud Scheduler (GCP)