Right now do !

[WAS] Web Application의 문자 encoding 그리고 charset

by 지금당장해

프롤로그

 인간세상에 존재하는 다양한 언어로 인한 혼란은 고스란히 컴퓨터 세상으로 옮겨 갔다. 처음에 컴퓨터를 서양에서 만들었으니 그들의 언어와 문자만 고려했다. 우리와 같이 독자적인 언어와 문자를 사용하는 민족에게는 영어로 자료를 작성하고 보관하는 일이 사실상 불가능한 일이었다. 우여곡절 끝에 ASCII code라고 하는 서양의 문자를 정의하기 위한 문자 집합을 정의하듯 다양하고 고유한 문자를 정의하기 위한 문자 집합이 정의되었다. 세상의 모든 문자를 표현하고자 하는 취지의 유니코드만 존재하면 참 좋겠는데 우여곡절을 거치는 동안 다양한 문자 집합이 탄생하게 되었고 charset encoding 혼란의 근본적인 원인이 되었다. client - server에 데이터 전송을 전제로 구축되는 Web system상에서는 charset encoding 문제가 더 복잡하게 작용한다.


문자 encoding과 문자집합(charset)

사람이 인식하는 문자를 컴퓨터 세계에서는 이진화된 숫자로 저장하고 유통한다. 문자 encoding 이란 미리 정의된 mapping 규칙(문자집합; charset)에 따라 문자를 숫자 화 하는 과정을 말한다. 특정 charset으로 encoding 문자를 해석하는 과정을 decoding이라고 하는데 encoding 사용한 charset 아닌 다른 charset 사용하게 되면 글자가 깨져보이는 현상이 발생 한다

문자집합은 문자가 어떤 숫자로 표현되어야 하는지를 정의한 mapping 테이블이다. UTF-8, UTF-16, ISO-8859-1 등을 예로 들수 있다. 

 

Extended ASCII charset

Web Application에서 encoding, charset

web application도 엄밀히 따지면 client - server 구조다. 클라이언트의 기반 프로그램은 통상 브라우저가 담당하고 HTML과 java script로 구성된 응용 프로그램이 동작하고 서버는 java, asp.net등 다양한 종류의 기술로 구축된  시스템이다. 따라서 client - server(*Web Application Server)간 에 유통되는 문자 encoding charset을 일치 시키지 않으면 서버에 잘못된 문자열이 저장 되거나 브라우저에 표현되는 페이지에 깨진 문자열이 보여진다.

이 글에서는 J2EE servlet specification 4.0 을 준수하는 *Web Application Server를 기반으로 설명하려 한다. 

GET Request 그리고 URL encode

요청할 웹 리소스의 주소를 정확히 알고 있다면 주소창에 이를 입력하고 엔터를 치면 해당 요청은 복잡한 네트웍 시스템들을 통과해 해당 시스템에 도달할 것이다. 주소를 모르더라도 하이퍼 링크라는 장치를 통해 요청이 전달되기도 한다. 이를 GET방식의 요청이라고 한다. 이때도 특수한 encoding 작용하는데 이를 URL encoding 이라 한다. 

 

URL 다음과 같은 구조를 갖는다. 

         foo://example.com:8042/over/there?name=ferret#nose
         \_/   \______________/\_________/ \_________/ \__/
          |           |            |            |        |
       scheme     authority       path        query   fragment
          |   _____________________|__
         / \ /                        \
         urn:example:animal:ferret:nose

URL의 모든 내용은 우리가 키보드로 칠 수 있는 문자로 이루어져 있는데 이 내용중에는 주소를 구분하는 '/' 와 같은 문자, 소위 쿼리 스트링이라고 하는 파라미터의 시작을 알리는 '?'와 같은 예약(reserved)문자가  포함되어 있다. 또한 ASCII 범위 벗어난 한글과 같은 확장 문자가 섞일수 있다. 사용자가 입력한 문자중 이러한 예약문자들을 주소를 구성하기 위한 요소인지 서버로 전송하기 위한 데이터의 일부인지 구분 없이 입력한다면 시스템은 물론 입력한 사람도 구분하기 힘들 것이다. 아래는 URL 구성을 위해 예약된 문자이다.

 

RFC 3986 - URI에 대한 표준(2005)

 

reserved    = gen-delims / sub-delims

gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"

sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"/ "*" / "+" / "," / ";" / "="

 

이를 위해 웹 응용프로그램 개발자는 URL encode 이라는 선택을 할 수 있다. 이 encoding은 reserved 문자 혹은 한글과 같은 ASCII 범위를 벗어난 문자를 %로 시작하는 숫자로 변환하는 encoding이다.  서버로 전달하려는 파라미터 중에 위에서 언급한 reserved 문자가 섞여 있어도 URL encoding을 이용하면 데이터와 URL 구분자가 뒤죽박죽 되는 일은 사라진다. 이렇게 안전하게 encoding된 문자열이 server 전송되면 URL decode과정을 통해 원래의 값으로 변환 있는데 이는 일반적으로 Web Application Server 알아서 처리 해준다. 별도로 decode 하는 과정 없이 ServletReqest#getParameter 메서드를 호출하면 decode된 값을 받아 볼 수 있다는 뜻이다. 

 

URL encode에서 사용해야 하는 charset

한 가지 집고 넘어가자. java.net.URLEncoder.encode 메서드에는 charset을 지정 할 수 있는 인자가 있다. url encoding에도 charset이 존재한다는 뜻이다. 따라서 url encoding이 필요한 경우 다음과 같이 utf-8로 charset을 지정해야 한다.

<%@ page import="java.net.URLEncoder" %>
<%@ page import="java.io.UnsupportedEncodingException" %>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<%!
    private String getUserName() throws UnsupportedEncodingException {
        return URLEncoder.encode("홍길동", "utf-8");
    }

    private String getUserHome() throws UnsupportedEncodingException {
        return URLEncoder.encode("https://www.dalcomlab.com", "utf-8");
    }
%>
<a href="hello-servlet?name=<%=getUserName()%>&home=<%=getUserHome()%>">Hello Servlet</a>
</body>
</html>

참고로 URLEncoder.encode 메서드에는 charset을 지정하지 않아도되는 overload가 있다. 이 메서드를 사용하면 file.encoding 값을 따르게 된다. 이는 시스템의 기본 로케일 정보에 의해 결정된다. 즉 어떤 시스템에 설치되는냐에 따라 chatset 이랬다 저랬다 할 수 있다는 말이다. 혼란스럽다. 그런 이유로 해당 메서드는 Deprecated 되었다. (왠만하면 쓰지 말자)


POST Request 그리고 Request charset

POST method를 이용하여 요청을 처리할 때 사용자가 입력한 데이터를 어떤 charset으로 encoding하여 전송할지 지정 할 수 있으며 서버에서는 클라이언트에서 사용한 charset을 지정하여 요청에 포함된 사용자 데이터(파라미터) 읽을 있다. 이를 Reqeust charset이라 한다.

클라이언트(브라우저)에서 request charset 지정하는 방법

참고로 최근 브라우저들은 charset 지정하는 방법을 제공하지 않는다. 이는 html page에서 지정한 값을 따르겠다는 뜻이다. HTML5 기준으로 다음과 같은 방법을 통해 page charset 지정한다.

<meta charset="utf-8">

이 meta정보는 2가지 의미를 내포한다. 하나는 이 페이지가 내용을 렌더링 할 때 HTML을 UTF-8로 해석해서 처리 하라는 의미를 가지고 있다. 또 다른 의미는 POST method로 텍스트 데이터를 전송할 때 (form tag accept-charset 지정하지 않았다면) 해당 charset으로 encoding하여 전송한다는 의미가 된다.

 만약 해당 meta 정보가 없다면 어떤 charset으로 동작할까? 필자가 테스트 한 결과 EUC-KR이 기본 charset으로 동작하는 것을 알수 있었다. 미국에서 만든 chrome이 EUC-KR을 기본 값으로 했을리가 없다. 브라우저의 언어 설정을 따른다고 한다. 사용자의 브라우저가 어떤 언어 설정을 했는가에 따라 한글이 잘 보이거나 깨지 거나 하는 혼돈 상태의 서비스를 제공하고 싶은 개발자는 한명도 없을 것이다. page에서 명시적으로 지정 할 것을 권장한다.

Web application(server측)에서 Request charset 지정할 있는 다양한 방법 그리고 주의사항

 브라우저에서 전달된 POST 파라미터가 지정된 charset으로 전달되면 서버에서는 같은 charset으로 요청을 처리해야 한글과 같은 문자를 제대로 처리 할 수 있다. 이를 위해 각 POST 파라미터를 읽기 전에 해당하는 charset 지정이 필요한데 여기에는 다양한 방법이 존재한다. 

 

방법 1. ServletRequest#setCharacterEncoding

해당 요청에 charset 지정하는 방법이다. Request charset 하나의 요청 프로세스 마다 결정된다. 요청 프로세스에서 최초 파라미터를 읽기 전이나 getReader 호출하기전에 메서드를 통해 charset 설정해야 한다. 만일 파라미터를 읽거나 getReader 메서드를 호출한 setCharacterEncoding메서드로 charset 설정해도 아무런 효과가 없다. 혼란스러운 포인트는 설정된 charset 확인 있는 getCharacterEncoding 이용해 설정된 값을 확인해보면 이후 설정한 값으로 변경되었는데도 불구하고 무용 지물이다. ( 이렇게 서블릿 스팩을 만들었는지 불만 스럽다. 하지만 악법도 법이니  없다.)

 

방법 2. ServletContext#setRequestCharacterEncoding

요청 프로세스에서 charset 결정하는 것은 번거로울 뿐만 아니라 혼란 스럽기까지 하다. 메서드는 이런 관점에 해당 애플리케이션 Context의 기본 Request charset 설정할 있는 기능이다. 메서드를 사용함에 있어 주의 사항이 있는데 context 생성 초기화 이후에는 사용될 없다. 만약 이후 해당 메서드를 호출하면 IllegalStateException 만나게 된다. 풀어 설명하면 Servlet Context Initializer 또는 ServletContext Listener에서만 사용이 가능하다는 뜻이다.

 

방법 3. web.xml 파일의 web-app / request-character-encoding 설정

이 방법은 ServletContext#setReqeustCharacterEncoding 메서드 설정과 동일한 효과를 볼 수 있다. 이 설정으로 해당 웹 애플리케이션 Context에 기본 Reqeust charset을 지정하는 효과를 볼 수 있다. 

방법 2, 3에서 설명한 웹 어플리케이션 영역의 기본 Reqeust charset 설정 방식은 servlet specification 4.0에서 추가된 사양 이다. 만약 이 이하의 서블릿 스팩을 사용하는 경우 해당 방법은 사용할 수 없다.

방법 4. Filter를 이용한 Request charset 설정

이 방법은 WAS에서 제공하는 기능은 아니다. 방법 1에서 제시한 ServletRequest#setCharacterEncoding을 사용하는 방식인데 다음과 같이 구현된 Filter를 요청 프로세스 상에서 파라미터를 읽거나 getReader를 사용하기 전에 배치되도록 하여 모든 요청의 기본 Request charset을 지정하는 방법이다. (서블릿 스팩 4.0이 나오기 전에 널리 쓰였던 방식이다.)

public class CharEncodingFilter extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
        throws IOException, ServletException {

        req.setCharacterEncoding("utf-8");

        chain.doFilter(req, res);
    }
}

Request charset 설정 방식의 적용 우선순위

다양한 방식의 charset설정을 섞어 쓴다면 혼란 스러운일이 발생 할 수 있다. 어떤 설정이 작용 했는지 혼란스러울수 있기 때문이다. 당연한 이야기지만 왠만하면 charset을 섞어 쓰지도 말아야 하고 charset을 설정하는 방식도 섞지 말아야 한다. 이런 원칙을 가지고 개발을 해도 이를 어기는 청개구리 개발자가 있기 마련이니 이 방법들이 적용되는 우선 순위를 정리 하고 넘어가겠다.

web.xml request-character-encoding 설정 > ServletContext#setRequestCharacterEncoding > ServletRequest#setCharacterEncoding

우선순위가 낮은 방식은 우선 순위가 높은 방식에 의해 설정된 Reqeust charset을 override한다. 예를 들어 web.xml에서 request-character-set을 euc-kr로 설정했는데 ServletContextListener에서 setReqeustCharacterEncoding으로 utf-8설정을 하면 charset은 utf-8이 된다. 사실 이는 각 방법간에 순서를 지정한 것은 아니고 Web application 기동시 각 컴포넌트들의 초기화 시점과 연관 되어 있다. 심지어 ServletRequest#setCharaterEncoding은 요청 프로세상에서 이루어지니 자연스레 적용 우선순위가 가장 낮다.


Response charset 

Response 그야말로 요청에 대한 응답을 말하며 Request 상반된 개념이며 요청 프로세스의 최종 단계이다. 응답이 어떤 형식(content-type) 취하고 있는지에 따라 클라이언트인 브라우저는 이를 화면에 렌더링 것인지 다른 어플리케이션을 열어 줄지 또는 그냥 파일로 다운로드 할지를 결정하는데 이를 mine type이라고 한다. 텍스트 기반의 mime type text/plain, text/html 등은 브라우저에 지정된 방식으로 문자열을 표현한다. 표현된 텍스트 중에는 한글과 같은 ASCII 범위를 벗어난 문자가 포함될 있다.

resp.setContentType("text/plain; charset=utf-8");

 

위와 같이 setContentType 메서드를 이용해 mine type과 더불어 Response charset을 설정할 수 있다. 이를 통해 지정된 Response charset은 이후 getWriter을 호출하는 시점에 적용된다. setContentType 또는 ServletResponse#setCharacterEncoding를 이용해 설정한 Response charset은 응답 content-type 헤더에 포함되며 이 정보는 브라우저의 문자 출력 charset에 적용된다.

 

응답 헤더 Content-Type

Response charset 지정할  있는 다양한 방법 그리고 주의사항

방법1. ServletResponse#setContentType

mime type charset 동시에 지정하는 목적으로 사용된다. 해당 요청 프로세스에서 최초 getWriter 호출하는 시점에 Response charset 결정되며 이후 호출되는 어떤 Response charset 설정 메서드 호출도 실제 output charset 영향을 미치지 못한다. Request 마찬가지로 ServletResponse#getCharacterEncoding 반환 값은 setCharacterEncoding호출 시점에 관계 업시 적용한 charset값을 반환한다. 

 

방법 2. ServletResponse#setCharacterEncoding

응답 객체의 getWriter 메서드로 반환되는 PrinterWriter 객체의 charset 결정하는 메서드다. 메서드 역시 getWriter 호출 이후 호출은 무용지물이 된다. 이 메서드는 mime type 설정 없이 output charset을 설정하는 역할만 한다. 만약 Response 헤더에 Content-Type이 없다면 이 메서드로 지정된 charset역시 적용되지 않는다. 만약 Content-Type이 없는 상태에서 utf-8로 encoding하면 한글 브라우저에서는 한글이 깨진다. 위에서 언급했다. 한글 브라우저의 기본 page charset은 euc-kr이기 때문이다. 이점 주의하기 바란다.

 

방법3. ServletContext#setResponseCharacterEncoding

해당 애플리케이션 Context에서 기본 Response charset 설정할 있는 기능이다.  메서드를 사용함에 있어 주의 사항이 있는데 context 생성  초기화 이후에는 사용될  없다. 만약 이후 해당 메서드를 호출하면 IllegalStateException 만나게 된다. 풀어 설명하면 Servlet Context Initializer 또는 ServletContext Listener에서만 사용이 가능하다는 뜻이다.

 

방법4. web.xml 파일의 web-app / response-character-encoding 설정

이 방법은 ServletContext#setReqeustCharacterEncoding 메서드 설정과 동일한 효과를 볼 수 있다. 이 설정으로 해당 웹 애플리케이션 Context에 기본 Reqeust charset을 지정하는 효과를 볼 수 있다. 

방법 3, 4에서 설명한 웹 어플리케이션 영역의 기본 Response charset 설정 방식은 servlet specification 4.0에서 추가된 사양 이다. 만약 이 이하의 서블릿 스팩을 사용하는 경우 해당 방법은 사용할 수 없다.

방법 5. Filter를 이용한 Response charset 설정

이 방법은 WAS에서 제공하는 기능은 아니다. 방법 1에서 제시한 ServletResponse#setCharacterEncoding을 사용하는 방식인데 다음과 같이 구현된 Filter를 요청 프로세스 상에서 파라미터를 읽거나 getWriter를 사용하기 전에 배치되도록 하여 모든 요청의 기본 Request charset을 지정하는 방법이다. (서블릿 스팩 4.0이 나오기 전에 널리 쓰였던 방식이다.)

다음 예제 코드를 살펴보자. Reqest charset에서 예를 든 Filter 소스에 한줄을 추가 했다.

public class CharEncodingFilter extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
        throws IOException, ServletException {

        req.setCharacterEncoding("utf-8");
        res.setCharacterEncoding("utf-8");
        
        chain.doFilter(req, res);
        
    }
}

방법 6. locale-encoding-mapping 

이는 서비스 대상지역 - character encoding mapping 배포서술 정보(web.xml)로 Response charset을 결정하는 방식이다. 고로 다음과 같은 web.xml 배포 정보가 존재해야 동작한다. 

<locale-encoding-mapping-list>
    <locale-encoding-mapping>
        <locale>ko</locale>
        <encoding>euc-kr</encoding>
    </locale-encoding-mapping>
</locale-encoding-mapping-list>

그리고 다음과 같은 조건이 필요하다.

  1. ServletResponse#setLocale로 locale 정보가 설정되어야 한다.
  2. ServletResponse#setCharacterEncoding으로 charset이 명시적으로 지정지 않은 경우에만 동작한다.

그리고 setCharacterEncoding과 마찬가지로 commit이 된 후, 개발자 입장에서 getWriter가 호출된 이후에는 이 메서드의 호출은 무효가 될 수 있다. 이점 주의한다.

 

※ 주의사항: locale 표기법

왜 그래야 하는지 아직 정확한 근거를 찾지 못했지만 locale-encoding-mapping/locale 설정에 언어-국가 정보를 모두 표기하면 TOMCAT에서 이 기능이 동작하지 않는다. 다시 설명하면 위 예제에서 ko-KR로 설정하면 euc-kr로 charset encoding이 동작하지 않는다는 말이다. 

반대로 ServletResponse#setLocale 메서드는 유연하다. 언어 정보만 주던 언어-국가 정보를 모두 주던 상관없이 동작 한다.

response.setLocale(Locale.KOREA);
// 아래 코드도 문제 없음
response.setLocale(Locale.KOREAN);

뭔가 근거를 찾으면 이 부분은 다시 정리 하겠다.

 

※ 주의사항: getLocale 메서드 반환 값의 혼란스러움

ServletResponse#getLocale을 통해 응답 Locale을 확인 할 수 있다. 이 메서드이 기본 값이 사용자를 혼란스럽게 하는데 setLocale로 locale정보를 설정하지 않은 경우 시스템 기본 값을 반환한다. 한글 OS의 경우 기본 값은 ko-KR이다. 더 혼란 스러운 것은 시스템 기본 값은 locale-encoding-mapping에 전혀 영향을 미치지 않는다는 것이다. 꼭 setLocale을 호출해야 한다.

 

Response charset 설정 방식의 적용 우선순위

Request와 마찬가지로 Response도 다양한 방식의 charset 설정 방식이 있고 이들간에도 같은 개념에서 적용 우선순위를 정리 해볼 필요가 있다. 차이 점이라고 하면 요청 프로세스 상에서 사용 할 수 있는 메서드가 2개가 더 있다는 점이다.

web.xml response-character-encoding 설정 > ServletContext#setResponseCharacterEncoding > ServletResponse#setCharacterEncoding or ServletRequest#setContentType nor setLocale (with locale-encoding-mapping)

Request,Response charset 설정 값을 확인할 수 있는 Servlet API

지금까지 설명한 character encoding 설정 메서드를 설명하는 과정에서 관련 API에 대한 언급이 있었다. 관련된 모든 Servlet API를 정리하고 이 메서드들의 동작을 정리하려 한다. 예를 들어 setRequestCharacterEncoding 메서드를 이용하여 설정한 charset을 getReqeustCharacterEncoding을 이용하여 확인 할 수 있다. servlet specification 4.0 부터 ServletContext 인터페이스에 Request, Response에 대한 charset을 설정할 수 있는 메서드와 이 설정 값을 확인 할 수 있는 메서드가 추가 되었다. 4.0 이전에는 ServletRequest#setCharacterEncoding, ServletResponse#setCharacterEncoding을 한번도 호출하지 않은 요청 프로세스에서getCharacterEncoding을 호출하게 되면 무조건 null을 반환 했다. 그러나 4.0 부터는 web.xml의 request-character-encoding, response-character-encoding 의 설정 혹은 ServletContext 인터페이스의 setRequestCharacterEncoding, setResponseCharacterEncoding 메서드를 호출하여 charset을 설정 하면 ServletRequest, ServletResponse getCharacterEncoding은 이 Servlet Context의 charset 기본 값을 반환한다.

 

이 내용을 아래와 같은 표로 정리 해봤다.

Interface Method 영향을 미치는 요소
ServletRequest getCharacterEncoding 기본 값 : null
web.xml의 web-app/request-character-encoding 설정 혹은 setReqestCharacterEncoding  메서드 호출,
ServletRequest#setCharacterEncoding
메서드 호출
ServletResponse getCharacterEncodeing 기본 값 : ISO-8859-1
web.xml의 web-app/response-character-encoding 설정 혹은 setResponseCharacterEncoding 메서드 호출,
ServletResponse#setCharacterEncoding 메서드 호출
ServletContext getReqestCharacterEncoding 기본 값 : null
web.xml의 web-app/request-character-encoding 설정 혹은 setRequestCharacterEncoding  메서드 호출
ServletContext getResponseCharacterEncoding 기본 값 : null
web.xml의 web-app/response-character-encoding 설정 혹은 setResponseCharacterEncoding 메서드 호출

애필로그 

모든 지식들이 그렇듯 알고나면 정리하고 나면 별것 아닌데 하는 생각이 든다. 별거 아닌데 막상 한글이 깨져보이면 원인이 뭐지 하고 한참이나 이것 저것을 뒤져 봐야한다. WAS를 만들어본 필자도 누가 이 주제를 물어보면 바로 답이 안 나온다. 한 번 정리하고 가야지 하는 생각에서 적어도 Java web application 상에서 등장하는 모든 charset을 정리 해봤다. 이 글을 바탕으로 charset을 설정했는데도 한글이 깨지면 그건 데이터다. DB 혹은 파일의 charset도 검토 해봐야 한다. 

블로그의 정보

지금 당장 해!!!

지금당장해

활동하기