Skip to content

Repository files navigation

code-for-code

AI가 코딩테스트 문제를 내주고, 사용자가 풀이를 등록하고 질문을 나누는 학습 커뮤니티 API 서버입니다.

프로젝트 정보

항목내용
프로젝트명code-for-code
기간2024.10 ~ 2024.11 (약 3주)
인원 / 역할팀 프로젝트 · 백엔드 담당 (인증, 게시판 API, AI 연동)
저장소https://github.com/Duskafka/code-for-code

기술 스택

  • Backend : Java 17, Spring Boot 3.3.5, Spring Security, Spring AOP, JWT(jjwt 0.11.5), Thymeleaf
  • Data : Spring Data JPA, QueryDSL 5.0.0, H2, Redis(Lettuce)
  • Infra / Docs : Gradle, GitHub Actions, springdoc-openapi(Swagger UI), commonmark

무엇을 만들었나

Spring Security와 JWT로 인증 계층을 구성하고, 발급한 토큰을 Redis에 저장해 세션 없이 사용자 정보를 조회할 수 있게 했습니다. Google Gemini API를 연동해 주제와 난이도를 입력하면 코딩테스트 문제를 생성하고, 마크다운 응답을 HTML로 변환해 화면에 보여줍니다. 문제 풀이(Solution) 등록·검색, 질문/댓글 게시판, 포인트 기반 사용자 랭킹을 QueryDSL로 구현했습니다. 전역 예외 처리(@RestControllerAdvice)와 @Trace 어노테이션 기반 AOP 로깅으로 에러 응답과 호출 로그 형식을 통일했습니다.

주요 API

도메인경로설명
인증POST /api/v1/auth, POST /api/v1/auth/login로그인 및 Access/Refresh 토큰 발급
회원POST /api/v1/user/signup, GET /api/v1/user/ranking회원가입, 포인트 상위 10명 랭킹
풀이POST /api/v1/solution, GET /api/v1/solution/search문제 등록 및 주제·난이도 검색
질문POST /api/v1/question, GET /api/v1/question/all질문 등록, 목록/페이지 조회
댓글POST /api/v1/comment, GET /api/v1/comment/question/{id}댓글 등록, 질문별 댓글 조회
토큰POST /api/v1/redis, GET /api/v1/redis/{token}Redis 토큰 저장 및 토큰으로 사용자 조회
AIGET /ai-test/generate, POST /ai-test/generateGemini로 코딩테스트 문제 생성

API 문서는 서버 실행 후 /swagger-ui.html 에서 확인할 수 있습니다.

트러블슈팅

1. 비밀번호가 두 번 암호화되어 로그인이 실패하던 문제

  • 상황 : 회원가입은 정상 처리되는데, 같은 비밀번호로 로그인하면 항상 비밀번호 불일치로 실패했습니다.
  • 원인 : User.password@Convert(converter = PasswordEncodeConverter.class)를 걸어 두어 JPA가 DB에 쓰는 시점마다 passwordEncoder.encode()를 호출하는 구조였습니다. 즉 인코딩 지점이 엔티티 쓰기 경로에 숨어 있어, 이미 해싱된 값이 다시 해싱되어 저장되는 경우가 생겼고 passwordEncoder.matches(raw, stored)가 성립할 수 없었습니다.
  • 해결 : 암호화가 딱 한 번만 일어나도록 인코딩 지점을 정리하고, 로그인은 QueryDSL로 사용자를 조회한 뒤 UserRepository.login()에서 matches() 한 번으로 검증하도록 바꿨습니다. (커밋 17d46f5)
  • 관련 코드 : user/domain/converter/PasswordEncodeConverter.java, user/repository/UserRepository.java

2. 매 요청마다 실행되던 랭킹 정렬 쿼리

  • 상황 : 랭킹 API(GET /api/v1/user/ranking)가 호출될 때마다 전체 사용자를 point 기준으로 정렬하는 쿼리가 나갔습니다.
  • 원인 : 랭킹은 하루 단위로만 바뀌면 충분한 데이터인데도, 조회 시점마다 정렬 + limit 10 쿼리를 그대로 수행하고 있었습니다.
  • 해결 : RankingServiceInitializingBean으로 만들어 애플리케이션 기동 시 랭킹을 한 번 적재하고, 마지막 갱신일이 하루 이상 지난 경우에만 쿼리를 다시 실행하도록 인메모리 캐시를 두었습니다.
  • 관련 코드 : user/repository/RankingService.java

회고 & 지금이라면 이렇게

지금 다시 보면 아쉬운 점이 분명한 프로젝트입니다. 남아 있는 한계와 개선 방향을 정리해 둡니다.

  • 인가 정책이 사실상 열려 있음 : 개발 편의를 위해 SecurityConfig에서 /**permitAll()로 두고 마무리했습니다. 지금이라면 공개 경로(회원가입·로그인·Swagger)만 화이트리스트로 열고 나머지는 authenticated()로 두었을 것입니다.
  • 테스트가 거의 없음 : 테스트가 컨텍스트 로드와 비밀번호 인코딩 확인 2개뿐입니다. 위 트러블슈팅 1번은 로그인 시나리오 테스트가 하나만 있었어도 훨씬 빨리 잡을 수 있었던 버그라, 서비스 계층 단위 테스트부터 채웠을 것입니다.
  • 설정과 시크릿 관리 : Gemini 키는 GitHub Actions Secret으로 옮겼지만, JWT secret은 여전히 application.yml에 튜토리얼 값 그대로 남아 있습니다. 프로파일 분리와 환경변수 주입으로 정리하는 것이 맞습니다.
  • 캐시 위치 : 랭킹 캐시를 애플리케이션 메모리에 두어 인스턴스가 늘어나면 값이 서로 달라집니다. 이미 Redis를 쓰고 있으므로 TTL을 건 Redis 캐시로 옮기는 편이 낫습니다.
  • 정리되지 않은 흔적 : AuthController에 디버깅용 로그와 TODO 주석이 남아 있고, CI 워크플로의 트리거 브랜치가 main으로 되어 있어 기본 브랜치(master)에서는 동작하지 않습니다.

실행 방법

사전 준비: JDK 17, 로컬 Redis(6379), H2 TCP 서버(jdbc:h2:tcp://localhost/~/querydsl)

# 1. src/main/resources/application.yml 의 gemini.api.key 에 발급받은 키 입력# 2. 실행
./gradlew bootRun

About

AI가 코딩 테스트를 내주는 코딩 테스트 사이트

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Used by

Contributors

Languages