Lab 04: 데이터·애플리케이션 계층과 완성
이 Lab의 흐름
S3의 화면이 부르고 있는 그 주소에, 이제 서버를 세웁니다.
서버 코드는
TODO로 비어 있습니다. 여러분이 채우는 게 아니라 Kiro에게 시킵니다.
학습 목표
여기서는 AI를 개발과 배포에 함께 씁니다. DB를 설치하는 일은 시켜서 하고, 비어 있는 서버 코드는 시켜서 채웁니다. 두 가지가 한 Lab 안에서 이어집니다.
- 데이터를 프로세스 밖(DB)으로 꺼내면 무엇이 달라지는지 확인합니다
- 손이 더 가는 대신 데이터 영속성을 얻는다는 것을 겪습니다
- DB 접속 정보를 서버에만 두는 이유를 이해합니다
- 3-Tier가 실제로 지속성·독립성·안전성을 주는지 직접 검증합니다
마지막 검증이 오늘의 결론입니다. 좋은 구조라는 말을 믿는 게 아니라, 서버를 껐다 켜고 개발자도구를 열어 내 눈으로 확인합니다.
Step 1: 데이터 계층 — MySQL 설치
Kiro에게 아래를 붙여넣습니다.
이 EC2(Amazon Linux 2023)에 MariaDB를 설치·실행하고,
`3.structured-quote-app/server/schema.sql`을 root로 실행해서 DB·테이블·계정을 만들어줘.Kiro가 실행하는 명령은 대략 이렇습니다. 직접 칠 필요는 없지만, 무슨 일이 벌어지는지는 알고 넘어갑니다.
sudo dnf install -y mariadb105-server
sudo systemctl enable --now mariadb
sudo mysql < /home/ec2-user/kiro-on-cloud/3.structured-quote-app/server/schema.sqlschema.sql 은 DB와 테이블을 만들고, 앱이 쓸 전용 계정까지 만듭니다. root 계정을 앱에 주지 않는 것이 기본입니다.
확인
초기 명언 2개가 들어갔는지 봅니다.
mysql -u appuser -papppassword quotes_db -e "SELECT * FROM quotes;"- MariaDB가 실행 중이다
-
quotes테이블에 명언 2개가 보인다
Step 2: 애플리케이션 계층 — FastAPI TODO 채우기
3.structured-quote-app/server/main.py 에는 구현이 비어 있는 TODO(KIRO) 가 세 개 있습니다. Kiro에게 채우게 합니다.
`3.structured-quote-app/server/main.py`의 `TODO(KIRO)` 세 개를 주석 설명대로 구현해줘.
MySQL의 `quotes` 테이블을 쓰고, 응답 형식은 1-Tier와 똑같이 유지해.마지막 문장이 핵심입니다. 응답 형식을 그대로 유지해야 Lab 03에서 이미 S3에 올린 화면이 수정 없이 붙습니다. 계층을 나눌 때 계층 사이의 약속(API 형식)을 지키는 것이 곧 호환입니다.
세 번째 TODO가 오늘의 포인트
/api/texts(저장된 명언 목록)를 보세요. 1-Tier에서는 메모리 배열 하나를 읽으면 끝이었지만, 서버를 껐다 켜면 그대로 사라졌습니다. 계층을 나누면 API 엔드포인트와 SQL 쿼리를 따로 만들어야 합니다. 그 대신 값이 DB에 남습니다. 서버를 재시작해도, 앱을 새로 띄워도 그대로 있습니다. 데이터 영속성을 갖게 되는 지점입니다.
화면은 이미 목록 UI를 갖고 있습니다. 서버만 채워지면 저장된 명언 N 버튼의 숫자가 살아납니다. 나눈다는 건 이렇게 없던 일이 생기는 것이기도 합니다.
-
main.py의 TODO 3개가 모두 구현됐다 - 응답 형식이 1-Tier와 같은지 코드에서 확인했다
Step 3: 서버 실행하고 8000 포트 열기
cd /home/ec2-user/kiro-on-cloud/3.structured-quote-app/server
cp .env.example .env
pip install -r requirements.txt
uvicorn main:app --host 0.0.0.0 --port 8000.env 의 값은 수정하지 않습니다. schema.sql 이 만든 계정과 이미 일치합니다.
두 가지를 기억합니다
--host 0.0.0.0 을 빼면 기본값이 127.0.0.1 이라 EC2 안에서만 닿습니다. S3에 올린 화면이 이 서버를 못 부릅니다.
.env 의 DB 비밀번호는 서버에만 둡니다. Step 4에서 이 비밀번호가 브라우저로 새지 않는 것을 직접 확인합니다.
확인
curl http://localhost:8000/api/textConnection refused 가 나온다면
Can't connect to MySQL server ... Connection refused 는 인증 문제가 아니라 3306 포트에서 아무도 듣고 있지 않다는 뜻입니다. MariaDB가 떠 있는지 확인합니다.
sudo systemctl status mariadb
sudo systemctl start mariadb 미션 ③ — 8000 포트 열기
curl 은 되는데 S3 화면에서는 아직 안 됩니다. 지금 응답을 받은 건 EC2 자기 자신이고, 밖에서 오는 요청은 여전히 막혀 있습니다. Lab 02에서 8080을 열었던 것과 똑같은 상황입니다.
이 EC2의 보안그룹에 TCP 8000 인바운드를 추가해줘.이제 S3 화면을 새로고침합니다
Lab 03에서 받은 S3 URL을 열고 다른 명언 보기 를 눌러봅니다. 동작합니다.
S3는 손대지 않았습니다. 다시 빌드하지도, 다시 올리지도 않았습니다. 화면이 부르던 주소에 서버가 나타나고 문이 열렸을 뿐입니다. 계층 사이의 약속이 주소와 응답 형식 두 가지뿐이라는 것이 여기서 드러납니다.
-
curl로 명언이 JSON으로 돌아왔다 -
--host 0.0.0.0으로 실행했다 - 8000 포트 인바운드가 추가됐다
- S3 화면에서 명언 받기와 추가가 동작한다
Step 4: 완성 확인 — 지속성 · 독립성 · 안전성
세 가지를 직접 해봅니다. 읽고 넘어가지 말고 손으로 합니다.
① 지속성
명언을 추가하고, FastAPI 서버를 재시작합니다(Ctrl+C 후 다시 uvicorn). 그리고 명언 받기를 눌러봅니다.
살아 있습니다. 데이터가 MySQL에 있기 때문입니다. Lab 02에서 사라졌던 그 데이터입니다.
② 독립성
서버(EC2)가 잠깐 죽어도 화면(S3)은 그대로 떠 있습니다. 계층이 분리됐기 때문입니다. 1-Tier였다면 화면까지 같이 죽었습니다.
Lab 03에서 이미 겪었습니다. 서버가 없어도 화면은 떴습니다. 그때는 미완성으로 보였지만, 사실 그게 독립성입니다.
③ 안전성
F12로 개발자도구를 열고 화면 코드를 다 뒤져봅니다. DB 비밀번호가 없습니다. .env 의 DB_PASSWORD 는 서버(EC2)에만 있습니다.
화면 코드로 나가는 건 next.config.js 의 env 에 적은 것뿐입니다. 번들에 담기는 건 전부 공개되기 때문에 프레임워크가 일부러 막아둔 것입니다.
위험한 일(DB 접속)은 닫힌 공간(서버)이 대신 합니다. 그래서 계층을 나눕니다.
- 서버를 재시작해도 추가한 명언이 남아 있다
- 서버가 죽어도 화면은 열린다
- 개발자도구에서 DB 비밀번호를 찾을 수 없다
막혔을 때
| 증상 | 원인 | 해결 |
|---|---|---|
| 브라우저에서 앱이 안 열림 | 보안그룹 포트가 안 열림 | Kiro에게 해당 포트 인바운드 추가 요청 |
포트 개방 실패 UnauthorizedOperation | 내 것이 아닌 보안그룹 | 인스턴스에 kiro-handson-내사용자명 이 붙어 있는지 확인 |
| 화면은 뜨는데 명언이 안 나옴 | 서버가 안 떠 있거나 SERVER_URL 없이 빌드함 (가장 흔함) | F12 Network에서 요청 주소를 확인. S3 주소로 가면 다시 빌드 후 재업로드 |
| S3 버킷 생성 실패 | 이름이 내 사용자명으로 시작하지 않음 | aws sts get-caller-identity 로 이름 확인 후 다시 |
Can't connect to MySQL ... Connection refused | MariaDB가 안 떠 있음 | sudo systemctl status mariadb 후 sudo systemctl start mariadb |
| 서버는 뜨는데 S3 화면에서만 실패 | --host 0.0.0.0 을 빼고 실행함 | uvicorn을 --host 0.0.0.0 으로 다시 실행 |
| EC2 재시작 후 화면이 먹통 | 퍼블릭 IP가 바뀜 | 새 IP로 재빌드 후 재업로드 |
어떤 에러든 Kiro에게 그대로 붙여넣는 것이 가장 빠릅니다. 에러 메시지 전체를 복사해서 “이 에러가 왜 나는지 알려주고 고쳐줘”라고 하면 됩니다. 요약하지 말고 통째로 붙여넣는 편이 낫습니다.
오늘 배운 것
- AI에게 시켜서 앱을 배포했습니다. 서버를 빌리고 포트를 여는 일에 더는 겁먹지 않습니다
- 한 덩어리(1-Tier) 는 빠르게 시작하기 좋고, 세 계층(3-Tier) 은 오래 버티기 좋습니다. 언제 무엇을 고를지 감이 생겼습니다
- 정적 배포와 동적 배포를 둘 다 손으로 했습니다. 올려두는 것과 띄워두는 것은 다른 일입니다
- 나누면 각자 독립적으로 살아남고, 비밀은 서버 안에 남습니다
- 나누는 순간 서로의 주소를 알아야 하고, 포트를 열어야 합니다 — 이게 클라우드 작업의 실체입니다
- AI에게 시키려면 무엇을 원하는지 말할 수 있어야 합니다. 그리고 결과는 내가 확인해야 합니다
- EC2 안에 비밀 키를 하나도 두지 않았습니다. IAM Role이 클라우드에서 권한을 다루는 정석입니다
한 걸음 더 — 실무는 어떻게 하나
오늘 DB는 EC2와 같은 서버 안에 있었습니다. 서버가 터지면 DB도 같이 갑니다.
실무에서는 DB를 별도 서버(AWS RDS) 로 분리합니다. 그러면 서버가 죽어도 데이터는 무사하고, 백업과 확장도 따로 할 수 있습니다. 데이터 계층을 물리적으로도 분리하는 것이 진짜 3-Tier입니다.
아키텍처를 더 깊이 파고드는 건 여기서부터입니다. 다만 오늘 확인한 것이 먼저입니다. 서버를 빌리고, 포트를 열고, 인터넷에 주소를 만드는 일을 말로 시켜서 끝까지 해냈다는 사실입니다.
부록: 저장소 구조
kiro-on-cloud/
├── 1.basic-game/ 워밍업용 단일 HTML 게임 4개
│
├── 2.single-app/ ① 한 덩어리 앱 (Next.js)
│ └── app/
│ ├── page.js 화면
│ ├── api/quotes-store.js 메모리 배열 (재시작하면 증발)
│ ├── api/text/route.js 명언 1개 조회 · 추가
│ └── api/texts/route.js 저장된 명언 목록
│
└── 3.structured-quote-app/ ② 세 계층으로 나눈 앱
├── client/ 프레젠테이션 — S3에 올릴 화면
│ ├── app/page.js API 라우트 없음. 서버에만 요청
│ ├── next.config.js output:"export" + SERVER_URL 주입
│ └── .env.example SERVER_URL 설정용
└── server/ 애플리케이션 — EC2에서 도는 API
├── main.py FastAPI 서버 (TODO 3개 ← Kiro가 채움)
├── schema.sql DB·테이블·계정 생성
├── requirements.txt
└── .env.example DB 접속 정보 (수정 불필요)직접 타이핑하는 코드는 없습니다. server/main.py 의 TODO 3개조차 Kiro가 채웁니다. 우리가 하는 일은 무엇을 원하는지 정확히 말하고, 결과를 확인하는 것입니다.
NxtCloud Workshop