자주 묻는 질문과 문제 해결 가이드
프로젝트 사용 중 발생할 수 있는 문제와 해결 방법을 정리했습니다.
증상:
Unsupported class file major version 65
원인: Java 21이 설치되지 않았거나, 다른 Java 버전이 사용되고 있습니다.
해결:
# 현재 Java 버전 확인
java -version
# Java 21 설치 (Ubuntu/Debian)
sudo apt install openjdk-21-jdk
# macOS (Homebrew)
brew install openjdk@21
# JAVA_HOME 설정
export JAVA_HOME=/path/to/jdk-21
export PATH=$JAVA_HOME/bin:$PATH증상:
Could not resolve dependencies
해결 1: Gradle 캐시 정리
./gradlew clean --refresh-dependencies
./gradlew build해결 2: Gradle Wrapper 재생성
gradle wrapper
./gradlew build해결 3: 프록시 설정 (회사 방화벽 등)
# ~/.gradle/gradle.properties
systemProp.http.proxyHost=proxy.company.com
systemProp.http.proxyPort=8080
systemProp.https.proxyHost=proxy.company.com
systemProp.https.proxyPort=8080증상 1: 포트 이미 사용 중
Port 8080 is already in use
해결:
# 사용 중인 프로세스 찾기
lsof -i :8080 # macOS/Linux
netstat -ano | findstr :8080 # Windows
# 프로세스 종료 후 재시작
kill -9 <PID>
./gradlew bootRun증상 2: Elasticsearch 연결 실패
Connection refused: localhost/127.0.0.1:9200
해결:
# Elasticsearch 실행 여부 확인
docker-compose ps
# 실행되지 않았다면 시작
docker-compose up -d elasticsearch
# 상태 확인
curl http://localhost:9200증상 1: 메모리 부족
es01 exited with code 137
해결: Docker 메모리 증가
# docker-compose.yml
services:
elasticsearch:
environment:
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" # 메모리 줄이기또는 Docker Desktop 설정에서 메모리 할당 증가 (최소 4GB 권장)
증상 2: 포트 충돌
Bind for 0.0.0.0:9200 failed: port is already allocated
해결:
# 9200 포트 사용 중인 프로세스 확인
lsof -i :9200
# 포트 변경 (docker-compose.yml)
ports:
- "9201:9200" # 다른 포트 사용
# application.yml도 수정
spring.ai.vectorstore.elasticsearch.url: http://localhost:9201증상:
No index found: product
원인: 자동 스키마 생성이 비활성화되었거나, 샘플 데이터 로딩 실패
확인:
# 인덱스 목록 확인
curl http://localhost:9200/_cat/indices?v
# product 인덱스 확인
curl http://localhost:9200/product해결:
# application.yml 확인
spring:
ai:
vectorstore:
elasticsearch:
initialize-schema: true # 이 설정이 true인지 확인수동 생성:
# Kibana Dev Tools 또는 curl 사용
PUT /product
{
"mappings": {
"properties": {
"embedding": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"content": {"type": "text"},
"metadata": {"type": "object"}
}
}
}해결:
# 인덱스 삭제
curl -X DELETE http://localhost:9200/product
# 애플리케이션 재시작 (자동으로 재생성 및 샘플 데이터 로딩)
./gradlew bootRun또는
# Kibana Dev Tools
DELETE /product
# 또는 모든 데이터만 삭제
POST /product/_delete_by_query
{
"query": {
"match_all": {}
}
}증상:
401 Unauthorized: Invalid API key
해결:
-
API 키 확인:
- Google AI Studio에서 키 재확인
- 올바른 키가
application.yml에 설정되었는지 확인
-
프로젝트 ID 확인:
- Google Cloud Console에서 프로젝트 ID 확인
application.yml의project-id가 정확한지 확인
-
API 활성화 확인:
- Google Cloud Console
- "API 및 서비스" → "라이브러리"
- "Generative Language API" 검색 후 활성화
증상:
429 Too Many Requests: Resource has been exhausted
원인: API 할당량 초과
확인:
# Google Cloud Console
# "API 및 서비스" → "할당량"해결:
-
무료 티어 한도:
- 분당 60 요청
- 일일 1,500 요청
-
요청 줄이기:
// 배치 처리
val embeddings = embeddingClient.embed(listOf(
text1, text2, text3 // 여러 텍스트 한 번에
))
// 캐싱
@Cacheable("embeddings")
fun getEmbedding(text: String): List<Double>- 유료 플랜 고려:
증상: 검색 응답이 5초 이상 걸립니다
원인: 매번 Gemini API 호출
해결:
- 쿼리 캐싱:
@Configuration
@EnableCaching
class CacheConfig {
@Bean
fun cacheManager() = ConcurrentMapCacheManager("embeddings")
}
@Cacheable("embeddings")
fun embed(text: String) = embeddingClient.embed(text)- 인기 검색어 사전 임베딩:
@PostConstruct
fun preloadPopularQueries() {
val popular = listOf("냉장고", "세탁기", "TV", ...)
popular.forEach { query ->
embeddingClient.embed(query) // 캐시에 저장
}
}원인 1: Threshold가 너무 높음
해결:
// ProductSearchService.kt
val threshold = 0.5 // 0.7 → 0.5로 낮춤원인 2: 샘플 데이터 미로딩
확인:
# 인덱스 문서 수 확인
curl http://localhost:9200/product/_count
# 결과가 0이면 샘플 데이터 없음해결:
# 애플리케이션 로그 확인
# "Added 54 products to vector store" 메시지 있는지 확인
# 없다면 SampleDataInit.kt 확인 및 재시작예시: "냉장고" 검색 시 세탁기가 나옴
원인:
- Threshold가 너무 낮음
- 샘플 데이터의 품질 문제
- 임베딩 모델의 한계
디버깅:
// 검색 결과의 similarity score 확인
fun search(request: ProductSearchRequest): ProductSearchResponse {
val documents = vectorStore.similaritySearch(...)
documents.forEach { doc ->
logger.info("Product: ${doc.metadata["productName"]}, " +
"Score: ${doc.similarityScore}")
}
// ...
}해결:
// 1. Threshold 조정
val threshold = 0.75 // 더 엄격하게
// 2. 제품 설명 개선
val text = """
제품명: $productName
상세설명: $description
카테고리: $category // 카테고리 정보 강조
...
""".trimIndent()
// 3. 메타데이터 필터 활용
// "냉장고" 검색 시 category 필터 자동 추가증상: 영어는 잘 되는데 한국어는 결과가 이상함
확인:
# 한국어 테스트 엔드포인트
curl "http://localhost:8080/api/search/test/korean"원인 가능성:
- 인코딩 문제
- Gemini API의 한국어 처리 문제
해결:
- URL 인코딩 확인:
# ❌ 잘못된 예
curl "http://localhost:8080/api/search/products?q=냉장고"
# ✅ 올바른 예
curl "http://localhost:8080/api/search/products?q=%EB%83%89%EC%9E%A5%EA%B3%A0"- Content-Type 확인:
curl -H "Content-Type: application/json; charset=UTF-8" \
-X POST http://localhost:8080/api/search/products/advanced \
-d '{"query": "냉장고"}'확인: SearchMode 확인
curl "http://localhost:8080/api/search/products?q=냉장고" | jq '.searchMode'
# SEMANTIC or HYBRID?이해:
- SEMANTIC: 필터 없을 때, threshold 0.7
- HYBRID: 필터 있을 때, threshold 0.5
테스트:
# SEMANTIC 모드 강제
curl "http://localhost:8080/api/search/products/semantic?q=냉장고"
# HYBRID 모드 (카테고리 필터)
curl "http://localhost:8080/api/search/products?q=냉장고&categories=김치냉장고"증상: 응답 시간이 2초 이상
진단:
// 로그에서 시간 확인
@Service
class ProductSearchService {
fun search(request: ProductSearchRequest): ProductSearchResponse {
val start = System.currentTimeMillis()
// ... 검색 로직 ...
val elapsed = System.currentTimeMillis() - start
logger.info("Search took ${elapsed}ms")
return response
}
}병목 지점:
- Gemini API 호출: ~200-500ms
- Elasticsearch 쿼리: ~50-200ms
- 필터링/정렬: ~10-50ms
해결:
- 캐싱:
// 검색 결과 캐싱
@Cacheable(value = ["searchResults"], key = "#request")
fun search(request: ProductSearchRequest): ProductSearchResponse- 인덱스 최적화:
# Force merge (읽기 전용 인덱스)
POST /product/_forcemerge?max_num_segments=1- 쿼리 최적화:
// topK 줄이기
SearchRequest.query(query)
.withTopK(50) // 100 → 50증상: 애플리케이션이 OutOfMemoryError 발생
확인:
# JVM 힙 메모리 확인
jps # Java 프로세스 찾기
jmap -heap <pid>해결:
- JVM 옵션 조정:
# application 실행 시
./gradlew bootRun -Dspring-boot.run.jvmArguments="-Xmx2g -Xms1g"- 캐시 크기 제한:
@Bean
fun cacheManager(): CacheManager {
return CaffeineCacheManager().apply {
setCaffeine(Caffeine.newBuilder()
.maximumSize(1000) // 최대 1000개
.expireAfterWrite(1, TimeUnit.HOURS))
}
}- 배치 크기 조정:
// 한 번에 너무 많은 문서 처리하지 않기
products.chunked(100).forEach { batch ->
vectorStore.add(batch.map { it.toDocument() })
}해결:
# Clean build
./gradlew clean build
# 캐시 삭제
rm -rf .gradle/ build/
# 재빌드
./gradlew build
# 실행
./gradlew bootRun증상:
Connection refused: elasticsearch
원인: Elasticsearch가 실행되지 않음
해결:
# Elasticsearch 실행
docker-compose up -d elasticsearch
# 테스트 실행
./gradlew test특정 테스트만 실행:
./gradlew test --tests "ProductSearchServiceTest"방법 1: SampleDataInit.kt 수정
@PostConstruct
fun initSampleData() {
val products = mutableListOf<Product>()
// 기존 제품들...
// 새 제품 추가
products.add(Product.of(
productName = "내 제품",
description = "설명",
brand = "브랜드",
maker = "제조사",
category = "카테고리",
price = 1000000
))
vectorStore.add(products.map { it.toDocument() })
}방법 2: API 추가 (프로덕션)
@RestController
@RequestMapping("/api/admin")
class ProductAdminController(
private val vectorStore: VectorStore
) {
@PostMapping("/products")
fun addProduct(@RequestBody product: Product): ResponseEntity<*> {
val document = product.toDocument()
vectorStore.add(listOf(document))
return ResponseEntity.ok("Product added")
}
}OpenAI로 변경:
- 의존성 변경 (
build.gradle.kts):
// 제거
implementation("org.springframework.ai:spring-ai-starter-model-google-genai-embedding:1.1.0-M1")
// 추가
implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter")- 설정 변경 (
application.yml):
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
embedding:
model: text-embedding-3-small # 또는 text-embedding-ada-002
dimensions: 1536 # OpenAI는 1536차원- Elasticsearch 인덱스 재생성:
# 기존 인덱스 삭제
DELETE /product
# dimensions 변경 필요Dockerfile:
FROM gradle:8-jdk21 AS build
WORKDIR /app
COPY . .
RUN gradle build -x test
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]docker-compose.yml (전체 스택):
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- GOOGLE_PROJECT_ID=${GOOGLE_PROJECT_ID}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- ELASTICSEARCH_URL=http://elasticsearch:9200
depends_on:
- elasticsearch
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:9.1.2
environment:
- discovery.type=single-node
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- esdata:/usr/share/elasticsearch/data
volumes:
esdata:실행:
docker-compose up -dapplication.yml:
spring:
ai:
google:
genai:
embedding:
project-id: ${GOOGLE_PROJECT_ID:default-project}
api-key: ${GOOGLE_API_KEY}
vectorstore:
elasticsearch:
url: ${ELASTICSEARCH_URL:http://localhost:9200}
index-name: ${ELASTICSEARCH_INDEX:product}
server:
port: ${SERVER_PORT:8080}.env 파일 (docker-compose):
GOOGLE_PROJECT_ID=my-project
GOOGLE_API_KEY=AIza...
ELASTICSEARCH_URL=http://elasticsearch:9200
SERVER_PORT=8080실행:
docker-compose --env-file .env up -dSpring Boot SSL 설정:
- 인증서 생성 (개발용):
keytool -genkeypair -alias springboot \
-keyalg RSA -keysize 2048 \
-storetype PKCS12 -keystore keystore.p12 \
-validity 3650- application.yml:
server:
port: 8443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: springboot- 프로덕션: Nginx/Apache 리버스 프록시 사용 권장
application.yml:
logging:
file:
name: logs/application.log
max-size: 10MB
max-history: 30
level:
root: INFO
com.dokkabei97.springaielasticsearch: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"로그 파일 위치:
logs/
application.log
application.log.2025-01-01.0.gz
application.log.2025-01-02.0.gz
# Elasticsearch 상태 확인
curl http://localhost:9200/_cluster/health?pretty
# 인덱스 통계
curl http://localhost:9200/product/_stats?pretty
# 매핑 확인
curl http://localhost:9200/product/_mapping?pretty
# 샘플 문서 조회
curl http://localhost:9200/product/_search?pretty&size=1
# 로그 실시간 확인
docker-compose logs -f app
# 컨테이너 재시작
docker-compose restart app- Java 21 설치 확인
- Docker & Docker Compose 실행 확인
- Elasticsearch 실행 확인 (http://localhost:9200)
- Gemini API 키 설정 확인
- 샘플 데이터 로딩 확인 (로그)
- 네트워크/방화벽 설정 확인
-
문서 참조:
- TUTORIAL.md - 단계별 가이드
- ARCHITECTURE.md - 시스템 구조
- CONCEPTS.md - 개념 설명
-
GitHub Issues: 새로운 문제 보고
-
커뮤니티:
이 FAQ에 없는 질문이 있다면 GitHub Issues로 제보해주세요!