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

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...
    }
}




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



donaricano-btn



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

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

비밀번호

댓글 쓴 사람
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12744정성태7/30/20215개발 환경 구성: 586. Azure Active Directory에 연결된 App 목록을 확인하는 방법?
12743정성태7/30/202121.NET Framework: 1083. Azure Active Directory - 외부 Token Cache 저장소를 사용하는 방법파일 다운로드1
12742정성태7/30/20214개발 환경 구성: 585. Azure AD 인증을 위한 사용자 인증 유형
12741정성태7/29/202153.NET Framework: 1082. Azure Active Directory - Microsoft Graph API 호출 방법파일 다운로드1
12740정성태7/29/202126오류 유형: 747. SharePoint - InvalidOperationException 0x80131509
12739정성태7/28/202121오류 유형: 746. Azure Active Directory - IDW10106: The 'ClientId' option must be provided.
12738정성태7/28/202121오류 유형: 745. Azure Active Directory - Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).
12737정성태7/28/202125오류 유형: 744. Azure Active Directory - The resource principal named api://...[client_id]... was not found in the tenant
12736정성태7/28/202132오류 유형: 743. Active Azure Directory에서 "API permissions"의 권한 설정이 "Not granted for ..."로 나오는 문제
12735정성태7/27/202167.NET Framework: 1081. C# - Azure AD 인증을 지원하는 데스크톱 애플리케이션 예제(Windows Forms)파일 다운로드1
12734정성태7/26/202177스크립트: 20. 특정 단어로 시작하거나/끝나는 문자열을 포함/제외하는 정규 표현식 - Look-around
12733정성태7/23/202191.NET Framework: 1081. Self-Contained/SingleFile 유형의 .NET Core/5+ 실행 파일을 임베딩한다면?파일 다운로드2
12732정성태7/23/202144오류 유형: 742. SharePoint - The super user account utilized by the cache is not configured.
12731정성태7/23/202148개발 환경 구성: 584. Add Internal URLs 화면에서 "Save" 버튼이 비활성화 된 경우
12730정성태7/23/202154개발 환경 구성: 583. Visual Studio Code - Go 코드에서 입력을 받는 경우
12729정성태7/22/202162.NET Framework: 1080. xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
12728정성태7/22/202168.NET Framework: 1079. MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
12727정성태7/21/2021129.NET Framework: 1078. C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?)
12726정성태7/21/2021102VS.NET IDE: 169. 비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법
12725정성태7/21/202146오류 유형: 741. Failed to find the "go" binary in either GOROOT() or PATH
12724정성태7/21/2021123개발 환경 구성: 582. 윈도우 환경에서 Visual Studio Code + Go (Zip) 개발 환경 [1]
12723정성태7/21/202154오류 유형: 740. SharePoint - Alternate access mappings have not been configured 경고
12722정성태7/20/2021102오류 유형: 739. MSVCR110.dll이 없어 exe 실행이 안 되는 경우
12721정성태7/20/202165오류 유형: 738. The trust relationship between this workstation and the primary domain failed. - 세 번째 이야기
12720정성태7/19/202188Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...