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 로깅으로 에러 응답과 호출 로그 형식을 통일했습니다.
| 도메인 | 경로 | 설명 |
|---|---|---|
| 인증 | 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 토큰 저장 및 토큰으로 사용자 조회 |
| AI | GET /ai-test/generate, POST /ai-test/generate | Gemini로 코딩테스트 문제 생성 |
API 문서는 서버 실행 후 /swagger-ui.html 에서 확인할 수 있습니다.
- 상황 : 회원가입은 정상 처리되는데, 같은 비밀번호로 로그인하면 항상 비밀번호 불일치로 실패했습니다.
- 원인 :
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
- 상황 : 랭킹 API(
GET /api/v1/user/ranking)가 호출될 때마다 전체 사용자를point기준으로 정렬하는 쿼리가 나갔습니다. - 원인 : 랭킹은 하루 단위로만 바뀌면 충분한 데이터인데도, 조회 시점마다 정렬 +
limit 10쿼리를 그대로 수행하고 있었습니다. - 해결 :
RankingService를InitializingBean으로 만들어 애플리케이션 기동 시 랭킹을 한 번 적재하고, 마지막 갱신일이 하루 이상 지난 경우에만 쿼리를 다시 실행하도록 인메모리 캐시를 두었습니다. - 관련 코드 :
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- Swagger UI : http://localhost:8080/swagger-ui.html
- H2 Console : http://localhost:8080/h2-console