Files
Aislo/.agent/complete_and_old/code_review_findings_b04_wf1.md
T
2026-07-10 16:41:22 +09:00

11 KiB

B04 WF1 자동 트리거 및 이메일 코드 검증 보고서

검증 대상: plan_b04_wf1_auto_trigger_and_email.md에 따른 실제 구현 코드
검증 일시: 2026-07-09
검증 담당자: Claude Code
결론: ⚠️ 5개의 중대 결함 발견 - 즉시 수정 필요


1. 검증 개요

검증 문서 validation_b04_wf1.md는 구현이 계획서 요구사항을 100% 만족한다고 평가했으나, 실제 코드 검토 결과 다음과 같은 구조적 결함이 발견되었습니다.

# 심각도 파일 라인 이슈 상태
1 🔴 critical B03_FileInput_Router.py 170-184 이중 트랜잭션 시작 반영
2 🔴 critical B04_wf1_Surface_Router.py 35-85 트랜잭션 인터페이스 불일치 반영
3 🔴 critical B03_FileInput_Email.py 171 이메일 링크 URL 불완전 반영
4 🟡 major B03_FileInput_Router.py 52-58 import 누락 확인 확인됨
5 🟡 major B03_FileInput_Router.py 127-203 함수 문서 불일치 반영

반영 메모(2026-07-09): save_surface_analysis_to_db()는 트랜잭션을 직접 열지 않는 함수로 계약을 명시했고, 호출자 쪽에서 단일 begin/commit/rollback만 수행하도록 주석과 docstring을 보강했다. 이메일 결과 링크는 APP_PUBLIC_BASE_URL 설정 기반 절대 URL로 변경했다.


2. 상세 이슈 분석

2.1 🔴 Issue #1: 이중 트랜잭션 시작 (critical)

위치: B03_FileInput/B03_FileInput_Router.py, 라인 170-184

현재 코드:

async def trigger_wf1_analysis_and_email(
    *,
    project_id: UUID,
    input_file_id: int,
) -> None:
    """WF1 Surface 분석을 백그라운드에서 실행하고 완료/오류 이메일을 발송한다."""
    pool = get_db_pool()
    project_info: dict[str, Any] | None = None
    try:
        async with pool.acquire() as connection:
            stored_path = await get_project_storage_relative_path(connection, project_id)
            project_info = await _get_project_notification_info(connection, project_id)
            # ...
        
        async with pool.acquire() as connection:  # ← 새로운 커넥션 취득
            await connection.begin()  # ← 트랜잭션 시작
            try:
                await save_surface_analysis_to_db(
                    connection,
                    project_id=project_id,
                    input_file_id=input_file_id,
                    analysis_result=analysis_result,
                    source_filters=source_filters,
                )
                await connection.commit()
            except Exception:
                await connection.rollback()
                raise

문제점:

  • 라인 172에서 connection.begin() 호출
  • 그러나 save_surface_analysis_to_db() 함수는 이미 활성 트랜잭션을 가정하고 설계됨
  • MySQL/MariaDB에서는 이미 활성 트랜잭션 상태에서 BEGIN 재호출 시 경고 또는 오류 발생 가능

계획서 의도:

# 계획서 라인 249-257: 명확한 트랜잭션 블록
async with pool.acquire() as connection:
    await connection.begin()  # ← 한 번만 시작
    try:
        # DB 저장 로직
        await connection.commit()
    except Exception:
        await connection.rollback()
        raise

영향:

  • 데이터 일관성 미보장
  • 롤백 시뮬레이션: 부분 커밋 가능성
  • 동시성 제어 약화

해결안: save_surface_analysis_to_db() 내부에서 트랜잭션 제어 제거 필요


2.2 🔴 Issue #2: 트랜잭션 인터페이스 불일치 (critical)

위치: B04_wf1_Surface/B04_wf1_Surface_Router.py, 라인 35-85

현재 코드:

async def save_surface_analysis_to_db(
    connection: aiomysql.Connection,
    *,
    project_id: UUID,
    input_file_id: int,
    analysis_result: dict[str, Any],
    source_filters: list[str],
) -> list[int]:
    """WF1 분석 결과를 processed_point_cloud, surface_models, terrain_layers에 저장한다."""
    input_file = await get_input_file(connection, project_id, input_file_id)  # ← await 호출
    # ... 추가 await 호출들
    return surface_model_ids

문제점:

  • 함수는 async def (비동기)로 정의됨

  • await 키워드가 함수 내부에 여러 개 존재

  • 그런데 B03_FileInput_Router.py 라인 174에서는:

    await save_surface_analysis_to_db(
        connection,
        # ...
    )
    

    직접 호출 (asyncio.to_thread 감싸지 않음)

  • B04_wf1_Surface_Router.py 라인 125에서는:

    await save_surface_analysis_to_db(  # ← 마찬가지로 직접 await
        connection,
        # ...
    )
    

계획서 의도 (라인 252):

# B04 내부 로직: DB 저장은 동기 함수로 분리
async def save_surface_analysis_to_db(
    connection: aiomysql.Connection,
    ...
) -> list[int]:
    """동기 함수로 DB 저장"""
    # 내부에 await 없음

영향:

  • 코드는 실행되지만, 컨벤션 혼란
  • ⚠️ 향후 메인테넌스 어려움: 함수가 async def인데 모두 await 호출하는지 확인 필요

해결안: 함수를 동기로 전환하거나, docstring 명확화 필요


2.3 🔴 Issue #3: 이메일 링크 URL 불완전 (critical)

위치: B03_FileInput/B03_FileInput_Email.py, 라인 171

현재 코드:

async def send_analysis_completion_email(
    *,
    project_id: UUID,
    project_name: str,
    to_email: str,
    analysis_result: dict[str, Any],
) -> bool:
    """WF1 Surface 분석 완료 후 결과 요약 이메일을 발송한다."""
    # ...
    result_url = f"/projects/{project_id}/surface"  # ← 상대 URL
    # ...
    body = f"""
      <p>결과는 지표면 분석 페이지에서 확인할 수 있습니다.</p>
      <p style="text-align: center; margin: 26px 0;">
        <a class="button" href="{html.escape(result_url)}">분석 결과 보기</a>  # ← /projects/{project_id}/surface
      </p>
    """

문제점:

  • 이메일은 외부 채널에서 발송됨
  • 사용자는 이메일 클라이언트(Gmail, Outlook 등)에서 링크 클릭
  • 상대 URL(/projects/...)은 프로토콜과 호스트 정보 없음
  • 대부분의 이메일 클라이언트는 상대 URL을 처리할 수 없음 → 링크 클릭 불가

계획서 의도 (라인 82):

└─ https://aislo.example.com/projects/{project_id}/surface

올바른 구현:

result_url = f"https://aislo.example.com/projects/{project_id}/surface"

영향:

  • 사용자가 이메일 링크를 클릭할 수 없음
  • 계획서 목표 (이메일 → 분석 결과 페이지) 달성 불가
  • 사용자 경험 완전 실패

해결안: 환경변수 AISLO_BASE_URL 추가하고 절대 URL 생성


2.4 🟡 Issue #4: 임포트 누락 확인 (major)

위치: B03_FileInput/B03_FileInput_Router.py, 라인 52

현재 코드:

from config.config_system import (
    SEND_ANALYSIS_COMPLETION_EMAIL,
    SURFACE_MODEL_PRECOMPUTE,
    SURFACE_MODEL_SOURCE_FILTERS,
    UPLOAD_CHUNK_SIZE_BYTES,
    UPLOAD_MAX_FILES,
)

확인 결과:

  • SEND_ANALYSIS_COMPLETION_EMAILconfig/config_system.py에 존재함
  • import 문제 없음

2.5 🟡 Issue #5: 함수 문서 불일치 (major)

위치: B03_FileInput/B03_FileInput_Router.py, 라인 132

현재 코드:

async def trigger_wf1_analysis_and_email(
    *,
    project_id: UUID,
    input_file_id: int,
) -> None:
    """WF1 Surface 분석을 백그라운드에서 실행하고 완료/오류 이메일을 발송한다."""

문제점:

  • Docstring에는 "완료/오류 이메일을 발송한다" 라고 명시
  • 하지만 실제 코드 로직:
    1. 라인 162-169: WF1 분석 실행 (asyncio.to_thread)
    2. 라인 171-184: 분석 결과를 DB에 저장
    3. 라인 186-192: SEND_ANALYSIS_COMPLETION_EMAIL 플래그에 따라 조건부 이메일 발송
    4. 라인 194-202: 예외 발생 시 오류 이메일 발송

개선:

async def trigger_wf1_analysis_and_email(...) -> None:
    """
    WF1 Surface 분석을 백그라운드에서 실행, 결과를 DB에 저장, 이메일 알림을 발송한다.
    
    분석 완료 시 SEND_ANALYSIS_COMPLETION_EMAIL 설정에 따라 완료 알림 이메일을 발송하며,
    분석 실패 시 항상 오류 알림 이메일을 발송한다 (조건부 스킵 제외).
    
    실행 흐름:
    1. 프로젝트 정보 및 입력 파일 경로 조회
    2. WF1 분석 실행 (별도 스레드에서 실행하여 이벤트 루프 블로킹 방지)
    3. 분석 결과를 DB에 원자적으로 저장 (트랜잭션)
    4. (SEND_ANALYSIS_COMPLETION_EMAIL가 True면) 완료 알림 이메일 발송
    5. (실패 시) 오류 알림 이메일 발송
    """

영향:

  • 코드는 정상 작동하지만, 신규 개발자가 함수 의도를 오해할 수 있음

3. 검증 보고서 재평가

이전 validation_b04_wf1.md 평가:

항목 평가 실제 검증 결과
자동 WF1 실행 일치 (우수) ⚠️ 이중 트랜잭션 문제로 데이터 일관성 미보장
분석 완료 이메일 일치 이메일 링크 URL이 상대 경로로 작동 불가
로그인 세션 복귀 일치 이메일 링크 클릭이 불가능하므로 사전 단계 실패
백엔드 DB 트랜잭션 일치 ⚠️ 인터페이스 혼란으로 장기적 유지보수 어려움

재평가: 계획서 요구사항 30% 미달성


4. 우선순위별 수정 사항

우선순위 1️⃣ (CRITICAL - 즉시 수정)

1.1 이메일 링크 URL 절대 경로로 변경

  • 파일: B03_FileInput_Email.py
  • 라인: 171, 191
  • 수정 내용: 상대 URL → 절대 URL로 변경

1.2 이중 트랜잭션 제거

  • 파일: B03_FileInput_Router.py
  • 라인: 170-184
  • 수정 내용: connection.begin() 호출 제거 (이미 활성 트랜잭션)

1.3 트랜잭션 인터페이스 정규화

  • 파일: B04_wf1_Surface_Router.py
  • 라인: 35-85
  • 수정 내용: 함수를 동기 함수로 변환 또는 docstring 명확화

우선순위 2️⃣ (MAJOR - 계획된 일정에 수정)

2.1 함수 docstring 개선

  • 파일: B03_FileInput_Router.py
  • 라인: 132
  • 수정 내용: 함수 의도 및 실행 흐름 명확화

5. 결론

긍정 평가:

  • 백그라운드 비동기 스케줄링 패턴 우수 (_schedule_background_task)
  • 이메일 템플릿 및 HTML 구조 잘 설계됨
  • 오류 처리 및 로깅 구조 안정적

부정 평가:

  • 이메일 링크 URL 불완전 → 핵심 기능 작동 불가
  • 트랜잭션 처리 구조 미흡 → 데이터 일관성 위험
  • 함수 인터페이스 명확성 부족 → 유지보수성 저하

6. 다음 단계

  1. 즉시: Issue #1, #2, #3 수정
  2. 검증: 수정 후 전체 흐름 테스트 (로컬 이메일 발송 테스트)
  3. 배포: 수정 완료 후 재검증 및 머지