[AWS] Cura에 Lambda를 도입한다면?
Cura는 이주민 환자가 자신의 의료 정보를 모국어로 확인할 수 있게 돕는 서비스다. 백엔드는 Spring Boot 3 + JPA를 Docker로 묶어 Railway에 올렸고, DB와 인증은 Supabase(PostgreSQL + JWT), 번역과 요약은 Gemini API를 쓴다. 프론트는 Next.js를 Vercel에 배포했다.
이 구조는 서버 한 대가 모든 일을 한다. HTTP 요청도 받고, Gemini를 호출해 기다리고, 그 결과를 DB에 쓴다. 지금 규모에서는 문제가 없지만, 번역 요청이 몰리거나 PDF 처리처럼 오래 걸리는 작업이 늘어나면 요청을 받는 스레드와 오래 걸리는 작업이 같은 서버 안에서 자원을 두고 다투게 된다. 이 글은 그렇게 오래 걸리고 가끔 몰리는 일을 AWS Lambda로 떼어낸다면 어떤 그림이 되는지, 코드보다 개념 위주로 정리한 것이다. 실제 적용은 다음 글에서 다룬다.
Lambda가 무엇인지 한 줄로
Lambda는 서버를 띄워두지 않고 함수 하나를 등록해 두면, 이벤트가 올 때만 AWS가 실행 환경을 만들어 그 함수를 돌려주는 서비스다. 요청이 없으면 비용이 0이고, 요청이 동시에 1,000개 오면 실행 환경도 그만큼 늘어난다. 서버의 수명과 용량을 내가 관리하지 않는다는 점이 EC2나 Railway 컨테이너와의 가장 큰 차이다.
실행 모델: 함수는 어떻게 살아 있는가
Lambda를 이해하려면 실행 환경이라는 단위를 먼저 알아야 한다. 함수가 호출되면 AWS는 격리된 작은 VM(Firecracker 마이크로VM)을 하나 만들고 그 안에서 런타임과 내 코드를 띄운다. 이 환경의 수명은 세 단계로 나뉜다.
| 단계 | 하는 일 | 비고 |
|---|---|---|
| Init | 런타임 기동, 핸들러 밖의 코드 실행(의존성 로드, DB 커넥션 생성 등) | 처음 한 번만 |
| Invoke | 핸들러 함수 실행 | 이벤트마다 반복 |
| Shutdown | 환경 폐기 | 일정 시간 유휴 상태면 발생 |
핵심은 두 가지다. 첫째, Init은 환경당 한 번만 일어나고, 그 뒤로 같은 환경은 여러 Invoke에 재사용된다. 그래서 DB 커넥션이나 HTTP 클라이언트처럼 만들기 비싼 객체는 핸들러 바깥에 두는 것이 정석이다. 둘째, 환경은 동시 요청 하나당 하나다. 요청이 10개 동시에 오면 환경도 10개가 뜬다. 스레드 풀로 동시성을 처리하는 Spring과 달리, Lambda의 동시성 단위는 프로세스다.
콜드 스타트
재사용할 환경이 없을 때 Init부터 다시 시작하는 것을 콜드 스타트라 한다. Node.js나 Python은 수백 ms 수준이지만 JVM은 스프링 컨텍스트까지 올리면 수 초가 걸릴 수 있다. Cura가 Java 21 + Spring Boot라는 점이 여기서 걸린다.
대응책은 세 가지다.
- SnapStart: Init이 끝난 시점의 메모리 스냅샷을 찍어두고, 콜드 스타트 때 그 스냅샷을 복원한다. Java 런타임에서 지원하며, 스프링 컨텍스트 기동 시간을 크게 줄여준다. 다만 스냅샷 시점에 만든 DB 커넥션이나 난수 시드 같은 상태가 복원 뒤에도 그대로라는 점을 처리해야 한다.
- Provisioned Concurrency: 미리 N개 환경을 데워둔다. 콜드 스타트는 사라지지만 데워두는 시간만큼 과금된다.
- 함수를 가볍게: 스프링 전체를 Lambda에 넣지 않고, 해당 작업만 하는 작은 함수를 Python이나 Node.js로 쓴다.
Cura에 Lambda를 넣는다면 3번이 현실적이다. 스프링 서버는 그대로 두고, 떼어낼 작업만 가벼운 런타임으로 만든다.
한도: 설계 전에 알아야 할 숫자
Lambda는 무한히 확장되는 서버가 아니라 제약이 분명한 실행 단위다. 설계에 영향을 주는 한도만 추렸다.
| 항목 | 한도 |
|---|---|
| 최대 실행 시간 | 15분 |
| 메모리 | 128MB ~ 10GB (CPU는 메모리에 비례해 배정) |
| 임시 저장소 /tmp | 최대 10GB |
| 배포 패키지 | zip 50MB(압축) / 250MB(해제), 컨테이너 이미지 10GB |
| 동시 실행 | 계정·리전당 기본 1,000 (상향 요청 가능) |
| 동기 호출 응답 크기 | 6MB |
15분 한도 때문에 긴 배치 작업은 Lambda에 맞지 않고, CPU가 메모리에 묶여 있으므로 CPU가 더 필요하면 메모리를 올려야 한다. 숫자는 바뀌므로 적용 시점에 공식 문서를 다시 확인한다.
이벤트 소스: 누가 함수를 깨우는가
Lambda 설계의 절반은 어떤 이벤트가 함수를 호출하게 할지 정하는 일이다. 호출 방식은 셋으로 나뉜다.
| 방식 | 대표 소스 | 동작 | 실패 시 |
|---|---|---|---|
| 동기 | API Gateway, Function URL, ALB | 호출자가 응답을 기다림 | 호출자가 재시도 책임 |
| 비동기 | S3, SNS, EventBridge | 이벤트를 큐에 넣고 즉시 202 반환 | Lambda가 기본 2회 재시도 후 DLQ/실패 대상으로 |
| 폴링 | SQS, Kinesis, DynamoDB Streams | Lambda가 소스를 읽어 배치로 호출 | 배치 실패 시 메시지가 큐로 되돌아감 |
비동기와 폴링 방식은 “최소 한 번 전달”이라 같은 이벤트가 두 번 올 수 있다. 그래서 함수는 멱등해야 한다. 같은 환자 기록을 두 번 번역해도 결과가 한 번 쓰이도록 요청 ID나 레코드 버전으로 중복을 걸러야 한다.
Cura에서 떼어낼 수 있는 일
Spring 서버에서 Lambda로 옮기기 좋은 작업은 요청과 응답이 같은 순간에 끝나지 않아도 되는 일이다. Cura에서는 세 가지가 보인다.
1. Gemini 번역·요약 파이프라인
지금은 요청을 받고 Gemini를 호출해 응답을 돌려주기까지 한 HTTP 요청 안에서 끝난다. Gemini 응답이 수 초 걸리면 그동안 스프링의 톰캣 스레드 하나가 묶인다. 사용자가 동시에 50명이면 스레드 50개가 Gemini를 기다리는 데 쓰인다.
Lambda로 옮기면 흐름이 이렇게 바뀐다.
클라이언트 → Spring: 번역 요청 (DB에 job 생성, status=PENDING) → 즉시 202 반환
Spring → SQS: job id 발행
SQS → Lambda: 메시지 배치 수신 → Gemini 호출 → 결과 DB 저장 (status=DONE)
클라이언트: 폴링 또는 Supabase Realtime으로 완료 확인
스프링은 요청을 받자마자 돌려주고, 기다리는 일은 Lambda가 한다. 번역이 몰리면 Lambda 환경이 늘어나고, 한가하면 0이 된다. Gemini API의 분당 호출 제한은 SQS 이벤트 소스의 배치 크기와 Lambda의 예약 동시성(reserved concurrency)으로 조절한다. 예를 들어 예약 동시성을 5로 두면 Gemini를 동시에 5개 이상 호출하지 않는다.
2. 처방전·검사지 파일 처리
사진이나 PDF를 올리면 OCR을 하고 텍스트를 뽑아 번역 파이프라인에 넣는 작업이다. 파일을 S3에 올리는 순간 S3 이벤트가 Lambda를 깨우는 구조가 자연스럽다.
클라이언트 → S3 (presigned URL로 직접 업로드)
S3 ObjectCreated 이벤트 → Lambda: OCR → 추출 텍스트를 SQS에 발행 → 1번 파이프라인 합류
파일이 스프링 서버를 거치지 않으므로 Railway 컨테이너의 메모리와 대역폭을 파일이 먹지 않는다. 이미지 처리 라이브러리는 용량이 커서 zip 한도를 넘기기 쉬운데, 이때는 컨테이너 이미지 배포를 쓴다.
3. 예약 작업
복약 알림 발송, 만료된 임시 파일 정리, 일별 통계 집계 같은 일은 EventBridge Scheduler가 cron 식으로 Lambda를 호출하게 두면 된다. 스프링의 @Scheduled는 컨테이너가 재시작되거나 두 개로 늘어나면 중복 실행 문제가 생기는데, EventBridge는 그 걱정이 없다.
옮기지 않을 것
로그인, 환자 정보 조회·수정 같은 일반 CRUD API는 그대로 스프링에 둔다. 응답이 빨라야 하고, JPA의 영속성 컨텍스트와 트랜잭션 경계가 요청 단위로 잘 맞으며, 콜드 스타트가 사용자 경험에 바로 드러나는 영역이기 때문이다. Lambda는 스프링을 대체하는 것이 아니라, 스프링이 하기엔 비효율적인 일을 덜어내는 보조 실행 환경으로 본다. 기존 시스템을 유지한 채 가장자리부터 조금씩 떼어내는 이 방식을 스트랭글러 패턴이라 부른다.
DB 연결: Lambda가 PostgreSQL을 괴롭히는 방식
Lambda에서 가장 자주 나는 사고는 DB 커넥션 고갈이다. 환경 하나가 커넥션 하나를 들고 있고 환경이 100개로 늘어나면 커넥션도 100개가 된다. PostgreSQL은 커넥션당 프로세스를 하나 띄우므로 수백 개 커넥션에 금방 한계가 온다.
Cura는 이미 Supabase의 PgBouncer를 트랜잭션 모드로 쓰고 있어 유리하다. PgBouncer가 앞단에서 커넥션을 받아 실제 DB 커넥션은 소수만 유지한다. Lambda에서는 Supabase의 풀러 포트(6543)로 붙고, 핸들러 안에서 커넥션을 열고 닫는 것이 아니라 Init 단계에서 하나 만들어 재사용한다. 트랜잭션 모드에서는 prepared statement와 세션 변수를 쓸 수 없다는 제약을 Lambda 코드에서도 똑같이 지켜야 한다.
AWS 안에서 RDS를 쓴다면 같은 역할을 RDS Proxy가 한다. 어느 쪽이든 함수 수와 DB 커넥션 수를 분리하는 층을 하나 두는 것이다.
인증: Supabase JWT를 Lambda에서
위 세 작업은 모두 스프링이나 S3가 깨우므로 Lambda가 사용자 토큰을 직접 검증할 일이 없다. 하지만 나중에 Lambda가 API Gateway 뒤에서 직접 요청을 받게 된다면, API Gateway의 Lambda Authorizer나 JWT Authorizer에 Supabase의 JWKS 주소를 등록해 토큰 검증을 게이트웨이에 맡길 수 있다. 스프링에서 Spring Security가 하던 일을 게이트웨이가 대신하는 셈이다. 함수 안에서 매번 검증하는 것보다 검증 결과가 캐시되어 빠르다.
비용: Railway와 비교
Lambda 요금은 호출 횟수와 실행 시간(GB-초)의 합이다. 메모리를 두 배로 올리면 GB-초 단가는 두 배가 되지만 CPU도 두 배라 실행 시간이 줄어, 총액은 오히려 줄기도 한다. 그래서 메모리를 필요한 만큼이 아니라 비용이 최소가 되는 지점으로 맞추는 튜닝이 따로 있다. 2025년부터 Init 단계 시간도 과금 대상에 들어갔으므로, 콜드 스타트가 잦은 무거운 런타임은 생각보다 비용이 나올 수 있다.
Railway는 과금 방식이 다르다. 컨테이너가 쓰는 vCPU와 RAM을 초 단위로 측정해 청구하고, Hobby 플랜은 월 $5에 $5 사용량이 포함된다. 사용량 과금이라는 점은 Lambda와 같아 보이지만, JVM은 요청이 없어도 힙을 놓지 않기 때문에 스프링 컨테이너의 RAM 비용은 사실상 고정비다. 두 서비스의 요금을 한 표에 놓으면 이렇다. 2026년 10월 기준이고 리전에 따라 조금 다르다.
| 항목 | Railway | Lambda |
|---|---|---|
| 과금 단위 | 컨테이너가 점유한 vCPU·RAM × 시간 | 호출 수 + 실행 시간(GB-초, Init 포함) |
| 유휴 비용 | 있음(JVM 상주 메모리) | 0 |
| 단가 | vCPU $20/월, RAM $10/GB·월, egress $0.05/GB | 호출 100만 건당 $0.20, GB-초당 $0.0000166667(x86), arm64는 약 20% 저렴 |
| 무료 구간 | Hobby 월 $5에 $5 사용량 포함 | 매달 호출 100만 건, 40만 GB-초 |
| 주변 비용 | 거의 없음 | SQS 요청 100만 건당 $0.40(100만 건 무료), CloudWatch Logs 수집 $0.50/GB, Secrets Manager 시크릿당 $0.40/월 |
Cura 번역 파이프라인으로 계산해 보면
번역 함수 하나를 256MB 메모리, 평균 5초(대부분 Gemini 응답 대기)로 잡는다. 대기 시간은 CPU를 쓰지 않지만 Lambda는 벽시계 시간으로 과금하므로 이 5초가 그대로 비용이다. 그래서 대기가 주된 함수는 메모리를 최소로 두는 것이 맞다.
| 월 번역 건수 | GB-초 | Lambda 요금 | SQS·로그 등 | 합계 |
|---|---|---|---|---|
| 1만 건 | 12,500 | $0(무료 구간) | $0 | $0 |
| 10만 건 | 125,000 | $0(무료 구간) | $0 안팎 | $0 안팎 |
| 100만 건 | 1,250,000 | 약 $14 | 약 $1~2 | 약 $15 |
같은 일을 Railway에서 하려면 번역을 전담하는 워커 서비스를 하나 더 띄우는 그림이 된다. 0.25 vCPU에 512MB면 월 $5 + $5 = $10이고, 이 금액은 번역이 0건이어도 나간다. 번역이 몰려 워커를 키우면 $20, $40으로 계단식으로 오른다. 반대로 Lambda는 1만 건이든 10만 건이든 0원이고, 100만 건에서야 $15 정도가 된다. 지금 Cura 규모에서는 둘 다 월 1만 원 안쪽이라 금액 차이는 의미가 없다. 차이는 비용 곡선의 모양이다. Railway는 미리 확보한 용량에 돈을 내고, Lambda는 실제로 일한 시간에 돈을 낸다. 양이 들쭉날쭉한 작업일수록 후자가 유리하다.
Lambda에서 돈이 새는 곳
Lambda 자체보다 주변에서 비용이 나오는 경우가 더 많다. Cura 설계에서 미리 피할 수 있는 것들이다.
- NAT Gateway: Lambda를 VPC 안에 넣으면 외부 인터넷으로 나가기 위해 NAT Gateway가 필요하고, 이것만 시간당 과금으로 월 $30 이상이 든다. Supabase와 Gemini는 둘 다 공개 인터넷 엔드포인트이므로 Cura의 Lambda는 VPC에 넣을 이유가 없다.
- Provisioned Concurrency: 데워두는 환경 수만큼 쓰지 않아도 과금된다. 백그라운드 작업에는 콜드 스타트가 문제가 안 되니 쓰지 않는다.
- CloudWatch Logs: 수집 GB당 $0.50에 보관 비용이 따로 붙는다. 호출마다 수십 KB씩 찍으면 Lambda 요금보다 로그 요금이 먼저 나온다. 로그 레벨을 낮추고 보존 기간을 정한다.
- Secrets Manager: 시크릿 하나당 월 $0.40이다. Gemini 키 하나면 무시할 수준이지만, 무료인 SSM Parameter Store(Standard)로도 충분하다.
- 데이터 전송: AWS는 월 100GB까지 무료고 그 뒤 GB당 약 $0.09로 Railway($0.05)보다 비싸다. 번역 결과는 수 KB라 상관없지만, S3에 올린 파일을 외부로 많이 내보내는 구조라면 셈해봐야 한다.
전부 옮기면 어떻게 되나
스프링 서버까지 Lambda로 옮기는 선택은 비용으로도 손해다. Hobby 플랜의 월 $5~8 안에서 돌아가는 CRUD API를 Lambda + API Gateway로 바꾸면 요금은 비슷하거나 조금 싸질 수 있지만, JVM 콜드 스타트를 막기 위해 SnapStart나 Provisioned Concurrency를 붙여야 하고 후자는 상시 과금이다. 아낄 돈은 몇 천 원이고 잃는 것은 응답 시간이다. 비동기 작업만 떼어내는 지금 설계가 비용 면에서도 가장 낫다.
보안: Railway와 비교
지금 Cura 백엔드의 보안 구조는 단순하다. 컨테이너 하나에 Supabase 서비스 키, Gemini API 키, DB 비밀번호가 환경변수로 들어 있고, 스프링이 그 모두를 쓴다. 단순한 만큼 약점도 하나다. 스프링에 취약점이 하나 생기면(의존성 CVE, SSRF, 파일 파서 버그 등) 공격자는 모든 키를 한 번에 손에 넣는다. 폭발 반경이 서비스 전체다.
| 관점 | Railway(지금) | Lambda(도입 후) |
|---|---|---|
| 책임 분담 | 호스트·네트워크는 Railway, 베이스 이미지와 OS 패키지 패치는 내 몫 | 런타임 패치까지 AWS가 함. 내 몫은 함수 코드와 의존성만 |
| 격리 단위 | 컨테이너(장수명, 공유 커널) | Firecracker 마이크로VM(단수명, 전용 커널) |
| 비밀 관리 | 프로젝트 환경변수. 프로젝트 멤버면 모두 열람 가능 | Secrets Manager/Parameter Store. KMS 암호화, IAM으로 함수별 접근 제어, 조회 기록 남음 |
| 권한 분리 | 서비스 단위. 컨테이너 안에서는 모든 키가 동등 | 함수마다 IAM 역할. 번역 함수는 자기 큐와 자기 시크릿만 |
| 공개 면 | 스프링 API 하나 | 스프링 API 하나 그대로. SQS·S3로 깨어나는 Lambda는 공개 엔드포인트 자체가 없음 |
| 감사 로그 | 프로젝트 활동 로그 수준 | CloudTrail에 모든 API 호출 기록(누가 어떤 시크릿을 언제 읽었는지까지) |
좋아지는 점
Lambda 도입이 보안에 주는 가장 큰 이득은 폭발 반경을 쪼개는 것이다. OCR 함수가 뚫려도 그 함수의 IAM 역할에 Supabase 서비스 키 읽기 권한이 없으면 공격자는 거기서 멈춘다. 번역 함수는 SQS 큐 하나와 Gemini 키 하나만 볼 수 있다. 스프링은 Gemini 키를 들고 있을 필요가 없어진다.
사용자가 올린 파일을 처리하는 위치도 바뀐다. 이미지와 PDF 파서는 CVE가 자주 나오는 영역인데, 지금은 그 코드가 모든 키를 가진 장수명 스프링 프로세스 안에서 돈다. Lambda로 옮기면 신뢰할 수 없는 입력을 격리된 일회성 환경에서 최소 권한으로 처리하게 된다. 파일이 S3 presigned URL로 직접 올라가므로 스프링은 파일 바이트를 만지지도 않는다.
새로 생기는 위험
클라우드가 둘이 되면 설정 면도 둘이 된다. Lambda가 만드는 위험은 대부분 설정 실수다.
- IAM 과권한: 급하게
*를 주면 함수별 격리의 의미가 사라진다. 함수마다 역할을 따로 만들고 리소스 ARN까지 좁힌다. - 장기 자격증명: GitHub Actions에 AWS 액세스 키를 시크릿으로 넣는 방식은 유출되면 끝이다. GitHub OIDC로 역할을 맡게 해 키 자체를 없앤다. 다만 스프링이 SQS에 메시지를 넣으려면 Railway 쪽에 AWS 자격증명이 하나는 있어야 한다. 이건 피할 수 없으니
sqs:SendMessage를 큐 하나에만 허용한 IAM 사용자로 만들고 주기적으로 교체한다. 이 키가 설계 전체에서 가장 약한 고리다. - S3 공개 설정: 버킷의 퍼블릭 액세스 차단을 켜고, presigned URL에는 만료 시간과 허용 Content-Type·크기 조건을 건다.
- 콘솔 권한: Lambda 환경변수는 KMS로 암호화되지만
lambda:GetFunctionConfiguration권한이 있으면 콘솔에서 평문으로 보인다. 비밀은 환경변수가 아니라 Secrets Manager나 Parameter Store에 둔다.
의료 데이터라서 더 신경 쓸 것
Cura가 다루는 것은 환자의 진료 기록이다. 서비스 경계를 넘는 지점마다 데이터가 어디에 남는지 봐야 한다.
- SQS 메시지에는 job id만 넣는다. 환자 텍스트는 DB에만 두고 Lambda가 id로 읽어온다. 그러면 큐, DLQ, CloudWatch 어디에도 원문이 남지 않는다. SQS 기본 암호화(SSE-SQS)는 무료이니 켠다.
- 로그에 원문을 찍지 않는다. CloudWatch Logs는 기본 보존이 무기한이다. 보존 기간을 명시한다.
- /tmp는 환경이 재사용될 때 그대로 남는다. OCR 함수가 받은 파일은 처리 후 지운다.
- 리전을 맞춘다. Supabase와 Lambda, S3를 같은 리전(서울)에 두어 데이터가 불필요하게 국경을 넘지 않게 한다.
- Gemini로 가는 데이터는 지금과 같다. Lambda 도입으로 바뀌는 것은 없지만, 외부 LLM에 환자 정보를 보내는 지점이라는 사실은 설계 문서에 분명히 남긴다.
운영: 보이지 않는 서버를 보는 법
서버가 없으니 SSH로 들어가 로그를 볼 수 없다. 표준 출력이 CloudWatch Logs로 가고, 호출 수·오류·지연·동시성 지표는 CloudWatch Metrics에 쌓인다. 여러 서비스를 거치는 요청을 하나로 이어 보려면 X-Ray를 켠다.
실패 처리는 호출 방식에 따라 다르다. SQS 소스라면 처리 실패한 메시지가 큐로 돌아가고, 최대 수신 횟수를 넘기면 데드 레터 큐로 보낸다. 비동기 호출이라면 실패 대상(destination)을 지정해 실패 이벤트를 다른 큐나 함수로 넘긴다. 어느 쪽이든 실패한 작업이 어디로 가는지 설계 단계에서 정해두지 않으면 조용히 사라진다.
배포는 코드로 한다. SAM이나 CDK로 함수, 큐, 이벤트 규칙, IAM 역할을 한 파일에 선언하고 GitHub Actions에서 배포한다. Cura가 이미 GitHub Actions로 Railway에 배포하고 있으니, 같은 워크플로에 Lambda 스택 배포 단계를 추가하는 그림이 된다. IAM은 함수마다 필요한 권한만 준다. 번역 함수는 SQS 읽기와 Secrets Manager의 Gemini 키 읽기만 있으면 된다.
정리
Cura에 Lambda를 넣는다면 다음 원칙으로 간다.
- 스프링 서버는 유지한다. 동기 CRUD API는 거기 남는다.
- 오래 걸리고 양이 들쭉날쭉한 일(Gemini 번역, 파일 OCR, 예약 작업)만 떼어낸다.
- 스프링과 Lambda 사이에는 SQS를 둔다. 직접 호출보다 재시도와 속도 조절이 쉽다.
- 함수는 멱등하게 만든다. 같은 이벤트가 두 번 와도 결과는 한 번이어야 한다.
- DB 커넥션은 Init에서 한 번 만들고, 반드시 풀러(PgBouncer)를 거친다.
- 실패한 작업이 가는 곳(DLQ)을 먼저 정한다.
- JVM 함수는 피하거나 SnapStart를 전제로 한다.
- Lambda는 VPC에 넣지 않는다. NAT Gateway 비용이 Lambda 요금 전체보다 크다.
- 함수마다 IAM 역할을 따로 두고, 비밀은 환경변수가 아니라 Secrets Manager나 Parameter Store에 둔다.
- SQS 메시지와 로그에는 환자 원문을 넣지 않는다. job id만 오간다.
이 글은 적용 전에 머릿속 그림을 정리한 것이다. 다음 글에서는 1번 번역 파이프라인을 실제로 SAM으로 올려보고, 스프링 서버의 스레드 사용량과 응답 시간이 어떻게 달라지는지 측정한 결과를 쓸 예정이다.
댓글