솔직히 이번파트는 거의 이해못했다.... 바이브코딩 만쉐이..

🎯 AI 브레인스토밍 플랫폼 개발 일지 - Day 5

📅 작업 일자

2024년 12월 1일 (Day 5)

🎯 오늘의 목표

  • ✅ Spring Boot ↔ Python FastAPI 연동
  • ✅ 브레인스토밍 전체 플로우 완성
  • ✅ 타임아웃 및 DB 이슈 해결
  • ✅ Python 코드 고도화 (로깅, Retry, 자동 청소)

✅ 완료 사항

1️⃣ Spring Boot ↔ Python 통합

BrainstormingController 구현

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/brainstorm")
public class BrainstormController {

    private final BrainstormingService brainstormingService;

    @PostMapping("/generate")
    public ResponseEntity<IdeasResponse> generateIdeas(@RequestBody BrainstormRequest request) {
        IdeasResponse response = brainstormingService.generate(
            request.getUserId(),
            request.getPurpose(),
            request.getAssociations()
        );
        return ResponseEntity.ok(response);
    }
}

BrainstormingService 구현

@Service
@RequiredArgsConstructor
public class BrainstormingService {

    private final RestTemplate restTemplate;
    private final IdeaRepository ideaRepository;
    private final UserRepository userRepository;

    @Value("${python.service.url}")
    private String pythonServiceUrl;

    public IdeasResponse generate(Long userId, String purpose, List<String> associations) {
        // 1. 세션 생성
        String sessionId = createSession();

        // 2. 목적 입력 (Q1)
        submitPurpose(sessionId, purpose);

        // 3. 워밍업 질문 생성 (Q2)
        getWarmupQuestions(sessionId);

        // 4. 워밍업 확인
        confirmWarmup(sessionId);

        // 5. 자유연상 입력 (Q3)
        submitAssociations(sessionId, associations);

        // 6. 아이디어 생성
        IdeasResponse response = generateIdeas(sessionId);

        // 7. DB 저장
        saveIdeasToDb(userId, response);

        // 8. 세션 삭제
        deleteSession(sessionId);

        return response;
    }
}

핵심 플로우:

Client → Spring Boot → Python FastAPI
   ↓           ↓              ↓
Request   RestTemplate    LLM 호출
   ↓           ↓              ↓
Response ← JSON 파싱 ← JSON 응답
   ↓
   DB 저장 (H2)

2️⃣ 타임아웃 문제 해결 🔥

문제 상황

java.net.SocketTimeoutException: Read timed out

원인:

  • LLM 호출이 4번 발생 (아이디어 생성 1회 + SWOT 분석 3회)
  • 예상 시간: 40-80초
  • 기존 타임아웃: 30초 ❌

해결

RestTemplateConfig.java 수정:

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(5000);      // 5초
        factory.setReadTimeout(120000);       // 120초 (2분) ✅
        return new RestTemplate(factory);
    }
}

결과: ✅ 타임아웃 해결, 40-80초 내 정상 응답


3️⃣ DB 컬럼 길이 문제 해결

문제 상황

Value too long for column "CONTENT CHARACTER VARYING(255)"

원인:

  • 기본 VARCHAR(255) 제한
  • 아이디어 설명(100-200자) + SWOT 분석(200-400자) = 300-600자 초과

해결

Idea.java 엔티티 수정:

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Idea extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long ideaId;

    @Column(nullable = false)
    private Long userId;

    @Column(nullable = false, length = 500)
    private String title;

    @Column(columnDefinition = "TEXT")  // ← TEXT 타입으로 변경 ✅
    private String content;

    private String purpose;
}

결과: ✅ 긴 아이디어 + SWOT 분석 정상 저장


4️⃣ Java DTO 구조 수정

문제 상황

Python이 보내는 JSON 구조와 Java DTO 불일치로 파싱 실패

Python 응답:

{
  "ideas": [
    {
      "title": "AI 기반 맞춤형 학습 플래너",
      "description": "학생들의 학습 스타일 분석...",
      "analysis": "📊 분석 결과:\n• 강점: ...\n• 약점: ..."
    }
  ]
}

Java 기대 (Before):

public static class IdeaDto {
    private String title;
    private String description;
    private SwotDto swot;  // ← 객체 타입 ❌
}

해결

IdeasResponse.java 수정:

public static class IdeaDto {
    private String title;
    private String description;
    private String analysis;  // ← 문자열 타입 ✅
}

BrainstormingService.java 수정:

private void saveIdeasToDb(Long userId, IdeasResponse response) {
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));

    for (IdeasResponse.IdeaDto ideaDto : response.getIdeas()) {
        // description + analysis 합치기
        String content = ideaDto.getDescription() + "\n\n" + ideaDto.getAnalysis();

        Idea idea = Idea.builder()
            .user(user)
            .title(ideaDto.getTitle())
            .content(content)
            .purpose(response.getIdeas().get(0).getDescription())
            .build();

        ideaRepository.save(idea);
    }
}

결과: ✅ JSON 파싱 성공, DB 저장 완료


5️⃣ Python 코드 고도화 🚀

새로 생성한 파일들

1. 프롬프트 템플릿 파일

python-service/app/api/v1/endpoints/prompts/idea_generation.txt
  • 2000+ 줄 프롬프트를 별도 파일로 분리
  • 템플릿 변수 사용 ({purpose}, {keywords}, {rag_context})

2. LLM 헬퍼 모듈

# python-service/app/api/v1/endpoints/utils/llm_helpers.py

def call_llm_with_retry(client, model, messages, temperature, max_tokens, max_retries=3):
    """
    LLM 호출 with Exponential Backoff Retry
    """
    for attempt in range(1, max_retries + 1):
        try:
            logger.info(f"LLM 호출 시도 {attempt}/{max_retries}")

            response = client.chat.completions.create(
                model=model,
                messages=messages,
                temperature=temperature,
                max_tokens=max_tokens
            )

            logger.info(f"LLM 호출 성공 (시도 {attempt})")
            return response.choices[0].message.content.strip()

        except Exception as e:
            logger.error(f"LLM 호출 실패 (시도 {attempt}): {e}")

            if attempt < max_retries:
                wait_time = 2 ** attempt  # 2초, 4초, 8초
                logger.info(f"{wait_time}초 대기 후 재시도...")
                time.sleep(wait_time)
            else:
                raise

3. Dependencies 모듈

# python-service/app/api/v1/endpoints/dependencies.py

def get_session_or_404(session_id: str) -> Dict:
    """
    세션 존재 여부 확인 (Dependency)
    """
    from session_manager import SessionManager

    session_manager = SessionManager()
    session = session_manager.get_session(session_id)

    if not session:
        raise HTTPException(status_code=404, detail="세션을 찾을 수 없습니다.")

    return session

개선된 brainstorming.py

로깅 시스템 추가:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

Depends 패턴 적용:

@router.get("/warmup/{session_id}", response_model=WarmupResponse)
async def get_warmup_questions(
    session_id: str,
    session: dict = Depends(get_session_or_404)  # ← 세션 자동 검증
):
    purpose = session.get('q1_purpose')
    # ...

자동 세션 청소:

@router.post("/session", response_model=SessionResponse)
async def create_session():
    """
    새로운 세션 시작

    5분 이상 된 빈 폴더 자동 정리
    """
    logger.info("🧹 오래된 Ephemeral 세션 폴더 청소 시작...")

    ephemeral_base_dir = Path(...) / "data" / "ephemeral"

    if ephemeral_base_dir.exists():
        deleted_count = 0
        current_time = time.time()
        cutoff_time = current_time - 300  # 5분

        for session_dir in ephemeral_base_dir.iterdir():
            if not session_dir.is_dir():
                continue

            mtime = session_dir.stat().st_mtime

            if mtime < cutoff_time:
                files = list(session_dir.iterdir())
                if len(files) == 0:  # 빈 폴더만
                    shutil.rmtree(session_dir)
                    deleted_count += 1

        if deleted_count > 0:
            logger.info(f"✅ {deleted_count}개의 오래된 빈 폴더 삭제됨")

    # 새 세션 생성
    session_id = session_manager.create_session()
    return SessionResponse(session_id=session_id, message="...")

Retry 로직 적용:

# Before
response = openai_client.chat.completions.create(...)
content = response.choices[0].message.content

# After
content = call_llm_with_retry(
    client=openai_client,
    model=llm_model,
    messages=[...],
    temperature=0.7,
    max_tokens=2000
)

6️⃣ 통합 테스트 성공 ✅

Postman 요청

POST http://localhost:8080/api/brainstorm/generate
Content-Type: application/json

{
  "userId": 1,
  "purpose": "학생들을 위한 학습 앱 아이디어",
  "associations": ["학습", "AI", "맞춤형", "학생", "효율"]
}

응답 (40-80초 소요)

{
  "ideas": [
    {
      "title": "AI 기반 맞춤형 학습 플래너",
      "description": "💡 핵심 문제:\n학생들은 자신에게 맞는 학습 방법을 몰라 비효율적으로 공부합니다...",
      "analysis": "📊 분석 결과:\n• 강점: AI가 학습 패턴 분석하여 개인별 최적 학습법 제안\n• 약점: 초기 데이터 수집 기간 필요..."
    },
    {
      "title": "학습 스타일 진단 및 추천 앱",
      "description": "...",
      "analysis": "..."
    },
    {
      "title": "실시간 학습 도우미 챗봇",
      "description": "...",
      "analysis": "..."
    }
  ]
}

Python 서버 로그

2024-12-01 18:21:49 - INFO - ✅ 새 세션 생성: 4a8a627f-ae0b-4586-af56-502f6262bdb2
2024-12-01 18:21:49 - INFO - ✅ 목적 입력 완료: 4a8a627f-ae0b-4586-af56-502f6262bdb2
2024-12-01 18:21:49 - INFO - 🤔 워밍업 질문 생성 시작
2024-12-01 18:21:50 - INFO - ✅ 워밍업 질문 생성 완료: 3개
2024-12-01 18:21:50 - INFO - 📝 자유연상 입력 시작
2024-12-01 18:21:53 - INFO - ✅ 자유연상 입력 완료: 5개 키워드
2024-12-01 18:21:53 - INFO - 💡 아이디어 생성 시작
2024-12-01 18:22:07 - INFO - ✅ 아이디어 생성 완료: 3개
2024-12-01 18:22:07 - INFO - 🗑️  세션 삭제 시작
2024-12-01 18:22:07 - INFO - ✅ 세션 삭제 완료

DB 저장 확인

SELECT * FROM idea;

ideaId | userId | title                          | content (TEXT)
-------|--------|--------------------------------|------------------
1      | 1      | AI 기반 맞춤형 학습 플래너      | 💡 핵심 문제:...
2      | 1      | 학습 스타일 진단 및 추천 앱     | ...
3      | 1      | 실시간 학습 도우미 챗봇         | ...

전체 플로우 완벽 동작! 🎉


💡 배운 점 & 트러블슈팅

1. RestTemplate 타임아웃 설정

왜 중요한가?

  • LLM 호출은 시간이 오래 걸림 (10-60초)
  • 기본 타임아웃(30초)으로는 부족
  • 읽기 타임아웃 ≠ 연결 타임아웃

설정 가이드:

factory.setConnectTimeout(5000);     // 연결 타임아웃: 짧게
factory.setReadTimeout(120000);      // 읽기 타임아웃: 길게

2. JPA 컬럼 타입 설정

VARCHAR vs TEXT:
| 타입 | 길이 | 용도 |
|------|------|------|
| VARCHAR(255) | 255자 | 제목, 이름, 이메일 |
| VARCHAR(500) | 500자 | 짧은 설명 |
| TEXT | 무제한 | 긴 내용, 본문 |

설정 방법:

@Column(length = 500)              // VARCHAR(500)
@Column(columnDefinition = "TEXT") // TEXT

3. Python Logging Best Practice

계층별 로그 레벨:

logger.debug("상세 디버그")    # 개발 시
logger.info("✅ 작업 완료")     # 정상 플로우
logger.warning("⚠️  주의")     # 경고
logger.error("❌ 실패")         # 에러

이모지로 시각화:

  • 🧹 청소 작업
  • ✅ 성공
  • ❌ 실패
  • 🤔 처리 중
  • 💡 아이디어 생성

4. FastAPI Dependencies 패턴

Before (코드 중복):

@router.get("/warmup/{session_id}")
async def get_warmup(session_id: str):
    session = session_manager.get_session(session_id)
    if not session:
        raise HTTPException(404, "세션 없음")
    # ...

@router.post("/associations/{session_id}")
async def submit_associations(session_id: str):
    session = session_manager.get_session(session_id)
    if not session:
        raise HTTPException(404, "세션 없음")
    # ...

After (Depends 사용):

@router.get("/warmup/{session_id}")
async def get_warmup(
    session_id: str,
    session: dict = Depends(get_session_or_404)  # ← 자동 검증
):
    # session은 이미 검증됨!

@router.post("/associations/{session_id}")
async def submit_associations(
    session_id: str,
    session: dict = Depends(get_session_or_404)
):
    # 코드 중복 제거!

5. Exponential Backoff Retry

왜 필요한가?

  • LLM API는 가끔 실패함 (네트워크, Rate Limit 등)
  • 즉시 재시도하면 계속 실패
  • 점진적으로 대기 시간 늘리기

구현:

retry_count = 0
wait_time = 2

while retry_count < 3:
    try:
        response = api_call()
        break
    except Exception:
        retry_count += 1
        time.sleep(wait_time)
        wait_time *= 2  # 2초 → 4초 → 8초

📊 프로젝트 현황

완료된 기능

✅ Spring Boot Backend (13개 API)
✅ Python FastAPI (브레인스토밍 API)
✅ Spring ↔ Python 통합
✅ LLM 기반 아이디어 생성
✅ SWOT 분석 자동화
✅ DB 저장 (H2)
✅ 타임아웃 처리
✅ 에러 핸들링 (Retry)
✅ 로깅 시스템
✅ 자동 세션 청소

디렉토리 구조

brainstorming-platform/
├── src/main/java/.../
│   ├── domain/
│   │   ├── user/          ✅
│   │   ├── idea/          ✅
│   │   ├── inquiry/       ✅
│   │   └── brainstorming/ ✅ NEW!
│   │       ├── controller/BrainstormController.java
│   │       ├── service/BrainstormingService.java
│   │       └── dto/
│   │           ├── BrainstormRequest.java
│   │           └── IdeasResponse.java
│   ├── config/
│   │   └── RestTemplateConfig.java ✅
│   └── global/
│       └── BaseEntity.java
│
└── python-service/
    ├── app/
    │   ├── api/v1/endpoints/
    │   │   ├── brainstorming.py        ✅ 개선됨!
    │   │   ├── dependencies.py         ✅ NEW!
    │   │   ├── utils/
    │   │   │   └── llm_helpers.py      ✅ NEW!
    │   │   └── prompts/
    │   │       └── idea_generation.txt ✅ NEW!
    │   └── domain/brainstorming/
    │       ├── session_manager.py
    │       ├── ephemeral_rag.py
    │       └── data/
    │           ├── chroma/              (영구 RAG)
    │           └── ephemeral/           (임시 세션, 자동 청소됨)
    └── main.py

🎯 다음 단계 (Day 6)

우선순위 1: 프론트엔드 개발 🎨

기술 스택 후보:

  • React (대중적, 취업 유리)
  • Vue.js (쉬움)
  • Next.js (SSR)

필수 화면:

  1. 로그인/회원가입
  2. 메인 대시보드
  3. 브레인스토밍 채팅 UI
  4. 아이디어 관리

우선순위 2: OAuth2 로그인 (선택)

  • Google OAuth2
  • Kakao OAuth2
  • JWT 토큰

우선순위 3: 배포

  • Frontend: Vercel or Netlify
  • Backend: AWS EC2 or Heroku
  • Python: Docker + EC2

📚 참고 자료


🔗 GitHub

  • Repository: brainstorming-platform
  • Commit: Day 5 - Spring-Python 통합 완성 및 고도화

💭 회고

잘한 점

  • 🔥 Spring ↔ Python 통합 성공 (핵심 목표 달성!)
  • 🐛 타임아웃, DB 컬럼 길이 문제 빠르게 해결
  • 🚀 Python 코드를 단순 동작에서 → 프로덕션 수준으로 개선
  • 📊 상세한 로깅으로 디버깅 용이
  • 🧹 자동 세션 청소로 불필요한 데이터 관리

개선할 점

  • 예외 처리를 더 세분화 (사용자 친화적 에러 메시지)
  • 테스트 코드 추가 (MockMvc, pytest)
  • API 응답 시간 최적화 (캐싱 고려)
  • 프론트엔드 없이 Postman으로만 테스트하는 한계

느낀 점

"백엔드 두 개를 연결하는 게 생각보다 어려웠다."

  • 타임아웃: LLM 호출이 예상보다 오래 걸림
  • JSON 파싱: Python과 Java의 DTO 구조 맞추기
  • 에러 추적: 어느 서버에서 에러가 났는지 파악하기

하지만:

  • ✅ 실제로 동작하는 AI 기능 완성!
  • ✅ 로그를 보면서 전체 플로우 이해
  • ✅ 프로덕션 수준의 에러 핸들링 경험

"이제 프론트엔드만 만들면 완성!" 🎉


🏆 Day 5 완료!

오늘의 성과:

  • Spring ↔ Python 통합 완성 ✅
  • 전체 브레인스토밍 플로우 동작 ✅
  • 타임아웃/DB 이슈 해결 ✅
  • Python 코드 고도화 ✅
  • 자동 세션 청소 구현 ✅

내일은 프론트엔드 개발을 시작합니다! 🚀


💡 Tip: 이 프로젝트의 핵심은 "Spring Boot와 Python FastAPI를 RestTemplate로 연결하는 방법"입니다. 같은 방식으로 어떤 외부 API든 통합할 수 있습니다!

+ Recent posts