Microsoft MVP성태의 닷넷 이야기
.NET Framework: 791. C# - ElasticSearch를 위한 Client 라이브러리 제작 [링크 복사], [링크+제목 복사]
조회: 19123
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 5개 있습니다.)
개발 환경 구성: 392. 윈도우 환경에서 curl.exe를 이용한 elasticsearch 6.x 기본 사용법
; https://www.sysnet.pe.kr/2/0/11663

개발 환경 구성: 393. 윈도우 환경에서 elasticsearch의 한글 형태소 분석기 설치
; https://www.sysnet.pe.kr/2/0/11664

개발 환경 구성: 394. 윈도우 환경에서 elasticsearch의 한글 블로그 검색 인덱스 구성
; https://www.sysnet.pe.kr/2/0/11669

.NET Framework: 791. C# - ElasticSearch를 위한 Client 라이브러리 제작
; https://www.sysnet.pe.kr/2/0/11676

개발 환경 구성: 507. Elasticsearch 6.6부터 기본 추가된 한글 형태소 분석기 노리(nori) 사용법
; https://www.sysnet.pe.kr/2/0/12309




C# - ElasticSearch를 위한 Client 라이브러리 제작

사실 Restful API 기반은 지난 글에서도 설명했지만,

C# - JIRA REST API 사용 정리
; https://www.sysnet.pe.kr/2/0/11566

json2csharp 사이트의 도움으로,

json2csharp
; http://json2csharp.com/

쉽게 strong-type의 엔티티 클래스로 구현을 매끄럽게 할 수 있습니다. 문제는, ElasticSearch가 그다지 잘 정립된 JSON 클래스를 사용하지 않는다는 점입니다. 예를 들어, 지난 글에서 실습한 my_blog 인덱스에 대해 GET을 요청하면 다음과 같은 응답이 옵니다.

curl -XGET "http://localhost:9200/my_blog

{
  "my_blog" : {
    "aliases" : { },
    "mappings" : {
      "articles" : {
        "properties" : {
          "contents" : {
            "type" : "text",
            "analyzer" : "blogtext_analyzer"
          },
          "name" : {
            "type" : "text",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          },
          "registered" : {
            "type" : "date"
          },
          "wid" : {
            "type" : "integer"
          },
          "writer" : {
            "type" : "text",
            "index" : false
          }
        }
      }
    },
    "settings" : {
      ...[생략]...
    }
  }
}

저걸 json 역직렬화하려면 다음과 같이 도메인에 특화된 용어가 클래스의 필드로 와야 합니다.

public class IndexGetResult
{
    public MyOrg my_org { get; set; }
}

달리 말하면, XML을 정의할 때 item과 price 값을 다음과 같이 구성한 것이 아니고,

<item name="monitor" value="500" />
<item name="computer" value="600" />

다음과 같은 식으로 구성한 식이 되는 것입니다.

<monitor value="500" />
<computer value="600" />

저 2개에 대해 XML Schema를 정의한다고 했을 때 첫 번째 사례는 item 노드만 정의하면 되지만 두 번째 사례는 monitor와 computer를 정의해야 합니다. 게다가 다른 제품이 추가되면 두 번째를 위한 Schema는 다시 변경해야 하는... 전혀 유연성이 없는 구조인 것입니다.




그렇다고는 해도, 어쨌든 잘 만들어진 라이브러리를 사용하면 됩니다. ^^ 닷넷의 경우도 검색해 보면 다음과 같이 elasticsearch-net 라이브러리가 있어서,

elastic/elasticsearch-net 
; https://github.com/elastic/elasticsearch-net

Install-Package Elasticsearch.Net

Fluent 방식으로 구성되어 있으니,

var response = client.Search<Tweet>(s => s
    .From(0)
    .Size(10)
    .Query(q => q
        .Term(t => t.User, "kimchy") || q
        .Match(mq => mq.Field(f => f.User).Query("nest"))
    )
);

잘 사용하시면 됩니다. ^^




물론, 직접 만드는 방법도 있습니다. 주의할 것은, 오픈 소스들의 특성상 변화가 무쌍하기 때문에 그에 따른 유지 보수는 책임질 각오를 해야 합니다. 그래도 만들고 싶다면 네트워크 호출 부분과 기본적인 Elasticsearch와의 통신 부분을 가볍게 나누는 것이 좋습니다.

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace ElasticSearchLib
{
    public class LLClient
    {
        HttpClient _httpClient;
        CookieContainer _cookies;

        public LLClient()
        {
            _cookies = new CookieContainer();

            HttpClientHandler handler = new HttpClientHandler();
            handler.CookieContainer = _cookies;

            _httpClient = new HttpClient(handler);
        }

        public async Task<string> GetAsync(string query)
        {
            HttpResponseMessage hrm = await _httpClient.GetAsync(query);
            return await hrm.Content.ReadAsStringAsync();
        }

        public async Task<string> DeleteAsync(string query)
        {
            HttpResponseMessage hrm = await _httpClient.DeleteAsync(query);
            return await hrm.Content.ReadAsStringAsync();
        }

        public async Task<string> PutAsync(string query, string putData)
        {
            StringContent putContent = new StringContent(putData);
            putContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
            
            HttpResponseMessage hrm = await _httpClient.PutAsync(query, putContent);
            return await hrm.Content.ReadAsStringAsync();
        }

        internal async Task<string> PostAsync(string query, string putData)
        {
            StringContent putContent = new StringContent(putData);
            putContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");

            HttpResponseMessage hrm = await _httpClient.PostAsync(query, putContent);
            return await hrm.Content.ReadAsStringAsync();
        }
    }
}

그다음, 이것을 이용해 Elasticsearch에 대한 것을 만들어 나가면 됩니다.

using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Threading.Tasks;

namespace ElasticSearchLib
{
    public class ElasticSearchClient
    {
        string _server;
        short _port;

        string _node;
        public string Node
        {
            get { return _node; }
        }

        LLClient _client;
        public LLClient NetworkClient
        {
            get { return _client; }
        }

        ErrorInfo _lastErrorInfo;
        public ErrorInfo LastErrorInfo
        {
            get { return _lastErrorInfo; }
        }

        protected bool DetectErrorInfo(dynamic resultObject)
        {
            if (resultObject.error != null)
            {
                _lastErrorInfo = resultObject.ToObject<ErrorInfo>();
                return true;
            }

            return false;
        }

        public ElasticSearchClient(string server, short port)
        {
            _server = server;
            _port = port;

            _node = string.Format("http://{0}:{1}", _server, _port);
            _client = new LLClient();
        }

        public async Task<bool> ConnectAsync(string expectedVersion)
        {
            string responseText = await _client.GetAsync(_node);
            if (string.IsNullOrEmpty(responseText) == true)
            {
                return false;
            }

            ServerInfo result = Newtonsoft.Json.JsonConvert.DeserializeObject<ServerInfo>(responseText);

            if (expectedVersion == null)
            {
                return true;
            }

            System.Version target = new System.Version(result.version.number);
            System.Version expected = new System.Version(expectedVersion);

            return target.Major == expected.Major
                && target.Minor == expected.Minor
                && target.Build == expected.Build
                && (target.Revision == -1 || target.Revision == expected.Revision);
        }

        protected string GetResurceText(Type baseType, string resourceName)
        {
            string fqdn = string.Format("{0}.Entity.{1}", baseType.Namespace, resourceName);
            using (Stream stream = baseType.Assembly.GetManifestResourceStream(fqdn))
            using (StreamReader reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();   
            }
        }

        string GetResurceText(string resourceName)
        {
            return GetResurceText(typeof(ElasticSearchClient), resourceName);
        }

        protected async Task<bool> CreateIndexAsync(string indexName)
        {
            string indexUrl = string.Format("{0}/{1}", _node, indexName);

            string putData = GetResurceText("CreateIndex.json");
            string responseText = await _client.PutAsync(indexUrl, putData);

            if (string.IsNullOrEmpty(responseText) == true)
            {
                return false;
            }

            dynamic indexResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
            JObject jObject = indexResult as JObject;

            if (DetectErrorInfo(indexResult) == true)
            {
                return false;
            }

            RequestResult result = jObject.ToObject<RequestResult>();
            return result.acknowledged;
        }

        public async Task<bool> RemoveIndexAsync(string indexName)
        {
            string indexUrl = string.Format("{0}/{1}", _node, indexName);
            string responseText = await _client.DeleteAsync(indexUrl);

            if (string.IsNullOrEmpty(responseText) == true)
            {
                return false;
            }

            dynamic indexResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
            JObject jObject = indexResult as JObject;

            if (DetectErrorInfo(indexResult) == true)
            {
                return false;
            }

            RequestResult result = jObject.ToObject<RequestResult>();
            return result.acknowledged;
        }

        public async Task<bool> CreateTypeAsync(string indexName, string typeName, string putData)
        {
            string indexUrl = string.Format("{0}/{1}/{2}/_mapping", Node, indexName, typeName);
            string responseText = await NetworkClient.PutAsync(indexUrl, putData);
            if (string.IsNullOrEmpty(responseText) == true)
            {
                return false;
            }

            dynamic typeResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
            JObject jObject = typeResult as JObject;

            if (DetectErrorInfo(typeResult) == true)
            {
                return false;
            }

            RequestResult result = jObject.ToObject<RequestResult>();
            return result.acknowledged;
        }

        public async Task<bool> ExistsTypeAsync(string indexName, string typeName)
        {
            string typeUrl = string.Format("{0}/{1}/{2}/_mapping", Node, indexName, typeName);
            return await ExistsTypeAsync(typeUrl);
        }

        protected async Task<bool> ExistsTypeAsync(string typeUrl)
        {
            string responseText = await NetworkClient.GetAsync(typeUrl);
            if (string.IsNullOrEmpty(responseText) == true)
            {
                return false;
            }

            dynamic typeResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
            JObject jObject = typeResult as JObject;

            if (DetectErrorInfo(typeResult) == true)
            {
                return false;
            }

            return true;
        }

        protected async Task<bool> AddOrUpdateDocumentAsync(string documentUrl, string putData)
        {
            string responseText = await NetworkClient.PutAsync(documentUrl, putData);
            if (string.IsNullOrEmpty(responseText) == true)
            {
                return false;
            }

            dynamic addResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
            JObject jObject = addResult as JObject;

            if (DetectErrorInfo(addResult) == true)
            {
                return false;
            }

            TypeAddResult result = jObject.ToObject<TypeAddResult>();
            return result._shards.successful == 1;
        }

        protected async Task<JToken> SearchDocumentsAsync(string typeUrl, string putData)
        {
            string responseText = await NetworkClient.PostAsync(typeUrl, putData);
            if (string.IsNullOrEmpty(responseText) == true)
            {
                return null;
            }

            dynamic searchResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
            JObject jObject = searchResult as JObject;

            if (DetectErrorInfo(searchResult) == true)
            {
                return null;
            }

            SearchResult result = jObject.ToObject<SearchResult>();
            if (result.hits.total == 0)
            {
                return null;
            }

            return jObject.GetValue("hits").SelectToken("hits");
        }
    }
}

이걸 바탕으로 여러분들의 도메인에 맞는 타입들을 맞춰가야 합니다. 그나마 다행이라면 6.x 때부터 Index와 Type이 1:1 관계여서 Type만으로 그냥 Index는 자동화해서 처리할 수 있기 때문에 로직이 약간 가벼워질 수 있습니다. 게다가, 도메인 관련 타입들은 분리해서 dynamic과 함께 JSON.NET의 JToken/JObject 타입의 ToObject 제네릭 메서드를 잘 섞으면 유연하게 처리할 수 있습니다.

using ElasticSearchLib;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlogSearchLib
{
    public class BlogSearchClient : ElasticSearchClient
    {
        public BlogSearchClient(string server, short port) : base(server, port)
        {
        }

        string GetResurceText(string resourceName)
        {
            return GetResurceText(typeof(BlogSearchClient), resourceName);
        }

        public async Task<bool> AddOrUpdateDocumentAsync(string indexName, string typeName, Article article)
        {
            string documentUrl = string.Format("{0}/{1}/{2}/{3}", Node, indexName, typeName, article.wid);
            string putData = Newtonsoft.Json.JsonConvert.SerializeObject(article);

            return await AddOrUpdateDocumentAsync(documentUrl, putData);
        }

        private string GetQuery(string keyword, int pageFrom, int pageSize)
        {
            string template = GetResurceText("SearchBlog.json");
            string searchOperator = "or";

            if (keyword.IndexOf("and") != -1)
            {
                searchOperator = "and";
                keyword = keyword.Replace("and", "");
            }

            return string.Format(template, pageFrom, pageSize, keyword, searchOperator);
        }

        public async Task<Article[]> SearchDocumentsAsync(string indexName, string typeName, string keyword, int pageFrom = 0, int pageSize = 25)
        {
            List<Article> articles = new List<Article>();

            string typeUrl = string.Format("{0}/{1}/{2}/_search", Node, indexName, typeName);
            string putData = GetQuery(keyword, pageFrom, pageSize);

            JToken jToken = await SearchDocumentsAsync(typeUrl, putData);
            if (jToken == null)
            {
                return articles.ToArray();
            }

            List<Hit> hits = jToken.ToObject<List<Hit>>();

            articles.AddRange(from hit in hits
                              select hit._source);

            return articles.ToArray();
        }

        public async Task<Article> GetDocumentAsync(string indexName, string typeName, int key)
        {
            string documentUrl = string.Format("{0}/{1}/{2}/{3}", Node, indexName, typeName, key);

            string responseText = await NetworkClient.GetAsync(documentUrl);
            if (string.IsNullOrEmpty(responseText) == true)
            {
                return null;
            }

            dynamic indexResult = Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);

            if (indexResult.found == false)
            {
                return null;
            }

            JObject jObject = indexResult as JObject;

            return jObject.GetValue("_source").ToObject<Article>();
        }

        public async Task<bool> CreateBlogTypeAsync(string indexName, string typeName)
        {
            if (await CreateIndexAsync(indexName) == false)
            {
                return false;
            }

            string putData = string.Format(GetResurceText("CreateBlogType.json"), typeName);
            return await CreateTypeAsync(indexName, typeName, putData);
        }

    }
}

그래서 다음과 같이 간단하게 CRUD 작업을 할 수 있습니다.

using BlogSearchLib;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        const string Server = "localhost";
        const short Port = 9200;

        const string _typeName = "article";
        const string _indexName = "article_idx";

        static async Task Main(string[] args)
        {
            BlogSearchClient client = new BlogSearchClient(Server, Port);

            if (await client.ConnectAsync("6.1.1.0") == false)
            {
                Console.WriteLine("Can't conncted");
                return;
            }

            await CreateType(client);
            Console.WriteLine();
            await AddSamples(client);
            Console.WriteLine();
            await GetSamples(client);
            Console.WriteLine();

            Thread.Sleep(5000);
            await SearchSamples(client);
            Console.WriteLine();

            Console.WriteLine("Press any key to exit...");
            Console.ReadLine();
        }

        private static async Task SearchSamples(BlogSearchClient client)
        {
            Console.WriteLine("[Keywrods: gacutil]");
            {
                Article [] articles = await client.SearchDocumentsAsync(_indexName, _typeName, "gacutil");

                foreach (Article article in articles)
                {
                    Console.WriteLine("Search Document: " + article.contents);
                }
            }
            Console.WriteLine();

            Console.WriteLine("[Keywrods: gacutil 한국어 테스트]");
            {
                Article[] articles = await client.SearchDocumentsAsync(_indexName, _typeName, "gacutil 한국어 테스트", 0, 2);

                foreach (Article article in articles)
                {
                    Console.WriteLine("Search Document: " + article.contents);
                }
            }
            Console.WriteLine();

            Console.WriteLine("[Keywrods: gacutil and 닷넷]");
            {
                Article[] articles = await client.SearchDocumentsAsync(_indexName, _typeName, "gacutil and 닷넷");

                foreach (Article article in articles)
                {
                    Console.WriteLine("Search Document: " + article.contents);
                }
            }
            Console.WriteLine();

            Console.WriteLine("[Keywrods: gacutil and 닷넷DLL]");
            {
                Article[] articles = await client.SearchDocumentsAsync(_indexName, _typeName, "gacutil and 닷넷DLL");

                foreach (Article article in articles)
                {
                    Console.WriteLine("Search Document: " + article.contents);
                }
            }
            Console.WriteLine();
        }

        private static async Task GetSamples(BlogSearchClient client)
        {
            {
                Article article = await client.GetDocumentAsync(_indexName, _typeName, 1);
                Console.WriteLine("Document 1: " + article.contents);
            }

            {
                Article article = await client.GetDocumentAsync(_indexName, _typeName, 2);
                Console.WriteLine("Document 2: " + article.contents);
            }

            {
                Article article = await client.GetDocumentAsync(_indexName, _typeName, 3);
                Console.WriteLine("Document 3: " + article.contents);
            }

            {
                Article article = await client.GetDocumentAsync(_indexName, _typeName, 4);
                Console.WriteLine("Expected null: " + article);
            }
        }

        private static async Task AddSamples(BlogSearchClient client)
        {
            {
                Article article = new Article
                {
                    contents = "gacutil.exe를 실행해 <a href='dotnet'>닷넷</a>DLL을 GAC에 등록하려 할 때 다음과 같은 식의 오류가 발생한다면",
                    registered = DateTime.Now,
                    wid = 1,
                    writer = "tester1",
                };

                Console.WriteLine("Added: " + await client.AddOrUpdateDocumentAsync(_indexName, _typeName, article));
            }

            {
                Article article = new Article
                {
                    contents = "한국어를 처리하는 예시입니닼ㅋㅋ",
                    registered = DateTime.Now,
                    wid = 2,
                    writer = "tester2",
                };

                Console.WriteLine("Added: " + await client.AddOrUpdateDocumentAsync(_indexName, _typeName, article));
            }

            {
                Article article = new Article
                {
                    contents = "테스트 이미지<img alt=\"test\">입니다",
                    registered = DateTime.Now,
                    wid = 3,
                    writer = "tester3",
                };

                Console.WriteLine("Added: " + await client.AddOrUpdateDocumentAsync(_indexName, _typeName, article));
            }
        }

        private static async Task CreateType(BlogSearchClient client)
        {
            if (await client.ExistsTypeAsync(_indexName, _typeName) == true)
            {
                Console.Write("deleting old articles type... ");
                bool deleteOldType = await client.RemoveIndexAsync(_typeName);
                Console.WriteLine(deleteOldType);
            }
            else
            {
                Console.Write("NO TYPE: articles - ");
                Console.Write(client.LastErrorInfo.status + " - ");
                Console.WriteLine(client.LastErrorInfo.error.reason);
            }

            Console.Write("creating myorg/articles type... ");
            bool createTypeResult = await client.CreateBlogTypeAsync(_indexName, _typeName);
            Console.WriteLine(createTypeResult);
        }
    }
}

물론, 살을 더 붙여나가야겠지만 기본은 이 정도만 있으면 충분할 것입니다. ^^

(첨부 파일은 이 글의 프로젝트를 포함합니다.)




이하 시행착오입니다.

우선, Elasticsearch는 기본적으로 127.0.0.1에만 소켓 바인딩을 합니다.

[2018-08-26T01:32:24,436][INFO ][o.e.n.Node               ] [] initializing ...
[2018-08-26T01:32:24,550][INFO ][o.e.e.NodeEnvironment    ] [FGFsQ64] using [1] data paths, mounts [[New Volume (F:)]], net usable_space [282.8gb], net total_space [931.5gb], types [NTFS]
...[생략]...
[2018-08-26T01:32:30,030][INFO ][o.e.t.TransportService   ] [FGFsQ64] publish_address {127.0.0.1:9300}, bound_addresses {127.0.0.1:9300}, {[::1]:9300}
[2018-08-26T01:32:33,103][INFO ][o.e.c.s.MasterService    ] [FGFsQ64] zen-disco-elected-as-master ([0] nodes joined), reason: new_master {FGFsQ64}{FGFsQ64DRmyG6k9JXVyBPQ}{paFlb_XkQ4a0TiADrp70tg}{127.0.0.1}{127.0.0.1:9300}
...[생략]...

즉, 외부 서비스는 할 수 없는 상태인데 이것을 바꾸려면 yml 설정 파일을 열어,

[설치폴더]\config\elasticsearch.yml

network.host 값을 바꿔주면 됩니다.

network.host: 202.254.158.1

그리고 JAVA의 특성상 힙 메모리를 지정하는데 elasticsearch의 경우 기본 1GB를 점유하도록 설정되어 있습니다. 가벼운 VM에서 시작하는 경우 이것은 꽤나 부담이 될 수 있는 수치이므로 VM 사이즈의 조정이 필요할 수 있습니다. (아니면 JVM 힙의 크기를 줄이거나.)




윈도우 환경에서 서비스하고 있는 경우 당연히 NT 서비스로 등록을 해야 하는데요, 명령행에서 elasticsearch.bat을 실행했던 바로 그 폴더에 포함된 elasticsearch-service.bat 파일을 다음과 같이 실행하면 됩니다.

elasticsearch-service.bat install

그런데, JAVA_HOME을 알아야 하므로 이것을 elasticsearch-service.bat 파일에 등록해 주어도 되긴 합니다.

@echo off

SET JAVA_HOME=C:\Program Files\Java\jdk1.8.0_181
...[생략]...

그럼 다음과 같은 설정으로 시스템에 등록하는데,

서비스 이름: elasticsearch-service-x64
유형: Manual
상태: Stopped
경로: C:\elasticsearch\bin\elasticsearch-service-x64.exe //RS//elasticsearch-service-x64

따라서 "Automatic"으로 바꾼 후 명시적으로 시작 상태로 만들어야 합니다. 그런데, 실제로 시작해 보면 다음과 같이 오류가 발생할 수 있습니다.

Windows could not start the Elasticsearch 6.1.1 (elasticsearch-service-x64) on Local Computer. For more information, review the System Event Log. If this is a non-Microsoft service, contact the service vendor, and refer to service-specific error code 1.


이벤트 로그를 살펴봐도 별다른 정보가 없고,

Log Name:      System
Source:        Service Control Manager
Date:          8/26/2018 8:49:09 AM
Event ID:      7024
Task Category: None
Level:         Error
Keywords:      Classic
User:          N/A
Computer:      vm1
Description:
The Elasticsearch 6.1.1 (elasticsearch-service-x64) service terminated with the following service-specific error: 
Incorrect function.

대신 elasticsearch 설치 폴더의 logs 폴더에 있는 로그 파일을 보면,

[2018-08-26 08:51:09] [info]  [ 3364] Commons Daemon procrun (1.0.15.0 64-bit) started
[2018-08-26 08:51:09] [info]  [ 3364] Running 'elasticsearch-service-x64' Service...
[2018-08-26 08:51:09] [info]  [ 4268] Starting service...
[2018-08-26 08:51:09] [error] [ 4268] Failed creating java %JAVA_HOME%\jre\bin\server\jvm.dll
[2018-08-26 08:51:09] [error] [ 4268] The system cannot find the path specified.
[2018-08-26 08:51:09] [error] [ 4268] ServiceStart returned 1
[2018-08-26 08:51:09] [error] [ 4268] The system cannot find the path specified.
[2018-08-26 08:51:09] [info]  [ 3364] Run service finished.
[2018-08-26 08:51:10] [info]  [ 3364] Commons Daemon procrun finished

위와 같이 JAVA_HOME 변수를 사용하고 있는 것을 볼 수 있습니다. 따라서 NT 서비스로 Elasticsearch를 구동하려면 어차피 전역 시스템 환경 변수에 JAVA_HOME을 등록해야 합니다.




영문 윈도우에서 한글 데이터를 쓰는 경우 WebClient 타입으로 코딩한 경우 다음과 같은 식의 오류가 발생합니다.

Caused by: com.fasterxml.jackson.core.JsonParseException: Invalid UTF-8 middle byte 0xcc
 at [Source: org.elasticsearch.common.bytes.BytesReference$MarkSupportingStreamInputWrapper@750bcb09; line: 1, column: 189]
        at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:1702) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:558) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._reportInvalidOther(UTF8StreamJsonParser.java:3550) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._reportInvalidOther(UTF8StreamJsonParser.java:3557) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._decodeUtf8_2(UTF8StreamJsonParser.java:3327) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._finishString2(UTF8StreamJsonParser.java:2517) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._finishAndReturnString(UTF8StreamJsonParser.java:2469) ~[jackson-core-2.8.10.jar:2.8.10]
        at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.getText(UTF8StreamJsonParser.java:315) ~[jackson-core-2.8.10.jar:2.8.10]
        at org.elasticsearch.common.xcontent.json.JsonXContentParser.text(JsonXContentParser.java:83) ~[elasticsearch-6.1.1.jar:6.1.1]
        at org.elasticsearch.common.xcontent.support.AbstractXContentParser.textOrNull(AbstractXContentParser.java:237) ~[elasticsearch-6.1.1.jar:6.1.1]
        at org.elasticsearch.index.mapper.TextFieldMapper.parseCreateField(TextFieldMapper.java:345) ~[elasticsearch-6.1.1.jar:6.1.1]
        at org.elasticsearch.index.mapper.FieldMapper.parse(FieldMapper.java:297) ~[elasticsearch-6.1.1.jar:6.1.1]
        ... 38 more

왜냐하면 WebClient는 기본 인코딩을 시스템에 설정된 언어로 처리하기 때문에 영문 윈도우인 경우 한글 처리가 안 되기 때문입니다. 그래서 다음과 같은 식으로 반드시 Encoding을 설정해야 합니다.

public string Put(string query, string putData)
{
    WebClient wc = new WebClient();
    wc.Headers["Content-Type"] = "application/json; charset=UTF-8";
    wc.Encoding = Encoding.UTF8;
    try
    {
        return wc.UploadString(query, "PUT", putData);
    }
    catch (System.Net.WebException e)
    {
        // e...
    }
}




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 8/29/2018]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12720정성태7/19/20216976Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
12719정성태7/19/20216135오류 유형: 737. SharePoint 설치 시 "0x800710D8 The object identifier does not represent a valid object." 오류 발생
12718정성태7/19/20216752개발 환경 구성: 581. Windows에서 WSL로 파일 복사 시 root 소유권으로 적용되는 문제파일 다운로드1
12717정성태7/18/20216729Windows: 195. robocopy에서 파일의 ADS(Alternate Data Stream) 정보 복사를 제외하는 방법
12716정성태7/17/20217523개발 환경 구성: 580. msbuild의 Exec Task에 robocopy를 사용하는 방법파일 다운로드1
12715정성태7/17/20219239오류 유형: 736. Windows - MySQL zip 파일 버전의 "mysqld --skip-grant-tables" 실행 시 비정상 종료 [1]
12714정성태7/16/20217942오류 유형: 735. VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll이 없어 exe 실행이 안 되는 경우
12713정성태7/16/20218496.NET Framework: 1077. C# - 동기 방식이면서 비동기 규약을 따르게 만드는 Task.FromResult파일 다운로드1
12712정성태7/15/20217897개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
12711정성태7/15/20218241개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/202110012개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/20216683Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202110032Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202177248오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/20218560.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [2]파일 다운로드1
12705정성태7/13/20218681VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/20217828개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/202113462개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/20218588오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/20218250.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/20218636.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/20217464VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202111441오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/20217334개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/20217884VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
12695정성태7/3/20217972VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...