본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 19. · 44 Views
API Gateway와 Lambda 연동 완벽 가이드
AWS에서 서버리스 아키텍처를 구축할 때 필수인 API Gateway와 Lambda 연동 방법을 실무 관점에서 상세히 알려드립니다. 프록시 통합, CORS 설정, 에러 처리까지 모두 다룹니다.
목차
1. Lambda 통합 설정
김개발 씨는 회사에서 첫 번째 서버리스 프로젝트를 맡게 되었습니다. "Lambda 함수는 만들었는데, 이걸 어떻게 외부에서 호출하죠?" 선배 박시니어 씨가 웃으며 말했습니다.
"API Gateway를 연결하면 됩니다."
Lambda 통합 설정은 API Gateway와 Lambda 함수를 연결하는 첫 번째 단계입니다. 마치 집에 현관문을 달아서 외부 방문객이 들어올 수 있게 하는 것과 같습니다.
이 설정을 통해 HTTP 요청이 Lambda 함수로 전달되고, 함수의 응답이 다시 클라이언트에게 돌아갑니다.
다음 코드를 살펴봅시다.
// Lambda 함수 기본 핸들러
exports.handler = async (event) => {
// API Gateway에서 전달된 이벤트 확인
console.log('Received event:', JSON.stringify(event));
// 요청 처리
const response = {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
// 응답 본문은 문자열이어야 함
body: JSON.stringify({
message: 'Lambda 함수가 정상 실행되었습니다',
timestamp: new Date().toISOString()
})
};
return response;
};
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 드디어 회사에서 서버리스 아키텍처를 활용한 프로젝트를 맡게 되었습니다.
팀장님께서 말씀하셨습니다. "우리 서비스에 새로운 API를 만들어야 하는데, Lambda로 구현해보세요." 김개발 씨는 Lambda 함수는 작성했지만, 이걸 어떻게 외부에서 호출할 수 있는지 막막했습니다.
코드는 분명히 완성했는데, 클라이언트가 이 함수를 호출할 방법이 없었습니다. 선배 박시니어 씨가 다가와서 친절하게 설명해주었습니다.
"Lambda 함수는 그 자체로는 외부에서 접근할 수 없어요. API Gateway를 통해 HTTP 엔드포인트를 만들어줘야 합니다." Lambda 통합 설정이란 무엇일까요?
쉽게 비유하자면, Lambda 함수는 건물 안에 있는 사무실이고, API Gateway는 건물의 현관과 같습니다. 아무리 훌륭한 사무실이 있어도 현관이 없으면 방문객이 들어올 수 없습니다.
API Gateway가 바로 그 현관 역할을 해주는 것입니다. 더 구체적으로 말하면, API Gateway는 클라이언트의 HTTP 요청을 받아서 Lambda 함수로 전달하고, Lambda 함수의 실행 결과를 다시 클라이언트에게 돌려주는 중간 다리 역할을 합니다.
왜 이런 구조가 필요할까요? Lambda 함수는 기본적으로 AWS 내부에서만 실행되는 코드 조각입니다.
외부 인터넷에서 직접 접근할 수 있는 엔드포인트가 없습니다. 만약 API Gateway 없이 Lambda를 사용하려면, AWS SDK를 직접 사용해서 함수를 호출해야 하는데, 이는 웹 브라우저나 모바일 앱에서 사용하기에는 너무 복잡합니다.
또한 보안 문제도 있습니다. AWS 자격 증명을 클라이언트에 노출해야 하는데, 이는 매우 위험한 방법입니다.
누군가 코드를 열어보면 AWS 계정에 접근할 수 있게 되니까요. 바로 이런 문제를 해결하기 위해 API Gateway가 등장했습니다.
API Gateway를 사용하면 일반적인 REST API처럼 HTTP 요청으로 Lambda 함수를 호출할 수 있습니다. https://api.example.com/users 같은 주소로 요청을 보내면, API Gateway가 자동으로 Lambda 함수를 실행시켜줍니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 exports.handler는 Lambda 함수의 진입점입니다.
AWS Lambda는 이 함수를 찾아서 실행합니다. event 매개변수에는 API Gateway가 전달한 모든 정보가 담겨 있습니다.
HTTP 메서드, 경로, 헤더, 쿼리 파라미터, 요청 본문 등이 모두 이 객체 안에 들어있습니다. response 객체는 Lambda가 API Gateway에게 돌려주는 응답입니다.
여기서 중요한 점은 형식입니다. 반드시 statusCode, headers, body 세 가지 속성을 포함해야 합니다.
이 형식을 지키지 않으면 API Gateway가 응답을 제대로 처리하지 못합니다. 특히 body는 반드시 문자열이어야 합니다.
객체를 그대로 반환하면 안 되고, JSON.stringify()를 사용해서 문자열로 변환해야 합니다. 이 부분을 잊어버리면 클라이언트가 예상치 못한 응답을 받게 됩니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 전자상거래 사이트를 개발한다고 가정해봅시다.
사용자가 상품 목록을 조회하는 API가 필요합니다. API Gateway에 /products 엔드포인트를 만들고, 이를 Lambda 함수와 연결합니다.
사용자가 앱에서 상품 목록 버튼을 누르면, 앱은 https://api.myshop.com/products로 요청을 보냅니다. API Gateway가 이 요청을 받아서 Lambda 함수를 실행하고, Lambda는 데이터베이스에서 상품 정보를 가져와서 응답합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 응답 형식을 잘못 맞추는 것입니다.
Lambda 함수에서 그냥 객체를 반환하거나, statusCode를 빼먹거나, body를 문자열로 변환하지 않는 경우가 많습니다. 이렇게 하면 API Gateway가 500 에러를 반환하게 됩니다.
또 다른 실수는 비동기 처리를 제대로 하지 않는 것입니다. Lambda 함수는 async 함수로 선언하거나, Promise를 반환해야 합니다.
그렇지 않으면 비동기 작업이 완료되기 전에 함수가 종료될 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, API Gateway가 현관문 역할을 하는 거군요!" Lambda 통합 설정을 제대로 이해하면 서버리스 아키텍처의 기본을 다질 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - Lambda 함수는 항상 statusCode, headers, body 형식으로 응답을 반환해야 합니다
- body는 반드시 JSON.stringify()로 문자열로 변환하세요
- CloudWatch Logs를 활용하여 event 객체의 구조를 먼저 파악하세요
2. 프록시 통합 이해
김개발 씨가 API Gateway 콘솔을 열어보니 "Lambda 프록시 통합"이라는 체크박스가 있었습니다. "이걸 체크하는 게 좋을까요, 말아야 할까요?" 박시니어 씨가 모니터를 가리키며 말했습니다.
"무조건 체크하세요. 안 그러면 고생합니다."
프록시 통합은 API Gateway가 모든 요청 정보를 그대로 Lambda에 전달하고, Lambda의 응답을 그대로 클라이언트에게 전달하는 방식입니다. 마치 우편함이 편지를 열어보지 않고 그대로 전달하는 것과 같습니다.
이 방식을 사용하면 Lambda에서 모든 것을 제어할 수 있어서 유연성이 높아집니다.
다음 코드를 살펴봅시다.
// 프록시 통합을 사용한 Lambda 함수
exports.handler = async (event) => {
// event에 모든 요청 정보가 담겨있음
const method = event.httpMethod; // GET, POST 등
const path = event.path; // /users/123
const queryParams = event.queryStringParameters; // ?name=kim
const headers = event.headers; // 모든 HTTP 헤더
const body = event.body ? JSON.parse(event.body) : null;
// 요청 메서드에 따라 분기 처리
if (method === 'GET') {
return {
statusCode: 200,
body: JSON.stringify({ message: 'GET 요청 처리' })
};
}
// POST 요청 처리
return {
statusCode: 201,
body: JSON.stringify({
message: '생성 완료',
data: body
})
};
};
김개발 씨는 API Gateway 콘솔에서 Lambda 함수를 연결하는 화면을 보고 있었습니다. 설정 항목이 여러 가지 있었는데, 그중에서도 "Lambda 프록시 통합 사용"이라는 체크박스가 눈에 띄었습니다.
체크를 해야 할까요, 말아야 할까요? 옆에서 지켜보던 박시니어 씨가 단호하게 말했습니다.
"무조건 체크하세요. 안 그러면 나중에 고생합니다." 김개발 씨는 궁금했습니다.
"프록시 통합이 뭐길래 이렇게 중요한 건가요?" 프록시 통합이란 정확히 무엇일까요? 쉽게 비유하자면, 프록시 통합은 우체국 직원이 편지를 전달하는 방식과 같습니다.
두 가지 방식이 있습니다. 첫 번째는 우체국 직원이 편지를 열어보고, 내용을 요약해서 전달하는 방식입니다.
두 번째는 편지를 열어보지 않고 봉투째로 그대로 전달하는 방식입니다. 프록시 통합은 두 번째 방식입니다.
API Gateway가 클라이언트의 요청을 가공하지 않고, 모든 정보를 그대로 Lambda 함수에 전달합니다. HTTP 메서드, 경로, 헤더, 쿼리 파라미터, 요청 본문 등 모든 것이 event 객체 안에 담겨서 Lambda로 전달됩니다.
프록시 통합을 사용하지 않으면 어떻게 될까요? 프록시 통합을 사용하지 않으면 "사용자 지정 통합" 방식이 됩니다.
이 방식에서는 API Gateway 콘솔에서 일일이 매핑 템플릿을 작성해야 합니다. "쿼리 파라미터의 이름은 어떻게 전달할까", "헤더는 어떤 것을 전달할까" 같은 설정을 하나하나 직접 해줘야 합니다.
이는 매우 번거롭고, 실수하기 쉽습니다. 새로운 헤더를 추가하거나 쿼리 파라미터를 변경할 때마다 API Gateway 설정을 수정해야 합니다.
코드 변경뿐만 아니라 인프라 설정까지 변경해야 하는 것입니다. 바로 이런 불편함을 해결하기 위해 프록시 통합이 등장했습니다.
프록시 통합을 사용하면 API Gateway 설정을 거의 건드리지 않아도 됩니다. 모든 로직은 Lambda 함수 안에서 처리할 수 있습니다.
새로운 기능을 추가하거나 수정할 때 Lambda 코드만 변경하면 됩니다. 위의 코드를 한 줄씩 살펴보겠습니다.
event.httpMethod에는 HTTP 메서드가 문자열로 들어있습니다. GET, POST, PUT, DELETE 등을 확인할 수 있습니다.
event.path에는 요청 경로가 들어있습니다. 예를 들어 /users/123이라는 경로로 요청이 들어오면, 이 값을 파싱해서 사용자 ID를 추출할 수 있습니다.
event.queryStringParameters는 객체 형태로 쿼리 파라미터를 담고 있습니다. URL이 /users?name=kim&age=30이라면, 이 객체는 { name: 'kim', age: '30' }이 됩니다.
event.headers에는 모든 HTTP 헤더가 들어있습니다. 인증 토큰을 확인하거나, Content-Type을 체크할 때 사용합니다.
event.body는 요청 본문인데, 주의할 점이 있습니다. 이 값은 문자열입니다.
POST나 PUT 요청으로 JSON 데이터를 보내도 문자열로 들어오므로, JSON.parse()로 파싱해야 합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 RESTful API를 만든다고 가정해봅시다. /users 엔드포인트 하나로 GET, POST, PUT, DELETE 요청을 모두 처리하고 싶습니다.
프록시 통합을 사용하면 Lambda 함수 하나로 이 모든 메서드를 처리할 수 있습니다. event.httpMethod로 메서드를 확인하고, switch 문으로 분기 처리하면 됩니다.
많은 스타트업에서 이런 패턴을 적극적으로 사용합니다. API Gateway 설정은 최소화하고, Lambda 함수 안에서 모든 비즈니스 로직을 처리하는 것입니다.
이렇게 하면 배포가 빨라지고, 코드 변경이 쉬워집니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 event.body를 파싱하지 않고 그대로 사용하는 것입니다. event.body는 문자열이므로, 객체처럼 접근하면 undefined가 나옵니다.
반드시 JSON.parse()로 파싱한 후 사용해야 합니다. 또 다른 실수는 event.queryStringParameters가 null일 수 있다는 점을 간과하는 것입니다.
쿼리 파라미터가 없는 요청이 들어오면 이 값은 null이 됩니다. 따라서 event.queryStringParameters?.name처럼 옵셔널 체이닝을 사용하거나, null 체크를 해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 이해했다는 듯이 말했습니다.
"아, 그래서 프록시 통합을 사용하면 Lambda에서 모든 걸 제어할 수 있는 거군요!" 프록시 통합을 제대로 활용하면 유연하고 관리하기 쉬운 서버리스 API를 만들 수 있습니다. 여러분도 꼭 프록시 통합을 사용해 보세요.
실전 팁
💡 - API Gateway 설정에서 "Lambda 프록시 통합 사용" 체크박스를 반드시 활성화하세요
- event.body는 문자열이므로 JSON.parse()로 파싱한 후 사용해야 합니다
- queryStringParameters는 null일 수 있으므로 옵셔널 체이닝을 사용하세요
3. 요청/응답 매핑
김개발 씨는 Lambda 함수를 배포했지만, 클라이언트가 받는 응답 형식이 이상했습니다. "왜 JSON이 이중으로 인코딩되어 있죠?" 박시니어 씨가 코드를 보더니 웃었습니다.
"body를 두 번 stringify하셨네요."
요청/응답 매핑은 클라이언트의 요청을 Lambda가 이해할 수 있는 형태로 변환하고, Lambda의 응답을 클라이언트가 이해할 수 있는 형태로 변환하는 과정입니다. 마치 통역사가 두 사람 사이에서 언어를 번역해주는 것과 같습니다.
올바른 매핑을 하지 않으면 데이터가 깨지거나 에러가 발생합니다.
다음 코드를 살펴봅시다.
// 올바른 요청/응답 매핑 예제
exports.handler = async (event) => {
// 1. 요청 본문 파싱 (문자열 -> 객체)
let requestBody = {};
if (event.body) {
requestBody = JSON.parse(event.body);
}
// 2. 비즈니스 로직 처리
const result = {
userId: requestBody.userId,
name: requestBody.name,
createdAt: new Date().toISOString()
};
// 3. 응답 생성 (객체 -> 문자열)
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
// 한 번만 stringify!
body: JSON.stringify(result)
};
};
김개발 씨는 처음으로 만든 Lambda API를 배포하고 테스트했습니다. 요청은 성공적으로 처리되는 것 같았는데, 클라이언트가 받는 응답이 이상했습니다.
JSON이 문자열로 한 번 더 감싸져 있었습니다. 프론트엔드 개발자 이민지 씨가 슬랙 메시지를 보냈습니다.
"백엔드 API 응답이 이상해요. JSON을 파싱했는데 또 문자열이 나와요." 당황한 김개발 씨는 박시니어 씨에게 도움을 요청했습니다.
박시니어 씨가 코드를 보더니 바로 문제를 찾아냈습니다. "body를 두 번 stringify하셨네요." 요청/응답 매핑이란 무엇일까요?
쉽게 비유하자면, 요청/응답 매핑은 두 나라 사람이 대화할 때 통역사가 하는 일과 같습니다. 한국 사람이 한국어로 말하면, 통역사가 영어로 번역해서 미국 사람에게 전달합니다.
그리고 미국 사람의 영어 답변을 다시 한국어로 번역해서 한국 사람에게 전달합니다. API에서도 마찬가지입니다.
클라이언트는 JSON 형태로 데이터를 보냅니다. 하지만 API Gateway와 Lambda 사이에서는 모든 것이 문자열로 전달됩니다.
따라서 Lambda 함수 안에서 문자열을 객체로 파싱해야 합니다. 반대로 Lambda가 응답을 보낼 때는 객체를 문자열로 변환해야 합니다.
초보 개발자들이 겪는 가장 흔한 문제는 무엇일까요? 첫 번째 문제는 이중 인코딩입니다.
event.body는 이미 문자열인데, 이걸 다시 JSON.stringify()로 감싸는 경우가 있습니다. 또는 응답에서 JSON.stringify()를 두 번 호출하는 실수를 합니다.
이렇게 되면 클라이언트는 문자열 안에 문자열이 들어있는 형태를 받게 됩니다. 두 번째 문제는 Content-Type 헤더를 설정하지 않는 것입니다.
헤더를 설정하지 않으면 브라우저나 클라이언트가 응답을 텍스트로 인식할 수 있습니다. 반드시 'Content-Type': 'application/json'을 설정해야 합니다.
세 번째 문제는 빈 본문을 처리하지 않는 것입니다. GET 요청처럼 본문이 없는 경우 event.body는 null이 됩니다.
이를 그대로 JSON.parse()에 넣으면 에러가 발생합니다. 바로 이런 문제들을 피하기 위해 올바른 매핑 패턴을 익혀야 합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 event.body가 존재하는지 확인합니다.
GET 요청처럼 본문이 없을 수 있기 때문입니다. 본문이 있으면 JSON.parse()로 파싱합니다.
이제 requestBody는 자바스크립트 객체가 되어서, requestBody.userId처럼 속성에 접근할 수 있습니다. 비즈니스 로직을 처리한 후, 결과 객체를 만듭니다.
이때 중요한 점은 이 객체를 그대로 두는 것입니다. 아직 문자열로 변환하지 않습니다.
응답을 만들 때가 되어서야 JSON.stringify(result)를 호출합니다. 딱 한 번만 호출합니다.
이렇게 하면 클라이언트는 정상적인 JSON을 받을 수 있습니다. headers 객체에서 Content-Type을 명시적으로 설정하는 것도 중요합니다.
이렇게 하면 브라우저가 응답을 JSON으로 올바르게 파싱합니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 회사에서는 이런 매핑 로직을 헬퍼 함수로 분리합니다. 매번 똑같은 파싱 로직을 작성하는 것은 비효율적이니까요.
예를 들어 parseRequestBody(event) 같은 함수를 만들어서 재사용합니다. 또한 응답을 만드는 헬퍼 함수도 많이 사용합니다.
createResponse(statusCode, data) 같은 함수를 만들면, 매번 headers와 body를 수동으로 작성하지 않아도 됩니다. 일부 팀에서는 미들웨어 패턴을 사용하기도 합니다.
Express.js의 미들웨어처럼, 요청을 자동으로 파싱하고 응답을 자동으로 직렬화하는 레이어를 추가하는 것입니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 JSON.stringify()를 잊어버리는 것입니다. body에 객체를 그대로 넣으면 "[object Object]"라는 문자열이 클라이언트에 전달됩니다.
이는 자바스크립트가 객체를 암묵적으로 문자열로 변환할 때 나타나는 현상입니다. 또 다른 실수는 중첩된 JSON 파싱입니다.
API Gateway 설정에서 이미 파싱이 된 경우인데도 Lambda에서 다시 파싱하려고 하면 에러가 발생합니다. 프록시 통합을 사용하는지 확인해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 코드를 수정한 김개발 씨는 테스트를 다시 실행했습니다.
이번에는 완벽하게 작동했습니다. 이민지 씨가 슬랙에 엄지 이모지를 보냈습니다.
요청/응답 매핑을 올바르게 처리하면 클라이언트와 서버가 원활하게 통신할 수 있습니다. 여러분도 이 패턴을 꼭 익혀두세요.
실전 팁
💡 - body는 정확히 한 번만 JSON.stringify()로 변환하세요
- Content-Type 헤더를 명시적으로 설정하여 클라이언트가 올바르게 파싱하도록 하세요
- 요청 본문이 없을 수 있으므로 event.body 존재 여부를 먼저 확인하세요
4. CORS 설정
김개발 씨는 로컬에서 프론트엔드를 실행하고 API를 호출했는데, 브라우저 콘솔에 빨간 에러가 나타났습니다. "CORS policy에 의해 차단되었습니다." 박시니어 씨가 말했습니다.
"Lambda 응답에 CORS 헤더를 추가해야 해요."
CORS 설정은 웹 브라우저가 다른 도메인의 API를 호출할 수 있도록 허용하는 보안 설정입니다. 마치 아파트 경비실에서 외부 방문객의 출입을 허가하는 것과 같습니다.
올바른 헤더를 응답에 포함시키지 않으면 브라우저가 요청을 차단합니다.
다음 코드를 살펴봅시다.
// CORS 헤더가 포함된 Lambda 함수
exports.handler = async (event) => {
// OPTIONS 요청 처리 (Preflight)
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
},
body: ''
};
}
// 실제 요청 처리
const response = {
message: 'CORS가 설정된 응답입니다'
};
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', // 모든 도메인 허용
'Access-Control-Allow-Credentials': 'true' // 쿠키 허용
},
body: JSON.stringify(response)
};
};
김개발 씨는 Lambda 함수를 완성하고 배포했습니다. 이제 로컬에서 실행 중인 React 앱에서 API를 호출해보려고 합니다.
localhost:3000에서 실행 중인 프론트엔드가 api.example.com의 Lambda API를 호출합니다. 버튼을 클릭했는데, 아무 일도 일어나지 않았습니다.
브라우저 콘솔을 열어보니 빨간색 에러 메시지가 가득했습니다. "Access to fetch at 'https://api.example.com/users' from origin 'http://localhost:3000' has been blocked by CORS policy." 당황한 김개발 씨는 박시니어 씨에게 물었습니다.
"Lambda 함수는 정상인데, 왜 브라우저가 차단하는 건가요?" CORS란 정확히 무엇일까요? CORS는 Cross-Origin Resource Sharing의 약자입니다.
쉽게 비유하자면, 웹 브라우저는 아파트 단지와 같습니다. 같은 단지 안에서는 자유롭게 이동할 수 있지만, 다른 단지로 가려면 경비실의 허가를 받아야 합니다.
웹에서 Origin은 프로토콜, 도메인, 포트의 조합입니다. http://localhost:3000과 https://api.example.com은 서로 다른 Origin입니다.
브라우저는 기본적으로 다른 Origin으로의 요청을 차단합니다. 이는 보안을 위한 것입니다.
왜 이런 제약이 필요할까요? 만약 CORS 제약이 없다면 악의적인 웹사이트가 사용자 몰래 다른 사이트의 API를 호출할 수 있습니다.
예를 들어 나쁜 사이트가 은행 API를 몰래 호출해서 계좌 이체를 시도할 수 있습니다. CORS는 이런 공격을 막기 위해 만들어진 보안 메커니즘입니다.
하지만 합법적인 경우에도 다른 도메인의 API를 호출해야 할 때가 많습니다. 프론트엔드와 백엔드가 다른 도메인에 있는 것은 매우 일반적입니다.
바로 이때 CORS 헤더를 사용합니다. 서버가 응답에 특정 헤더를 포함시키면, 브라우저는 "아, 이 서버는 다른 도메인의 요청을 허용하는구나"라고 인식하고 요청을 허용합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 event.httpMethod가 OPTIONS인지 확인합니다.
브라우저는 실제 요청을 보내기 전에 Preflight 요청이라는 것을 먼저 보냅니다. 이는 OPTIONS 메서드를 사용하는 요청으로, "이 서버가 CORS를 지원하나요?"라고 물어보는 것입니다.
OPTIONS 요청에는 본문 없이 빈 응답을 보내면 되지만, 헤더는 반드시 포함해야 합니다. Access-Control-Allow-Origin은 어떤 도메인을 허용할지 지정합니다.
*는 모든 도메인을 허용한다는 뜻입니다. Access-Control-Allow-Headers는 클라이언트가 보낼 수 있는 헤더 목록입니다.
일반적으로 Content-Type과 Authorization을 허용합니다. Access-Control-Allow-Methods는 허용할 HTTP 메서드 목록입니다.
실제 요청 처리에서도 동일한 CORS 헤더를 포함시켜야 합니다. Preflight에서만 헤더를 보내고 실제 응답에서 빼먹으면 여전히 에러가 발생합니다.
실제 현업에서는 어떻게 활용할까요? 대부분의 회사에서는 프론트엔드와 백엔드를 별도 도메인으로 분리합니다.
예를 들어 프론트엔드는 www.example.com, 백엔드는 api.example.com처럼 구성합니다. 이 경우 CORS 설정이 필수입니다.
보안을 중요시하는 서비스에서는 Access-Control-Allow-Origin을 * 대신 특정 도메인으로 제한합니다. 예를 들어 'Access-Control-Allow-Origin': 'https://www.example.com'처럼 정확한 도메인을 지정합니다.
환경변수를 활용하는 것도 좋은 방법입니다. 개발 환경에서는 localhost를 허용하고, 프로덕션에서는 실제 도메인만 허용하도록 설정할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 OPTIONS 요청을 처리하지 않는 것입니다.
실제 GET이나 POST 요청에만 헤더를 추가하고, OPTIONS는 잊어버리는 경우가 많습니다. 브라우저는 Preflight에서 실패하면 실제 요청을 보내지 않습니다.
또 다른 실수는 와일드카드(*)와 자격 증명을 함께 사용하려는 것입니다. Access-Control-Allow-Credentials: true를 사용하면서 Access-Control-Allow-Origin: *를 사용하면 에러가 발생합니다.
자격 증명을 사용하려면 정확한 도메인을 지정해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 조언대로 CORS 헤더를 추가한 김개발 씨는 다시 테스트했습니다. 이번에는 API 호출이 성공했습니다.
브라우저 콘솔에도 에러가 없었습니다. CORS 설정을 올바르게 하면 프론트엔드와 백엔드가 다른 도메인에 있어도 원활하게 통신할 수 있습니다.
여러분도 CORS의 개념을 정확히 이해하고 적용해 보세요.
실전 팁
💡 - OPTIONS 메서드를 반드시 처리하여 Preflight 요청에 응답하세요
- 프로덕션에서는 Access-Control-Allow-Origin을 특정 도메인으로 제한하세요
- 자격 증명(쿠키)을 사용한다면 와일드카드 대신 정확한 도메인을 지정하세요
5. 에러 응답 처리
김개발 씨의 Lambda 함수가 운영 환경에서 갑자기 에러를 뿜어냈습니다. 하지만 클라이언트는 "500 Internal Server Error"만 보고 무슨 문제인지 알 수 없었습니다.
박시니어 씨가 말했습니다. "에러도 제대로 응답해야 합니다."
에러 응답 처리는 Lambda 함수에서 발생한 에러를 적절한 HTTP 상태 코드와 메시지로 변환하여 클라이언트에 전달하는 것입니다. 마치 병원에서 환자에게 증상을 설명해주는 것과 같습니다.
명확한 에러 메시지가 없으면 클라이언트는 문제를 해결할 수 없습니다.
다음 코드를 살펴봅시다.
// 에러 처리가 포함된 Lambda 함수
exports.handler = async (event) => {
try {
// 요청 파싱
const body = JSON.parse(event.body || '{}');
// 유효성 검증
if (!body.email) {
return createErrorResponse(400, 'EMAIL_REQUIRED', '이메일은 필수입니다');
}
if (!isValidEmail(body.email)) {
return createErrorResponse(400, 'INVALID_EMAIL', '올바른 이메일 형식이 아닙니다');
}
// 비즈니스 로직 실행
const result = await processUser(body);
return createSuccessResponse(200, result);
} catch (error) {
console.error('Lambda 실행 에러:', error);
// 에러 종류에 따른 응답 처리
if (error.name === 'ValidationError') {
return createErrorResponse(400, 'VALIDATION_ERROR', error.message);
}
if (error.statusCode === 404) {
return createErrorResponse(404, 'NOT_FOUND', '리소스를 찾을 수 없습니다');
}
// 예상치 못한 에러는 500으로 처리
return createErrorResponse(500, 'INTERNAL_ERROR', '서버 내부 오류가 발생했습니다');
}
};
// 헬퍼 함수들
function createErrorResponse(statusCode, errorCode, message) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
error: errorCode,
message: message
})
};
}
function createSuccessResponse(statusCode, data) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(data)
};
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
async function processUser(body) {
// 실제 비즈니스 로직
return { id: 1, email: body.email };
}
김개발 씨의 Lambda 함수가 운영 환경에 배포되었습니다. 처음 며칠은 잘 작동했는데, 어느 날 갑자기 장애 알림이 울렸습니다.
모니터링 도구를 확인해보니 에러율이 급증하고 있었습니다. 더 큰 문제는 고객 지원팀에서 날아온 메시지였습니다.
"사용자들이 뭐가 문제인지 모르겠다고 하소연합니다. 그냥 '서버 에러'만 나온대요." 박시니어 씨가 로그를 살펴보더니 말했습니다.
"Lambda 함수에서 에러가 발생했는데, 제대로 된 응답을 안 보내고 있네요." 에러 응답 처리가 왜 중요할까요? 쉽게 비유하자면, 에러 응답은 의사가 환자에게 진단 결과를 설명하는 것과 같습니다.
의사가 "아프시죠?"라고만 하고 구체적인 설명을 하지 않으면 환자는 어떻게 해야 할지 모릅니다. "독감에 걸리셨습니다.
약을 드시고 푹 쉬세요"라고 명확히 말해줘야 환자가 대응할 수 있습니다. API도 마찬가지입니다.
에러가 발생했을 때 "500 Internal Server Error"만 보내면 클라이언트는 아무것도 할 수 없습니다. 서버 문제인지, 클라이언트 요청이 잘못된 건지, 어떻게 고쳐야 하는지 알 수 없습니다.
Lambda 함수에서 에러가 발생하면 어떻게 될까요? 만약 try-catch로 에러를 잡지 않으면, Lambda 함수가 예외를 던지면서 종료됩니다.
이 경우 API Gateway는 기본적으로 500 상태 코드를 클라이언트에 반환합니다. 응답 본문에는 Lambda의 에러 메시지가 그대로 노출될 수도 있는데, 이는 보안상 위험합니다.
예를 들어 데이터베이스 연결 에러가 발생하면, 에러 메시지에 데이터베이스 주소나 자격 증명 정보가 포함될 수 있습니다. 이런 정보가 클라이언트에 그대로 노출되면 큰 문제가 됩니다.
바로 이런 문제를 해결하기 위해 명시적인 에러 응답을 만들어야 합니다. 위의 코드를 한 줄씩 살펴보겠습니다.
전체 로직을 try-catch로 감쌉니다. 이렇게 하면 어떤 에러가 발생하더라도 Lambda 함수가 적절한 응답을 반환할 수 있습니다.
요청 파싱과 유효성 검증에서 문제가 발견되면, 즉시 400 상태 코드로 응답합니다. 400은 "Bad Request"를 의미하며, 클라이언트의 요청이 잘못되었다는 뜻입니다.
이때 EMAIL_REQUIRED 같은 에러 코드와 함께 사람이 읽을 수 있는 메시지를 함께 보냅니다. catch 블록에서는 에러의 종류를 판별합니다.
ValidationError처럼 특정한 에러는 400으로, 리소스를 찾을 수 없는 경우는 404로 응답합니다. 예상치 못한 에러는 500으로 처리하되, 구체적인 에러 정보는 로그에만 남기고 클라이언트에는 일반적인 메시지만 보냅니다.
createErrorResponse 헬퍼 함수는 에러 응답을 일관된 형식으로 만들어줍니다. 이렇게 하면 모든 에러 응답이 동일한 구조를 가지게 되어, 클라이언트가 처리하기 쉬워집니다.
실제 현업에서는 어떻게 활용할까요? 대부분의 회사에서는 에러 코드 체계를 정의합니다.
예를 들어 USER_NOT_FOUND, INVALID_TOKEN, RATE_LIMIT_EXCEEDED 같은 코드를 미리 정의해둡니다. 클라이언트는 이 코드를 보고 적절한 UI를 표시할 수 있습니다.
일부 팀에서는 에러 응답에 requestId를 포함시킵니다. 이렇게 하면 고객이 에러를 보고할 때, 이 ID로 로그를 추적할 수 있습니다.
"requestId가 abc-123인 요청에서 에러가 발생했어요"라고 하면, 개발팀이 정확한 로그를 찾을 수 있습니다. 다국어 지원을 하는 서비스에서는 에러 코드만 보내고, 클라이언트가 현지화된 메시지를 표시하기도 합니다.
서버는 INVALID_EMAIL이라는 코드만 보내고, 영어권 사용자에게는 "Invalid email format", 한국어 사용자에게는 "올바른 이메일 형식이 아닙니다"라고 표시하는 것입니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 에러 메시지에 너무 많은 정보를 포함시키는 것입니다. 스택 트레이스나 내부 변수 값을 그대로 클라이언트에 보내면 보안 문제가 될 수 있습니다.
상세한 정보는 CloudWatch Logs에만 남기고, 클라이언트에는 필요한 정보만 보내야 합니다. 또 다른 실수는 모든 에러를 500으로 처리하는 것입니다.
클라이언트의 요청이 잘못된 경우(400), 인증 실패(401), 권한 부족(403), 리소스 없음(404) 등 상황에 맞는 상태 코드를 사용해야 합니다. 이렇게 하면 클라이언트가 적절하게 대응할 수 있습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 에러 처리를 개선한 김개발 씨는 코드를 배포했습니다.
이번에는 사용자들이 "이메일 형식이 잘못되었습니다"라는 명확한 메시지를 받았습니다. 고객 지원팀의 문의도 크게 줄었습니다.
에러 응답을 제대로 처리하면 사용자 경험이 크게 개선되고, 디버깅도 쉬워집니다. 여러분도 명확한 에러 응답을 만드는 습관을 들이세요.
실전 팁
💡 - 모든 로직을 try-catch로 감싸서 예상치 못한 에러도 적절히 응답하세요
- 에러 코드와 메시지를 함께 제공하여 클라이언트가 구체적으로 대응할 수 있게 하세요
- 상세한 에러 정보는 CloudWatch에만 로깅하고, 클라이언트에는 최소한의 정보만 전달하세요
6. 테스트와 디버깅
김개발 씨는 Lambda 함수를 배포했지만, 운영 환경에서만 이상하게 작동했습니다. "로컬에서는 잘 되는데요?" 박시니어 씨가 말했습니다.
"Lambda 콘솔에서 직접 테스트해보셨어요?"
테스트와 디버깅은 Lambda 함수가 다양한 상황에서 올바르게 작동하는지 확인하는 과정입니다. 마치 자동차를 출고하기 전에 여러 조건에서 시험 주행하는 것과 같습니다.
API Gateway와 Lambda의 통합은 복잡하므로, 체계적인 테스트 전략이 필요합니다.
다음 코드를 살펴봅시다.
// 테스트 가능한 Lambda 함수 구조
exports.handler = async (event) => {
try {
// 1. 입력 로깅 (디버깅용)
console.log('Event:', JSON.stringify(event, null, 2));
// 2. 환경 변수 확인
const stage = process.env.STAGE || 'dev';
console.log('Current stage:', stage);
// 3. 요청 파싱
const body = parseRequestBody(event);
console.log('Parsed body:', body);
// 4. 비즈니스 로직
const result = await processRequest(body);
console.log('Result:', result);
return createResponse(200, result);
} catch (error) {
// 에러 상세 로깅
console.error('Error details:', {
message: error.message,
stack: error.stack,
event: event
});
return createResponse(500, { error: 'Internal server error' });
}
};
// 테스트 헬퍼 함수
function parseRequestBody(event) {
// 로컬 테스트를 위한 간단한 처리
if (typeof event.body === 'object') {
return event.body;
}
return JSON.parse(event.body || '{}');
}
function createResponse(statusCode, data) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify(data)
};
}
async function processRequest(body) {
// 비즈니스 로직을 분리하여 단위 테스트 가능하게
return { success: true, data: body };
}
// Jest를 사용한 단위 테스트 예제
// test/handler.test.js
describe('Lambda Handler Tests', () => {
test('정상 요청 처리', async () => {
const event = {
httpMethod: 'POST',
body: JSON.stringify({ name: 'test' }),
headers: {}
};
const response = await exports.handler(event);
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body).success).toBe(true);
});
test('빈 본문 처리', async () => {
const event = {
httpMethod: 'GET',
body: null,
headers: {}
};
const response = await exports.handler(event);
expect(response.statusCode).toBe(200);
});
});
김개발 씨는 Lambda 함수를 완성하고 자신있게 운영 환경에 배포했습니다. 로컬 환경에서는 완벽하게 작동했으니까요.
하지만 실제 사용자들이 API를 호출하자 이상한 문제들이 나타나기 시작했습니다. 어떤 요청은 성공하고 어떤 요청은 실패했습니다.
패턴을 찾을 수 없었습니다. 김개발 씨는 당황했습니다.
"로컬에서는 완벽했는데, 왜 운영에서는 안 되는 거죠?" 박시니어 씨가 물었습니다. "Lambda 콘솔에서 직접 테스트해보셨어요?
CloudWatch Logs는 확인하셨나요?" Lambda 테스트와 디버깅은 왜 특별할까요? 일반적인 웹 서버와 달리, Lambda는 AWS 클라우드에서만 실행됩니다.
로컬에서 디버거를 붙여서 단계별로 실행해볼 수 없습니다. 또한 API Gateway와의 통합에서 미묘한 차이가 있을 수 있습니다.
로컬에서는 테스트 데이터를 직접 만들지만, 실제로는 API Gateway가 변환한 데이터가 들어옵니다. 따라서 Lambda만의 특별한 테스트와 디버깅 전략이 필요합니다.
첫 번째 전략은 충분한 로깅입니다. 위 코드를 보면 곳곳에 console.log()가 있습니다.
Lambda에서 console.log()로 출력한 내용은 모두 CloudWatch Logs에 기록됩니다. 이는 Lambda 디버깅의 가장 기본적이고 강력한 도구입니다.
중요한 지점마다 로그를 남겨야 합니다. 함수가 시작될 때 입력 이벤트 전체를 로깅합니다.
요청을 파싱한 후 결과를 로깅합니다. 비즈니스 로직을 실행한 후 결과를 로깅합니다.
이렇게 하면 어느 단계에서 문제가 발생했는지 정확히 파악할 수 있습니다. 두 번째 전략은 Lambda 콘솔 테스트입니다.
Lambda 콘솔에서는 테스트 이벤트를 직접 만들어서 함수를 실행해볼 수 있습니다. API Gateway가 보내는 이벤트 형식에 맞춰서 JSON을 작성하고, "테스트" 버튼을 누르면 즉시 결과를 확인할 수 있습니다.
AWS는 미리 만들어진 이벤트 템플릿도 제공합니다. "API Gateway AWS Proxy" 템플릿을 선택하면, 실제 API Gateway가 보내는 것과 동일한 형식의 이벤트가 생성됩니다.
여기에 원하는 값을 채워넣고 테스트하면 됩니다. 세 번째 전략은 단위 테스트 작성입니다.
위 코드를 보면 비즈니스 로직이 processRequest 함수로 분리되어 있습니다. 이렇게 하면 이 함수만 따로 테스트할 수 있습니다.
Jest 같은 테스트 프레임워크로 다양한 입력에 대해 단위 테스트를 작성합니다. 단위 테스트는 로컬에서 빠르게 실행할 수 있어서, 배포 전에 문제를 찾아낼 수 있습니다.
또한 코드를 수정할 때 기존 기능이 망가지지 않았는지 확인하는 회귀 테스트 역할도 합니다. 네 번째 전략은 통합 테스트입니다.
단위 테스트는 개별 함수를 테스트하지만, 통합 테스트는 전체 시스템을 테스트합니다. 실제 API Gateway 엔드포인트에 HTTP 요청을 보내고, 응답이 올바른지 확인합니다.
이를 통해 Lambda 함수뿐만 아니라 API Gateway 설정, CORS, 권한 등이 모두 제대로 작동하는지 검증할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
대부분의 회사에서는 CI/CD 파이프라인에 자동화된 테스트를 통합합니다. 코드를 push하면 자동으로 단위 테스트가 실행되고, 통과하면 개발 환경에 배포됩니다.
개발 환경에서 통합 테스트가 실행되고, 통과하면 운영 환경으로 배포됩니다. 일부 팀에서는 카나리 배포를 사용합니다.
새 버전을 전체 트래픽의 10%에만 먼저 배포하고, 에러율을 모니터링합니다. 문제가 없으면 점진적으로 비율을 높여갑니다.
문제가 발견되면 즉시 롤백합니다. 또한 X-Ray를 활용하여 Lambda 함수의 성능을 분석하기도 합니다.
X-Ray는 함수 실행 시간, 외부 API 호출 시간, 데이터베이스 쿼리 시간 등을 시각화해줍니다. 어느 부분이 느린지 한눈에 파악할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 로그에 민감한 정보를 남기는 것입니다.
사용자의 비밀번호, 신용카드 번호, 인증 토큰 등을 로그에 찍으면 안 됩니다. CloudWatch Logs는 여러 사람이 볼 수 있으므로, 민감한 정보는 마스킹해야 합니다.
또 다른 실수는 과도한 로깅입니다. 모든 변수를 다 로깅하면 로그가 너무 많아져서 정작 중요한 정보를 찾기 어려워집니다.
또한 CloudWatch Logs는 사용량에 따라 비용이 발생하므로, 적절한 수준의 로깅이 필요합니다. 테스트를 작성하지 않는 것도 큰 실수입니다.
"빨리 만들어야 하니까 나중에 테스트를 추가하자"고 생각하지만, 나중은 오지 않습니다. 그리고 버그가 운영 환경에서 발견되면 그때는 이미 늦습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언대로 CloudWatch Logs를 확인한 김개발 씨는 문제를 발견했습니다.
특정 헤더가 없을 때 에러가 발생하고 있었습니다. 로컬 테스트에서는 항상 해당 헤더를 보냈지만, 실제 클라이언트는 그렇지 않았던 것입니다.
코드를 수정하고 단위 테스트를 추가한 김개발 씨는 이번에는 자신있게 배포했습니다. 이번에는 문제없이 작동했습니다.
체계적인 테스트와 디버깅 전략을 갖추면 자신있게 코드를 배포할 수 있습니다. 여러분도 충분한 로깅, 자동화된 테스트, 체계적인 모니터링을 구축해 보세요.
실전 팁
💡 - CloudWatch Logs에 충분한 로그를 남겨서 문제 발생 시 빠르게 원인을 파악하세요
- Lambda 콘솔에서 API Gateway 이벤트 템플릿을 사용하여 실제 환경과 동일한 조건에서 테스트하세요
- 단위 테스트와 통합 테스트를 CI/CD 파이프라인에 통합하여 자동으로 검증하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
실전 프로젝트 커스텀 채널 추가하기
확장 가능한 메시징 시스템에 새로운 채널을 추가하는 방법을 실전 프로젝트로 배웁니다. 어댑터 패턴을 활용한 채널 설계부터 테스트, 배포까지 전 과정을 다룹니다.
Cron과 Webhooks 완벽 가이드
Node.js 환경에서 자동화의 핵심인 Cron 작업과 Webhooks를 활용하는 방법을 다룹니다. 정기적인 작업 스케줄링부터 외부 서비스 연동까지, 실무에서 바로 적용할 수 있는 자동화 기법을 배워봅니다.
보안 모델 및 DM Pairing 완벽 가이드
Discord 봇의 DM 보안 정책과 페어링 시스템을 체계적으로 학습합니다. dmPolicy 설정부터 allowlist 관리, 페어링 코드 구현까지 안전한 봇 운영의 모든 것을 다룹니다.
Media Pipeline 완벽 가이드
실무에서 자주 사용하는 미디어 파일 처리 파이프라인을 처음부터 끝까지 배웁니다. 이미지 리사이징, 오디오 변환, 임시 파일 관리까지 Node.js로 구현하는 방법을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다.
Slack 통합 완벽 가이드 Bolt로 시작하는 기업용 메신저 봇 개발
Slack Bolt 프레임워크를 활용하여 기업용 메신저 봇을 개발하는 방법을 초급자도 이해할 수 있도록 단계별로 설명합니다. 이벤트 구독, 모달 인터랙션, 실전 배포까지 실무 활용 사례와 함께 다룹니다.