1. 프로젝트 주차 목표
7주차에 AWS 콘솔 기반으로 구축한 프로덕션 인프라(VPC, EC2, RDS, S3, ALB, Route53, ACM, CloudWatch)를 Terraform 코드로 전환하여 IaC(Infrastructure as Code) 체계를 완성하는 것을 목표로 한다. 단일 진입점 구조에서 벗어나 modules/ 디렉토리에 8개의 재사용 가능한 모듈(network, security, storage, compute, rds, ingress, dns, monitoring)을 분리하고, environments/dev와 environments/prod로 환경별 진입점을 분리하여 동일한 모듈을 다른 파라미터로 호출하는 구조를 설계한다. locals/for_each/count 등 Terraform의 핵심 패턴을 실제 모듈에 적용하여 코드 중복을 제거하고, dev.tfvars/prod.tfvars로 환경별 설정을 외부화하여 시크릿과 일반 값의 관리를 분리한다. 8주차 지도교수 피드백("iOS 앱과 백엔드 응답 형식의 정합성을 확보할 필요가 있다")을 반영하여, iOS 앱의 BusArrivalService/BusStopService와 모델 코드를 분석하고 백엔드의 스키마와 정규화 로직을 iOS 모델 키와 1:1 일치시켜 양쪽이 동일한 JSONDecoder로 디코딩 가능하도록 구성한다. iOS 시뮬레이터, 백엔드 Docker 스택, 서울 TOPIS 라이브 API를 연결한 풀스택 검증 스크립트를 작성하여 각 레이어의 정합성을 자동 검증한다. 모든 모듈에 README를 작성하여 입력 변수, 출력값, 의존성을 문서화하고 terraform fmt/validate로 구성 정합성을 검증한다.
2. 프로젝트 주차 진행 내용
2-1. Terraform 모듈 재사용 구조 설계
7주차까지의 Terraform 코드는 단일 진입점에 모든 리소스가 평면적으로 정의되어 있어, 환경 간 값 차이를 분리하기 어렵고 부분 변경 시 영향 범위를 예측하기 어려웠다. 9주차에서는 책임 단위로 8개의 모듈로 분리하여 modules/ 디렉토리 하위에 배치하였다.
network 모듈은 VPC, 퍼블릭/프라이빗 서브넷, 인터넷 게이트웨이, NAT 게이트웨이, 라우트 테이블을 담당한다. security 모듈은 ALB/EC2/RDS의 3단계 보안 그룹 체인을 정의한다. storage 모듈은 S3 버킷의 버전 관리, 암호화, 퍼블릭 액세스 차단, CORS 설정을 담당한다. compute 모듈은 EC2 인스턴스, IAM 역할/인스턴스 프로파일, Key Pair, Elastic IP를 묶어 관리한다. rds 모듈은 PostgreSQL 15 인스턴스, DB 서브넷 그룹, 파라미터 그룹을 정의한다. ingress 모듈은 ALB와 HTTPS 리스너, 타겟 그룹, HTTP→HTTPS 리다이렉트 규칙을 담당한다. dns 모듈은 Route53 호스팅 영역과 ACM 인증서, A 레코드(별칭) 생성을 책임진다. monitoring 모듈은 CloudWatch 대시보드, 알람, SNS 토픽, AWS Budgets를 통합한다.
각 모듈은 main.tf, variables.tf, outputs.tf, README.md의 4개 파일 구조로 통일하여, 모듈 사용자가 입력/출력만 보고 내부 구현을 이해하지 않아도 사용할 수 있도록 구성하였다.
2-2. dev/prod 환경 분리
environments/dev와 environments/prod 디렉토리를 신설하여 각 환경의 진입점(main.tf, variables.tf, outputs.tf)을 분리하였다. 두 환경은 동일한 modules/ 코드를 참조하지만, 호출 시 전달하는 파라미터와 활성화 모듈이 다르다.
dev 환경은 t3.micro EC2와 db.t3.micro RDS를 Single-AZ 구성으로 사용하며, NAT Gateway는 비용 절감을 위해 비활성화하고 ALB/DNS/Monitoring 모듈은 호출하지 않아 최소 구성으로 동작한다. VPC CIDR은 10.10.0.0/16을 사용한다. prod 환경은 t3.small EC2와 db.t3.small RDS를 사용하며 Multi-AZ는 옵션으로 두고(기본 false) ALB, DNS, Monitoring 모듈을 모두 호출하여 7주차에 콘솔에서 구성한 프로덕션 환경과 동일한 형상을 재현한다. VPC CIDR은 10.20.0.0/16을 사용하여 dev와 격리하였다. AWS Budgets 모듈로 월 $30 한도를 설정하였다.
7주차의 단일 환경 진입점(infra/terraform/main.tf 등)은 호환성 유지를 위해 보존하되, 신규 작업은 environments/ 하위에서 수행하도록 README에 명시하였다.
2-3. Terraform
locals 블록을 사용하여 환경별 공통값을 한 곳에서 계산하도록 구성하였다. environment, name_prefix, common_tags를 locals로 선언하고, provider "aws"의 default_tags에 common_tags를 연결하여 모든 리소스에 Project, Environment, ManagedBy, CostCenter 태그가 자동 부여되도록 하였다.
network 모듈의 서브넷 생성에는 for_each 패턴을 도입하였다. 기존에는 count 인덱스로 서브넷을 생성했으나, 가용영역 배열의 순서가 변경되면 기존 리소스가 재생성되는 문제가 있었다. 가용영역을 키로 하는 맵을 locals에서 빌드하고 for_each로 순회하여, 인덱스 변경에도 리소스 식별자가 안정적으로 유지되도록 하였다.
NAT Gateway, Elastic IP 등 환경별 활성화 여부가 다른 리소스에는 count 패턴을 적용하였다. count = var.enable_nat_gateway ? 1 : 0 형태로 작성하여, dev 환경에서는 NAT Gateway가 0개, prod 환경에서는 1개 생성되도록 분기하였다.
2-4. tfvars 분리 및 시크릿 관리
환경별 변수는 dev.tfvars, prod.tfvars 파일로 분리하여 git에 포함시키되, 시크릿(DB 비밀번호 등)은 terraform.tfvars 파일에 두고 .gitignore로 제외하였다. terraform.tfvars.example 템플릿 파일을 함께 제공하여 신규 환경 구성 시 참조할 수 있도록 하였다. CI/CD 환경에서는 TF_VAR_* 환경변수로 시크릿을 주입하는 방식을 README에 명시하였다.
2-5. iOS 앱 코드 분석 (지도교수 피드백 반영)
8주차 지도교수 피드백("iOS 앱과 백엔드 응답 형식의 정합성을 확보할 필요가 있다")을 반영하여, ComfortableMove iOS 앱(/Users/parkseonggeun/Desktop/개발프로젝트/bfdream/무제/ComfortableMove)의 코드를 분석하였다.
분석 대상은 Core/Manager/BusArrivalService.swift(getStationByUid 호출), Core/Manager/BusStopService.swift(getStationByPos 호출), Core/Model/BusArrivalResponse.swift(BusArrivalItem 모델), Core/Model/StationByPosResponse.swift(StationItem 모델)이다. 분석 결과, iOS 앱은 현재 백엔드를 거치지 않고 서울시 TOPIS 공공데이터 API(ws.bus.go.kr/api/rest)를 직접 호출하고 있으며, 응답을 JSONDecoder로 디코딩하여 MsgHeader, MsgBody, BusArrivalItem, StationItem 구조체로 변환하는 구조임을 확인하였다.
이를 토대로 백엔드 응답 형식을 iOS 모델과 1:1 일치시키면, iOS 앱이 동일한 디코더 코드로 백엔드와 서울 API 양쪽을 처리할 수 있고 향후 백엔드 프록시로 전환할 때 모델 코드를 수정할 필요가 없다는 정합 원칙을 수립하였다.
2-6. 백엔드 스키마와 서비스를 iOS 모델로 정합
backend/app/schemas/bus.py를 재정의하여 BusArrivalResponse, MsgHeader, BusArrivalItem, StationByPosResponse, StationItem 스키마를 iOS 모델의 키와 동일하게 작성하였다. 기존에 한글로 변환되던 필드를 모두 제거하고 서울 API의 원본 키(rtNm, arrmsg1, adirection, routeType, isFullFlag1, isLast1, congestion1, stationId, stationNm, arsId, gpsX, gpsY, dist, stationTp 등)를 그대로 노출하도록 변경하였다.
backend/app/services/seoul_bus_api.py에 normalize_arrival_response, normalize_station_response 메서드를 추가하였다. 서울 API가 itemList에 단일 결과를 dict로 반환하고 다중 결과를 list로 반환하는 비일관성을 해소하기 위해, 항상 list로 정규화하도록 구성하였다. itemCount는 문자열로 반환되는 경우가 있어 정수형으로 변환하였고, 라이브 응답에서 itemCount가 0이지만 itemList가 비어있지 않은 quirk를 발견하여 itemCount = len(item_list)로 자동 보정하도록 처리하였다. 기존에 존재하던 한글/영문 필드명 변환 로직은 모두 제거하였다.
backend/app/api/v1/bus.py를 재작성하여 GET /api/v1/bus/arrivals?ars_id=…, GET /api/v1/bus/stations?tmX=&tmY=&radius= 두 엔드포인트를 노출하였다. 쿼리 파라미터명을 iOS 호출 파라미터와 동일하게 맞춰, iOS에서 baseURL만 백엔드로 교체하면 동작하도록 구성하였다.
2-7. 테스트 코드 재작성
스키마 변경에 따라 backend/tests/unit/test_seoul_bus_service.py, backend/tests/integration/test_seoul_bus_api.py, backend/tests/integration/test_api_bus.py를 재작성하였다. 기존의 한영 변환 메서드 테스트는 제거하고, normalize_arrival_response와 normalize_station_response의 정규화 결과를 검증하는 테스트로 교체하였다. dict→list 변환, itemCount 정수화, itemCount 자동 보정 케이스를 각각 검증한다.
2-8. iOS 모델 ↔ 백엔드 응답 매핑 정의
iOS 모델 필드와 백엔드 응답 필드의 1:1 매핑을 명시적으로 정의하였다. MsgHeader.headerCd/headerMsg/itemCount는 백엔드 응답의 msgHeader 객체와 동일하게 매핑되며, BusArrivalItem.rtNm/arrmsg1/adirection/routeType/isFullFlag1/isLast1/congestion1은 msgBody.itemList[]의 각 요소와 매핑된다. StationItem.stationId/stationNm/arsId/gpsX/gpsY/dist/stationTp도 동일하게 매핑된다. 매핑 표를 docs/week9/week9_report.md에 기록하여 향후 필드 추가 시 참조 자료로 활용하도록 하였다.
2-9. iOS 연동 헬스체크 스크립트 작성
tools/ios-integration/healthcheck.sh를 작성하여 백엔드의 3개 엔드포인트(/api/v1/health, /api/v1/bus/arrivals?ars_id=23288, /api/v1/bus/stations?tmX=126.9707&tmY=37.5547&radius=100)를 호출하고, 응답에 iOS 모델의 필수 키(msgHeader.headerCd, msgHeader.itemCount, BusArrivalItem.rtNm, BusArrivalItem.routeType, StationItem.stationId 등)가 모두 존재하는지 jq로 검증하도록 구성하였다. 누락된 키가 발견되면 즉시 실패하고 종료 코드 1을 반환하여, CI 파이프라인에 통합 가능하도록 하였다.
2-10. iOS 앱의 백엔드 분기 코드 추가
iOS 앱의 App/Sources/AppConfig.swift에 BackendConfig 구조체를 추가하고 useBackend 플래그와 baseURL(Info.plist의
BACKEND_BASE_URL 키에서 로드)을 정의하였다. Core/Manager/BusArrivalService.swift와 Core/Manager/BusStopService.swift에 useBackend 분기를 추가하여, true일 때는 백엔드 엔드포인트를 호출하고 false일 때는 기존 서울 API 직접 호출 경로로 폴백하도록 구성하였다. 개발 환경의 평문 HTTP 통신을 허용하기 위해 Info.plist에 NSAppTransportSecurity.NSAllowsArbitraryLoads = true를 추가하였으며, 프로덕션에서는 ALB+ACM의 HTTPS만 사용하도록 코멘트로 명시하였다.
2-11. terraform fmt/validate 검증
environments/dev와 environments/prod 양쪽에서 terraform init -backend=false, terraform validate를 실행하여 두 환경 모두 "Success! The configuration is valid." 응답을 확인하였다. terraform fmt -recursive를 실행하여 전체 코드의 포맷 정합성을 검증하였으며, 변경 사항 없음을 확인하였다.
2-12. 풀스택 통합 검증
tools/ios-integration/fullstack_verify.sh를 작성하여 iOS, Backend, Infra 세 레이어가 함께 동작하는지 자동 검증하도록 구성하였다. 검증 항목은 (1) Docker 컨테이너 3종(backend, db, redis)의 healthy 상태 확인, (2) /api/v1/health에서 db/redis/seoul_bus_api connected 응답 확인, (3) /api/v1/bus/arrivals?ars_id=23288 호출 후 BusArrivalItem 7개 필수 키 누락 0건, (4) /api/v1/bus/stations?tmX=127.0276&tmY=37.4979&radius=200 호출 후 StationItem 7개 필수 키 누락 0건, (5) terraform validate dev/prod 양쪽 통과, (6) iOS 시뮬레이터에 ComfortableMove 앱 설치 확인의 6개 항목이다.
검증 환경은 docker compose -f docker-compose.yml -f docker-compose.dev.yml up으로 기동된 백엔드 스택(comfortablemove_backend_dev, comfortablemove_db_dev, comfortablemove_redis_dev), Xcode 26.2의 iPhone 16e 시뮬레이터, 서울 TOPIS 라이브 API(ws.bus.go.kr/api/rest)이다. 6개 항목 모두 통과(PASS=10, FAIL=0)하였으며, /api/v1/bus/arrivals는 라이브 24개 노선, /api/v1/bus/stations는 14개 정류소를 반환하였다.
3. 프로젝트 주차 진행 결과
Terraform IaC 측면에서는 단일 진입점 구조에서 modules/ + environments/ 구조로 전환하여 8개 모듈(network, security, storage, compute, rds, ingress, dns, monitoring)과 2개 환경(dev, prod)을 분리하였다. 모든 모듈은 main.tf/variables.tf/outputs.tf/README.md의 4파일 구조로 통일하였고, locals/for_each/count 패턴을 실제 모듈에 적용하여 환경 간 차이를 파라미터로 표현하였다. dev.tfvars, prod.tfvars로 환경별 설정을 외부화하고 terraform.tfvars(시크릿)는 .gitignore로 제외하였다. terraform fmt -recursive와 terraform validate로 dev/prod 양쪽 정합성을 검증하였다.
iOS 연동 측면에서는 iOS 앱의 BusArrivalService, BusStopService, BusArrivalResponse, StationByPosResponse 코드를 분석하여 모델 키를 식별하였다. 백엔드의 backend/app/schemas/bus.py를 iOS 모델과 1:1 일치시키고, backend/app/services/seoul_bus_api.py에 정규화 메서드(dict→list, itemCount 정수화, itemCount 자동 보정)를 추가하였다. backend/app/api/v1/bus.py의 엔드포인트를 /api/v1/bus/arrivals와 /api/v1/bus/stations로 정리하여 iOS 호출 파라미터(ars_id, tmX, tmY, radius)와 일치시켰다. 단위/통합 테스트 3개 파일을 새 스키마 기준으로 재작성하였다.
검증 도구 측면에서는 tools/ios-integration/healthcheck.sh로 백엔드 응답의 iOS 모델 필수 키 존재 여부를 검증하고, tools/ios-integration/fullstack_verify.sh로 Docker 백엔드 스택, /api/v1/health, /api/v1/bus/arrivals 라이브, /api/v1/bus/stations 라이브, terraform validate, iOS 시뮬레이터 앱 설치의 6개 항목을 일괄 검증하도록 구성하였다. 풀스택 검증 결과 PASS=10, FAIL=0으로 모든 항목이 통과하였다.
iOS 앱 측면에서는 BackendConfig.useBackend 분기를 추가하여 백엔드 프록시 전환 코드를 준비하였으며, 기존 서울 API 직접 호출 경로는 폴백으로 보존하였다. Info.plist에 평문 HTTP 허용 설정을 추가하여 개발 환경에서 백엔드 접근이 가능하도록 하였다.
최종적으로 7주차 대비 Terraform 모듈 8개와 환경별 진입점 2개, iOS 연동 검증 도구 2개, 백엔드 스키마/서비스/라우터 정합 변경 7개 파일, 테스트 재작성 3개 파일을 추가하여, 콘솔 기반 인프라를 코드로 재현 가능한 형태로 전환하고 iOS 앱과 백엔드 사이의 응답 형식 정합성을 확보하였다.
4. 기타(문제점, 해결방법, 자기평가 등)
4-1. 문제점 및 해결방법
Postgres 컨테이너 패스워드 충돌 문제가 발생하였다. 기존 Docker 볼륨이 다른 자격증명으로 초기화되어 있어 docker compose 기동 시 password authentication failed 에러가 반복되었다. docker compose down -v로 볼륨을 제거한 후, .env.dev의 POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB를 호스트 환경변수로 export하고 재기동하여 해결하였다. 컨테이너 환경변수가 초기화 시점에만 적용되고 이후에는 볼륨에 저장된 값이 우선되는 동작을 학습하였다.
서울 API의 itemCount=0 quirk를 발견하였다. 라이브 응답에서 itemCount 필드가 0으로 반환되지만 실제 itemList에는 24개의 노선이 포함된 케이스가 존재하였다. iOS 앱이 itemCount로 결과 유무를 분기하면 빈 결과로 오인되는 문제가 있었다. backend/app/services/seoul_bus_api.py의 _normalize_item_list에서 itemCount = len(item_list)로 자동 보정하도록 수정하여, iOS가 itemCount로 분기해도 정확한 결과를 받도록 해결하였다.
API 키의 사용 범위 혼동이 있었다. data.go.kr에서 새로 발급받은 키(2a1d258c…)가 도착정보 API에서 인증 실패하는 현상을 조사한 결과, 신규 키는 api.odcloud.kr/api/15067528의 정적 정류장 데이터셋용이며 도착정보/위치기반 정류소는 서울 TOPIS API(ws.bus.go.kr)에서 별도 키(29b4ab63…)를 사용해야 한다는 것을 확인하였다. backend/.env*에 두 키를 분리하여 보관하고, 서비스 레이어에서 엔드포인트별로 적절한 키를 선택하도록 정리하였다. iOS 앱은 두 키 모두 더 이상 직접 보유하지 않고 백엔드를 통해 접근하는 server-side 전용 모델로 전환하기로 결정하였다.
Terraform 모듈 분리 시 출력값 누락 문제가 있었다. environments/prod에서 network 모듈의 출력값(vpc_id, public_subnet_ids, private_subnet_ids)을 ingress 모듈에 전달해야 하는데, network 모듈의 outputs.tf에 일부 값이 누락되어 terraform validate에서 정의되지 않은 참조 에러가 발생하였다. 각 모듈의 outputs.tf를 점검하여 다른 모듈에서 필요로 하는 모든 값을 노출하도록 보강하였다. 모듈 간 의존성을 outputs/inputs로 명시적으로 표현하는 것이 IaC의 핵심임을 학습하였다.
iOS 시뮬레이터의 평문 HTTP 차단 문제가 발생하였다. iOS 14 이상의 App Transport Security가 기본적으로 평문 HTTP를 차단하기 때문에, 시뮬레이터에서 http://localhost:8000을 호출하면 즉시 실패하였다. Info.plist에 NSAppTransportSecurity.NSAllowsArbitraryLoads = true를 추가하여 개발 환경에서만 허용하도록 하였으며, 프로덕션 빌드에서는 해당 설정을 제거하고 ALB+ACM의 HTTPS만 사용하도록 분기 처리할 계획을 수립하였다.
4-2. 자기평가
가장 큰 성과는 7주차 콘솔 기반 인프라를 모듈 단위로 분해하여 Terraform 코드로 재현한 점이다. 단일 파일에 모든 리소스를 평면적으로 작성하던 방식에서 벗어나, 책임 단위로 8개 모듈을 분리하고 dev/prod 환경별 진입점을 두는 구조를 직접 설계하면서 모듈 경계를 어디에 두어야 하는지에 대한 감각을 익힐 수 있었다. 특히 network 모듈에 for_each를 도입하면서 count 인덱스 기반 리소스가 가지는 재생성 위험을 실제로 체감할 수 있었다.
iOS 앱과 백엔드 응답 형식의 정합성을 확보한 점도 의미 있었다. 8주차 지도교수 피드백을 반영하여 iOS 모델 코드를 먼저 분석하고, 백엔드를 그 형식에 맞춰 정렬하는 방향으로 작업하면서 클라이언트 우선 설계의 가치를 이해할 수 있었다. 정규화 로직(dict→list, itemCount 정수화, itemCount 자동 보정)을 백엔드 한 곳에서 처리하여 iOS는 단순한 디코더만 유지할 수 있도록 한 구성도 만족스러웠다.
풀스택 검증 스크립트를 작성하여 매 변경마다 iOS-백엔드-인프라 정합성을 자동 확인할 수 있는 체계를 갖춘 점도 성과였다. Docker 컨테이너 상태, 백엔드 헬스체크, 라이브 API 응답 키 누락 검사, terraform validate, iOS 시뮬레이터 앱 설치를 한 번에 검증하면서, 풀스택 프로젝트의 변경 영향도를 빠르게 파악할 수 있게 되었다.
8주차 지도교수 피드백을 적극 반영하여 iOS와 백엔드의 모델 정합을 완성한 점에서 피드백 수용 능력을 향상시켰다. 단순히 새 엔드포인트를 추가하는 데 그치지 않고, iOS 코드를 직접 읽어 모델 구조를 분석한 후 백엔드를 거기에 맞추는 역방향 설계를 시도한 점이 학습 측면에서 가치가 있었다.
아쉬운 점은 dev/prod 환경의 Terraform 코드를 작성하긴 했지만 실제 dev 환경에 terraform apply로 배포하지는 못한 점이다. terraform validate 통과까지만 검증하고 실제 AWS 리소스 생성/소멸 사이클은 다음 주차로 미루었다. 또한 iOS 앱에서 BackendConfig.useBackend = false로 두어 실제 백엔드 호출은 폴백 경로(서울 API 직접 호출)로 동작하고 있으며, 시뮬레이터에서 useBackend = true로 전환한 UI 검증은 다음 주차에 진행할 계획이다. 향후 fullstack_verify.sh를 GitHub Actions에 통합하여 매 PR마다 자동 검증되도록 구성하고, iOS xcconfig의 API_KEY를 제거하여 클라이언트에서 키가 노출되지 않도록 정리할 예정이다.
반응형
'Infra > 드림학기제' 카테고리의 다른 글
| 11주차 활동 내용 (0) | 2026.05.12 |
|---|---|
| 10주차 활동 내용 (0) | 2026.05.08 |
| 7주차 활동 내용 (2) | 2026.04.15 |
| 6주차 활동내용 (0) | 2026.04.09 |
| 5주차 활동내용 (0) | 2026.03.31 |