Now Loading ...
-
0. Intro
우리는 PG사 API와 연동하여 결제 서비스를 클라이언트에게 제공해주고 있다.
결제 서비스 내에서도 결제 수단 등록/삭제, 결제 수단 조회와 등록된 결제 수단을 통한 책정 금액 결제 또는 책정 금액의 청구서를 카카오톡으로 발송
우리는 가맹점과 소비자의 중간 매개체 역할이며, 소비자가 이용한 서비스에 대한 책정 금액을 가맹점이 결정하여 우리에게 전달한다.
우리는 책정된 금액과 소비자 정보를 전달받아 결제 방식을 결정한다.
만약 소비자가 우리의 플랫폼 APP의 회원일 경우, PG사에서 제공해주는 자동 결제 시스템을 통해 소비자가 미리 등록한 결제 수단으로 즉시 결제 요청한다.
소비자가 미리 등록한 결제 수단이 없거나 결제 수단에 문제가 있다고 판단될 경우, 소비자의 연락처를 통해 카카오톡 청구서를 발송한다.
소비자가 우리의 플랫폼 APP의 회원이 아닐 경우, 즉시 카카오톡 청구서 방식으로 요청한다.
결제 요청 후, PG사로부터 Callback API를 통해 결제 요청 결과를 수신 받는다.
환불이 필요할 경우, 결제 요청 당시 생성한 청구서 ID와 요청 금액을 전달하여 환불 요청한다.
재결제가 필요할 경우, 환불 요청 후, 새로운 청구서 ID를 생성하여 결제 요청한다.
기존 결제 서비스는 모든 외부 모듈들이 필수로 주입 받고 있는 common 모듈 안에 구현되어있어서 결제 서비스가 필요하지 않더라도 인스턴스를 생성하고 있었다.
우리는 기존 서비스 기능에 대한 고도화 작업 및 신규 기능 추가 작업으로 인해 점차 리소스 비용이 증가하고 있었다.
따라서 결제 서비스 전용 모듈을 분리하여 MSA 아키텍처로 전환하기로 결정하였다.
이를 통해 결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄이며, 향후 기능 확장 및 유지보수를 용이하게 할 수 있을 것으로 기대하고 있다.
또한, MSA 아키텍처로의 전환은 각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련해줄 것이다.
목표
결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄인다.
향후 기능 확장 및 유지보수를 용이하게 한다.
각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련한다.
추상화 구조로 설계해본다. 어댑터 패턴 (Adapter Pattern) 이란?
기존 복잡한 VIEW TABLE을 사용하지 않는 방향으로, 제거하는 방향으로 설계해보자.
결제 모듈만이 관리하는 테이블과 각 클라이언트가 관리하는 테이블을 분리하고 각자 관리한다.
-
-
-
-
3. PG사 외부 API 통신을 위한 RestClient 설계
Spring Framework 6.1에서 도입된 RestClient와 @HttpExchange 어노테이션을 활용한 선언적 HTTP 클라이언트 구현에 대해 설명합니다.
인터페이스 기반 RestClient 설계
Spring의 @HttpExchange 어노테이션을 사용하여 선언적으로 HTTP 클라이언트를 정의할 수 있습니다.
@Component
@HttpExchange
public interface PgPaymentApiRestClient {
@PostExchange("...")
바로결제ResponseBody 바로결제(@RequestBody 바로결제RequestBody request);
@PostExchange("...")
청구서카카오톡발송ResponseBody 청구서카카오톡발송(@RequestBody 청구서카카오톡발송RequestBody request);
@PostExchange("...")
청구서URL조회ResponseBody 청구서URL조회(@RequestBody 청구서URL조회RequestBody request);
@PostExchange("...")
결제정보조회ResponseBody 결제정보조회(@RequestBody 결제정보조회RequestBody request);
@PostExchange("...")
환불요청ResponseBody 환불요청(@RequestBody 환불요청RequestBody request);
@PostExchange("...")
청구서파기ResponseBody 청구서파기(@RequestBody 청구서파기RequestBody request);
}
인터페이스 설명
PgPaymentApiRestClient 인터페이스는 다음과 같은 특징을 가집니다:
선언적 HTTP 통신
@HttpExchange 어노테이션으로 HTTP 클라이언트임을 선언
각 메서드에 @PostExchange 어노테이션으로 POST 요청 정의
요청/응답 DTO를 통한 타입 안전성 보장
메서드 구성
바로결제: 즉시 결제 요청 처리
청구서카카오톡발송: 결제 청구서 카카오톡 발송
청구서URL조회: 결제 청구서 URL 정보 조회
결제정보조회: 기존 결제 내역 조회
환불요청: 결제 취소 및 환불 처리
청구서파기: 발급된 청구서 무효화
요청/응답 매핑
@RequestBody를 통한 요청 본문 자동 매핑
응답 데이터의 자동 역직렬화
RestClient 설정
아래는 RestClient의 구성과 설정을 담당하는 설정 클래스입니다.
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RestClientConfig {
@Value("${payment.pg.api.url}")
private String pgApiUri;
@Bean
public PgPaymentApiRestClient pgPaymentApiRestClient() {
return HttpServiceProxyFactory.builderFor(
RestClientAdapter.create(paymentRestClientBuilder().baseUrl(pgApiUri).build()))
.build()
.createClient(PgPaymentApiRestClient.class);
}
@Bean
public RestClient.Builder paymentRestClientBuilder() {
return RestClient.builder()
.defaultHeader(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.defaultStatusHandler(
HttpStatusCode::isError,
(request, response) -> {
log.error("httpRequest: {}, {}", request.getHeaders(), request.getURI());
log.error("httpResponse: {}, {}", response.getStatusCode(), response.getStatusText());
throw new PaymentException(response.toString());
});
}
}
설정 클래스 주요 특징
환경 설정
@Value 어노테이션으로 PG사 API URL 주입
외부 설정을 통한 유연한 환경 관리
RestClient 빌더 설정
Content-Type과 Accept 헤더 기본값 설정
에러 상태 코드에 대한 전역 핸들러 구성
상세한 에러 로깅 구현
프록시 팩토리 설정
HttpServiceProxyFactory를 통한 인터페이스 구현체 자동 생성
RestClient 어댑터 구성
동적 프록시를 통한 HTTP 클라이언트 생성
장점
코드 간결성
인터페이스 선언만으로 HTTP 클라이언트 구현
보일러플레이트 코드 최소화
타입 안전성
컴파일 시점에 타입 검증
DTO를 통한 요청/응답 데이터 구조화
유지보수성
선언적 프로그래밍으로 가독성 향상
중앙화된 설정으로 일관성 있는 구현
확장성
새로운 API 엔드포인트 추가가 용이
공통 설정의 유연한 확장
주의사항
에러 처리
적절한 예외 처리 로직 구현 필요
상세한 로깅으로 문제 추적 가능성 확보
타임아웃 설정
적절한 타임아웃 값 설정 필요
네트워크 지연 상황 고려
보안
민감한 정보의 로깅 제외
적절한 인증/인가 처리
이러한 설계를 통해 PG사와의 안정적이고 유지보수가 용이한 HTTP 통신을 구현할 수 있습니다.
-
4. 결제 서비스 Runnable 패턴 구현
결제 처리 과정에서 발생하는 다양한 이벤트를 처리하기 위한 PaymentAdapterRunnable 구현에 대해 설명합니다.
이 클래스는 결제 처리의 각 단계에서 발생하는 이벤트(UUID 생성, 성공, 실패)를 함수형 인터페이스를 통해 처리합니다.
클래스 구조
public record PaymentAdapterRunnable<T>(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<T> successRunnable,
ErrorRunnable errorRunnable) {
public static PaymentAdapterRunnable<BaseResponse> paymentRunnable(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<BaseResponse> successRunnable,
ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(createUuidRunnable, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<Void> callbackRunnable(
SuccessRunnable<Void> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<TalkResponse> sendBillKakaoTalkRunnable(
SuccessRunnable<KakaoTalkResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<UrlResponse> getBillUrlRunnable(
SuccessRunnable<UrlResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<RefundResponse> refundRunnable(
SuccessRunnable<RefundResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
public static PaymentAdapterRunnable<DestroyResponse> destroyRunnable(
SuccessRunnable<DestroyResponse> successRunnable, ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
주요 컴포넌트
1. Record 클래스 정의
public record PaymentAdapterRunnable<T>(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<T> successRunnable,
ErrorRunnable errorRunnable) {
Java 21의 Record 기능을 활용한 불변 데이터 클래스
제네릭 타입 <T>를 통한 다양한 응답 타입 지원
세 가지 핵심 Runnable 컴포넌트 포함:
CreateUuidRunnable: UUID 생성 시점의 콜백
SuccessRunnable<T>: 성공 처리 콜백
ErrorRunnable: 오류 처리 콜백
2. 정적 팩토리 메서드
결제 요청 Runnable
public static PaymentAdapterRunnable<BaseResponse> paymentRunnable(
CreateUuidRunnable createUuidRunnable,
SuccessRunnable<BaseResponse> successRunnable,
ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(createUuidRunnable, successRunnable, errorRunnable);
}
모든 콜백을 포함한 전체 Runnable 생성
결제 요청 시 사용되는 기본 구현
콜백 Runnable
public static PaymentAdapterRunnable<Void> callbackRunnable(
SuccessRunnable<Void> successRunnable,
ErrorRunnable errorRunnable) {
return new PaymentAdapterRunnable<>(uuid -> {}, successRunnable, errorRunnable);
}
UUID 생성이 필요 없는 콜백 처리용
빈 UUID 생성 콜백 포함
기타 특화된 Runnable
sendBillKakaoTalkRunnable: 카카오톡 청구서 발송
getBillUrlRunnable: 청구서 URL 조회
refundRunnable: 환불 처리
destroyRunnable: 청구서 파기
설계 특징
1. 함수형 프로그래밍 활용
람다식을 통한 간결한 콜백 구현
함수형 인터페이스를 통한 유연한 이벤트 처리
2. 제네릭을 통한 타입 안전성
응답 타입에 따른 타입 파라미터 지정
컴파일 시점의 타입 검증
3. 불변성 보장
Record 클래스를 통한 불변 객체 구현
스레드 안전성 확보
4. 메서드 특화
각 결제 프로세스에 특화된 팩토리 메서드
불필요한 콜백 제거로 효율성 향상
사용 예시
// 결제 요청 Runnable 생성
PaymentAdapterRunnable<BaseResponse> runnable = PaymentAdapterRunnable.paymentRunnable(
uuid -> log.info("UUID created: {}", uuid),
(uuid, response) -> log.info("Payment success: {}", response),
(uuid, error) -> log.error("Payment failed: {}", error)
);
// 환불 요청 Runnable 생성
PaymentAdapterRunnable<RefundResponse> refundRunnable = PaymentAdapterRunnable.refundRunnable(
(uuid, response) -> log.info("Refund success: {}", response),
(uuid, error) -> log.error("Refund failed: {}", error)
);
장점
코드 재사용성
공통 콜백 패턴 표준화
중복 코드 제거
유연성
다양한 결제 프로세스에 대응
콜백 조합의 자유로운 구성
유지보수성
명확한 책임 분리
테스트 용이성
타입 안전성
컴파일 타임 타입 체크
런타임 에러 방지
주의사항
콜백 체인 관리
과도한 콜백 중첩 방지
적절한 에러 처리 구현
메모리 관리
콜백 객체의 적절한 해제
순환 참조 방지
디버깅
콜백 실행 추적을 위한 로깅 구현
명확한 에러 메시지 제공
이러한 Runnable 패턴 구현을 통해 결제 처리의 각 단계에서 발생하는 이벤트를 효율적이고 유연하게 처리할 수 있습니다.
-
Dockerfile 완벽 가이드: 구조와 모범 사례
Dockerfile 기본 구조
Dockerfile은 Docker 이미지를 생성하기 위한 스크립트입니다. 각 명령어는 새로운 레이어를 생성하며, 이미지의 최종 상태를 결정합니다.
1. 기본 설정 명령어
FROM
FROM node:18-alpine
베이스 이미지 지정
모든 Dockerfile은 FROM으로 시작
가능한 경우 공식 이미지 사용 권장
Alpine 기반 이미지로 크기 최소화 가능
ARG
ARG VERSION=latest
ARG BUILD_ENV=production
빌드 시점 변수 정의
docker build --build-arg 로 외부 주입 가능
ENV와의 주요 차이점:
빌드 시에만 사용 가능
이미지에 저장되지 않음
컨테이너 실행 시 접근 불가
2. 파일 시스템 설정
WORKDIR
WORKDIR /app
작업 디렉토리 설정
절대 경로 사용 권장
존재하지 않는 경우 자동 생성
이후 명령어의 기준 경로가 됨
COPY
# 단일 파일 복사
COPY package.json .
# 여러 파일 복사
COPY ["file1", "file2", "./"]
# .dockerignore 적용
COPY . .
호스트에서 이미지로 파일 복사
상대 경로는 WORKDIR 기준
.dockerignore로 제외 파일 설정 가능
3. 빌드 및 실행 설정
RUN
# 쉘 형식
RUN npm install
# 실행 형식
RUN ["npm", "install"]
# 레이어 최소화를 위한 체이닝
RUN apt-get update && \
apt-get install -y \
package1 \
package2 && \
rm -rf /var/lib/apt/lists/*
새 레이어에서 명령어 실행
이미지 빌드 중 실행
레이어 수 최소화를 위해 명령어 체이닝 권장
ENV
ENV NODE_ENV=production
ENV PATH=$PATH:/usr/local/bin
환경 변수 설정
빌드 시점 및 런타임에서 사용 가능
컨테이너 실행 시에도 유지됨
CMD와 ENTRYPOINT
# CMD 예시
CMD ["npm", "start"]
# ENTRYPOINT 예시
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
CMD: 기본 실행 명령어 (오버라이드 가능)
ENTRYPOINT: 고정 실행 명령어
보통 둘 중 하나만 사용
ENTRYPOINT + CMD 조합도 가능
Docker Build 명령어 상세 가이드
기본 빌드 명령어
docker build -t my-app:1.0 .
주요 빌드 옵션
옵션
설명
예시
-t, --tag
이미지 태그 지정
docker build -t app:1.0 .
-f, --file
도커파일 경로 지정
docker build -f prod.Dockerfile .
--build-arg
빌드 인자 전달
docker build --build-arg ENV=prod .
-q, --quiet
진행 상황 출력 생략
docker build -q .
빌드 컨텍스트 최적화
.dockerignore 활용
# 빌드에서 제외할 파일
node_modules
*.log
.git
.env*
레이어 캐시 활용
# 효율적인 캐시 활용
COPY package*.json ./
RUN npm install
COPY . .
멀티 스테이지 빌드 예시
# 빌드 스테이지
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 실행 스테이지
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm install --production
CMD ["npm", "start"]
모범 사례 및 팁
1. 이미지 크기 최적화
Alpine 기반 이미지 사용
불필요한 패키지 제거
멀티 스테이지 빌드 활용
2. 보안 강화
루트가 아닌 사용자로 실행
최소 권한 원칙 적용
보안 스캔 도구 활용
3. 캐시 최적화
자주 변경되는 레이어는 나중에 배치
의존성 설치는 별도 레이어로 분리
.dockerignore 적극 활용
참고 문헌
Docker 공식 문서: Dockerfile 모범 사례
Docker 빌드 최적화 가이드
컨테이너 보안 가이드라인
-
Flutter App Android 백화 현상 해결
문제 상황
Flutter 앱에서 백그라운드에서 포그라운드로 전환될 때 화면이 하얗게 변하는 백화 현상이 발생하는 문제가 있었습니다. 이 문제를 해결하기 위해 앱의 라이프사이클 관리 방식을 개선했습니다.
현재 구조
MyApp 클래스가 StatelessWidget을 상속
WebView 기반의 하이브리드 앱 구조
GetX를 사용한 상태 관리
기존 설계 배경
현재 프로젝트는 WebView 방식의 하이브리드 앱으로, Flutter는 네이티브 기능을 제공하는 래퍼(wrapper) 역할을 합니다.
따라서 Widget의 상태 변화가 빈번하지 않아 StatelessWidget을 사용했습니다.
문제 해결 접근
1. 라이프사이클 모니터링 구현
백화 현상의 원인을 파악하기 위해 앱의 라이프사이클을 모니터링할 필요가 있었습니다. 이를 위해 WidgetsBindingObserver를 사용하기로 했습니다.
StatelessWidget의 한계
현재 구조에서 다음과 같은 제약사항이 있었습니다:
WidgetsBindingObserver는 State 객체에서만 사용 가능
StatelessWidget은 lifecycle 메서드 (initState(), dispose() 등) 미지원
Observer를 등록/해제할 방법이 없음
구조 변경 필요성
라이프사이클 모니터링을 위해서는 다음 중 하나의 방식으로 구조를 변경해야 했습니다:
StatefulWidget으로 전환
GetX Controller를 통한 라이프사이클 관리
해결 방법
1. StatefulWidget으로 변경 (권장)
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_appLifecycleState = state;
});
print('앱 상태 변경: $state');
// 상태에 따른 로직 처리
switch (state) {
case AppLifecycleState.resumed:
print('앱이 활성화됨');
break;
case AppLifecycleState.paused:
print('앱이 일시정지됨');
break;
case AppLifecycleState.detached:
print('앱이 종료됨');
break;
case AppLifecycleState.inactive:
print('앱이 비활성화됨');
break;
case AppLifecycleState.hidden:
print('앱이 숨겨짐');
break;
}
}
String? myInitRoute() {
//...
}
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
translations: Languages(),
locale: const Locale('ko', 'KR'),
initialBinding: BindingsBuilder(() {
webViewCtl = Get.put<MyWebViewController>(MyWebViewController());
homeCtl = Get.put<MyHomeController>(MyHomeController());
}),
getPages: [
GetPage(name: '/', page: () => const MyWebView()),
GetPage(name: '/home', page: () => const MyHome()),
],
initialRoute: myInitRoute(),
home: Scaffold(
appBar: AppBar(title: const Text('백그라운드 복귀 테스트')),
body: Center(
child: Text('앱 상태: $_appLifecycleState'),
),
),
);
}
}
2. GetX Controller에서 처리하는 방법
만약 StatelessWidget을 유지하고 싶다면, GetX Controller에서 lifecycle을 관리할 수 있습니다:
class AppLifecycleController extends SuperController with WidgetsBindingObserver {
@override
void onInit() {
super.onInit();
WidgetsBinding.instance.addObserver(this);
}
@override
void onClose() {
WidgetsBinding.instance.removeObserver(this);
super.onClose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// 앱 상태 변경 처리
}
@override
void onDetached() {}
@override
void onInactive() {}
@override
void onPaused() {}
@override
void onResumed() {}
@override
void onHidden() {}
}
StatefulWidget 방식이 더 직관적이고 권장되는 방법입니다.
확실한 재현 방법은 모르겠지만, 로컬 환경에서 앱을 구동시키다 보면 아래와 같은 에러가 발생하는 것이 확인된다.
해당 에러가 발생하면 빈 페이지가 로드되고, 어떠한 동작도 수행할 수 없는 상태가 된다.
[sentry.platformError] [error] Uncaught Platform Error
LateInitializationError: Field '...' has not been initialized.
#0 ...
#1 ...
#2 ...
#3 ...
#4 ...
[sentry.platformError] [error] Uncaught Platform Error
LateInitializationError: Field '...' has not been initialized.
#0 ...
#1 ...
#2 ...
#3 ...
#4 ...
위와 같은 에러를 해결하기 위해서는 각 컨트롤러 간의 의존성 문제로 인해 발생하는 것으로 확인된다.
특정 인스턴스가 로드되어있지 않은 상태에서 외부 인스턴스를 생성하려고 할 때 발생할 것 같다.
각 컨트롤러는 의존성이 필요한 외부 컨트롤러 인스턴스를 late 키워드로 선언된 변수로 관리 및 사용하고 있는 상태이다.
문제의 근본 원인
컨트롤러 관리 방식의 문제
현재 컨트롤러 인스턴스는 전역 변수로 관리되고 있습니다:
late MyWebViewController webViewCtl; // 전역 변수로 관리
class MyWebViewController extends GetxController {
reload() {
homeCtl.reload();
}
goHome() {
homeCtl.goHome();
}
backButtonPress(){
homeCtl.backButtonPress();
}
}
기존 초기화 방식
MyApp 클래스에서 빌드 시점에 컨트롤러 인스턴스를 생성하고 전역 변수에 할당:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
translations: Languages(),
locale: const Locale('ko', 'KR'),
initialBinding: BindingsBuilder(() {
// 전역 변수에 직접 할당
webViewCtl = Get.put<MyWebViewController>(MyWebViewController());
myHomeCtl = Get.put<MyHomeController>(MyHomeController());
}),
getPages: [
GetPage(name: '/', page: () => const MyWebView()),
GetPage(name: '/home', page: () => const MyHome()),
],
initialRoute: myInitRoute(),
);
}
}
개선된 접근 방식
GetX의 의존성 주입 시스템을 활용하여 더 안정적인 방식으로 변경할 수 있습니다:
전역 변수 사용을 피하고 Get.find<T>() 활용
LateInitializationError 방지
컨트롤러 간 의존성 명확하게 관리
최종 구현
1. 개선된 컨트롤러 구조
class MyWebViewController extends GetxController {
// GetX의 의존성 주입을 활용한 컨트롤러 참조
MyHomeController get homeCtl => Get.find<MyHomeController>();
reload() {
homeCtl.reload();
}
goHome() {
homeCtl.goHome();
}
backButtonPress() {
homeCtl.backButtonPress();
}
}
2. 안전한 의존성 주입 설정
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
translations: Languages(),
locale: const Locale('ko', 'KR'),
initialBinding: BindingsBuilder(() {
// permanent 옵션으로 인스턴스 생명주기 관리
Get.put<MyWebViewController>(MyWebViewController(), permanent: true);
Get.put<MyHomeController>(MyHomeController(), permanent: true);
}),
getPages: [
GetPage(name: '/', page: () => const MyWebView()),
GetPage(name: '/home', page: () => const MyHome()),
],
initialRoute: myInitRoute(),
);
}
결과 및 권장사항
개선 효과
안정성
LateInitializationError 발생 방지
의존성 관리 개선
백화 현상 해결
유지보수성
명확한 의존성 구조
코드 가독성 향상
디버깅 용이성 증가
성능
효율적인 메모리 관리
안정적인 상태 관리
라이프사이클 최적화
권장 사항
의존성 관리
Get.find<T>() 활용
전역 변수 사용 최소화
permanent 옵션 적절히 활용
상태 관리
라이프사이클 이벤트 모니터링
컨트롤러 간 의존성 명확화
메모리 누수 방지
테스트 및 모니터링
백그라운드/포그라운드 전환 테스트
메모리 사용량 모니터링
성능 지표 추적
return GetMaterialApp(
debugShowCheckedModeBanner: false,
translations: Languages(),
locale: const Locale('ko', 'KR'),
initialBinding: BindingsBuilder(() {
// permanent: 영구적으로 인스턴스를 메모리에 유지
Get.put<MyWebViewController>(MyWebViewController(), permanent: true);
Get.put<MyHomeController>(MyHomeController(), permanent: true);
}),
getPages: [
GetPage(name: '/', page: () => const MyWebView()),
GetPage(name: '/home', page: () => const MyHome()),
],
initialRoute: myInitRoute(),
);
//...
-
Intro
우리는 PG사 API와 연동하여 결제 서비스를 클라이언트에게 제공해주고 있다.
결제 서비스 내에서도 결제 수단 등록/삭제, 결제 수단 조회와 등록된 결제 수단을 통한 책정 금액 결제 또는 책정 금액의 청구서를 카카오톡으로 발송
우리는 가맹점과 소비자의 중간 매개체 역할이며, 소비자가 이용한 서비스에 대한 책정 금액을 가맹점이 결정하여 우리에게 전달한다.
우리는 책정된 금액과 소비자 정보를 전달받아 결제 방식을 결정한다.
만약 소비자가 우리의 플랫폼 APP의 회원일 경우, PG사에서 제공해주는 자동 결제 시스템을 통해 소비자가 미리 등록한 결제 수단으로 즉시 결제 요청한다.
소비자가 미리 등록한 결제 수단이 없거나 결제 수단에 문제가 있다고 판단될 경우, 소비자의 연락처를 통해 카카오톡 청구서를 발송한다.
소비자가 우리의 플랫폼 APP의 회원이 아닐 경우, 즉시 카카오톡 청구서 방식으로 요청한다.
결제 요청 후, PG사로부터 Callback API를 통해 결제 요청 결과를 수신 받는다.
환불이 필요할 경우, 결제 요청 당시 생성한 청구서 ID와 요청 금액을 전달하여 환불 요청한다.
재결제가 필요할 경우, 환불 요청 후, 새로운 청구서 ID를 생성하여 결제 요청한다.
기존 결제 서비스는 모든 외부 모듈들이 필수로 주입 받고 있는 common 모듈 안에 구현되어있어서 결제 서비스가 필요하지 않더라도 인스턴스를 생성하고 있었다.
우리는 기존 서비스 기능에 대한 고도화 작업 및 신규 기능 추가 작업으로 인해 점차 리소스 비용이 증가하고 있었다.
따라서 결제 서비스 전용 모듈을 분리하여 MSA 아키텍처로 전환하기로 결정하였다.
이를 통해 결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄이며, 향후 기능 확장 및 유지보수를 용이하게 할 수 있을 것으로 기대하고 있다.
또한, MSA 아키텍처로의 전환은 각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련해줄 것이다.
목표
결제 서비스의 독립성을 확보하고, 서비스 간의 의존성을 줄인다.
향후 기능 확장 및 유지보수를 용이하게 한다.
각 서비스의 배포 및 확장을 독립적으로 수행할 수 있는 기반을 마련한다.
추상화 구조로 설계해본다. 어댑터 패턴 (Adapter Pattern) 이란?
기존 복잡한 VIEW TABLE을 사용하지 않는 방향으로, 제거하는 방향으로 설계해보자.
결제 모듈만이 관리하는 테이블과 각 클라이언트가 관리하는 테이블을 분리하고 각자 관리한다.
-
-
Introduction
컴퓨터 네트워크
컴퓨터와 프린터, 스마트폰과 같은 기타 장치들이 서로 통신할 수 있도록 연결한 집합이다.
통신을 위해 컴퓨터와 장치는 정보 전송을 지원하는 물리적 매체로 연결되어야한다.
구리 케이블 및 광섬유와 같은 유선 매체와 무선 주파수 매체를 포함하여 물리적 매체에 대한 다양한 기술이 개발되었다.
컴퓨터는 다양한 네트워크 토폴로지로 매체에 연결될 수 있다.
네트워크를 통신하기 위해 컴퓨터는 사용되는 매체와 상관없이 통신 프로토콜이라는 합의된 규칙을 사용한다.
컴퓨터 네트워크에는 개인용 컴퓨터, 서버, 네트워킹 하드웨어 또는 기타 특수 목적 또는 일반 목적을 호스트가 포함될 수 있다.
이들은 네트워크 주소로 식별되며 호스트명을 가질 수 있다.
호스트명은 노드에 대한 기억하기 쉬운 레이블 역할을 하며 초기 할당 후에는 거의 변경되지 않는다.
네트워크 주소는 인터넷 프로토콜과 같은 통신 프로토콜에 의해 노드를 찾고 식별하는 데 사용된다.
네트워크 패킷
대부분의 최신 컴퓨터 네트워크는 패킷 모드 전송을 기반으로 하는 프로토콜을 사용한다.
네트워크 패킷은 교환 네트워크에서 전송되는 형식화된 데이터 단위이다.
패킷은 제어 정보와 사용자 데이터(Payload)의 두 가지 유형의 데이터로 구성된다.
제어 정보는 네트워크가 사용자 데이터를 전달하는 데 필요한 데이터를 제공한다.
예를 들어, 소스 및 대상 네트워크 주소, 오류 감지 코드 및 시퀀싱 정보 등이 있다.
일반적으로 패킷 헤더 및 트레일러에 있으며, 사용자 데이터(Payload)는 그 사이에 있다.
패킷을 사용하면 네트워크가 회선 교환되는 경우보다 전송 매체의 대역폭을 사용자 간에 더 잘 공유할 수 있다.
OSI 7 Layer
Layer
Protocol Data Unit(PDU)
e.g.
Function
Host Layer
7
Application Layer
Data
HTTP, SMTP, SNMP, FTP, TELNET, NFS, NTP
리소스 공유나 원격 파일 접근을 위한 고급 프로토콜
Host Layer
6
Presentation Layer
Data
ASCII, MIDI, MPEG
네트워킹 서비스와 애플리케이션 간 데이터 변환 (문자 인코딩, 데이터 압축, 암호화/복호화)
Host Layer
5
Session Layer
Data
FIFO, NetBIOS, SSL, TLS
통신 세션 관리, 즉 두 노드 간의 여러 번의 왕복 전송 형태로 정보를 지속적으로 교환하는 것
Host Layer
4
Transport Layer
Segment
TCP, UDP, RTP, SCTP
패킷 분할, ACK 및 멀티플렉싱를 포함하여 네트워크의 지점 간 데이터 세그먼트의 안정적인 전송
Media Layer
3
Network Layer
Packet, Datagram
IP, ICMP, IPsec
Addressing, Routing, Traffic Control을 포함한 다중 노드 네트워크 구조화 및 관리
Media Layer
2
Data Link Layer
Frame
Ethernet
물리적 계층으로 연결된 두 노드 간 데이터 프레임 전송
Media Layer
1
Physical Layer
Bit, Symbol
X.25, RS-232
물리적 매체를 통한 원시 비트 스트림의 전송 및 수신
브리지 및 스위치
브리지와 스위치는 통신에 관련된 포트로만 프레임을 전달한다.
브리지는 두 개의 포트만 있지만, 스위치는 일반적으로 수많은 포트를 가지고 있어 장치에 대한 스타 토폴로지를 용이하게 하고 추가 스위치를 계단식으로 연결할 수 있다.
브리지와 스위치는 OSI 모형의 데이터 링크 계층(2 Layer)에서 작동하며 두 개 이상의 네트워크 세그먼트 간의 트래픽을 브리지하여 단일 로컬 네트워크를 형성한다.
브리지와 스위치는 네트워크의 충돌 도메인을 분할하지만 단일 브로드캐스트 도메인을 유지한다. 브리징 및 스위칭을 통한 네트워크 세분화는 크고 혼잡한 네트워크를 더 작고 효율적인 네트워크의 집합으로 분해하는 데 도움이 된다.
라우터
라우터는 패킷에 포함된 주소 지정 또는 라우팅 정보를 처리하여 네트워크 간에 패킷을 전달하는 인터네트워킹 장치이다. 라우팅 정보는 종종 라우팅 테이블과 함께 처리된다.
라우터는 라우팅 테이블을 사용하여 전달할 위치를 결정한다.
방화벽
방화벽은 네트워크 보안 및 엑세스 규칙을 제어하는 네트워크 장치 또는 소프트웨어이다.
방화벽은 내부 네트워크와 인터넷과 같은 잠재적으로 안전하지 않은 외부 네트워크 간의 연결에 삽입된다.
방화벽은 일반적으로 인식되지 않는 소스에서의 엑세스 요청을 거부하고 인식된 소스에서의 작업을 허용하도록 구성된다.
점점 증가하는 사이버 공격 관련 이슈가 발생하는 현 시대에서 방화벽은 네트워크 보안에서 중요한 역할로 여겨지고 있다.
SSL(Secure Socket Layer)/TLS(Transport Layer Security)
SSL은 인증서가 있는 서버를 필요로 한다.
클라이언트가 SSL 보안 서버에 대한 엑세스를 요청하면 서버는 인증서 사본을 클라이언트에 보낸다.
SSL 클라이언트는 전달받은 인증서를 확인 후 검증되면 서버가 인증된다.
클라이언트는 세션에서 사용할 대칭 키 암호를 협상한다.
세션은 SSL 서버와 SSL 클라이언트 간의 매우 안전한 암호화된 터널에서 통신할 수 있게된다.
References
https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%93%A8%ED%84%B0_%EB%A7%9D
https://ko.wikipedia.org/wiki/OSI_%EB%AA%A8%ED%98%95
-
Kubernetes Helm 차트 분석
Helm 개요
Helm은 Kubernetes의 패키지 매니저로서, charts라는 패키지 포맷을 통해 애플리케이션을 관리합니다.
Helm Charts의 이해
기본 개념
Charts: Kubernetes 리소스를 설명하는 파일들의 집합
Release: Kubernetes 클러스터에 배포된 Chart의 인스턴스
Repository: Charts를 저장하고 공유하는 저장소
Charts의 활용 사례
단순 배포:
단일 서비스(예: Memcached) 배포
독립적인 마이크로서비스 구성
복잡한 애플리케이션:
웹 애플리케이션 전체 스택
데이터베이스 + 캐시 + 서버 조합
마이크로서비스 아키텍처
Chart 구조와 기본 명령어
myapp/
Chart.yaml # 메타데이터 파일
values.yaml # 기본 설정값
templates/ # 템플릿 디렉토리
deployment.yaml # 배포 설정
service.yaml # 서비스 설정
ingress.yaml # 인그레스 설정
charts/ # 종속성 차트들
주요 명령어
# Chart 다운로드
helm pull chartrepo/chartname
# Chart 설치
helm install release-name chartrepo/chartname
# 릴리스 목록 조회
helm list
# Chart 업그레이드
helm upgrade release-name chartrepo/chartname
<https://helm.sh/ko/docs/topics/charts/>
## Helm 템플릿 시스템
### 템플릿 기본 사항
Helm의 템플릿 시스템은 Go 템플릿을 기반으로 하며, 강력한 변수 치환과 조건부 로직을 제공합니다.
#### 템플릿 문법 예시
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
data:
# 단순 값 참조
database: {{ .Values.database.name }}
# 조건문 사용
{{- if .Values.enableFeature }}
feature.enabled: "true"
{{- end }}
# 반복문 사용
ports: |
{{- range .Values.service.ports }}
- {{ . }}
{{- end }}
로컬 템플릿 테스트
템플릿을 실제 클러스터에 적용하기 전에 로컬에서 테스트할 수 있습니다.
# 기본 템플릿 렌더링
helm template .
# 특정 값 파일 사용
helm template . -f values.dev.yaml
# 특정 템플릿만 렌더링 후 적용
helm template . -s templates/config.yaml --values=dev.yaml | kubectl apply -f -
주의사항
클러스터 상태 의존적인 값들은 로컬에서 테스트할 때 가상(faked) 값으로 대체됩니다
서버 측 유효성 검사는 수행되지 않으므로, 실제 적용 전 주의가 필요합니다
Helm Values 시스템
Values 파일 구조화
values.yaml 파일은 Helm 차트의 기본 설정값을 정의합니다. 환경별로 다른 설정이 필요한 경우 values-{env}.yaml 파일을 사용할 수 있습니다.
# values.yaml 예시
global:
environment: production
application:
name: myapp
replicas: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
HPA (Horizontal Pod Autoscaling) 설정
HorizontalPodAutoscaler는 워크로드의 자동 스케일링을 담당하는 Kubernetes 리소스입니다.
# HPA 템플릿 예시
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ .Release.Name }}-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ .Release.Name }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
Ingress 설정
Ingress는 클러스터 외부에서 내부 서비스로의 HTTP/HTTPS 라우팅을 관리합니다.
# Ingress 템플릿 예시
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-ingress
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-service
port:
number: 80
Kubernetes 리소스 관리
ReplicaSet 관리
ReplicaSet은 지정된 수의 파드 복제본이 항상 실행되도록 보장하는 Kubernetes 리소스입니다.
ReplicaSet 주요 구성 요소
# ReplicaSet 템플릿 예시
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: {{ .Release.Name }}-replicaset
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
requests:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
limits:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
ReplicaSet 설정 가이드
셀렉터 설정
파드 식별을 위한 레이블 매칭 규칙 정의
적절한 레이블링 전략 수립 필요
레플리카 수 관리
초기 레플리카 수 설정
HPA와 연동 시 고려사항 검토
파드 템플릿 설계
컨테이너 스펙 정의
리소스 요청/제한 설정
헬스 체크 구성
DaemonSet
데몬셋은 모든 노드 또는 일부 노드가 파드의 사본을 실행하도록 한다.
노드가 클러스터에 추가되면 파드도 추가된다. 노드가 클러스터에서 제거되면 해당 파드는 가비지로 수집된다.
데몬셋을 삭제하면 데몬셋이 생성한 파드들이 정리된다.
데몬셋의 일부 대표적인 용도
모든 노드에서 클러스터 스토리지 데몬 실행
모든 노드에서 로그 수집 데몬 실행
모든 노드에서 노드 모니터링 데몬 실행
각 데몬 유형의 처리를 위해서 모든 노드를 커버하는 하나의 데몬셋이 사용된다. 더 복잡한 구성에서는 단일 유형의 데몬에 여러 데몬셋을 사용할 수 있지만, 각기 다른 하드웨어 유형에 따라 서로 다른 플래그, 메모리, CPU 요구가 달라진다.
컨테이너 리소스 관리
Kubernetes에서 효율적인 리소스 관리는 안정적인 애플리케이션 운영의 핵심입니다.
리소스 요청과 제한
# 리소스 설정 예시
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
리소스 요청 (Requests)
컨테이너가 필요로 하는 최소 리소스 양
스케줄링 결정에 사용
노드의 리소스 예약에 활용
리소스 제한 (Limits)
컨테이너가 사용할 수 있는 최대 리소스 양
초과 시 컨테이너 제한 또는 재시작
OOM Killer 동작 기준
CPU 리소스 단위 이해
단위
설명
예시
코어
1.0 = 1개 CPU 코어
cpu: "1.0"
밀리코어
1000m = 1개 CPU 코어
cpu: "500m"
백분율
CPU 코어의 백분율
cpu: "0.5" (50%)
CPU 리소스 특징
절대값 기준: 노드 스펙과 무관하게 동일한 컴퓨팅 파워
최소 단위: 1m (0.001) CPU
일반적인 설정:
가벼운 서비스: 100m-250m
중간 규모: 500m-1000m
리소스 집약적: 1000m 이상
메모리 리소스 관리
spec:
containers:
- name: app
resources:
requests:
memory: "128Mi"
limits:
memory: "256Mi"
메모리 단위 체계
단위
설명
사용 예
Ki
킬로바이트 (1024)
memory: "64Ki"
Mi
메가바이트 (1024^2)
memory: "128Mi"
Gi
기가바이트 (1024^3)
memory: "2Gi"
참고: 쿠버네티스에서 CPU 리소스를 1m보다 더 정밀한 단위로 표기할 수 없다. 이 때문에, CPU 단위를 1.0 또는 1000m보다 작은 밀리CPU 형태로 표기하는 것이 유용하다. 예를 들어, 0.005 보다는 5m으로 표기하는 것이 좋다.
메모리 리소스 단위
memory 에 대한 제한 및 요청은 바이트 단위로 측정된다. E, P, T, G, M, k 와 같은 수량 접미사 중 하나를 사용하여 메모리를 일반 정수 또는 고정 소수점 숫자로 표현할 수 있다. Ei, Pi, Ti, Gi, Mi, Ki와 같은 2의 거듭제곱을 사용할 수도 있다. 예를 들어, 다음은 대략 동일한 값을 나타낸다.
128974848, 129e6, 129M, 128974848000m, 123Mi
접미사의 대소문자에 유의한다. 400m의 메모리를 요청하면, 이는 0.4 바이트를 요청한 것이다. 이 사람은 아마도 400 메비바이트(mebibytes) (400Mi) 또는 400 메가바이트 (400M) 를 요청하고 싶었을 것이다.
https://kubernetes.io/ko/docs/concepts/configuration/manage-resources-containers/
-
Legacy API 리팩토링
특정 프로세스에서 다수의 LEFT JOIN으로 부가 데이터를 함께 조회하던 쿼리를 제거하고,
그 쿼리에 의존하던 로직들을 단일 책임 원칙에 맞게 분리·수정한다.
Composable 함수
export function TermsAgreement(publisherId: Ref<number>) {
const { $api } = useNuxtApp();
const { data: termsResponse } = useAsyncData('terms-response', () => {
if (!publisherId.value) {
return null;
}
return $api.termsService.getTerms(publisherId.value);
});
const terms = computed(() => {
if (!termsResponse.value) {
return [];
}
return termsResponse.value;
});
function getTermsAgreements(userIds: number[]) {
if (!publisherId.value || userIds.length === 0) {
throw new Error('Invalid publisherId or userIds.');
}
return $api.termsService.getTermsAgreements({ publisherId: publisherId.value, userIds: userIds });
}
function isAgreementRequired(userIds: number[], termsAgreements: { [userId: number]: TermsAgreementResponse }) {
if (!publisherId.value || userIds.length === 0) {
return true;
}
if (Object.keys(termsAgreements).length === 0) {
return true;
}
const everyoneAgreedMustTerms = userIds.every((userId) => termsAgreements[userId]?.isMust);
if (!everyoneAgreedMustTerms) {
console.debug('필수 약관 동의하지 않은 유저가 있음.');
return true;
}
console.debug('모두 필수 약관 동의함.');
const hasOptionalTerms = terms.value.some(
(term) => !term.mainTermsClause?.isMust || term.subTermsClauses?.some((subTerms) => !subTerms.isMust),
);
if (hasOptionalTerms) {
const everyoneCheckedOptionalTerms = userIds.every((userId) => termsAgreements[userId]?.agreeOptionalTerms);
if (!everyoneCheckedOptionalTerms) {
console.debug('선택 약관을 확인하지 않은 유저가 있음.');
return true;
}
console.debug('모두 선택 약관 확인함.');
}
return false;
}
return {
getTermsAgreements,
isAgreementRequired,
terms,
};
}
Page Script
const publisherId = computed(() => publisher.value.id);
const { getTermsAgreements, isAgreementRequired, terms } = TermsAgreement(publisherId);
const requestUserIds = computed(() => {
return requests.map((request) => request.userId).filter((id) => id);
});
// 약관 동의 흐름
// 1) 요청 유저들의 약관 동의 이력을 조회한다.
getTermsAgreements(requestUserIds.value).then((response) => {
// 2) 동의 절차 필요 여부 판단
if (isAgreementRequired(requestUserIds.value, response)) {
// 3) 필요 시 약관 동의 모달 표시
showTermsModal();
return;
}
// 4) 불필요하면 다음 단계로 진행
goComplete();
});
// ...
컴포저블 함수를 통해 Script 코드 라인을 줄였다.
약관 관련 로직에 대한 책임은 TermsAgreement 컴포저블 함수에게만 위임된다.
불필요한 시점에는 약관 관련 API를 요청 및 조회하지 않는다.
유저 상세 정보 페이지에서 /my/api/user/{id} API로부터 함께 내려오던 부가 데이터들에 대해
클라이언트/프론트 측면에서 분석해보고 개선점을 찾으려한다.
@GetMapping(value = "/user/{id}")
public Map<String, Object> findUser(@PathVariable("id") String id) {
UserDetail user = userFacadeService.getUserDetail(id);
if (user == null) {
throw MyException("not found user.");
}
Map<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("friends", user.getFriends());
return map;
}
@GetMapping(value = "/user/{id}")
public UserDetail findUser(@PathVariable("id") String id) {
return userFacadeService.getUserDetail(id);
}
public UserDetail getUserDetail(String id) {
UserVO user =
Optional.ofNullable(userService.findById(id))
.orElseThrow(() -> MyException("data not found"));
userService.setAgreement(user);
return UserDetail.from(user, userService.findFriend(id));
}
public UserDetail getUserDetail(String id) {
return Optional.ofNullable(userService.findById(id))
.map(UserDetail::from)
.orElseThrow(() -> MyException("data not found"));
}
기존에 DAO단에서부터 많은 부가적인 데이터를 많이 조회 및 전달하고 있던 로직들을 단일 책임 원칙을 반영하여 유저의 기본 정보만을 조회 및 전달하도록 리팩토링하였다.
유저의 친구들 조회 API 생성
@GetMapping(value = "/user/{id}/friends")
public List<UserFriend> findFriends(@PathVariable("id") String id) {
return userFacadeService.getAllFriends(id);
}
public List<UserFriend> getAllFriends(String id) {
List<UserFriendVO> friends = service.findAllFriends(id);
return UserFriend.from(friends);
}
유저 결제 수단 조회 API 생성
@GetMapping(value = "/user/{id}/credit-card")
public UserCreditCard findUserCreditCard(
@PathVariable("id") String id) {
return userFacadeService.getUserCreditCard(id);
}
public UserCreditCard getUserCreditCard(String id) {
return UserCreditCard.from(userCreditCardService.getCompleteCreditCard(id));
}
유저와 연관된 부가 정보는 별도 API로 데이터를 제공하는 방식으로 리팩토링하여
각각의 API를 프론트 단에서 조립하여 설계하는 방향으로 가보려고 한다.
기존 일반 함수에서 직접 API를 동기식으로 요청하던 방식에서
tanstack/useQuery를 통해 특정 데이터에 의존하여 적절한 시점에 API를 순차적으로 요청하도록 설계해보았다.
const userId = computed(() => {
if (props.userId) {
return props.userId;
}
return route.params.userId;
});
// userId 값이 유효하다면 유저 기본 정보 조회 API 호출
const { data: userDetail, refetch: refetchUserDetail } = useQuery({
queryKey: ['user-detail', () => userId.value],
queryFn: () => $api.userService.userDetail(userId.value),
enabled: computed(() => !!userId.value),
initialData: {},
});
const { data: userFriends } = useQuery({
queryKey: ['user-friends', () => userDetail.value.id],
queryFn: () => $api.userService.getUserFriends(userDetail.value.id),
enabled: computed(() => !!userDetail.value.id),
initialData: [],
});
const { data: userCreditCard } = useQuery({
queryKey: ['user-credit-card', () => userDetail.value.id],
queryFn: () => $api.userService.getUserCreditCard(userDetail.value.id),
enabled: computed(() => !!userDetail.value.id),
});
-
-
-
-
-
-
네트워크 인프라 구성도 그리기
구성 요소별 핵심 역할
👨💻 사용자 (Client)
웹 브라우저를 통해 서비스에 접속하는 최종 사용자입니다.
🌐 도메인 등록 및 DNS 서버
사용자가 도메인 주소(예: www.your-service.com)를 입력하면, 해당 주소를 클라우드 플랫폼의 서버 IP 주소로 변환하여 연결해주는 역할을 합니다.
☁️ 클라우드 플랫폼
VPC (Virtual Private Cloud): 클라우드 플랫폼 내에 생성된 논리적으로 분리된 가상의 네트워크 공간입니다. 외부로부터 격리되어 있어 보안이 강화됩니다.
로드 밸런서 (Load Balancer): 사용자의 요청(트래픽)을 여러 대의 웹 서버로 자동 분산하여 특정 서버에 과부하가 걸리는 것을 방지합니다. 이를 통해 서비스의 안정성을 높이고, SSL 인증서를 적용하여 HTTPS 통신을 처리합니다.
Public Subnet (외부망): 인터넷과 직접 통신이 가능한 영역입니다.
웹 서버 (Nginx): 프론트엔드 애플리케이션을 사용자에게 제공합니다. 정적 파일을 서빙하고, 사용자의 동적 요청을 내부망에 있는 WAS로 전달하는 리버스 프록시(Reverse Proxy)의 핵심 역할을 수행합니다. Docker 컨테이너로 배포됩니다.
Private Subnet (내부망): 인터넷에서 직접 접근할 수 없는 내부 네트워크 영역으로, 중요한 자원을 배치하여 보안을 강화합니다.
WAS: 백엔드 애플리케이션으로, 실제 비즈니스 로직을 처리하고 데이터베이스와 통신합니다. 웹 서버를 통해서만 요청을 받기 때문에 외부에 직접 노출되지 않아 안전합니다. 마찬가지로 Docker 컨테이너로 배포됩니다.
DB 서버: 서비스의 모든 데이터를 저장하고 관리하는 데이터베이스입니다. 클라우드 플랫폼의 관리형 서비스를 사용하거나, 서버에 Docker 컨테이너로 직접 구축할 수 있습니다. 가장 안쪽의 격리된 네트워크에 위치하며 오직 WAS의 요청에만 응답합니다.
데이터 흐름
DNS 조회: 사용자가 브라우저에 도메인 주소를 입력하면, DNS가 클라우드 플랫폼 로드 밸런서의 IP 주소를 알려줍니다.
요청 전달: 사용자의 요청은 클라우드 플랫폼의 로드 밸런서로 전달됩니다.
트래픽 분산: 로드 밸런서는 받은 요청을 Public Subnet에 있는 여러 웹 서버 중 하나로 전달합니다.
프론트엔드 서빙 및 API 요청: 웹 서버는 프론트 엔드 코드로 만들어진 화면을 사용자에게 보여줍니다. 만약 데이터 처리가 필요하면(예: 로그인, 게시글 조회), 웹 서버는 해당 요청을 Private Subnet에 있는 WAS로 전달합니다.
비즈니스 로직 처리: WAS는 요청을 받아 비즈니스 로직을 수행하고, 필요시 DB 서버에 데이터 조회를 요청합니다.
데이터 응답: DB 서버는 WAS의 요청을 처리한 후 결과를 반환합니다.
API 응답: WAS는 DB에서 받은 데이터를 가공하여 웹 서버로 최종 결과를 전달합니다.
최종 렌더링: 웹 서버는 WAS로부터 받은 데이터를 포함하여 사용자에게 최종 화면을 보여줍니다.
보안/네트워킹 권장 설정
VPC 분리: 퍼블릭 서브넷, 프라이빗 서브넷
SG(Security Group): 최소 포트만 허용
LB→Web Server: 443/80(내부)
Web Server→WAS: 8080(내부)
WAS→DB: 3306(내부)
HTTPS 강제 및 HSTS, 보안 헤더(Content-Security-Policy 등) 적용
DB 프라이빗 접속만 허용, 백업/복제/모니터링
배포 파이프라인(예시)
CI: 테스트 → Docker 이미지 빌드 → 취약점 스캔 → 레지스트리에 push
CD: 서버/쿠버네티스에 배포, 헬스체크 후 트래픽 전환(블루/그린 또는 롤링)
포트/엔드포인트 요약
외부: 443(HTTPS) → LB → Web Server
내부: 8080(WAS), 3306(DB)
관리: SSH는 Bastion Host 또는 SSM 계열 툴로 제한적 허용
운영 모니터링
애플리케이션: Prometheus + Grafana, Spring Actuator
인프라: Cloud Platform Monitoring, 로드밸런서/네트워크 지표, 로그 수집(ELK/EFK)
-
메시지 교환 방식
네트워크로 메시지를 주고받는 방식은 대표적으로 회선 교환 방식과 패킷 교환 방식으로 나눌 수 있다.
각 방식을 사용하는 네트워크를 각각 회선 교환 네트워크, 패킷 교환 네트워크라고 한다.
회선 교환 방식 (Circuit Switching)
회선 교환 방식은 먼저 메시지 전송로인 회선을 설정하고 이를 통해 메시지를 주고 받는 방식이다.
회선 교환 네트워크에서는 호스트들이 메시지를 주고받기 전에 두 호스트를 연결한 후, 연결된 경로로 메세지를 주고 받는다.
회선 교환 방식은 우선 두 호스트 사이에 연결을 확보한 후에 메시지를 주고 받는 특성 덕분에 주어진 시간 동안 전송되는 정보의 양이 비교적 일정하다는 장점이 있다.
회선 교환 네트워크가 올바르게 동작하기 위해서는 호스트 간의 회선을 적절하게 설정해야 한다.
이 역할을 수행하는 회선 교환 네트워크 장비로는 회선 스위치가 있다. 즉, 회선 스위치는 호스트 사이에 일대일 전송로를 확보하는 네트워크 장비이다.
전통적인 전화망이 회선 교환 방식의 대표적인 사례이다.
누군가에게 전화를 걸면 수신자가 전화를 받기 전에 송신자와 수신자 사이에 연결이 설정되어야 하고, 한번 연결이 설정되면 연결된 전송로를 통해서만 통화가 가능해진다.
회선 교환 방식에는 회선의 이용 효율이 낮아질 수 있다는 단점이 존재한다.
가능한 모든 회선에 끊임없이 메시지가 흐르고 있어야만 회선의 이용 효율이 높아진다. 즉, 메시지를 주고 받지 않으면서 회선을 점유하고 있다면 낭비라 여겨진다.
패킷 교환 방식 (Packet Switching)
패킷 교환 방식은 회선 교환 방식의 문제점을 해결한 방식으로, 메시지를 패킷이라는 작은 단위로 쪼개어 전송한다.
패킷은 패킷 교환 네트워크 상에서 송수신되는 메시지의 단위입니다.
패킷 교환 네트워크는 회선 교환 네트워크와는 달리 메시지를 송수신하는 두 호스트가 하나의 전송 경로를 점유하지 않기에 네트워크 이용 효율이 상대적으로 높다.
사전에 설정된 경로만으로 통신하는 회선 교환 방식과는 달리, 패킷 교환 방식은 정해진 경로만으로 메시지를 송수신하지 않는다.
이 과정에서 메시지는 다양한 중간 노드를 거칠 수 있는데, 이때 중간 노드인 패킷 스위치는 패킷이 수신지까지 올바르게 도달할 수 있도록 최적의 경로를 결정하거나 패킷의 송수신지를 식별합니다. 대표적인 패킷 스위치는 네트워크 장비로는 라우터, 스위치가 있다.
패킷을 통해 전송하고자 하는 데이터를 Payload라고 한다.
Payload와 더불어 header라는 정보도 패킷 앞에 포함된다.
때로는 패킷 뒤에 trailer라는 정보가 포함되기도 한다.
헤더와 트레일러는 패킷에 붙는 일종의 부가 정보. 즉, 제어 정보이다.
패킷 교환 네트워크에서 주고 받는 메시지의 단위는 패킷이며, 패킷은 전송하고자 하는 데이터인 페이로드와 부가 정보인 헤더 및 트레일러로 구성된다.
Refenrences
https://hongong.hanbit.co.kr/network-%ED%9A%8C%EC%84%A0-%EA%B5%90%ED%99%98-%EB%B0%A9%EC%8B%9D%EA%B3%BC-%ED%8C%A8%ED%82%B7-%EA%B5%90%ED%99%98-%EB%B0%A9%EC%8B%9D/
-
병원 검색 API 성능 최적화: 분할 정복 접근법
개요
본 문서는 병원 검색 시스템의 성능 최적화 과정을 다룹니다. 특히 API 응답 시간 개선과 클라이언트 렌더링 최적화에 중점을 두었습니다.
1차 개선: 쿼리 최적화 접근
개선 방법: 부가 정보 조회 쿼리 분리 및 데이터 병합
서버 측 영향
✅ 개선된 부분
DB 체류 시간 감소 (복잡한 쿼리 해소)
코드 가독성 및 유지보수성 향상
메모리 효율성 개선 (Map 구조 활용)
⚠️ 한계점
DB 접근 횟수 증가
전체 처리 시간 유지
클라이언트 측 영향
❌ 미해결 문제
API 응답 속도 개선 미미
렌더링 성능 목표 미달성
사용자 체감 성능 개선 부족
성능 측정 결과
측정 항목
기존
1차 개선 후
개선율
DB 쿼리 실행 시간
2.5s
0.8s
68% ↓
전체 API 응답 시간
3.2s
2.9s
9% ↓
메모리 사용량
450MB
280MB
38% ↓
2차 개선: API 분할 전략
1차 개선의 한계를 극복하기 위해 API 분할 전략을 도입했습니다. 이 접근법은 데이터의 사용 시점과 중요도에 따라 API를 분리하는 방식입니다.
페이지 구조 분석
핵심 데이터 식별
즉시 필요한 데이터
병원명
원격진료 활성화 여부
예약 신청 가능 여부
바로 접수 신청 가능 여부
진료 과목 리스트
최적화 전략
데이터 분류
즉시 필요한 핵심 데이터
지연 로딩 가능한 부가 데이터
성능 개선 효과
초기 응답 시간: 65% 감소
메모리 사용량: 45% 감소
첫 렌더링 시간: 58% 개선
API 분할 구현
1. 진료실 정보 처리
@GetMapping("/api/hospitals")
public Map<Long, HospitalDto> getAllHospitals() {
// 병원 기본 정보 조회
List<Hospital> hospitals = hospitalRepository.findAll();
// 진료실 정보 맵핑
Map<Long, List<Room>> roomMap = hospitals.stream()
.collect(Collectors.groupingBy(
Hospital::getId,
Collectors.mapping(
hospital -> roomRepository.findByHospitalId(hospital.getId()),
Collectors.toList()
)
));
return hospitals.stream()
.collect(Collectors.toMap(
Hospital::getId,
hospital -> new HospitalDto(hospital, roomMap.get(hospital.getId()))
));
}
2. 즐겨찾기 정보 분리
// 즐겨찾기 API 인터페이스
interface FavoriteResponse {
hospitalId: number;
isFavorite: boolean;
}
// 별도 API 엔드포인트
@GetMapping("/api/hospitals/favorites")
public Map<Long, Boolean> getFavorites(@RequestParam Long userSeq) {
return favoriteRepository.findByUserSeq(userSeq)
.stream()
.collect(Collectors.toMap(
Favorite::getHospitalId,
favorite -> true
));
}
3. 운영 시간 정보 최적화
// 운영 시간 조회 인터페이스
interface OperatingHours {
weekday: string[];
weekend: string[];
holiday: string[];
}
// 지연 로딩을 위한 별도 API
@GetMapping("/api/hospitals/{hospitalId}/hours")
public Map<String, List<String>> getOperatingHours(@PathVariable Long hospitalId) {
Hospital hospital = hospitalRepository.findById(hospitalId)
.orElseThrow(() -> new NotFoundException("Hospital not found"));
return hospital.getOperatingHours()
.stream()
.collect(groupingBy(
OperatingHour::getType,
mapping(OperatingHour::getTimeSlot, toList())
));
}
클라이언트 최적화
1. 컴포저블 함수 리팩토링
// 검색 로직을 컴포저블 함수로 추상화
export function useHospitalSearch() {
const hospitals = ref<Hospital[]>([])
const filteredHospitals = computed(() => {
return filterHospitals(hospitals.value, searchOptions.value)
})
// 반응형 검색 결과 처리
watch(filteredHospitals, async (newHospitals) => {
if (newHospitals.length) {
await fetchOperatingHours(newHospitals.map(h => h.id))
}
})
return {
hospitals,
filteredHospitals
}
}
2. 검색 필터링 로직
interface FilterOptions {
keyword: string;
speciality?: string;
sortBy?: 'default' | 'distance';
}
function filterHospitals(hospitals: Hospital[], options: FilterOptions): Hospital[] {
let filtered = hospitals;
// 1. 키워드 기반 필터링
if (options.keyword) {
filtered = filtered.filter(hospital => {
// 진료 유형 검색
if (isTreatmentType(options.keyword)) {
return hospital.acceptsTreatmentType(options.keyword);
}
// 진료 과목 검색
if (isSpeciality(options.keyword)) {
return hospital.hasSpeciality(options.keyword);
}
// 병원명 검색
return hospital.name.includes(options.keyword);
});
}
// 2. 진료 과목 추가 필터링
if (options.speciality) {
filtered = filtered.filter(h => h.hasSpeciality(options.speciality));
}
// 3. 정렬 적용
return sortHospitals(filtered, options.sortBy);
}
3. 정렬 로직
function sortHospitals(hospitals: Hospital[], sortBy?: 'default' | 'distance'): Hospital[] {
switch (sortBy) {
case 'distance':
return sortByDistance(hospitals);
default:
return sortByOperatingStatus(hospitals);
}
}
// 현재 운영 중인 병원 우선 정렬
function sortByOperatingStatus(hospitals: Hospital[]): Hospital[] {
return hospitals.sort((a, b) => {
if (a.isCurrentlyOperating && !b.isCurrentlyOperating) return -1;
if (!a.isCurrentlyOperating && b.isCurrentlyOperating) return 1;
return 0;
});
}
4. 즐겨찾기 관리
export function useFavorites() {
const favorites = ref<Set<number>>(new Set());
const toggleFavorite = async (hospitalId: number) => {
try {
await api.post(`/hospitals/${hospitalId}/favorite`);
await refreshFavorites();
} catch (error) {
console.error('Failed to toggle favorite:', error);
}
};
// 즐겨찾기 목록 갱신
const refreshFavorites = async () => {
const response = await api.get('/hospitals/favorites');
favorites.value = new Set(Object.keys(response.data).map(Number));
};
return {
favorites,
toggleFavorite,
refreshFavorites
};
}
성능 개선 결과
측정 지표
개선 전
개선 후
향상율
초기 로딩 시간
3.2s
0.8s
75%
검색 응답 시간
1.5s
0.3s
80%
메모리 사용량
450MB
180MB
60%
렌더링 시간
2.1s
0.5s
76%
-
-
클라우드 컴퓨팅
서비스 모델
서비스 지향 아키텍쳐가 “모든 것을 서비스로”(everything as a service, EaaS, XaaS, aas)를 지지하지만 클라우드 컴퓨팅 제공자들은 각기 다른 모델에 따라 자신들의 서비스들을 제공하며,
이 모델의 세 가지 NIST 표준 모델은 서비스형 인프라스트럭처(IaaS), 서비스형 플랫폼(PaaS), 서비스형 소프트웨어(SaaS)가 있다. 이 모델들은 추상화를 증가시킨다.
IaaS (Infrastructure as a Service)
국제 인터넷 표준화 기구(IETF)에 따르면 가장 기본적인 클라우드 서비스 모델은 컴퓨팅 인프라스트럭처, 즉 가상 머신과 기타 자원들을 구독자에 대한 서비스로 제공하는 제공자들의 모델이다.
IaaS는 물리적 컴퓨팅 자원, 위치, 데이터 파티셔닝, 확장, 보안, 백업과 같은 인프라스트럭처의 세세한 부분으로부터 사용자를 이끌어내는 온라인 서비스들을 가리킨다.
젠, 버추얼박스, 오라클 VM, KVM, VM웨어 ESX/ESXi, 하이퍼-V
와 같은 하이퍼바이저는 게스트로 가상 머신을 실행한다.
클라우드 운영 체제 내의 하이퍼바이저 풀들은 많은 수의 가상 머신들 및 고객의 다양한 요구에 맞는 서비스의 확장/축소 기능을 지원할 수 있다.
리눅스 컨테이너들은 물리 하드웨어 상에서 직접 구동되는 단일 리눅스 커널의 격리된 파티션에서 실행된다. 리눅스 cgroups와 이름공간들은 컨테이너의 격리, 보안, 관리에 사용되는 기반 리눅스 커널 기술들이다.
컨테이너화는 가상화 보다 더 높은 성능을 제공하는데, 그 이유는 하이퍼바이저 부하가 없기 때문이다. 또, 컨테이너 용량은 컴퓨터 부하와 함께 동적으로 규모를 조절하므로 과도한 공급 문제를 해결하고 사용률 기반의 과금을 가능케 한다.
IaaS 클라우드는 가상 머신 디스크 이미지 라이브러리, 로우(raw) 블록 스토리지, 파일 또는 오브젝트 스토리지, 방화벽, 로드 밸런서, IP 주소, 가상 근거리 통신망, 소프트웨어 번들 과 같은 추가적인 자원들을 종종 제공한다.
IaaS 클라우드 제공자들은 요청이 오면 이러한 자원을 데이터 센터에 설치된 수많은 장비를 통해 제공해준다.
광역 연결의 경우 고객들은 인터넷이나 캐리어 클라우드(전용 가상 근거리 통신망) 중 하나를 사용할 수 있다.
응용 프로그램을 배치시키기 위해 클라우드 사용자들은 운영 체제 이미지와 자신들의 응용 소프트웨어를 클라우드 인프라스트럭처에 설치한다.
이 모델에서 클라우드 사용자는 운영 체제와 응용 소프트웨어의 패치 및 유지보수를 수행한다.
클라우드 제공자들은 일반적으로 IaaS 서비스를 유틸리티 컴퓨팅 기반으로 과금한다: 비용은 할당 및 소비된 자원의 양을 반영한다.
AWS에서 제공하는 EC2 가 대표적인 예이다.
이는 단순히 서버 등의 자원을 제공해 주면서 사용자가 디바이스에 제약없이 데이터에 접근할 수 있도록 해준다.
PaaS(Platform as a Service)
PaaS 벤더들은 응용 프로그램 개발자들에게 개발 환경을 제공한다. 제공자는 일반적으로 개발을 위한 툴킷과 표준, 그리고 배포 및 지불을 위한 채널을 개발한다.
PaaS 모델에서 클라우드 제공자들은 일반적으로 운영 체제, 프로그래밍 언어 실행 환경, 데이터베이스, 웹 서버를 포함한 컴퓨팅 플랫폼을 배급한다.
응용 프로그램 개발자들은 기반 하드웨어 및 소프트웨어 계층을 구매하고 관리하는 비용이나 복잡성 없이도 자신들의 소프트웨어 솔루션을 클라우드 플랫폼에서 개발, 실행할 수 있다.
마이크로소프트 애저, 구글 앱 엔진과 같은 일부 PaaS에서 기반이 되는 컴퓨터, 스토리지 자원은 응용 프로그램 수요에 맞추기 위해 자동으로 규모를 조정하며 이로써 클라우드 사용자는 자원을 수동으로 할당하지 않아도 된다.
사용자(개발자)가 소프트웨어 개발을 할 수 있는 환경을 제공해 주는 PaaS의 예로, 구글의 APP 엔진, Heroku 등이 대표적인 예다.
SaaS (Software as a Service)
SaaS 모델에서 사용자들은 응용 소프트웨어와 데이터베이스에 대한 접근 권한을 가진다. 클라우드 제공자들은 응용 프로그램을 실행하는 인프라스트럭처와 플랫폼을 관리한다.
SaaS는 “주문형 소프트웨어”(on-demand software)를 가리키기도 하며 가격 정책은 일반적으로 종량제(pay-per-use) 기반이거나 구독 비용 기반이다.
SaaS 모델에서 클라우드 제공자들은 클라우드에 응용 소프트웨어를 설치, 운영하며 클라우드 사용자들은 클라우드 클라이언트의 소프트웨어를 접근한다. 클라우드 사용자들은 이러한 응용 프로그램들이 실행하는 클라우드 인프라스트럭처와 플랫폼을 관리하지 않는다.
이로 말미암아 클라우드 사용자 자신의 컴퓨터에 응용 프로그램을 설치하고 실행할 필요가 없으므로 유지보수와 지원이 단순하게 된다.
클라우드 응용 프로그램들은 확장성 관점에서 다른 응용 프로그램들과 차이가 있는데, 확장성은 변화하는 작업 수요를 충족하기 위해 런타임 중에 태스크들을 여러 개의 가상 머신으로 복제함으로써 수행할 수 있다.
로드 밸런서들은 가상 머신들의 집합을 통해 작업을 분산시킨다. 이 과정은 오직 하나의 단일 액세스 포인트를 바라보는 클라우드 사용자에게는 투명하게 보인다.
수많은 클라우드 사용자들을 수용하기 위해 클라우드 애플리케이션들은 멀티테넌트일 수 있으며,
즉 어떠한 머신이라도 하나 이상의 클라우드 사용자 조직에 서비스를 제공할 수 있다는 것을 뜻한다.
SaaS 애플리케이션의 가격 정책 모델은 사용자 당 월별, 연별 고정 수수료인 것이 보통이므로 특정 지점에서 사용자가 추가되거나 제거되면 가격은 조정될 수 있다. 지지자들은 SaaS가 하드웨어 및 소프트웨어 유지보수 및 지원을 클라우드 제공자에게 위탁함으로써 비즈니스에 드는 IT 운영 비용을 줄일 가능성을 제공한다고 주장한다. 즉, 다른 목표를 충족하기 위해 비즈니스가 하드웨어/소프트웨어 및 인력에 드는 IT 운용 비용을 재할당할 수 있게 한다. 게다가 중앙 관리되는 애플리케이션들을 통해 사용자가 새로운 소프트웨어를 설치하지 않고도 업데이트를 출시할 수 있다. SaaS의 단점은 사용자 데이터를 클라우드 제공자의 서버에 저장하는 점에서 비롯된다. 그 결과 데이터에 대해 인증되지 않은 접근이 있을 가능성이 있다. 이러한 이유로 사용자들은 인텔리전트 서드파티 키 관리 시스템을 채택하여 자신들의 데이터의 보안을 지원한다.
SaaS의 경우, 네이버에서 제공하는 네이버 클라우드, 드롭박스, 구글 문서 등을 예로 들 수 있다.
서버리스 컴퓨팅
클라우드 제공자가 서비스 요청의 필요에 따라 가상 머신을 시작하고 중단하는 일을 온전히 관리하며, 시간 당, 가상 머신 당 개념이 아닌 요청 충족에 필요한 자원의 추상적인 측정에 의해 과금된다.
서버리스 컴퓨팅이라는 이름에도 불구하고 실제로 서버 없는 코드의 실행은 수반되지 않는다.
서버리스 컴퓨팅이라는 이름이 붙여진 이유는 시스템을 소유하는 사업체나 사람이 백엔드 코드를 실행하기 위해 서버나 가상 머신을 구매, 임대, 예비할 필요가 없기 때문이다.
배치 모델
폐쇄형 클라우드
폐쇄형 클라우드(private cloud, 프라이빗 클라우드)는 오직 하나의 단체를 위해서만 운영되는 클라우드 인프라스트럭처의 하나로, 내부적으로나 서드파티에 의해 관리를 받거나 내외부적으로 호스팅된다.
폐쇄형 클라우드 프로젝트를 떠맡을 경우 어느 정도 수준의 비즈니스 환경 가상화가 수반될 필요가 있으며 단체가 기존 자원에 대한 결정사항들을 재평가할 필요가 있다.
잘 마쳤을 때 비즈니스를 개선할 수 있지만 프로젝트의 모든 단계가 심각한 취약점 예방을 해야 하는 보안 문제를 일으킨다. 자체 운영 데이터 센터들은 일반적으로 자본 집약적이다. 공간, 하드웨어, 환경 제어의 할당이 필요한 중대한 물리적 발자취가 있다. 이러한 자산들은 주기적으로 갱신되어야 하며, 그 결과 추가적인 자본 비용이 소요된다. 사용자들이 여전히 이것들을 구매, 빌드, 관리해야 하므로 직접 챙겨야 하는 관리가 덜하다는 데에서부터 이점을 얻는 것이 아니기 때문에 비평을 받았다.
공개형 클라우드
공개형 클라우드(public cloud, 퍼블릭 클라우드)는 공개적 이용을 위해 열린 네트워크를 통해 렌더링되는 클라우드이다. 공개형 클라우드 서비스들은 무료일 수 있다.
기술적으로 공개형과 폐쇄형 클라우드 아키텍처 간 차이가 거의 없거나 아예 없을 수 있으나, 통신이 신뢰되지 않은 네트워크 상에서 영향을 받으면서 대중을 위해 서비스 제공자에 의해 이용 가능하게 된 서비스들마다 보안적으로 고려할 사항이 본질적으로 다를 수 있다.
일반적으로, 네이버클라우드플랫폼, 아마존 웹 서비스(AWS), 마이크로소프트, 구글과 같은 공개형 클라우드 서비스 제공자들은 자신들의 데이터 센터를 소유, 운영하고 있으며 접근은 일반적으로 인터넷을 통한다.
AWS와 마이크로소프트는 각각 “AWS Direct Connect”와 “Azure ExpressRoute”라는 직접 연결 서비스들을 제공하며,
이러한 연결은 클라우드 제공자가 제공하는 피어링 포인트에 대한 폐쇄형 연결의 구매나 임대를 고객에게 요구한다.
혼합형 클라우드
혼합형 클라우드(hybrid cloud, 하이브리드 클라우드)는 뚜렷한 실체는 유지하지만 함께 묶여있는 둘 이상의 클라우드(폐쇄형, 커뮤니티, 또는 공개형)의 조합으로, 여러 개의 배치 모델들의 이점을 제공한다.
혼합형 클라우드는 병설(collocation), 매니지드 및 데디케이티드 서비스를 클라우드 자원과 연결하는 기능을 의미할 수도 있다.
가트너는 혼합형 클라우드를 다른 서비스 제공자들로부터 제공되는 폐쇄형, 공개형, 커뮤니티 클라우드 서비스들이 어느 정도 복합된 하나의 클라우드 컴퓨팅 서비스로 정의한다.
혼합형 클라우드 서비스는 격리 부분과 제공자 간 경계를 교차하므로 단순히 폐쇄형, 공개형, 커뮤니티 클라우드 서비스 중 하나의 분류에 집어넣을 수 없다. 다른 클라우드 서비스와의 애그리게이션, 연동, 커스터마이즈를 통해 클라우드 서비스의 용적의 확장을 허용한다.
Reference
https://ko.wikipedia.org/wiki/%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C_%EC%BB%B4%ED%93%A8%ED%8C%85
Touch background to close