Java 백엔드 개발자라면 한 번쯤 Elasticsearch를 붙여본 경험이 있을 거다. 다만 Spring Boot 프로젝트에서 Elasticsearch RestClient를 설정하는 순간 'Connection refused' 에러가 뜨면서 벽에 부딪히는 경우가 많다. 포트 번호가 틀렸나, 서버가 안 떠있나, 아니면 설정이 잘못됐나 하면서 무작정 인터넷에서 가져온 코드를 여기저기 붙여본다. 이번에는 이 문제를 어디서 비롯되는지, 어떻게 해결하는지 실무 기반으로 정확히 정리해서 소개하겠다.
Elasticsearch는 기본적으로 REST API 기반이다. 클라이언트는 HTTP로 Elasticsearch 서버의 특정 포트에 연결해야 한다. Spring Boot에서는 여러 가지 클라이언트 라이브러리를 쓸 수 있는데, 가장 많이 쓰는 것은 'Elasticsearch RestClient'(저수준)와 'RestHighLevelClient'(고수준)다. 다만 RestHighLevelClient는 7.17 버전 이후로 deprecated 되었으니, 최신 버전에서는 RestClient를 써야 한다.
Connection refused 에러는 보통 다음 두 가지 원인에서 비롯된다. 첫 번째는 Elasticsearch 서버 자체가 안 떠있거나 포트가 잘못됐을 때다. 두 번째는 Spring Boot 설정에서 RestClient가 올바르게 초기화되지 않았을 때다. 이 둘을 순서대로 점검해야 한다.
Elasticsearch 기본 포트는 9200(REST API)과 9300(클러스터 통신)이다. Spring Boot 클라이언트는 9200으로 연결해야 한다. 로컬 머신이거나 Docker로 떠있다면, 먼저 이 명령어로 Elasticsearch가 제대로 응답하는지 확인하자.
curl -X GET http://localhost:9200
성공하면 클러스터 정보가 JSON으로 돌아온다. 만약 'Connection refused'가 뜬다면 서버가 안 떠있거나 포트가 다르다는 뜻이다.
먼저 pom.xml(Maven)에 정확한 버전의 라이브러리를 넣어야 한다. Spring Boot 버전과 Elasticsearch 버전의 호환성이 중요하다.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-boot-es</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Elasticsearch 8.x 이상용 -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.10.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
Spring Boot 3.1.5는 Elasticsearch 8.10.0과 호환된다. 다른 버전 조합을 쓴다면 반드시 공식 호환성 문서를 확인하자.
이제 Spring의 @Configuration으로 RestClient 빈을 만든다. 이 부분에서 설정 실수가 가장 많이 일어난다.
@Configuration
public class ElasticsearchConfig {
@Bean
public ElasticsearchClient elasticsearchClient() {
// 기본값으로만 설정해서 타임아웃 없고 인증도 없음
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200, "http")
).build();
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
}
이 코드의 문제점은 무엇인가. 첫째, 타임아웃 설정이 없어서 네트워크 지연이 발생하면 무한 대기한다. 둘째, 연결 풀이나 재시도 로직이 없다. 셋째, HttpHost만 하드코딩되어 있어서 환경별로 설정을 바꾸기 어렵다.
@Configuration
public class ElasticsearchConfig {
@Value("${elasticsearch.host:localhost}")
private String esHost;
@Value("${elasticsearch.port:9200}")
private int esPort;
@Value("${elasticsearch.username:}")
private String esUsername;
@Value("${elasticsearch.password:}")
private String esPassword;
@Bean
public ElasticsearchClient elasticsearchClient() {
// HttpHost 생성
HttpHost host = new HttpHost(esHost, esPort, "http");
// RestClient 빌더 설정
RestClientBuilder builder = RestClient.builder(host);
// 기본 인증 설정
if (esUsername != null && !esUsername.isEmpty()) {
builder.setHttpClientConfigCallback(httpClientBuilder -> {
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(esUsername, esPassword.toCharArray())
);
return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
});
}
// 타임아웃 설정
builder.setRequestConfigCallback(requestConfigBuilder -> {
requestConfigBuilder.setConnectTimeout(5000);
requestConfigBuilder.setSocketTimeout(30000);
return requestConfigBuilder;
});
RestClient restClient = builder.build();
ElasticsearchTransport transport = new RestClientTransport(
restClient,
new JacksonJsonpMapper()
);
return new ElasticsearchClient(transport);
}
}
이 코드가 올바른 이유는 다음과 같다. 첫째, application.yml에서 호스트와 포트를 읽어오므로 환경별로 쉽게 바꿀 수 있다. 둘째, 타임아웃을 명시적으로 설정했다(연결 5초, 소켓 30초). 셋째, Elasticsearch에 인증이 필요하면 기본 인증을 지원한다. 넷째, 에러 처리와 재연결 로직을 RestClient 내부에서 자동으로 처리한다.
application.yml 파일에 Elasticsearch 연결 정보를 넣는다.
spring:
application:
name: es-app
elasticsearch:
host: localhost
port: 9200
username: elastic
password: changeme
로컬 개발 환경에서는 인증이 필요 없을 수 있다. 그 경우 username과 password를 빈 문자열로 두면 된다.
이제 실제로 Elasticsearch에 데이터를 저장하고 검색해보자.
@Service
public class ElasticsearchService {
private final ElasticsearchClient esClient;
public ElasticsearchService(ElasticsearchClient esClient) {
this.esClient = esClient;
}
// 문서 색인
public void indexDocument(String indexName, String docId, Map<String, Object> document) throws IOException {
esClient.index(i -> i
.index(indexName)
.id(docId)
.document(document)
);
System.out.println("Document indexed: " + docId);
}
// 검색
public List<Map<String, Object>> search(String indexName, String keyword) throws IOException {
SearchResponse<Map> response = esClient.search(s -> s
.index(indexName)
.query(q -> q
.match(m -> m
.field("title")
.query(keyword)
)
),
Map.class
);
List<Map<String, Object>> results = new ArrayList<>();
for (Hit<Map> hit : response.hits().hits()) {
results.add(hit.source());
}
return results;
}
}
이 코드는 'title' 필드에서 키워드를 찾는다. Search API 응답에서 hits() 배열을 반복하면서 각 문서를 추출한다.
| 상황 | 원인 | 해결책 |
|---|---|---|
| Connection refused | Elasticsearch 서버가 안 떠있음 | curl로 9200 포트 응답 확인 |
| SocketTimeoutException | 타임아웃 설정이 너무 짧음 | setSocketTimeout() 값 증가 |
| 401 Unauthorized | 인증 정보가 잘못됐거나 없음 | username/password 확인 또는 빈 문자열로 설정 |
| TooManyRequests (429) | 클라이언트가 요청을 너무 빨리 보냄 | 요청 사이에 delay 추가 또는 배치 처리 도입 |
| IndexNotFoundException | 색인이 존재하지 않음 | 먼저 색인 생성 API 호출하기 |
Elasticsearch에 처음 문서를 색인할 때, 색인이 없으면 자동으로 생성된다. 다만 매핑(필드 타입)을 미리 정의하려면 색인을 수동으로 생성하는 게 낫다.
@Component
public class ElasticsearchIndexInitializer {
private final ElasticsearchClient esClient;
public ElasticsearchIndexInitializer(ElasticsearchClient esClient) {
this.esClient = esClient;
}
@PostConstruct
public void initializeIndex() throws IOException {
String indexName = "products";
// 색인이 이미 있는지 확인
boolean exists = esClient.indices().exists(e -> e.index(indexName)).value();
if (!exists) {
esClient.indices().create(c -> c
.index(indexName)
.mappings(m -> m
.properties("title", p -> p.text(t -> t))
.properties("description", p -> p.text(t -> t))
.properties("price", p -> p.integer(i -> i))
)
);
System.out.println("Index " + indexName + " created");
}
}
}
@PostConstruct는 스프링이 빈을 초기화한 직후 메서드를 자동으로 호출한다. 이렇게 하면 애플리케이션 시작 시 Elasticsearch 색인이 자동으로 준비된다.
Elasticsearch를 Docker로 띄워서 로컬에서 테스트하는 것이 가장 간단하다.
docker run -d \
--name elasticsearch \
-p 9200:9200 \
-p 9300:9300 \
-e discovery.type=single-node \
-e xpack.security.enabled=false \
docker.elastic.co/elasticsearch/elasticsearch:8.10.0
이 명령어는 보안을 비활성화한 싱글 노드 Elasticsearch 컨테이너를 띄운다. 로컬 테스트용이므로 프로덕션에서는 절대 쓰면 안 된다.
Elasticsearch 연동은 설정이 정확해야 한다. Connection refused 에러는 대부분 포트 번호, 타임아웃, 인증 정보 중 하나가 잘못됐다는 신호다. 환경 설정 파일로 연결 정보를 외부화하고, 타임아웃을 명시적으로 설정하고, 색인을 미리 생성하는 이 세 가지 습관이 모여서 안정적인 검색 시스템을 만든다. 이 글의 설정 클래스와 서비스 코드를 참고해서 프로젝트에 적용하면, Elasticsearch 연동 문제의 90%를 해결할 수 있을 것이다.