Elasticsearch 와의 통합 준비
Spring Boot 애플리케이션 내에서 Elasticsearch 기능을 사용하기 위해서는 적절한 의존성 도입과 클라이언트 객체의 초기화가 필요합니다. 본 가이드에서는 REST 기반의 고수준 클라이언트를 사용하여 인덱스 관리부터 문서 수준의 CRUD 작업까지 구현하는 과정을 설명합니다.
1. 빌드 도구 설정
프로젝트 초기화 시 기본적으로 제공하는 Starter 의존성 외에 Elasticsearch 라이브러리의 버전을 명시적으로 관리해야 합니다. 공식 서버 버전과 라이브러리 버전이 일치하지 않으면 호환성 오류가 발생할 수 있으므로 pom.xml을 수정하여 안정된 버전을 고정하는 것이 좋습니다.
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.10.0</version>
</dependency>
2. 인프라스트럭처 구성
애플리케이션 컨텍스트에서 사용할 Elasticsearch 인스턴스를 연결하기 위한 빈(Bean) 을 정의합니다. 싱글톤으로 관리되도록 구성 클래스를 작성하며, 향후 클러스터 확장 시 호스트 정보를 유연하게 추가할 수 있도록 구조화합니다.
@Configuration
public class EsConnectionConfig {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 9200;
private static final String PROTOCOL = "http";
@Bean
public RestHighLevelClient esClient() {
return new RestHighLevelClient(
RestClient.builder(new HttpHost(SERVER_HOST, SERVER_PORT, PROTOCOL))
);
}
}
3. 데이터 모델 정의
색인될 데이터를 표현하기 위해 내부 상태만 담는 간단한 도메인 객체를 생성합니다. Lombok 라이브러리를 활용하면 getter/setter 및 생성자 코드를 간결하게 유지할 수 있습니다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberRecord {
private String fullName;
private int yearsOld;
private String residence;
}
4. 서비스 계층 구현
비즈니스 로직을 테스트 코드와 분리하여 재사용성을 높입니다. 인덱스 생성, 삭제, 문서 저장 등 주요 작업을 메서드로 추출한 서비스 클래스를 작성합니다.
@Service
public class MemberSearchService {
private final RestHighLevelClient client;
private final ObjectMapper objectMapper;
private static final String INDEX_ID = "member_db";
public MemberSearchService(RestHighLevelClient client) {
this.client = client;
this.objectMapper = new ObjectMapper();
}
public void initializeIndex() throws IOException {
IndicesExistsRequest checkReq = new IndicesExistsRequest(INDEX_ID);
boolean exists = client.indices().exists(checkReq, RequestOptions.DEFAULT);
if (!exists) {
System.out.println("Index 생성 중...");
// 실제로는 mapping 정의를 포함할 수도 있음
}
}
public void storeMember(MemberRecord data) throws IOException {
IndexRequest req = new IndexRequest(INDEX_ID);
String jsonData = objectMapper.writeValueAsString(data);
req.source(jsonData, XContentType.JSON);
IndexResponse res = client.index(req, RequestOptions.DEFAULT);
System.out.println("저장 상태: " + res.status());
}
public Optional<MemberRecord> findMember(String memberId) throws IOException {
GetRequest getReq = new GetRequest(INDEX_ID, memberId);
GetResponse getRes = client.get(getReq, RequestOptions.DEFAULT);
if (getRes.isSourceEmpty()) {
return Optional.empty();
}
return Optional.of(objectMapper.readValue(getRes.getSourceAsString(), MemberRecord.class));
}
public void deleteMember(String memberId) throws IOException {
DeleteRequest delReq = new DeleteRequest(INDEX_ID, memberId);
client.delete(delReq, RequestOptions.DEFAULT);
}
public List<MemberRecord> getAllMembers() throws IOException {
SearchRequest searchReq = new SearchRequest(INDEX_ID);
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchAllQuery());
searchReq.source(builder);
SearchResponse response = client.search(searchReq, RequestOptions.DEFAULT);
return Arrays.stream(response.getHits().getHits())
.map(hit -> objectMapper.readValue(hit.getSourceAsString(), MemberRecord.class))
.collect(Collectors.toList());
}
}
5. 기능 검증
구현된 서비스 메서드가 정상 작동하는지 단위 테스트로 확인합니다. 스프링 부트의 테스트 어노테이션을 활용하여 컨텍스트를 로드하고 실제 API 호출을 수행합니다.
@SpringBootTest
class IntegrationTestSuite {
@Autowired
private MemberSearchService searchService;
@Test
void validateCrudProcess() throws IOException {
MemberRecord p1 = new MemberRecord("김철수", 30, "서울시");
MemberRecord p2 = new MemberRecord("박영희", 28, "부산시");
// 1. 저장
searchService.storeMember(p1);
// 2. 조회
var result = searchService.findMember("1");
assert(result.isPresent());
// 3. 전체 검색
List<MemberRecord> list = searchService.getAllMembers();
assert(!list.isEmpty());
// 4. 삭제
searchService.deleteMember("1");
System.out.println("통합 테스트 완료");
}
}