네크워크 공부

FastAPI 백엔드 API 개발 및 프론트 대시보드 연동 기록

otopligrm 2026. 5. 19. 00:01

초기에는 프론트엔드 대시보드가 더미데이터 또는 브라우저 localStorage 기반으로 동작하고 있었다. 즉, 사용자가 시험 정보를 입력하더라도 실제 DB에 저장되지 않았고, 학생이 VDI에 접속해도 대시보드의 좌석 상태가 실제로 바뀌지 않았다.

그래서 이번 개발의 핵심 목표는 다음과 같았다.

1. 사용자가 웹 페이지에서 시험 정보를 입력하면 DB에 저장한다.
2. 학생이 VDI 안에서 로그인하면 학생 정보가 DB에 저장된다.
3. 학생이 접속한 VDI를 식별해 해당 좌석 상태를 ACTIVE로 변경한다.
4. 교수 대시보드는 DB 값을 조회해 좌석 상태를 시각화한다.
5. 개발이 끝난 뒤에는 FastAPI 서버를 수동 실행하지 않고 systemd 서비스로 운영한다.
 

최종적으로 구현하고자 한 흐름은 다음과 같다.

사용자
/professor/ 접속
→ 교수 식별 정보 입력
→ 교수 정보 DB 저장 또는 수정

/exam/ 접속
→ 과목명, 시험 유형, 시작/종료 시간, 정원 입력
→ 시험 정보 DB 저장 또는 수정
→ 기존 VDI 좌석 row에 현재 시험 ID 연결

학생
VDI 접속
→ VDI 내부에서 /login/ 자동 실행
→ 이름과 학번 입력
→ 백엔드가 요청 IP를 기준으로 VDI 좌석 식별
→ 해당 좌석 상태를 ACTIVE로 변경

대시보드
/home/ 접속
→ 시험 정보와 VDI 세션 정보 조회
→ 미접속 좌석은 회색, 접속 좌석은 초록색으로 표시
 

2. 전체 시스템 구조

이번에 구성한 구조는 다음과 같다.

[교수자 PC / 학생 VDI 브라우저]
        ↓
[Nginx]
- 정적 파일 제공
- /login/
- /home/
- /professor/
- /exam/

        ↓ /api/ 요청 프록시

[FastAPI 백엔드]
- 교수 정보 저장
- 시험 정보 저장/수정
- 학생 로그인 처리
- 대시보드 조회 API 제공

        ↓

[MariaDB]
- 교수 정보
- 학생 정보
- 시험 정보
- VDI 세션 상태
- 상태 변경 이력 저장
 

Nginx는 웹 화면을 제공하고, /api/로 시작하는 요청은 FastAPI 백엔드로 전달한다. FastAPI는 DB와 연결되어 교수자, 학생, 시험, VDI 세션 데이터를 처리한다.


3. 사용 기술

이번 작업에서 사용한 주요 기술은 다음과 같다.

운영체제
- Rocky Linux 계열 서버

웹 서버
- Nginx

백엔드
- Python
- FastAPI
- Uvicorn
- PyMySQL
- python-dotenv

DB
- MariaDB

운영 관리
- systemd

프론트엔드
- HTML
- CSS
- JavaScript
- Fetch API
- localStorage
 

각 기술을 사용한 이유는 다음과 같다.

FastAPI
→ Python 기반으로 빠르게 REST API를 구현할 수 있고, 요청 모델 검증이 편리하다.

Uvicorn
→ FastAPI 앱을 실행하기 위한 ASGI 서버다.

PyMySQL
→ Python에서 MariaDB와 연결하기 위해 사용했다.

python-dotenv
→ DB 접속 정보와 상태값을 코드에 직접 쓰지 않고 .env 파일로 분리하기 위해 사용했다.

Nginx
→ 정적 HTML/CSS/JS 파일 제공과 API reverse proxy 역할을 동시에 수행하기 위해 사용했다.

systemd
→ FastAPI를 터미널 수동 실행이 아닌 운영 서비스로 등록하기 위해 사용했다.
 

4. 주요 DB 테이블 역할

프로젝트의 실제 DB 테이블명은 공개하지 않고, 블로그에서는 역할 중심으로 정리했다.

계정 테이블
→ 학생과 교수자의 공통 로그인/식별 정보를 저장한다.

학생 프로필 테이블
→ 학번, 이름 등 학생 정보를 저장한다.

교수 프로필 테이블
→ 교수자 이름, 소속 등 교수자 정보를 저장한다.

시험 정보 테이블
→ 과목명, 시험 유형, 시작 시간, 종료 시간, 정원 정보를 저장한다.

세션 상태 테이블
→ OUT, ACTIVE 등 VDI 상태 코드를 저장한다.

VDI 세션 테이블
→ 각 VDI 좌석의 IP, 좌석 번호, 현재 접속 학생, 상태를 저장한다.

상태 이력 테이블
→ VDI 상태가 변경될 때마다 이력을 저장한다.
 

이번 개발에서 가장 중요한 테이블은 VDI 세션 테이블이었다. 이 테이블은 처음에는 시험마다 새 세션 row를 만드는 방식으로 설계하려고 했지만, 최종적으로는 이미 등록된 고정 VDI row의 exam_id만 업데이트하는 방식으로 수정했다.


5. 백엔드 프로젝트 생성

FastAPI 백엔드 프로젝트는 서버의 애플리케이션 전용 디렉터리에 구성했다.

공개 블로그에서는 실제 경로 대신 다음처럼 표현한다.

 
mkdir -p <BACKEND_PROJECT_DIR>
cd <BACKEND_PROJECT_DIR>
 

Python 가상환경을 생성했다.

 
python3 -m venv venv
source venv/bin/activate
 

가상환경을 사용하는 이유는 서버 전체 Python 환경과 프로젝트 의존성을 분리하기 위해서다. 이렇게 하면 특정 프로젝트에서 설치한 패키지가 다른 프로젝트나 시스템 Python에 영향을 주지 않는다.


6. Python 패키지 설치

FastAPI 백엔드 개발을 위해 다음 패키지를 설치했다.

 
pip install fastapi uvicorn pymysql python-dotenv
 

각 패키지의 역할은 다음과 같다.

fastapi
→ API 서버 프레임워크

uvicorn
→ FastAPI 앱 실행 서버

pymysql
→ MariaDB 연결 라이브러리

python-dotenv
→ .env 파일에서 환경변수 로드
 

설치 후에는 main.py에서 FastAPI 앱을 구성하고, DB 접속 정보를 .env에서 불러오도록 구성했다.


7. 환경변수 파일 구성

DB 접속 정보는 코드에 직접 적지 않고 .env 파일로 분리했다.

공개용 예시는 다음과 같다.

 
DB_HOST=<DB_HOST>
DB_PORT=3306
DB_USER=<DB_USER>
DB_PASSWORD=<DB_PASSWORD>
DB_NAME=<DB_NAME>

STATE_OUT_ID=1
STATE_ACTIVE_ID=2
 

실제 개발 중에는 같은 서버 안에서 FastAPI와 MariaDB가 동작했기 때문에 DB 호스트는 내부 접속 주소를 사용했다. 공개 블로그에서는 실제 DB명이나 계정명, 비밀번호 유무를 절대 노출하지 않는다.

.env 파일은 민감 정보를 포함하므로 권한도 제한했다.

프론트엔드 개발을 할때에도 민감한 정보들은 전부 env에 넣어서 작업을 하고 
gitignore에 env를 포함시키면서 작업을 했었다.
chmod 600 <BACKEND_PROJECT_DIR>/.env
 

8. DB 연결 확인

DB 연결 전에는 MariaDB에 직접 접속해 데이터베이스와 테이블 상태를 확인했다.

 
mysql -u <DB_USER> -p
 

DB 선택:

 
USE <DB_NAME>;
SHOW TABLES;
 

이 단계에서 확인한 것은 다음과 같다.

1. 프로젝트용 DB가 존재하는지
2. 계정/교수/학생/시험/VDI 세션 관련 테이블이 존재하는지
3. VDI 세션 테이블에 고정 VDI row가 미리 등록되어 있는지
 

초기에는 DB 이름을 잘못 입력해 접속 오류가 발생했다. 이 경험을 통해 실제 운영에서는 DB명과 .env 값이 정확히 일치하는지 먼저 확인해야 한다는 것을 알 수 있었다.


9. 한글 저장 오류와 문자셋 수정

교수자 이름, 학생 이름, 학과명, 과목명 등 한글 데이터가 DB에 저장되어야 했다. 그런데 초기 테스트 중 다음과 같은 오류가 발생했다.

Incorrect string value
 

원인은 일부 DB 또는 테이블의 문자셋이 한글 저장에 적합하지 않았기 때문이다.

그래서 주요 한글 입력 테이블을 utf8mb4 기반 문자셋으로 변환했다.

공개용 SQL 예시는 다음과 같다.

 
ALTER DATABASE <DB_NAME>
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

ALTER TABLE <USER_TABLE>
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

ALTER TABLE <STUDENT_PROFILE_TABLE>
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

ALTER TABLE <PROFESSOR_PROFILE_TABLE>
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

ALTER TABLE <EXAM_INFO_TABLE>
CONVERT TO CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
 

일부 테이블은 외래키 제약 때문에 바로 변환할 수 없었다. 예를 들어 세션 ID가 여러 테이블에서 외래키로 참조되고 있으면 컬럼 변경이 제한된다. 이 경우에는 한글이 실제로 저장되는 주요 테이블을 우선 변환하고, FK가 걸린 테이블은 별도 절차가 필요하다고 판단했다.


10. FastAPI 기본 구조

백엔드의 핵심 파일은 main.py였다.

구성한 API는 다음과 같다.

GET /api/health
→ 서버 상태 확인

POST /api/professor/setup
→ 교수 정보 저장 또는 수정

POST /api/exam/setup
→ 시험 정보 저장 또는 수정

GET /api/exam/latest
→ 최신 시험 정보 조회

POST /api/vdi/login
→ 학생 VDI 로그인

POST /api/vdi/heartbeat
→ 학생 VDI 접속 상태 갱신

GET /api/admin/exam
→ 대시보드 시험 정보 조회

GET /api/admin/sessions
→ 대시보드 좌석 목록 조회

GET /api/admin/summary
→ 대시보드 요약 정보 조회
 

초기에는 API 서버를 직접 다음 명령어로 실행했다.

 
uvicorn main:app --host 127.0.0.1 --port 8000
 

하지만 운영 단계에서는 이 방식을 사용하지 않고 systemd 서비스로 등록했다.


11. JSON 직렬화 오류 해결

대시보드에서 요약 API를 호출할 때 다음 오류가 발생했다.

Object of type Decimal is not JSON serializable
 

원인은 MariaDB에서 COUNT, SUM 등의 결과가 Python의 Decimal 타입으로 넘어왔고, FastAPI의 JSONResponse가 이를 바로 JSON으로 변환하지 못했기 때문이다.

해결 방향은 다음과 같았다.

1. Decimal 값을 int 또는 float로 변환한다.
2. datetime 값은 문자열로 변환한다.
3. dict/list 내부에 중첩된 값까지 재귀적으로 변환한다.
 

공개용 예시 코드는 다음과 같다.

 
def json_safe(value):
    if value is None:
        return None

    if isinstance(value, Decimal):
        if value == value.to_integral_value():
            return int(value)
        return float(value)

    if hasattr(value, "isoformat"):
        return value.isoformat(sep=" ", timespec="seconds")

    return value


def to_jsonable(value):
    if isinstance(value, dict):
        return {key: to_jsonable(val) for key, val in value.items()}

    if isinstance(value, list):
        return [to_jsonable(item) for item in value]

    return json_safe(value)
 

이후 모든 API 응답을 만들 때 to_jsonable()을 거치도록 수정해 Decimal 직렬화 오류를 해결했다.


12. Nginx와 FastAPI 연결

Nginx는 정적 파일을 제공하고, /api/ 요청만 FastAPI로 전달하도록 설정했다.

공개용 Nginx 설정 예시는 다음과 같다.

 
location /api/ {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
 

이 설정을 추가한 이유는 다음과 같다.

/login/, /home/, /professor/, /exam/
→ Nginx가 정적 HTML/CSS/JS 제공

/api/...
→ Nginx가 FastAPI로 프록시 전달
 

특히 X-Real-IP를 전달한 이유가 중요하다. 학생들이 VDI 내부에서 같은 로그인 URL로 접속할 때, 백엔드가 실제 요청 IP를 확인해야 어느 VDI에서 로그인했는지 식별할 수 있기 때문이다.

설정 변경 후에는 다음 명령어로 문법을 확인하고 적용했다.

 
nginx -t
systemctl reload nginx
 

13. Nginx 편집 중 swap 파일 오류

Nginx 설정 파일을 편집하다가 vi swap 파일 경고가 발생했다.

원인은 이전 편집 세션이 비정상 종료되었거나, 누군가 같은 파일을 동시에 편집했기 때문이었다.

해결 절차는 다음과 같았다.

1. 현재 해당 파일을 편집 중인 프로세스가 있는지 확인
2. 실제 편집 중인 사람이 없는지 확인
3. 설정 파일 백업
4. swap 파일 삭제
5. 다시 편집
 

사용한 명령어는 다음과 같다.

 
ps -ef | grep <CONFIG_FILE_NAME>
who
cp <NGINX_CONFIG_PATH> <NGINX_CONFIG_PATH>.bak_$(date +%Y%m%d_%H%M%S)
rm -f <NGINX_SWAP_FILE>
 

여러 명이 같은 서버에서 작업할 때는 이런 충돌이 자주 발생할 수 있으므로, 설정 파일은 동시에 편집하지 않도록 규칙을 정하는 것이 중요하다.


14. Nginx 50x 오류 해결

초기에는 /api/health를 호출했을 때 Nginx 기본 50x 페이지가 보였다.

가능한 원인은 다음과 같았다.

1. FastAPI가 8000번 포트에서 실행 중이 아님
2. Nginx가 FastAPI로 프록시하지 못함
3. Nginx 설정 변경 후 reload를 하지 않음
4. 방화벽 또는 SELinux 문제
 

확인 순서는 다음과 같았다.

 
curl http://127.0.0.1:8000/api/health
curl http://127.0.0.1/api/health
 

판단 기준은 다음과 같다.

127.0.0.1:8000이 실패
→ FastAPI 자체가 꺼져 있음

127.0.0.1:8000은 성공하지만 127.0.0.1/api/health가 실패
→ Nginx 프록시 설정 문제
 

15. 학생 로그인 방식 변경

처음에는 학생 로그인 URL에 좌석 번호를 붙이는 방식을 생각했다.

/login/?exam_id=<EXAM_ID>&seat_no=<SEAT_NO>&vdi_name=<VDI_NAME>
 

하지만 실제 운영에서는 학생에게 URL을 다르게 제공하는 구조가 적합하지 않았다. 학생들은 이미 각자 다른 VDI IP/PW로 접속하므로, VDI 안에서는 모두 같은 로그인 페이지가 뜨는 것이 더 자연스러웠다.

최종 방식은 다음과 같다.

모든 VDI
→ 같은 /login/ URL 사용

학생 로그인
→ 이름/학번 입력

백엔드
→ 요청 IP 확인
→ VDI_SESSION.vdi_ip와 매칭
→ 해당 VDI row ACTIVE 처리
 

즉 seat_no 기반 로그인에서 request_ip 기반 로그인으로 변경했다.


16. VDI 내부 IP 확인

같은 로그인 URL을 사용하려면, 서버에서 VDI별 IP가 다르게 보이는지 확인해야 했다.

서버에서 Nginx access log를 확인했다.

 
tail -f /var/log/nginx/access.log
 

VDI 여러 대에서 /login/에 접속한 뒤 로그를 확인했다.

공개용 로그 예시는 다음과 같이 마스킹했다.

<VDI_PRIVATE_IP_A> - - [timestamp] "GET /login/ HTTP/1.1" 200
<VDI_PRIVATE_IP_B> - - [timestamp] "GET /login/ HTTP/1.1" 200
 

로그에서 VDI별 내부 IP가 다르게 찍히는 것을 확인했고, 따라서 vdi_ip 기반 매핑이 가능하다고 판단했다.


17. VDI 내부 접속 URL 결정

VDI가 내부 NAT 네트워크에 있기 때문에 외부 공인 IP로 접속하는 것이 항상 안정적이지는 않았다. 테스트 결과, VDI 내부에서는 VDI 게이트웨이 또는 호스트 내부 주소를 사용하는 것이 더 적합했다.

공개용 표현:

교수자 또는 외부 관리 PC
→ http://<SERVER_PUBLIC_IP>/home/

학생 VDI 내부
→ http://<VDI_GATEWAY_IP>/login/
 

즉 교수자와 학생 VDI의 접속 URL을 다르게 운용할 수 있다.


18. VDI_SESSION 사용 방식 수정

가장 중요한 설계 변경은 VDI_SESSION 처리 방식이었다.

초기 방식은 시험 생성 시 다음과 같은 새 row를 만드는 것이었다.

EXAM<EXAM_ID>-VDI01
EXAM<EXAM_ID>-VDI02
EXAM<EXAM_ID>-VDI03
 

하지만 실제 DB에는 이미 고정 VDI row가 등록되어 있었다.

공개용 예시:

VDI-1 | seat_no=1 | vdi_ip=<VDI_PRIVATE_IP_1>
VDI-2 | seat_no=2 | vdi_ip=<VDI_PRIVATE_IP_2>
VDI-3 | seat_no=3 | vdi_ip=<VDI_PRIVATE_IP_3>
 

따라서 최종 방식은 다음처럼 수정했다.

시험 생성/수정 시 새 VDI_SESSION row 생성 X

기존 VDI-1, VDI-2, VDI-3 row 유지

해당 row들의 exam_id만 현재 시험 ID로 UPDATE
 

이렇게 하면 VDI IP 매핑 정보를 잃지 않고, 학생 로그인 시 request_ip와 vdi_ip를 비교해 좌석을 찾을 수 있다.


19. 시험 정보가 계속 추가되는 문제 해결

초기에는 /exam/에서 시험 정보를 저장할 때마다 EXAM_INFO에 새 row가 계속 추가되었다.

원인은 API가 매번 INSERT만 수행했기 때문이다.

수정 방향은 다음과 같다.

1. 프론트에서 localStorage에 exam_id가 있으면 함께 전송한다.
2. 백엔드는 exam_id가 있으면 해당 시험 UPDATE
3. exam_id가 없으면 해당 교수의 최신 시험 조회
4. 최신 시험이 있으면 UPDATE
5. 기존 시험이 전혀 없으면 최초 1회만 INSERT
 

이렇게 수정한 이유는 교수자가 시험 정보를 다시 입력할 때마다 새로운 시험이 생성되는 것이 아니라, 같은 시험 정보가 수정되는 것이 더 자연스럽기 때문이다.


20. /api/exam/setup 최종 정책

최종 /api/exam/setup의 정책은 다음과 같다.

1. 교수 정보가 존재하는지 확인한다.
2. exam_id가 전달되면 해당 시험을 찾는다.
3. exam_id가 없으면 해당 교수의 최신 시험을 찾는다.
4. 기존 시험이 있으면 EXAM_INFO를 UPDATE한다.
5. 없으면 최초 1회 INSERT한다.
6. 이전 로직으로 잘못 생성된 EXAM* 세션 row를 정리한다.
7. 기존 VDI-1~VDI-N row에 현재 exam_id를 연결한다.
8. 정원보다 큰 VDI row는 이번 시험에서 제외한다.
 

여기서 핵심은 새 VDI_SESSION row를 만들지 않는 것이다.


21. 학생 로그인 API 최종 정책

최종 /api/vdi/login의 정책은 다음과 같다.

1. 이름 검증
   - 공백 제거
   - 4글자 이하

2. 학번 검증
   - 숫자만 허용
   - 9자리

3. 시험 정보 존재 확인

4. 계정 테이블에서 학생 계정 조회
   - 없으면 생성
   - 있으면 재사용

5. 학생 프로필 조회
   - 없으면 생성
   - 있으면 이름 업데이트 가능

6. VDI 세션 조회
   - seat_no가 있으면 exam_id + seat_no로 조회
   - seat_no가 없으면 exam_id + request_ip로 조회

7. 이미 다른 학생이 사용 중이면 차단

8. 정상 로그인 시
   - student_id 연결
   - state_id를 ACTIVE로 변경
   - started_at, last_heartbeat 업데이트
   - 상태 변경 이력 기록
 

학생은 같은 /login/ URL을 사용하고, 실제 VDI 구분은 백엔드에서 요청 IP로 처리한다.


22. 학생 로그인 예외처리 문제

학생 로그인 화면에서 학번을 9자리 입력해도 8자리로 인식되는 문제가 있었다.

원인은 HTML input의 maxlength="9"였다.

예를 들어 사용자가 다음 값을 붙여넣는 경우:

2024-12345
 

브라우저가 먼저 9글자로 자르면:

2024-1234
 

가 되고, JS가 숫자만 추출하면 8자리가 된다.

그래서 해결 방식은 다음과 같다.

1. HTML input에서 maxlength 제거
2. JS에서 숫자만 추출
3. JS에서 slice(0, 9)로 제한
4. 최종적으로 정확히 9자리인지 검증
 

공개용 JS 로직 예시는 다음과 같다.

 
function normalizeStudentNo(value) {
  return String(value || "").replace(/\D/g, "").slice(0, 9);
}
 

그리고 기존에는 seat_no가 없으면 로그인 자체를 막았지만, 최종 구조에서는 같은 URL을 사용하므로 해당 검사를 제거했다.


23. 로그인 페이지 localStorage 사용

로그인 성공 후 브라우저에는 다음 정보를 저장했다.

vdi_session_id
student_no
student_name
exam_id
seat_no
vdi_ip
request_ip
 

공개 블로그에서는 실제 값은 노출하지 않고, 역할만 설명한다.

이 값을 저장한 이유는 이후 heartbeat 또는 세션 연동 기능에서 현재 로그인 세션을 계속 추적하기 위해서다.

또한 브라우저 UUID도 생성했다.

vdi_browser_uuid
 

단, 브라우저 UUID는 내부 API 요청 식별에는 사용할 수 있지만, 일반 웹페이지 JS만으로는 외부 사이트 전체 요청에 헤더를 삽입할 수 없기 때문에 프록시 전체 트래픽 식별의 핵심 수단으로는 VDI IP 매핑이 더 적합하다고 판단했다.


24. 대시보드 API 연동

대시보드는 다음 API를 호출한다.

GET /api/admin/exam?exam_id=<EXAM_ID>
→ 시험 정보 조회

GET /api/admin/sessions?exam_id=<EXAM_ID>
→ 좌석별 VDI 세션 목록 조회

GET /api/admin/summary?exam_id=<EXAM_ID>
→ 전체 좌석 수, 접속 수, 미접속 수 조회
 

좌석 상태 색상은 다음 기준으로 설계했다.

OUT
→ 회색

ACTIVE
→ 초록

주의/재접속
→ 노랑

AI 탐지/차단
→ 빨강
 

현재 구현 단계에서는 OUT, ACTIVE를 우선 사용했다. 추후 프록시나 탐지 로직과 연결하면 AI 탐지 상태를 추가할 수 있다.


25. FastAPI systemd 서비스 등록

개발 초기에는 터미널에서 직접 FastAPI를 실행했다.

 
uvicorn main:app --host 127.0.0.1 --port 8000
 

하지만 이 방식은 터미널이 닫히면 서버도 종료된다. 실제 시험 환경에서는 부적합하다.

그래서 systemd 서비스로 등록했다.

공개용 서비스 파일 예시는 다음과 같다.

 
[Unit]
Description=VDI Login FastAPI Backend
After=network.target mariadb.service

[Service]
WorkingDirectory=<BACKEND_PROJECT_DIR>
EnvironmentFile=<BACKEND_PROJECT_DIR>/.env
ExecStart=<BACKEND_PROJECT_DIR>/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=3
User=<SERVICE_USER>

[Install]
WantedBy=multi-user.target
 

등록 명령어:

 
systemctl daemon-reload
systemctl start <FASTAPI_SERVICE_NAME>
systemctl enable <FASTAPI_SERVICE_NAME>
systemctl status <FASTAPI_SERVICE_NAME>
 

이제 서버가 재부팅되어도 FastAPI가 자동 실행된다.


26. 포트 중복 오류

서비스 등록 후 직접 uvicorn을 다시 실행하자 다음 오류가 발생했다.

address already in use
 

원인은 systemd 서비스가 이미 8000번 포트를 사용 중인데, 같은 포트로 FastAPI를 또 실행하려 했기 때문이다.

확인 명령어:

 
ss -ltnp | grep 8000
systemctl status <FASTAPI_SERVICE_NAME>
 

이후 운영 방식은 다음처럼 정리했다.

직접 uvicorn 실행 X

코드 수정 후:
python -m py_compile main.py
systemctl restart <FASTAPI_SERVICE_NAME>
systemctl status <FASTAPI_SERVICE_NAME>
 

27. 여러 명이 같은 서버에서 개발할 때의 문제

팀원 여러 명이 같은 서버에서 작업하면서 다음 문제가 발생할 수 있었다.

1. 같은 포트에 FastAPI 중복 실행
2. 같은 설정 파일 동시 편집
3. vi swap 파일 발생
4. main.py 수정 중 다른 사람이 서비스 재시작
5. DB 데이터를 서로 덮어씀
 

그래서 작업 규칙을 정리했다.

1. FastAPI 실행은 systemd 서비스 하나만 사용한다.
2. uvicorn 직접 실행은 금지한다.
3. main.py 수정 전 백업한다.
4. 수정 후 문법 검사를 한다.
5. 이상 없을 때만 서비스를 재시작한다.
6. DB UPDATE/DELETE 전 SELECT로 대상 row를 먼저 확인한다.
7. Nginx 설정 파일은 동시에 편집하지 않는다.
 

백업 명령어 예시는 다음과 같다.

 
cp <BACKEND_PROJECT_DIR>/main.py <BACKEND_PROJECT_DIR>/main.py.bak_$(date +%Y%m%d_%H%M%S)
cp <LOGIN_JS_PATH> <LOGIN_JS_PATH>.bak_$(date +%Y%m%d_%H%M%S)
cp <EXAM_JS_PATH> <EXAM_JS_PATH>.bak_$(date +%Y%m%d_%H%M%S)
 

28. 운영 전 점검 명령어

실제 사용 전에는 다음 순서로 확인한다.

 
systemctl status nginx
systemctl status mariadb
systemctl status <FASTAPI_SERVICE_NAME>
 

포트 확인:

 
ss -ltnp | grep ':80'
ss -ltnp | grep ':8000'
 

API 확인:

 
curl http://127.0.0.1/api/health
curl http://<SERVER_PUBLIC_IP>/api/health
 

VDI 내부에서는 다음 주소로 확인한다.

http://<VDI_GATEWAY_IP>/api/health
 

로그 확인:

 
tail -f <NGINX_ACCESS_LOG_PATH>
journalctl -u <FASTAPI_SERVICE_NAME> -f
 

29. 정상 DB 상태 예시

최종적으로 VDI 세션 테이블은 다음과 같은 형태가 되어야 한다.

session_id | exam_id     | student_id | state_id | seat_no | vdi_ip
VDI-1      | <EXAM_ID>   | NULL       | OUT      | 1       | <VDI_PRIVATE_IP_1>
VDI-2      | <EXAM_ID>   | NULL       | OUT      | 2       | <VDI_PRIVATE_IP_2>
VDI-3      | <EXAM_ID>   | NULL       | OUT      | 3       | <VDI_PRIVATE_IP_3>
 

학생이 두 번째 VDI에서 로그인하면 다음처럼 바뀐다.

session_id | exam_id   | student_id     | state_id | seat_no | vdi_ip
VDI-2      | <EXAM_ID> | <STUDENT_ID>   | ACTIVE   | 2       | <VDI_PRIVATE_IP_2>
 

대시보드에서는 해당 좌석이 회색에서 초록색으로 바뀐다.


30. 최종 사용자 흐름

최종 사용자 흐름은 다음과 같다.

1. 서버 부팅
2. Nginx, MariaDB, FastAPI 서비스 자동 실행
3. 교수자 /professor/ 접속
4. 교수 정보 입력
5. 교수자 /exam/ 접속
6. 시험 정보 입력
7. 기존 VDI row에 현재 exam_id 연결
8. 학생이 VDI 접속
9. VDI 내부에서 /login/ 자동 실행
10. 학생 이름/학번 입력
11. FastAPI가 요청 IP 확인
12. VDI 세션 테이블의 vdi_ip와 매칭
13. 해당 VDI row를 ACTIVE로 변경
14. 대시보드가 세션 API 조회
15. 해당 좌석이 초록색으로 표시
 

31. 이번 개발에서 발생한 주요 오류와 해결

1) DB 이름 오류

원인:
.env 또는 SQL에서 실제 DB 이름과 다른 값을 사용했다.

해결:
SHOW DATABASES로 실제 DB 이름을 확인하고 .env 값을 맞췄다.
 

2) 한글 저장 오류

원인:
DB 또는 테이블 문자셋이 한글 저장에 적합하지 않았다.

해결:
주요 한글 저장 테이블을 utf8mb4 계열 문자셋으로 변환했다.
 

3) Decimal JSON 변환 오류

원인:
MariaDB 집계 결과가 Decimal 타입으로 반환되었고 JSONResponse가 변환하지 못했다.

해결:
Decimal, datetime을 JSON 가능한 값으로 바꾸는 변환 함수를 만들었다.
 

4) Nginx 50x 오류

원인:
FastAPI가 꺼져 있거나 Nginx 프록시 설정이 올바르지 않았다.

해결:
FastAPI 직접 호출과 Nginx 경유 호출을 분리해서 확인했다.
 

5) vi swap 파일 오류

원인:
설정 파일 편집 세션이 비정상 종료되었거나 동시 편집 가능성이 있었다.

해결:
프로세스 확인 후 백업하고 swap 파일을 정리했다.
 

6) address already in use

원인:
FastAPI 서비스가 이미 8000번 포트를 사용 중인데 직접 uvicorn을 다시 실행했다.

해결:
직접 uvicorn 실행을 중단하고 systemd 서비스로만 관리했다.
 

7) 학생 로그인 404

원인:
API 경로 자체가 없어서가 아니라, 요청 IP와 VDI 세션 테이블의 vdi_ip가 매칭되지 않았다.

해결:
VDI access log에서 실제 요청 IP를 확인하고, VDI 세션 테이블의 vdi_ip와 exam_id를 맞췄다.
 

8) 학번 9자리 검증 오류

원인:
HTML maxlength가 먼저 적용되어 붙여넣기 값이 잘리고, JS에서 숫자만 추출하면서 8자리로 인식되었다.

해결:
HTML maxlength를 제거하고 JS에서 숫자만 추출한 뒤 9자리 검증을 수행했다.
 

32. 보안상 공개하지 않은 정보

공개 블로그에는 다음 정보는 실제 값으로 적지 않았다.

실제 서버 공인 IP
실제 VDI 내부 IP
실제 DB 이름
실제 DB 계정
실제 프로젝트 경로
실제 로그 원문
실제 학생 이름
실제 학번
실제 교수 식별번호
실제 Nginx 설정 파일 경로
실제 systemd 서비스명
 

대신 다음과 같이 마스킹했다.

<SERVER_PUBLIC_IP>
<VDI_GATEWAY_IP>
<VDI_PRIVATE_IP>
<DB_NAME>
<DB_USER>
<BACKEND_PROJECT_DIR>
<FASTAPI_SERVICE_NAME>
<STUDENT_ID>
<PROFESSOR_ID>
 

33. 최종 정리

이번 작업을 통해 기존 프론트엔드 중심 대시보드는 실제 DB와 연결된 VDI 시험 감독 대시보드로 발전했다.

핵심 성과는 다음과 같다.

1. FastAPI 기반 백엔드 API 구축
2. MariaDB와 교수/학생/시험/VDI 세션 데이터 연동
3. Nginx reverse proxy 설정
4. VDI 내부 IP 기반 좌석 식별 구조 설계
5. 교수 대시보드와 DB 상태 연동
6. 학생 로그인 시 좌석 상태 자동 변경
7. FastAPI systemd 서비스 등록
8. 실제 운영을 고려한 서버 관리 방식 정리
 

처음에는 단순히 학생 이름과 학번을 DB에 저장하는 기능처럼 보였지만, 실제 구현 과정에서는 VDI 네트워크 구조, NAT 환경, Nginx reverse proxy, DB 세션 설계, systemd 운영 방식까지 함께 고려해야 했다.

특히 가장 중요한 설계 변경은 학생별 URL을 따로 주지 않고, 같은 로그인 URL을 사용하면서 요청 IP 기반으로 VDI 좌석을 식별하도록 만든 것이었다.

이 구조 덕분에 학생은 복잡한 URL을 알 필요 없이 VDI에 접속해 이름과 학번만 입력하면 되고, 교수자는 대시보드에서 실시간으로 어떤 좌석이 접속했는지 확인할 수 있다.

향후에는 프록시 로그 또는 정책 위반 탐지 결과를 VDI 세션과 연결해, 단순 접속 상태뿐 아니라 시험 중 이상 행위 탐지 결과까지 대시보드와 최종 보고서에 연결할 수 있다.