백엔드갈아엎기 시리즈초기 기록

Spring Boot 첫 배포에 깔린 보안 빚 — POP-SPOT v1.0 회고

김동현
··3분 읽기

GCP VM · Oracle · Gemini Free 로 빠르게 띄운 첫 배포. 시크릿 평문 커밋, JWT default_secret, CORS *, BCrypt 10 — v1.1 에서 한 번에 갚을 기술 빚을 정리한다.

넥스트아이티 교육센터에서 Spring Boot 와 Next.js 를 배우면서 시작한 첫 배포다. "일단 가입·로그인·팝업 조회만 돌면 된다" 는 기준으로 빠르게 만들었고, 그 대가로 다음 버전에서 한 번에 갚게 될 기술 빚을 가득 안고 띄웠음.

이 글에서 다루는 것

  • POP-SPOT 의 출발점 — 무엇을 만들려고 했고 무엇을 미뤘는지
  • v1.0 의 스택 (GCP VM · Oracle · Gemini) 과 각 선택의 이유
  • 다음 버전(v1.1)에서 한 번에 갚게 될 "빚" 의 정체 — 평문 시크릿, JWT default_secret, CORS *, BCrypt 10
  • "동작은 했지만 안전하진 않았다" 라는 첫 배포의 흔한 함정
  • 모르는 단어 한 줄로

    용어한 줄 설명
    JWT로그인 한 사람이라는 걸 증명하는 짧은 문자열. 사이트 출입증과 같음
    CORS브라우저가 "이 사이트가 다른 도메인 호출해도 되냐" 미리 묻는 규칙
    BCrypt비밀번호를 원본 그대로 저장하지 않고 한 방향으로 뒤섞는 함수. strength 가 클수록 깨기 어렵다
    Sequence (Oracle)"다음 번호" 를 발급해주는 DB 객체. INSERT 때마다 손으로 가져와 넣어줘야 한다
    RPMRequests Per Minute. 1 분에 보낼 수 있는 요청 수. 무료 AI 의 한도 단위로 자주 쓰인다

    무엇이 바뀌었나

    v1.0 은 "처음 만든 것" 이라 비교할 이전 버전이 없다. 어떻게 생긴 시스템이었는지만 정리한다.

    레이어v1.0
    프론트Vercel · Next.js
    백엔드GCP Compute Engine · Spring Boot 3.x · 8080 포트 직접 노출
    DBOracle (Sequence 직접 관리)
    AIGemini Free — 분당 10 회 (RPM 10)
    인증JWT (시크릿 default_secret) · OAuth2 (카카오·네이버·구글)
    비밀번호BCrypt strength 10
    관측없음. System.out 과 콘솔만

    스택 선정 이유

  • Spring Boot — 넥스트아이티 교육센터 풀스택 과정에서 가장 많이 다뤘고, 자료가 많아 동작 보장이 빠르다.
  • Next.js — 검색 노출이 중요한 서비스 (팝업스토어 정보) 라 서버 렌더링이 기본인 Next 가 적합했음. 같은 교육 과정에서 React 와 함께 익혔다.
  • Oracle — 교육 과정 실습 환경의 기본 DB 였다. 선택이라기보다는 흐름이었다.
  • Gemini Free — 그냥 무료라서. RPM 10 도 일단 충분하다고 봤음.

  • 코드로는 어떻게 (필요한 부분만)

    가장 위험했던 건 시크릿 평문이었다.

    javascript
    # v1.0 — application.properties 에 평문 시크릿. 깃 커밋까지 같이 됨
    
    jwt.secret=default_secret
    #           ^^^^^^^^^^^^^^ 라이브러리 예제의 디폴트 값. 같은 디폴트를 쓰는 누구도
    #                           동일한 토큰을 만들 수 있음 → 사실상 인증 없음
    
    gemini.api-key=AIzaSy_실제키_평문
    #              ^^^^^^^^^^^^^^^^^^^^^ 진짜 키를 그대로 텍스트로 적음
    #                                    깃에 올라가는 시각 Google 스캐너가 탐지
    
    spring.datasource.password=1234
    #                          ^^^^ DB 비밀번호. 레포 볼 수 있는 모든 사람에게 노출

    JWT 의 default_secret 은 라이브러리 예제 코드의 디폴트다. 즉 같은 디폴트를 쓰는 누구든 같은 토큰을 만들 수 있었다는 뜻.

    Oracle 의 Sequence 는 INSERT 할 때 직접 가져와 채워줘야 한다. 안 채우면 NULL 이 들어가서 ORA-01400 에러가 난다.

    파일: Oracle SQL·Plus / DataGrip 에서 직접 실행한 쿼리 (코드 파일 아닌 DB 콘솔 용)

    sql
    -- v1.0 — Oracle Sequence 를 손으로 다루는 구조
    
    SELECT chat_msg_seq.NEXTVAL FROM dual;
    --     ^^^^^^^^^^^^^         ^^^^ 한 행을 돌려주는 Oracle 의 더미 테이블
    --     |                          (SELECT 에 FROM 절 필수라 인위적으로 존재)
    --     +-- chat_msg_seq 가 있다면 다음 숫자를 발급해 준다 (예: 7)
    
    -- 그런 다음 그 7 을 가지고 INSERT 의 id 컬럼에 직접 넣어야 함
    -- INSERT INTO chat_messages (id, content) VALUES (7, '안녕');
    
    -- 근데 id 안 넣고 바로 INSERT 하면:
    --   INSERT INTO chat_messages (content) VALUES ('안녕');
    --   → ORA-01400: cannot insert NULL into ("PK"... id)
    --                 ↑ id 에 NULL 이 들어갈 수 없다. PK 는 NOT NULL

    v1.1 에서 PostgreSQL 의 IDENTITY (자동 채번) 로 바뀐다.


    비하인드 · 사고

    수료 과제 발표 끝나고 며칠 뒤, Google 의 시크릿 스캐너가 깃에 올라간 Gemini 키를 자동 감지했음. 그 결과 프로젝트 전체 quota 가 0 으로 강제 변경됨. 새 키를 발급해도 같은 프로젝트면 차단이 풀리지 않았다. v1.1 에서 결국 Groq 로 LLM 통째 이사를 가게 된 직접 원인이 이거다.

    교훈 한 줄. "동작했다" 와 "안전하다" 는 다른 말이다. v1.0 은 동작은 했고, 안전하진 않았다. 첫 배포 때 가장 흔히 빠지는 함정이라고 생각함.


    관련 글

  • 다음 글 — v1.1, 위 일곱 가지 빚을 한 번에 갚는 이야기
  • 공유

    댓글