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: "my-function@project.iam.gserviceaccount.com"
* - 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 등
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
FunctionCreateRequest DTO 생성
FunctionUpdateRequest DTO 생성
FunctionDeleteRequest DTO 생성
FunctionQueryRequest DTO 생성
FunctionInvokeRequest DTO 생성
FunctionResponse DTO 생성
FunctionController 구현
- API 문서화 (Swagger)
Phase 5: Capability 및 설정
- CapabilityRegistry에 Function 등록
- 설정 파일 업데이트
- Bean 등록 확인
Phase 6: 테스트
- 단위 테스트 작성
- 통합 테스트 작성
- 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)
AWS Serverless Function(Lambda) 자원 관리 기능 구현 계획
개요
AWS Lambda, Azure Functions, GCP Cloud Functions를 헥사고날 아키텍처로 구현합니다.
도메인 추상화명은 FUNCTION(Serverless Function)을 사용합니다.
추상화명
아키텍처 구조
헥사고날 아키텍처를 준수하여 다음과 같이 구성합니다:
구현 단계
Phase 1: 포트 및 모델 정의
1.1 Command 모델 정의 (
port/model/function/)FunctionCreateCommand.java
FunctionUpdateCommand.java
FunctionDeleteCommand.java
FunctionQuery.java
FunctionInvokeCommand.java
GetFunctionCommand.java
1.2 포트 인터페이스 정의 (
port/outbound/function/)FunctionManagementPort.java
FunctionDiscoveryPort.java
FunctionInvocationPort.java
Phase 2: AWS 어댑터 구현
2.1 Config 클래스 (
adapter/outbound/aws/config/AwsFunctionConfig.java)2.2 Mapper 클래스 (
adapter/outbound/aws/function/AwsFunctionMapper.java)2.3 Management Adapter (
adapter/outbound/aws/function/AwsFunctionManagementAdapter.java)2.4 Discovery Adapter (
adapter/outbound/aws/function/AwsFunctionDiscoveryAdapter.java)2.5 Invocation Adapter (
adapter/outbound/aws/function/AwsFunctionInvocationAdapter.java)Phase 3: Service 계층 구현
3.1 Port Router (
service/function/FunctionPortRouter.java)3.2 UseCase Service (
service/function/FunctionUseCaseService.java)Phase 4: Controller 및 DTO 구현
4.1 DTO 클래스 (
dto/)FunctionCreateRequest.java
FunctionUpdateRequest.java
FunctionDeleteRequest.java
FunctionQueryRequest.java
FunctionInvokeRequest.java
FunctionResponse.java
4.2 Controller (
controller/FunctionController.java)Phase 5: Capability 등록
CapabilityRegistry에 Function 기능 등록
고려사항
1. 헥사고날 아키텍처 준수
adapter/outbound/aws/function/에만 존재CloudResource엔티티 활용2. CSP 독립성 보장
providerSpecificConfig로 확장 가능3. 보안 및 자격증명 관리
4. 리소스 동기화
CloudResourceRepository에 저장lastSync필드 활용)5. 에러 처리
CloudErrorCode에 Function 관련 에러 코드 추가6. 트랜잭션 관리
@Transactional(readOnly = true)7. Capability 검증
8. Serverless Function 특화 고려사항
8-1. 코드 배포 방식
8-2. 런타임 지원
nodejs18.x,python3.11,java17,go1.x8-3. 핸들러/엔트리포인트
package.Class::method또는filename.handlerscriptFile.entryPoint(예:index.handler)entryPoint함수 이름handler필드로 통일8-4. 메모리 및 타임아웃
8-5. 실행 역할(Role)
roleArn필드로 통일 (실제 값 형식은 CSP별로 다름)8-6. VPC 연결
vpcId필드로 시작하되, 세부 설정은providerSpecificConfig로8-7. 환경 변수
8-8. 함수 실행
9. CSP 중립성 보장
FunctionName→functionName(모든 CSP 공통)Runtime→runtime(모든 CSP 공통)Handler→handler(모든 CSP 공통)MemorySize→memorySize(MB, 모든 CSP 공통)Timeout→timeout(초, 모든 CSP 공통)Role→roleArn(실행 역할, 모든 CSP 공통)subnetIds,securityGroupIds(AWS 전용) →providerSpecificConfig로 이동hostingPlan(Azure 전용) →providerSpecificConfig로 이동vpcConnector(GCP 전용) →providerSpecificConfig로 이동providerSpecificConfig로 CSP별 특화 설정 지원layers,reservedConcurrentExecutions,deadLetterQueueTargetArn등hostingPlan,appServicePlanId,bindings등serviceAccountEmail,vpcConnector,maxInstances,minInstances등10. 코드 업로드 및 배포
10-1. 코드 배포 전략
10-2. 코드 업데이트
11. 트리거/이벤트 소스 (Phase 2)
Phase 1에서는 함수 생성/수정/삭제/조회에 집중하고, 트리거 관리는 Phase 2에서 구현합니다.
Phase 2 예정 기능:
12. 테스트 전략
13. 성능 고려사항
14. 모니터링 및 로깅
15. 보안 고려사항
구현 체크리스트
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구현Phase 4: Controller 및 DTO
FunctionCreateRequestDTO 생성FunctionUpdateRequestDTO 생성FunctionDeleteRequestDTO 생성FunctionQueryRequestDTO 생성FunctionInvokeRequestDTO 생성FunctionResponseDTO 생성FunctionController구현Phase 5: Capability 및 설정
Phase 6: 테스트
참고 자료
Phase 2 예정: 트리거 및 이벤트 소스 관리
Serverless Function 기본 기능 구현 후, 트리거 및 이벤트 소스 관리 기능을 Phase 2로 구현합니다.
Phase 2 주요 기능
HTTP 트리거
이벤트 소스 연결
스케줄 트리거