일반 자바에서 UTF-8 개발에 관한 정리 (2)

황제낙엽 2006.10.06 17:50 조회 수 : 2466 추천:206

개요

  • utf-8 은 유니코드를 위한 『?堧?문자 인코딩 방식중 하나로 켄 톰프슨과 롭 파이크가 만들었다.
  • 유니코드 한문자를 표시하기 위해 1byte~4byte 까지 가변적으로 표현한다.
  • U+0000부터 U+007F 범위에 있는 아스키 문자들은 UTF-8에서 1바이트만으로 표시된다. 4바이트로 표현되는 문자는 모두 기본 다중언어 평면(BMP) 바깥의 유니코드 문자이며, 거의 사용되지 않는다.

구조

  • U+007F(0~127) 까지의 문자는 7비트 아스키 문자와 동일하게 표시된다.
  • U+007F 이후의 문자는 4byte 까지의 비트 패턴으로 표시되며 7비트 아스키 문자와 혼동되지 않게 하기 위해 모든 바이트의 최상위 비트는 1 이다.
코드 범위 UTF-16BE 표현 UTF-8 표현 설명
000000 ~00007F 00000000 0xxxxxxx 0xxxxxxx 아스키와 동일한 범위
000080 ~0007FF 00000xxx xxxxxxxx 110xxxxx 10xxxxxx 첫바이트는 110 으로 시작하고, 나머지 바이트는 10으로 시작함
000800 ~00FFFF xxxxxxxx xxxxxxxx 1110xxxx 10xxxxxx 10xxxxxx 첫바이트는 1110으로 시작하고 , 나머지는 10으로 시작함
010000 ~10FFFF 110110yy yyxxxxxx 110111xx xxxxxxxx 11110zzz 10zzxxxx 10xxxxxx 10xxxxxx UTF-16 "surrogate pair" 영역(yyyy=zzzzz -1)


utf-8 인코딩 예제

'위'(한글) --> U+C7404(유니코드)
U+0800부터 U+FFFF 사이의 영역에 있으므로, 표에 따라 1110xxxx 10xxxxxx 10xxxxxx 형식으로 인코딩
C704(16진수) --> 1100-0111-0000-0100(2진수) --> 11101100 10011100 10000100(utf-8 인코딩)
결과적으로 이 문자는 3바이트로 인코딩 되면 16진수로 표기하면 EC 9C 84가 된다.

(첫 128 문자는 1바이트로 표시되고, 그 다음 1920 문자 (판독 기호가 붙은 라틴 문자, 그리스 문자, 키릴 문자, 콥트 문자, 아르메니아 문자,
히브리 문자, 아랍 문자)는 2바이트로 표시되며, 나머지 문자들 중 BMP 안에 들어 있는 것은 3바이트, 아닌 것은 4바이트로 표시된다.)

관련자료

utf-8 체크

    1. 입력된 문자열이 utf-8로 인코딩 되었는지 하기 위해서는 utf-8 의 인코딩 패턴을 조사한다. 즉 바이트 배열의 패턴이 utf-8 인코딩 패턴(1110xxxx 10xxxxxx 10xxxxxx)과 맞는지를 비교한다.
    2. 000080 ~0007FF 범위의 110xxxxx 10xxxxxx 패턴 일 경우엔 중복되는 코드값으로 인해 utf-8 인코딩인지 아닌지 체크하는것은 사실상 불가능하다.
      110xxxxx 10xxxxxx 패턴의 utf-8 판독

      110xxxxx 10xxxxxx 패턴의 코드일 경우 utf-8로 인코딩 되었는지 아닌지를 확인하는것은 사실상 불가능하다.
      하지만 utf-8로 인코딩 되면 아스키 영역은 1byte 한글은 3byte 내지는 4byte를 사용하므로 단순하게 생각하여 2 바이트 110xxxxx 10xxxxxx 패턴의 경우는 utf-8 인코딩이 아님을 유추할수 있다.
      판독 기호가 붙은 라틴 문자, 그리스 문자, 키릴 문자, 콥트 문자, 아르메니아 문자, 히브리 문자, 아랍 문자 등이 입력되면 판독 오류가 발생하겠지만 이는 무시해도 좋을듯 하다.

    3. Null 값의 경우 자바에서는 변형된 utf-8 인코딩 방식에 따라 1byte가 아닌 2byte(11000000 10000000)로 표기하므로 110xxxxx 10xxxxxx 의 경우 null 인지 비교해야 정확한 판독이 가능하지만 입력된 값의 인코딩여부를 판독하므로 상황에 따라서는 무시해도 괜찮을듯 하다.
    4. CJK 2바이트+한글 2바이트 가 '1110xxxx 10xxxxxx 10xxxxxx 0xxxxxxx' 와 같은 패턴으로 utf-8 3바이트 와 아스키1바이트 조합과 유사할 경우 앞자리 3바이트를 유니코드 형태로 역치환 해서 000800 ~00FFFF 범위가 맞는지 체크한다.

utf-8 인코딩 체크 예제

utf-8 인코딩 판독 예제

1. '위'.getBytes("ISO-8859-1") : -20 , -100, -124
2. unsigned byte = 236, 156, 132
3. 16진수 : EC , 9C, 84
4. 2진수 : 11101100 10011100 10000100
5. urf-8 패턴제거: 00001100 00011100 00000100
6. 유니코드 타입으로 치환 : 00001100<<12 + 0011100<<6 + 00000100 = 11000000 00000100
7. 16진수 : 000800 <= 00E704 =< 00FFFF

package util;

import java.io.UnsupportedEncodingException;

/** 
 *  <pre>
 *  작성자 : 이종희 (qola@naver.com)
 *  JAlbum.com (http://jalbum.net/download.jsp?ref=list) 소스 참고함.   
 *  <b>특정 문자열이 utf-8 인코딩인지 아닌지 체크한다.</b>
 * 
 *  utf-8 인코딩 패턴인  110xxxxx 10xxxxxx , 1110xxxx 10xxxxxx 10xxxxxx 인지 비교한다.
 *  2바이트 (110xxxxx 10xxxxxx) 패턴의 유니 코드 중복으로 인해 100% 검증할수 없지만 
 *  한글의 경우 3byte 로 표기 되므로  2바이트 패턴일 경우 utf-8 인코딩이 아닌것으로 간주한다.
 *  
 *  따라서 000080 ~0007FF 영역의 라틴 문자, 그리스 문자, 키릴 문자, 콥트 문자, 
 *  아르메니아 문자, 히브리 문자, 아랍 문자 등은 utf-8 인코딩을 비교할수 없다. 
 *  
 *  수정된 utf-8의 의한 null값 u0000 은 11000000 10000000 로 표기되지만 무시하기로 한다.
 *  
 *  </pre>
 */

public class UTFUtil {
	   
	
	public static boolean isUTF8(String str) throws Exception{
		byte[] bytes=str.getBytes("ISO-8859-1");
		return isUTF8(bytes,0,bytes.length);
	}
	   
	public static boolean isUTF8(byte[] buf, int offset, int length) {

	       boolean yesItIs = false;
	       for (int i=offset; i<offset+length; i++) {
	          if ((buf[i] & 0xC0) == 0xC0) { // 11xxxxxx 패턴 인지 체크 
	             int nBytes;
	             for (nBytes=2; nBytes<8; nBytes++) {
	                int mask = 1 << (7-nBytes);
	                if ((buf[i] & mask) == 0) break;
	             }
	            
                      //CJK영역이나 아스키 영역의 경우 110xxxxx 10xxxxxx 패턴으로 올수 없다.
	             if(nBytes==2) return false;
	             
	             // Check that the following bytes begin with 0b10xxxxxx
	             for (int j=1; j<nBytes; j++) {
	                if (i+j >= length || (buf[i+j] & 0xC0) != 0x80) return false;
	             }
                
                if(nBytes==3){
                	// 유니코드 형태로 역치환 해서 0x0800 ~ 0xFFFF 사이의 영역인지 체크한다. 
                    char c = (char) (((buf[i] & 0x0f) << 12) + ((buf[i+1] & 0x3F) << 6) + (buf[i+2] & 0x3F));
                    if(!(c >= 0x0800 && c <= 0xFFFF)){
                        return false;
                    }	                	
                }
	                
	             yesItIs = true;
	          }
	       }
	       return yesItIs;
	 }
	    
}


 

 

 

한글 URL 사용시 문제 발생 케이스

  • IE에는 유니코드 지원을 위해 기본적으로 URL을 UTF-8 로 전송해 주지만 옵션 사항이므로 사용자 마다 환경이 다를수 있다.
  • utf-8로 다시 보내는 기능을 제공하지 않는 브라우저도 존재한다.
  • RSS 리더와 같은 별도의 클라이언트를 사용할경우 서버인코딩, 클라이언트, 브라우저 인코딩 이 모두 일치하여야만 한다.

해결책

  • REQUEST 요청시 URL이 UTF-8 인코딩 방식인지 아닌지를 체크하여 UTF-8 방식이면 "ISO-8859-1 --> UTF-8" 형태로 인코딩 변환을 해주고 그렇지 않으면 "ISO-8859-1 --> MS949" 로 인코딩 변환을 시켜준다.

구현 예제

  • APACHE MOD_REWRITE
    • 아래 예와 같이 MOD_REWRITE를 이용해 특정 패턴의 URL을 REWRITE 시킨다.
      RewriteEngine   on
          # // http://tag.naver.com/tag2/한글 --> http://tag.naver.com/tag/index.jsp?tag=한글
          RewriteRule ^/tag2/(.+)$  /tag/index.jsp?tag=$1 [PT]
  • JSP 샘플
    • isUTF8() 체크 메소드를 이용해 파라메터의 인코딩을 각각 처리해준다.
      <%@page language="java" contentType="text/html;charset=utf-8"%>
      <%@page import="java.net.URLEncoder, util.*" %>
      
      <html>
      <head>
      	<meta http-equiv="Content-type" content="text/html; charset=utf-8">
      </head>
      
      <body>
        index.jsp... <br>
        
        <%
          String uri=request.getRequestURL().toString();
        	String tag=request.getParameter("tag");
        	byte[] tagBytes=tag.getBytes("ISO-8859-1");
        	
        	out.println("byte[]===");
        		for(int i=0;i<tagBytes.length;i++){
        			out.println(tagBytes[i]+",");
        		}
        	out.println("<br>");  
        	
        	boolean isUTF8=UTFUtil.isUTF8(tag);
        	
        	if(isUTF8) {
        		out.println(util.Enco.toUtf(tag));
        		out.println("<br>");
        		out.println(isUTF8);
        		
        	}else{ 
        		out.println(new String(tag.getBytes("ISO-8859-1"),"ms949"));
        		out.println("<br>");
        		out.println(isUTF8);
        	}
        %>
        
        <a href=""http://tag_test.com/tag2/한글">http://tag_test.com/tag2/한글</a><br>
        <a href=""http://tag_test.com/tag2/코리아">http://tag_test.com/tag2/코리아</a><br>
        <a href=""http://tag_test.com/tag2/위">http://tag_test.com/tag2/위</a>
      </body>
      </html>
번호 제목 글쓴이 날짜 조회 수
347 java.util.Optional 에서의 map() 함수의 이해 황제낙엽 2026.06.24 666
346 linux(ubuntu) 의 jdk 21 이 설정된 환경에서 jdk 17 로 다운그레이드 절차 황제낙엽 2025.09.22 1532
345 [ChatGPT] javax.* 패키지와 jakarta.* 패키지의 혼합 사용 방안 황제낙엽 2025.04.15 2459
344 [ChatGPT] *.servlet.http.HttpServlet 클래스를 사용할 수 있는 주요 WAS 목록 황제낙엽 2025.04.14 2038
343 [ChatGPT] JDBC API에서 java.sql과 javax.sql 패키지의 차이점 황제낙엽 2025.01.31 2218
342 jakarta.servlet 패키지를 이용한 File Upload 구현 (2024.03.03, Servlet 5.0) 황제낙엽 2025.01.02 2310
341 [ChatGPT] Java EE가 Jakarta EE로 전환 황제낙엽 2025.01.01 2525
340 [ChatGPT] File.delete() 함수로 파일 삭제가 되지 않는 경우 황제낙엽 2024.12.10 2809
339 [ChatGPT] <jsp:include>에서 flush="true"를 설정시의 몇 가지 문제점 과 대안 황제낙엽 2024.07.19 2493
338 [Copilot] JSP, Servlet, web.xml, mysql 한글 설정 황제낙엽 2024.07.12 2555
337 Base64 클래스의 encode, decode 에서 URL-safe 황제낙엽 2024.07.10 2624
336 [Copilot] JSP 에서 다른 페이지를 현재 페이지에 포함시키는 문법 (include) 황제낙엽 2024.06.26 2570
335 Exception 출력에 대한 고찰 황제낙엽 2024.06.10 2198
334 google-auth-library-oauth2-http 라이브러리 다운로드 황제낙엽 2023.11.19 4025
333 firebase-admin-java 라이브러리 다운로드 (firebase admin sdk library) 황제낙엽 2023.11.19 2805
332 Enum 활용 (개인블로그, Effective Java) file 황제낙엽 2023.11.02 2759
331 Enum 활용 (우아한기술블로그) file 황제낙엽 2023.11.02 2790
330 servlet name 에 camel case 를 사용해서는 안되는 이유 황제낙엽 2023.10.24 2508
329 org.apache.commons.fileupload.servlet.ServletFileUpload 이용 예시 황제낙엽 2023.08.25 3336
328 ServletFileUpload.parseRequest() 실행 결과 리스트가 비어있는 오류 황제낙엽 2023.08.25 2935