ElasticSearch to OpenSearch(라이센스 체크우회)

2026. 1. 16. 02:02·기타

Nori to Seunjeon

{
  "analysis": {
    "analyzer": {
      "facilities_analyzer": {
        "char_filter": [],
        "tokenizer": "nori_tokenizer",
        "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
      }
    }
  }
}

💡

nori_analyzer 구성을 직접 명시한 모습

엘라스틱서치에서 한국어 형태소 분석기를 nori_analyzer 로 사용했습니다.

GET _cat/plugins?v

그런데 analysis-nori 플러그인은 존재하지 않았고 대신에 analysis-seunjeon 로 사용해야합니다. (은전한닢)

{
  "analysis": {
    "tokenizer": {
      "korean_seunjeon_tokenizer": {
        "type": "seunjeon_tokenizer",
        "index_eojeol": false,
        "pos_tagging": false
      }
    },
    "analyzer": {
      "facilities_analyzer": {
        "type": "custom",
        "tokenizer": "korean_seunjeon_tokenizer",
        "filter": [
          "lowercase"
        ]
      }
    }
  }
}
  • "index_eojeol": false: 어절(띄어쓰기 단위) 그대로 저장하지 않고, 형태소 단위로 쪼개서 저장(검색에 유리)
  • 플러그인 자체에서 조사/어미 등을 분리해서 처리하므로 별도의 pos 필터 제외

💡

참고로 nori 와 은전한닢은 같은 사전으로 학습한 형태소 분석기이나 nori 가 훨씬 가볍다.

당장은 용량에 관한 걱정이 없으니 nori 를 설치하는 대신에 은전한닢으로 교체한 모습입니다.

모든것을 바꿔야하나?

AWS opensearch 는 엘라스틱서치가 오픈소스로 푼 7버전(?)을 개량해서 자체적으로 개발한버전입니다.

따라서 사용할 라이브러리부터 다릅니다.

      import co.elastic.clients.elasticsearch._types.GeoDistanceType;
    import co.elastic.clients.elasticsearch._types.GeoLocation;
    import co.elastic.clients.elasticsearch._types.LatLonGeoLocation;
    import co.elastic.clients.elasticsearch._types.query_dsl.*;

그런데 opensearch 사용법을 찾아보는데 자료가 너무 부족했고 각 의존성이 opensearch에서는 어떻게 사용해야할지 막막했습니다.

이거저거 찾다가 AWS CloudSearch 에 대해서도 알게되었는데 버전이 낮은대신 aws가 자체적으로 간리하는 친구입니다. 해당 서비스에 내 데이터를 형식에 맞게 세팅해서 넣고 rest api 로 통신이 가능했습니다. 다만 비용이 조금 비싸다고 알고있어서 비용 때문에 바꾸려고하는 제 상황과는 맞지 않았습니다.

생각해보기

CloudSearch 가 Rest API 로 통신하는구나… 맞다 생각해보니 어플리케이션을 실행하면, 엘라스틱서치와 연결이 제대로 되었나 ping pong 하는 부분이 있습니다.

실제로 https 로 요청을 보냈다는 로그가 찍혔습니다. 그렇다면 이 RestClient의 빈을 재정의해서 opensearch 로 우회하는 방식으로 사용할 수 있지 않을까?

일단 opensearch 도메인을 생성할 때 마스터 사용자의 이름과 암호를 입력하는걸로 봐서는 디폴트로 HTTP 기본 인증을 사용하는 것 같습니다.

세분화된 엑세스 제어 항목에서 다른 인증항목들이 있는걸로 보아 더욱 확신됩니다.

코드

public ElasticsearchClient elasticsearchClient(ObjectMapper objectMapper) {

        // 2. 인증 설정 (Basic Auth)
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials(username, password));

        // 3. RestClient 빌더 생성
        RestClient restClient = RestClient.builder(new HttpHost(uris, 443, "https"))
                .setDefaultHeaders(new Header[]{
                        new BasicHeader("Content-Type", "application/json")
                })
                .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
                        .setDefaultCredentialsProvider(credentialsProvider)
                        // ★ 핵심: AWS OpenSearch가 안 보내주는 헤더를 강제로 주입하여 속임
                        .addInterceptorLast((HttpResponseInterceptor) (response, context) ->
                                response.addHeader("X-Elastic-Product", "Elasticsearch"))
                )
                .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
                        .setConnectTimeout(5000)
                        .setSocketTimeout(120000)
                )
                .build();

        // 4. Transport 생성
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper(objectMapper));

        // 5. Client 리턴 (기존 코드에서 쓰던 그 클라이언트 객체)
        return new ElasticsearchClient(transport);
    }

인증 설정

CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials(username, password));

저수준 HTTP 통신을 담당하는 Apache HttpClient 라이브러리의 표준적인 인증 처리 방식에 기반합니다.

HTTP 요청 헤더에 Authorization: Basic [Base64 인코딩된 ID:PW]를 넣기 위한 표준적인 준비 과정

클래스 역할 설명
UsernamePasswordCredentials 인증 정보 저장소 말 그대로 사용자 이름(username)과 비밀번호(password) 쌍을 저장하는 객체입니다. HTTP Basic 인증의 원료입니다.
BasicCredentialsProvider 인증 정보 제공자 UsernamePasswordCredentials를 받아서 저장하고 관리합니다. 이 객체는 RestClient가 요청을 보낼 때 "요청에 필요한 인증 정보를 제공해주는 역할"을 수행합니다.
AuthScope 인증 범위 지정 이 자격 증명(ID/PW)이 어떤 요청에 유효한지 그 범위를 지정합니다. AuthScope.ANY는 "어떤 호스트, 어떤 포트, 어떤 프로토콜에도 이 ID/PW를 사용하라"는 의미로 가장 넓은 범위를 지정합니다.

RestClient 생성

.setDefaultHeaders(new Header[]{
                        new BasicHeader("Content-Type", "application/json")
                })

헤더에 application/json 을 명시해줘야합니다.

Caused by: org.elasticsearch.client.ResponseException: method [PUT], host [https://search-gh-opensearch-e7rsx6oyqucfsztn7uwlrbz6sm.ap-northeast-2.es.amazonaws.com:443], URI [/facilities], status line [HTTP/1.1 406 Not Acceptable]
{"error":"Content-Type header [application/vnd.elasticsearch+json; compatible-with=8] is not supported","status":406}

최종적으로 ElasticsearchClient 를 생성해서 통신하는데, 이 친구는 application/vnd.elasticsearch+json 헤더를 통해 통신하기 때문입니다.

  • AWS OpenSearch는 Elasticsearch 7.10 버전이라서, 최신 8.x 전용 헤더를 이해하지 못하기 때문입니다.
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
    .setDefaultCredentialsProvider(credentialsProvider)
)

인증 주입: credentialsProvider에 저장된 사용자 이름과 비밀번호를 HTTP 클라이언트에 주입하여, 모든 요청에 Basic Authentication 헤더(Authorization: Basic ...)가 자동으로 포함되도록 설정합니다.

 .addInterceptorLast((HttpResponseInterceptor) (response, context) ->
                                response.addHeader("X-Elastic-Product", "Elasticsearch"))

해당부분을 추가하지 않으면

Caused by: co.elastic.clients.transport.TransportException: node: 
https://search-gh-opensearch-e7rsx6oyqucfsztn7uwlrbz6sm.ap-northeast-2.es.amazonaws.com:443/, 
status: 200, [es/indices.exists] Missing [X-Elastic-Product] header. 
Please check that you are connecting to an Elasticsearch instance, 
and that any networking filters are preserving that header.

이런 오류가 발생한데 오류내용으로는 X-Elastic-Product 헤더가 없다는 내용이다. contentType과 달리 다른 메서드에 추가를 해줘야하는데 이는 헤더의 역할과 HTTP 통신 과정 중 헤더를 주입하는 시점이 다르기 때문이다.

💡

setDefaultHeaders

  • 헤더 타입: 요청 헤더 (Request Header)
  • 추가 시점: HTTP 요청(Request)이 OpenSearch 서버로 나가기 직전.

addInterceptorLast

  • 헤더 타입: 응답 헤더 (Response Header)를 조작
  • 추가 시점: 서버로부터 HTTP 응답(Response)이 클라이언트(Java 애플리케이션)에 도착한 직후

ElasticSearch 와 다르게 OpenSearch 는 응답시 위 헤더를 안주기 때문에 라이브러리 입장에서는 우리 ElasticSearch에서 준게 아닌데? 라고 생각해서 연결을 끊어버립니다.

.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
                        .setConnectTimeout(5000)
                        .setSocketTimeout(120000)
                )

스프링 배치로 데이터를 적재하는 과정에서 타임아웃이 발생하여 시간을 넉넉하게 확보한 모습입니다.

ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper(objectMapper));

transport : 통역사

RestClient와 Mapper 를 하나로 묶어주는 코드입니다.

(objectMapper를 넘겨주는 이유는, 스프링 부트가 이미 가지고 있는 JSON 설정(날짜 포맷, 스네이크 케이스 등)을 그대로 따르기 위해서)

현재 FacilityDocument 로 그대로 DB에 넣듯이 쓰기 위해서 필요한 과정입니다. 이게 없으면 모든 데이터를
직접 JSON 문자열("{\"name\":...}")로 짜서 보내야 합니다.

법적인 문제는 없을까?

주체 고려 사항 결론 (사용자님의 책임)
서버 (AWS OpenSearch) 이 서버를 사용할 수 있는 라이선스가 있는가? YES. Apache 2.0 라이선스를 기반으로 하므로 자유롭게 사용 가능합니다.
라이브러리 (Elastic Client) 이 라이브러리의 사용 조건을 위반했는가? NO. 라이브러리의 라이선스(ELv2)는 사용자가 돈을 내는 서비스를 만들 때 적용되는 경우가 많으며, 연결 제한은 라이선스 위반이 아닌 기술적 제한입니다.

제미나이 피셜 법적 판단 여부는 연결하는 쪽에서만 고려하면 된다고합니다.
추후에 연결오류가 생긴다면 그때가서 opensearch-java 기반으로 전환하든 고려하면 될 것 같습니다.

신경 쓸 포인트

결국에는 사용환경에 맞지않는 라이브러리를 사용하므로 비정상적으로 동작하지 않는것을 항상 고려해야합니다.

elasticsearchClient.search(s->s.knn())
Failed to parse list: expecting START_ARRAY but got VALUE_STRING

예를들어 knn 메서드를 사용하면 오류가 발생합니다. 에러로그를 살펴보면 Float 형이 직렬화되면서 문자열로 변경되는 듯 합니다.

SearchResponse<Map> response = elasticsearchClient.search(s -> s
                .index("item_recommendation_index")
                .size(5)
                .query(q -> q
                        ._custom("knn", knnField)
                ), Map.class
        );

해당 문제는 _custom 메서드를 사용하여 'knn'이라는 이름의 쿼리로 전달하는 방식으로 해결했습니다.

'기타' 카테고리의 다른 글

Opensearch 데이터/설정/인덱스 초기화 이슈  (0) 2026.02.26
AWS Cognito 로 OpenSearch 대시보드 접근하기  (0) 2026.02.12
'기타' 카테고리의 다른 글
  • Opensearch 데이터/설정/인덱스 초기화 이슈
  • AWS Cognito 로 OpenSearch 대시보드 접근하기
gucoding
gucoding
gucoding 님의 블로그 입니다.
  • gucoding
    gucoding 님의 블로그
    gucoding
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • spring (1)
      • 회고 (1)
      • DB (3)
      • 개념잡기 (4)
      • Java (2)
      • 기타 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    opensearch
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
gucoding
ElasticSearch to OpenSearch(라이센스 체크우회)
상단으로

티스토리툴바