프로젝트 개발 과정에서 발생한 주요 문제점들과 해결 방법을 정리한 문서입니다.
🚨 문제상황:
Could not create connection to database server. Attempted to reconnect 3 times. Giving up.
💡 해결방법:
-
MySQL 서버 실행 상태 확인:
sudo systemctl status mysql # 또는 brew services list mysql -
포트 충돌 확인:
netstat -tulpn | grep 3306 -
application.properties설정 확인:spring.datasource.url=jdbc:mysql://your-rds-endpoint:3306/your_database?serverTimezone=Asia/Seoul spring.datasource.username=your_db_username spring.datasource.password=your_db_password
🚨 문제상황:
한글 데이터가 '???' 또는 깨진 문자로 저장됨
💡 해결방법:
-
데이터베이스 생성 시 문자셋 지정:
CREATE DATABASE bookstore_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-
MySQL 설정 파일(
my.cnf) 수정:[mysql] default-character-set=utf8mb4 [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci
🚨 문제상황:
Cannot add or update a child row: a foreign key constraint fails
💡 해결방법:
- 데이터 삽입 순서 조정 (부모 테이블 → 자식 테이블)
- 외래키 체크 임시 비활성화:
SET FOREIGN_KEY_CHECKS = 0; -- 데이터 삽입 작업 SET FOREIGN_KEY_CHECKS = 1;
🚨 문제상황:
// 기존 코드 - 실패
@Query("SELECT s.afterQuantity FROM Stock s WHERE s.isbn = :isbn ORDER BY s.updateDate DESC")
Integer findLatestStockByIsbn(@Param("isbn") String isbn);- JPA 메서드명과 테이블/컬럼명 매핑 오류 발생
💡 해결방법:
// 수정된 코드 - 성공
@Query(value = "SELECT after_quantity FROM stock WHERE isbn = :isbn ORDER BY update_date DESC LIMIT 1", nativeQuery = true)
Integer findLatestStockByIsbn(@Param("isbn") String isbn);- Native SQL 쿼리 사용으로 직접 테이블/컬럼명 지정
🚨 문제상황:
LazyInitializationException: could not initialize proxy - no Session
💡 해결방법:
-
@Transactional어노테이션 추가:@Transactional(readOnly = true) public List<ProductResponse> getProducts() { // 메서드 내용 }
-
즉시 로딩으로 변경 (필요시):
@ManyToOne(fetch = FetchType.EAGER) private Category category;
🚨 문제상황:
- 연관 엔티티 조회 시 과도한 쿼리 실행
💡 해결방법:
-
@EntityGraph사용:@EntityGraph(attributePaths = {"category", "stock"}) List<Product> findAllWithCategory();
-
Fetch Join 사용:
@Query("SELECT p FROM Product p JOIN FETCH p.category") List<Product> findAllWithCategory();
🚨 문제상황:
TemplateInputException: Error resolving template [product/list]
💡 해결방법:
-
경로 설정 확인:
spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html
-
디렉토리 구조 확인:
src/main/resources/templates/ ├── layout/ ├── product/ ├── user/ └── admin/
🚨 문제상황:
CSS, JS 파일이 404 오류로 로드되지 않음
💡 해결방법:
- WebConfig 설정:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/static/"); } }
🚨 문제상황:
Access to fetch at 'http://localhost:8080/api/...' from origin 'http://localhost:3000' has been blocked by CORS policy
💡 해결방법:
@CrossOrigin(origins = "*")
@RestController
public class ProductController {
// 컨트롤러 내용
}🚨 문제상황:
// 실패한 요청
fetch('/api/products/9788932473901/stock')- 404 Not Found 오류 발생
💡 해결방법:
// Controller 매핑 확인
@RequestMapping("/product") // 컨트롤러 레벨 매핑
@RestController
public class ProductController {
@GetMapping("/api/stock/{isbn}") // 메서드 레벨 매핑
public ResponseEntity<Map<String, Integer>> getStock(@PathVariable String isbn) {
// 실제 URL: /product/api/stock/{isbn}
}
}// 수정된 요청
fetch('/product/api/stock/9788932473901')🚨 문제상황:
// 문제가 있는 응답
return ResponseEntity.ok("재고: " + stock);💡 해결방법:
// 개선된 응답
Map<String, Integer> response = Map.of("stock", stock);
return ResponseEntity.ok(response);🚨 문제상황:
// 실패: 파라미터명 불일치
@PostMapping("/cart/add")
public String addToCart(@RequestParam("productId") String isbn) {
}💡 해결방법:
// 성공: 정확한 파라미터명
@PostMapping("/cart/add")
public String addToCart(@RequestParam("isbn") String isbn) {
}🚨 문제상황:
// 문제: 전역 변수가 undefined
if (window.isAdmin) {
// 실행되지 않음
}💡 해결방법:
<!-- header.html에서 전역 변수 설정 -->
<script th:inline="javascript">
window.isAdmin = /*[[${session.isAdmin != null ? session.isAdmin : false}]]*/ false;
</script>🚨 문제상황:
// 문제: DOM 로드 전 실행
document.getElementById('cartBtn').addEventListener('click', function() {
// 요소를 찾을 수 없음
});💡 해결방법:
// 해결: DOM 로드 후 실행
document.addEventListener('DOMContentLoaded', function() {
const cartBtn = document.getElementById('cartBtn');
if (cartBtn) {
cartBtn.addEventListener('click', function() {
// 정상 실행
});
}
});🚨 문제상황:
// 문제: 에러 처리 누락
fetch('/api/cart/add', {
method: 'POST',
body: formData
}).then(response => response.text());💡 해결방법:
// 해결: 완전한 에러 처리
fetch('/api/cart/add', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.text();
})
.then(data => {
console.log('성공:', data);
})
.catch(error => {
console.error('오류:', error);
alert('요청 처리 중 오류가 발생했습니다.');
});🚨 문제상황:
- 수량 조절 버튼 클릭 시마다 재고 확인 API 호출
- 사용자가 빠르게 클릭할 때 서버 과부하 발생
💡 해결방법:
// 클라이언트 사이드 캐싱 구현 (product_detail.js:315 참조)
let stockCache = new Map();
const CACHE_DURATION = 30000; // 30초
function getCachedStock(isbn) {
const cached = stockCache.get(isbn);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return Promise.resolve(cached.stock);
}
return fetch(`/product/api/stock/${isbn}`)
.then(response => response.json())
.then(data => {
stockCache.set(isbn, {
stock: data.stock,
timestamp: Date.now()
});
return data.stock;
});
}🚨 문제상황:
- 상품 목록 페이지에서 모든 상품의 재고를 개별적으로 조회
💡 해결방법:
// 페이지 로드 시 일괄 재고 조회
function loadInitialStocks() {
const isbns = Array.from(document.querySelectorAll('[data-isbn]'))
.map(el => el.dataset.isbn);
if (isbns.length > 0) {
fetch('/api/stocks/batch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({isbns: isbns})
})
.then(response => response.json())
.then(stockData => {
// 캐시에 저장
Object.entries(stockData).forEach(([isbn, stock]) => {
stockCache.set(isbn, {stock, timestamp: Date.now()});
});
});
}
}🚨 문제상황:
{
"error": "Invalid request",
"error_description": "Missing required parameter: cid"
}💡 해결방법:
-
application.properties설정 확인:kakaopay.admin-key=your_admin_key_here kakaopay.cid=your_cid_here
-
API 요청 헤더 확인:
HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "KakaoAK " + adminKey); headers.set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
🚨 문제상황:
사용자가 결제 창을 닫았을 때 주문 데이터가 그대로 남아있음
💡 해결방법:
@GetMapping("/payment/cancel")
public String paymentCancel(@RequestParam String paymentId) {
// 주문 상태를 'CANCELLED'로 변경
orderService.cancelOrder(paymentId);
return "redirect:/cart?message=payment_cancelled";
}🚨 문제상황:
관리자가 일반 사용자 기능(장바구니, 마이페이지)에 접근 가능
💡 해결방법:
-
서버 사이드 검증:
@GetMapping("/cart") public String cart(HttpSession session) { Boolean isAdmin = (Boolean) session.getAttribute("isAdmin"); if (isAdmin != null && isAdmin) { return "redirect:/admin"; } // 장바구니 로직 }
-
클라이언트 사이드 UI 제어:
if (window.isAdmin) { const cartLink = document.getElementById('headerCartLink'); cartLink.style.opacity = '0.5'; cartLink.style.cursor = 'not-allowed'; cartLink.addEventListener('click', function(e) { e.preventDefault(); alert('일반 사용자 전용 기능입니다.'); }); }
🚨 문제상황:
사용자 로그아웃 후에도 세션 정보가 남아있음
💡 해결방법:
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate(); // 전체 세션 무효화
return "redirect:/main";
}🚨 문제상황:
코드 변경 후 자동 재시작되지 않음
💡 해결방법:
-
pom.xml에 DevTools 의존성 추가:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>
-
IntelliJ 설정:
- Settings → Build → Compiler → Build project automatically 체크
- Registry → compiler.automake.allow.when.app.running 체크
🚨 문제상황:
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
💡 해결방법:
<!-- pom.xml에서 테스트 스킵 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>💡 설정 방법:
# application.properties
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true💡 구현 방법:
@Component
public class PerformanceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
long executeTime = endTime - startTime;
if (executeTime > 1000) { // 1초 이상 걸린 요청 로깅
System.out.println("Slow API: " + request.getRequestURI() +
" took " + executeTime + "ms");
}
}
}- MySQL 서버 실행 상태 확인
- 데이터베이스 연결 정보 정확성 확인
- 문자 인코딩 설정 확인
- 외래키 제약 조건 확인
- 테이블 구조와 엔티티 매핑 확인
- 컨트롤러 매핑 경로 확인
- 요청/응답 형식 확인
- HTTP 메서드 일치 확인
- 파라미터 바인딩 확인
- 예외 처리 구현 확인
- DOM 로드 순서 확인
- JavaScript 오류 콘솔 확인
- API 요청 URL 정확성 확인
- 이벤트 리스너 등록 확인
- 전역 변수 초기화 확인
프로젝트 개발 중 해결되지 않는 문제가 있다면:
- 로그 확인: 먼저 애플리케이션 로그와 브라우저 개발자 도구를 확인
- 단계별 디버깅: 문제를 작은 단위로 나누어 단계별로 확인
- 공식 문서 참조: Spring Boot, JPA, MySQL 공식 문서 확인
- 커뮤니티 활용: Stack Overflow, Spring 커뮤니티 검색
이 문서는 프로젝트 개발 과정에서 실제 발생한 문제들을 바탕으로 작성되었습니다.