최근 Spring Boot와 JPA를 활용하여 칵테일 레시피를 공유하는 'CocktailLab' 프로젝트의 코드를 리뷰할 기회가 있었습니다.
이 프로젝트는 전반적으로 Spring Boot의 정석적인 아키텍처를 잘 따르고 있었지만, 실제 배포와 운영을 고려했을 때 보안과 확장성 측면에서 몇 가지 중요한 개선 포인트가 발견되었습니다. 이번 포스팅에서는 해당 프로젝트의 잘된 점(Good Practices)과 아쉬운 점(Bad Practices), 그리고 더 나은 코드를 위한 리팩토링 제안을 정리해 보았습니다.
1. 👏 칭찬할 만한 점 (Good Points)
입문 단계에서 놓치기 쉬운 설계의 기본기가 아주 탄탄하게 잡혀 있는 프로젝트였습니다.
- 깔끔한 레이어드 아키텍처 (Layered Architecture)
- Controller, Service, Repository, Entity, DTO, Exception 등으로 패키지 구조가 명확하게 분리되어 있습니다. 각 계층의 역할이 섞이지 않도록 설계하여 유지보수성을 높였습니다.
- JPA의 능숙한 활용
- Auditing 적용: BaseTimeEntity를 통해 생성일/수정일을 자동화하여 중복 코드를 제거했습니다.
- 성능 고려: @ManyToOne 관계에서 기본값인 EAGER 대신 FetchType.LAZY를 명시하여 불필요한 조인 쿼리를 방지했습니다.
- N+1 문제 해결 노력: findAllWithLikes 메서드 등에서 JOIN FETCH를 사용하여 연관 데이터를 한 번의 쿼리로 가져오도록 최적화했습니다.
- 예외 처리의 중앙화
- GlobalExceptionHandler를 도입하여 유효성 검증(@Valid) 실패와 비즈니스 로직 예외를 일관된 JSON 포맷으로 응답하도록 설계했습니다. 프론트엔드 입장에서 에러 처리가 매우 용이한 구조입니다.
- Entity와 DTO의 철저한 분리
- DB 모델인 Entity를 API 응답으로 직접 노출하지 않고, Request/Response DTO로 변환하여 사용했습니다. 이는 API 스펙 변경이 DB 스키마에 영향을 주지 않도록 하는 좋은 습관입니다.
2. 🚨 반드시 개선해야 할 점 (Critical Issues)
기능 구현은 완벽하지만, "실제 서비스로 배포한다면?" 이라는 가정하에 심각한 문제가 될 수 있는 부분들입니다.
① "내 컴퓨터에서만 돼요" - 하드코딩된 절대 경로
CocktailController와 application.yaml 설정 파일에 파일 업로드 경로와 DB 경로가 C:\khWorkspace\...와 같이 절대 경로로 박혀 있었습니다.
- 문제점: 작성자의 윈도우 PC 외에 맥(Mac), 리눅스 서버, 혹은 다른 팀원의 컴퓨터에서는 코드가 아예 실행되지 않습니다. (이식성 0점)
- 해결책: 경로는 application.yaml의 프로퍼티(@Value)로 분리하고, OS에 종속되지 않는 상대 경로를 사용하거나 환경 변수 처리를 해야 합니다.
② 심각한 보안 취약점 (Security)
가장 시급하게 수정해야 할 부분입니다.
- 비밀번호 평문 저장: 사용자의 비밀번호를 암호화(Hashing) 없이 DB에 그대로 저장하고 있습니다. DB가 유출될 경우 사용자 정보가 무방비로 노출됩니다.
- 취약한 인증 방식: 로그인 후 세션이나 JWT 토큰을 사용하는 대신, API 요청 헤더에 X-Member-No를 담아 사용자를 식별하고 있습니다. 악의적인 사용자가 이 숫자를 조작하면 타인의 계정을 쉽게 도용할 수 있습니다.
- 해결책: Spring Security를 도입하여 BCrypt로 비밀번호를 암호화하고, JWT 혹은 세션 기반의 안전한 인증 인가 시스템을 구축해야 합니다.
③ 불필요한 라이브러리 의존성
JPA만 사용하고 있음에도 build.gradle에 mybatis-spring-boot-starter가 포함되어 있었습니다. 사용하지 않는 의존성은 빌드 속도를 늦추고 애플리케이션 용량만 차지하므로 제거하는 것이 좋습니다.
3. 🛠 더 나은 코드를 위한 제안 (Refactoring Suggestions)
코드의 품질(Quality)과 안정성(Stability)을 한 단계 높이기 위한 제안입니다.
① 파일 덮어쓰기 방지 (UUID 적용)
현재 파일 업로드 로직은 originalFilename을 그대로 사용합니다. 만약 다른 사용자가 우연히 똑같은 이름(image.jpg)의 파일을 올린다면, 기존 파일이 덮어씌워지는 사고가 발생합니다.
Solution: UUID.randomUUID()를 파일명 앞에 붙여 저장된 파일명이 고유하도록 만들어야 합니다. (예: uuid-image.jpg)
② 로깅(Logging) 습관화하기
코드 내에 로그를 남기는 부분이 전무했습니다. 실무에서 로그가 없으면 에러가 발생했을 때 원인을 추적하기가 불가능합니다.
Solution: 롬복의 @Slf4j를 활용하여 요청의 시작/끝, 예외 발생 시점 등에 log.info(), log.error()를 남기는 습관을 들여야 합니다.
③ Controller와 Service의 역할 분리 (SoC)
컨트롤러에서 File 객체를 생성하고 저장하는 로직(transferTo)을 직접 수행하고 있습니다. 컨트롤러는 "요청을 받아 서비스에 넘기는 역할"에 집중해야 합니다.
Solution: 파일 저장 로직은 FileService나 S3Service로 분리해야 합니다. 그래야 나중에 로컬 저장을 AWS S3 저장으로 변경할 때 컨트롤러 코드를 수정하지 않아도 됩니다.
④ @ElementCollection의 한계와 정규화
칵테일 재료를 List<String>과 @ElementCollection으로 단순하게 저장하고 있습니다. 초기 구현에는 편하지만, "진(Gin)이 포함된 칵테일 검색" 같은 쿼리를 짤 때 성능 이슈가 발생할 수 있습니다.
Solution: 확장성을 고려한다면 Ingredient 엔티티를 별도로 만들고 다대다(N:M) 관계로 정규화하는 것이 좋습니다.
📝 마치며
이번 코드 리뷰를 통해 기능 구현(Make it work) 단계를 넘어 올바른 구현(Make it right) 으로 나아가는 과정을 살펴볼 수 있었습니다.
특히 JPA 활용 능력과 아키텍처 설계는 훌륭했지만, 보안(Security) 과 환경 설정(Configuration) 부분은 실무 레벨로 가기 위해 반드시 보완이 필요해 보입니다. 위 피드백을 반영하여 리팩토링한다면 훨씬 더 안정적이고 완성도 높은 프로젝트가 될 것입니다.
'Engineering > Backend Core' 카테고리의 다른 글
| [Code Review] Spring Boot 영화 감상일지 프로젝트 리뷰: 아키텍처와 JPA 의존성 분리 (0) | 2025.12.22 |
|---|---|
| [Spring Boot] 백엔드 핵심 흐름: JPA와 DDD, 그리고 AWS S3 (0) | 2025.12.09 |
| [Backend] DB 성능 최적화: 커넥션 풀(Connection Pool)과 HikariCP의 등장 (0) | 2025.12.09 |
| TIL: MVC 패턴의 본질과 Service 인터페이스의 진실 (0) | 2025.12.09 |
| 코딩테스트 연습 - 홀짝 구분하기 (0) | 2025.09.30 |