기본적으로 네트워크 부분이 eth0 등으로 부여되던 이름이 개별적인 이름으로 바뀌었음.
바뀌어도 너무 많이 바뀜.
1. ifconfig -> ip addr
ip 명령으로 바뀜. 물론 ifconfig 을 별도 설치할 수 있긴 함.
ifup / ifdown 은 그대로 존재하나, ip addr 을 통해 장치 이름 확인 후 그 장치 이름을 써야 함
2. 네트워크 설정 -> nmtui
이젠 기본으로 TUI 가 최소 설치에도 등장. 하지만 설치 방법은 매우 불편해짐.
3. service or chkconfig -> systemctl
service 가 존재하나 별도의 서비스들을 제어하려면 systemctl 로 해야함.
chkconfig 으로 등록하고 제어하던 시작 프로그램들 역시 systemctl 에서 제어함. 목록은 systemctl list-unit-files (기본값인 list-units 는 복잡함) 로 확인 가능.
4. iptables -> firewalld
기존에는 service stop iptables 로 서비스를 껐으나, 이젠 systemctl stop firewalled 로 끌 수 있음.
5. SELINUX
setenforce 로 SELINUX 을 제어하는 것은 동일. 하지만 설정 위치가 /etc/selinux 에서 /etc/selinux/config 으로 변경됨.
2015년 10월 15일 목요일
2015년 7월 6일 월요일
인코딩(Encoding)에 대한 이해 - (3) JSP 에서의 인코딩 설정 및 한글 매개변수 가져오기에 대한 고찰.
웹 개발을 하면서 처음에는 까다롭게 다가오다가 어느 순간 의미도 모른채 그냥 그 설정 그대로 유지하는게 바로 언어에 대한 인코딩이 아닌가 합니다. 특히 정형화된 구조에서 크게 문제가 발생하지 않기 때문에 신경을 쓰지 않다가 새로운 환경으로 이전을 하게 된다거나 할 때 한글이 깨지는 등의 문제가 발생하여 고생을 할 때가 종종 있습니다.
그래서 인코딩에 대한 최소한의 내용을 알려드려 크게 당황하지 않도록 도움을 드리고자 합니다.
이 글은 초보자를 위한 글이므로, 인코딩(Encoding) 정도는 우습다는 분은 패스해주길...^^;;;
------------------------------------------------------------------------------------------------------
이번에는 HTML 이 아닌 JSP 의 인코딩 설정에 대해서 먼저 간단하게 집고 넘어가겠습니다.
JSP 는 ASP, PHP 와 같은 웹 언어들과 비슷한 것 같지만, 비교 우위에 있는 큰 장점이 하나 있습니다. 바로 훌륭한 개발툴인 이클립스(Eclipse) 등의 존재입니다.
이클립스와 같은 개발툴은 JSP 을 지원했다기 보다는 Java 와 같은 개발 언어를 지원하는데, 이를 JSP 에서도 도움을 받을 수 있어서 실제 개발을 하다 보면 ASP 나 PHP 는 ultraedit 나 editplus 와 같은 일반 텍스트 에디터로 개발을 많이 하는 반면 JSP 를 개발하는 분들은 십중팔구 이클립스를 이용합니다.
그런데, 이클립스와 같은 개발툴이 JSP 의 인코딩 방식이 무엇인지 알 수 있게 선언할 수 있는 방법이 있습니다. 바로 파일 상단에 이에 대한 선언을 하는 것입니다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
이런 선언을 많이 보셨을 겁니다. 이렇게 선언을 하면 이클립스와 같은 IDE 는 해당 파일이 UTF-8 로 인코딩된 파일이라는 것을 눈치채고 파일을 열어주게 됩니다.
그럼 왜 UTF-8 이 두 번 선언이 되어 있을까요?
JSP 는 먼저 서버에서 Java 로 변환이 된 뒤에 그 결과(=HTML)를 클라이언트에 전송하고, 클라이언트는 HTML 만을 해석하여 사용자에게 제공을 합니다. 그래서 HTML 의 <meta> 는 브라우저를 위한 설정값이라면, 위의 contentType 와 pageEncoding 은 서버를 위한 설정값인데, contentType 은 서버에서 생성될 HTML 의 캐릭터셋, pageEncoding 은 JSP 내의 Java 코드에 대한 캐릭터셋에 대한 정보입니다. 그래서 아래와 같이 두 가지 다른 캐릭터셋으로 정의를 한 뒤 출력할 수도 있습니다.
<%@ page language="java" contentType="text/html; charset=UNICODE" pageEncoding="EUC-KR" %>
예전에는 이렇게 정의를 해서 개발을 했던 적도 실제 있습니다. 실제 HTML 부분에는 한글 등이 없고 DBMS 에서 불러온 값만 출력해주는데, 이 데이터들이 UTF-16 으로 인코딩되어 있을 경우 pageEncoding 까지 UTF-16 으로 선언하면 Java 코드의 크기까지 같이 커지기 때문에 이와 같이 다른 캐릭터셋으로 선언해서 개발하던 적도 있었습니다. 하지만, 요즘은 그렇게 개발을 하지는 않습니다. 바로 디스크의 용량이 비약적으로 커지고 소스 크기는 그렇게 커지지 않았기 때문에 용량에 대한 부담이 거의 없기 때문입니다. 그래서 요즘은 그냥 UTF-8 로 통일해놓고 개발하는 것을 다시 한 번 권해드리는 겁니다.
그런데, HTML 에서 UTF-8 로 인코딩되어 사용자에게 정보가 보여졌는데, 게시글 입력과 같이 사용자가 한글이나 한자 등의 데이터를 입력해서 서버에 전송하게 될 때는 어떤 일이 벌어질까요?
아주 예전에 웹을 설계하던 사람들은 우리의 기대를 저버리지 않고 다국어에 대한 고려를 전혀 하지 않았습니다. 그래서 웹에서 전송되는 문자는 모두 ASCII 을 기준으로 데이터가 전송되는 것으로 설계가 되었습니다. 그래서 HTML 에서 영어가 아닌 다른 캐릭터셋으로 문서를 작성해서 전송하려면 문제가 발생했습니다. 그래서 강제로 문자열을 웹에 맞는 ASCII 로 변환한 뒤 이를 다시 원하는 문자열로 변환시켜주는 작업이 필요했습니다. 이런 작업이 URL Encoding/Decoding 입니다. ASP 에서는 Server.URLEncode 로 변환을 제공하고, Java 에서는 java.net.URLEncoder 의 encode 매서드를 이용해서 변환을 한 뒤 서버로 전송하게 해줬습니다. 혹은 Javascript 에서 변환시켜주기도 했고, 넘어온 문자열을 깨지지 않고 받기 위해서 URLDecoder.decode() 을 이용해서 받거나 request.setCharacterEncoding() 을 이용해서 직접적인 선언을 해서 받아줬습니다. 또한 받은 매개변수를 정확한 내용으로 변경하기 위해 new String(request.getParameter("매개변수명").getBytes("ISO-8859-1"), "UTF-8) 와 같이 길게 변환을 해주는 작업까지 직접 해줘야 했습니다.
하지만, 이런 불편함을 해결하기 위해서 Java 진영에서는 WAS 에서 문자열을 정해진 인코딩으로 직접 받아오는 방법을 제공하기 시작했습니다.
Java 에서 가장 많이 사용되는 Apache Tomcat 에서 주로 사용하는 <Connector> 의 URIEncoding 은 이렇게해서 만들어진 것입니다. 지금은 그냥 공식처럼 <Connector URIEncoding="UTF-8" /> 와 같이 선언하면 매개변수값을 UTF-8 로 그냥 받을 수 있지만, 예전에는 그렇지 않았습니다. 물론 이 또한 GET 에서만 제공되는 방법이고, WAS 마다 GET 을 위한 URL Encoding 명시 방법은 다르기 때문에 사용하는 WAS 에서 어떤 방식을 제공하는지 확인 후 사용해야 할 것입니다. 많이 사용하는 웹로직만 간단히 예를 들면, 웹로직의 가장 기본이 되는 weblogic.xml 파일을 생성한 뒤 다음과 같은 내용을 입력하면 Tomcat 의 URIEncoding 와 같은 효과를 가지게 됩니다.
<wls:charset-params>
<wls:input-charset>
<wls:resource-path>/</wls:resource-path>
<wls:java-charset-name>EUC-KR</wls:java-charset-name>
</wls:input-charset>
</wls:charset-params>
하지만, 이것들은 WAS 에 종속적이므로 소스 상에서 직접 제어를 하고자 할 때에는 request.setCharacterEncoding() 으로 선언하는 방법을 쓸 수 있습니다. 하지만, 개인적으로는 비효율적이라 생각합니다.
그럼 POST 방식은 어떻게 구현할까요? Spring 을 쓰시는 분들은 web.xml 에 아래와 같이 선언한 것을 많이들 보셨을 겁니다.
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>false</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
필터를 이용한 자동 변환 방법인데, 사실 예전에 Spring 에서 이와 같이 필터를 제공해주기 전에는 CharacterEncodingFilter 와 같은 역활을 해주는 필터를 직접 사용자가 구현해서 필터로 등록해서 사용했습니다. Spring 이 많은 편리함을 제공해준다는게 이런 곳에서도 잘 나타나는 것이죠.
개인적으로 바빠지다 보니 점점 글의 내용이 빈약해진다는게 저 스스로도 느껴지네요. 다음 번에는 인코딩에 대한 마지막 글인 MySQL 에서 정보를 가져올 때 인코딩 설정을 어떻게 하는지 간략하게 알아보고 당분간 팁을 작성하는 것은 쉬어야겠네요.
그래서 인코딩에 대한 최소한의 내용을 알려드려 크게 당황하지 않도록 도움을 드리고자 합니다.
이 글은 초보자를 위한 글이므로, 인코딩(Encoding) 정도는 우습다는 분은 패스해주길...^^;;;
------------------------------------------------------------------------------------------------------
이번에는 HTML 이 아닌 JSP 의 인코딩 설정에 대해서 먼저 간단하게 집고 넘어가겠습니다.
JSP 는 ASP, PHP 와 같은 웹 언어들과 비슷한 것 같지만, 비교 우위에 있는 큰 장점이 하나 있습니다. 바로 훌륭한 개발툴인 이클립스(Eclipse) 등의 존재입니다.
이클립스와 같은 개발툴은 JSP 을 지원했다기 보다는 Java 와 같은 개발 언어를 지원하는데, 이를 JSP 에서도 도움을 받을 수 있어서 실제 개발을 하다 보면 ASP 나 PHP 는 ultraedit 나 editplus 와 같은 일반 텍스트 에디터로 개발을 많이 하는 반면 JSP 를 개발하는 분들은 십중팔구 이클립스를 이용합니다.
그런데, 이클립스와 같은 개발툴이 JSP 의 인코딩 방식이 무엇인지 알 수 있게 선언할 수 있는 방법이 있습니다. 바로 파일 상단에 이에 대한 선언을 하는 것입니다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
이런 선언을 많이 보셨을 겁니다. 이렇게 선언을 하면 이클립스와 같은 IDE 는 해당 파일이 UTF-8 로 인코딩된 파일이라는 것을 눈치채고 파일을 열어주게 됩니다.
그럼 왜 UTF-8 이 두 번 선언이 되어 있을까요?
JSP 는 먼저 서버에서 Java 로 변환이 된 뒤에 그 결과(=HTML)를 클라이언트에 전송하고, 클라이언트는 HTML 만을 해석하여 사용자에게 제공을 합니다. 그래서 HTML 의 <meta> 는 브라우저를 위한 설정값이라면, 위의 contentType 와 pageEncoding 은 서버를 위한 설정값인데, contentType 은 서버에서 생성될 HTML 의 캐릭터셋, pageEncoding 은 JSP 내의 Java 코드에 대한 캐릭터셋에 대한 정보입니다. 그래서 아래와 같이 두 가지 다른 캐릭터셋으로 정의를 한 뒤 출력할 수도 있습니다.
<%@ page language="java" contentType="text/html; charset=UNICODE" pageEncoding="EUC-KR" %>
예전에는 이렇게 정의를 해서 개발을 했던 적도 실제 있습니다. 실제 HTML 부분에는 한글 등이 없고 DBMS 에서 불러온 값만 출력해주는데, 이 데이터들이 UTF-16 으로 인코딩되어 있을 경우 pageEncoding 까지 UTF-16 으로 선언하면 Java 코드의 크기까지 같이 커지기 때문에 이와 같이 다른 캐릭터셋으로 선언해서 개발하던 적도 있었습니다. 하지만, 요즘은 그렇게 개발을 하지는 않습니다. 바로 디스크의 용량이 비약적으로 커지고 소스 크기는 그렇게 커지지 않았기 때문에 용량에 대한 부담이 거의 없기 때문입니다. 그래서 요즘은 그냥 UTF-8 로 통일해놓고 개발하는 것을 다시 한 번 권해드리는 겁니다.
그런데, HTML 에서 UTF-8 로 인코딩되어 사용자에게 정보가 보여졌는데, 게시글 입력과 같이 사용자가 한글이나 한자 등의 데이터를 입력해서 서버에 전송하게 될 때는 어떤 일이 벌어질까요?
아주 예전에 웹을 설계하던 사람들은 우리의 기대를 저버리지 않고 다국어에 대한 고려를 전혀 하지 않았습니다. 그래서 웹에서 전송되는 문자는 모두 ASCII 을 기준으로 데이터가 전송되는 것으로 설계가 되었습니다. 그래서 HTML 에서 영어가 아닌 다른 캐릭터셋으로 문서를 작성해서 전송하려면 문제가 발생했습니다. 그래서 강제로 문자열을 웹에 맞는 ASCII 로 변환한 뒤 이를 다시 원하는 문자열로 변환시켜주는 작업이 필요했습니다. 이런 작업이 URL Encoding/Decoding 입니다. ASP 에서는 Server.URLEncode 로 변환을 제공하고, Java 에서는 java.net.URLEncoder 의 encode 매서드를 이용해서 변환을 한 뒤 서버로 전송하게 해줬습니다. 혹은 Javascript 에서 변환시켜주기도 했고, 넘어온 문자열을 깨지지 않고 받기 위해서 URLDecoder.decode() 을 이용해서 받거나 request.setCharacterEncoding() 을 이용해서 직접적인 선언을 해서 받아줬습니다. 또한 받은 매개변수를 정확한 내용으로 변경하기 위해 new String(request.getParameter("매개변수명").getBytes("ISO-8859-1"), "UTF-8) 와 같이 길게 변환을 해주는 작업까지 직접 해줘야 했습니다.
하지만, 이런 불편함을 해결하기 위해서 Java 진영에서는 WAS 에서 문자열을 정해진 인코딩으로 직접 받아오는 방법을 제공하기 시작했습니다.
Java 에서 가장 많이 사용되는 Apache Tomcat 에서 주로 사용하는 <Connector> 의 URIEncoding 은 이렇게해서 만들어진 것입니다. 지금은 그냥 공식처럼 <Connector URIEncoding="UTF-8" /> 와 같이 선언하면 매개변수값을 UTF-8 로 그냥 받을 수 있지만, 예전에는 그렇지 않았습니다. 물론 이 또한 GET 에서만 제공되는 방법이고, WAS 마다 GET 을 위한 URL Encoding 명시 방법은 다르기 때문에 사용하는 WAS 에서 어떤 방식을 제공하는지 확인 후 사용해야 할 것입니다. 많이 사용하는 웹로직만 간단히 예를 들면, 웹로직의 가장 기본이 되는 weblogic.xml 파일을 생성한 뒤 다음과 같은 내용을 입력하면 Tomcat 의 URIEncoding 와 같은 효과를 가지게 됩니다.
<wls:charset-params>
<wls:input-charset>
<wls:resource-path>/</wls:resource-path>
<wls:java-charset-name>EUC-KR</wls:java-charset-name>
</wls:input-charset>
</wls:charset-params>
하지만, 이것들은 WAS 에 종속적이므로 소스 상에서 직접 제어를 하고자 할 때에는 request.setCharacterEncoding() 으로 선언하는 방법을 쓸 수 있습니다. 하지만, 개인적으로는 비효율적이라 생각합니다.
그럼 POST 방식은 어떻게 구현할까요? Spring 을 쓰시는 분들은 web.xml 에 아래와 같이 선언한 것을 많이들 보셨을 겁니다.
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>false</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
필터를 이용한 자동 변환 방법인데, 사실 예전에 Spring 에서 이와 같이 필터를 제공해주기 전에는 CharacterEncodingFilter 와 같은 역활을 해주는 필터를 직접 사용자가 구현해서 필터로 등록해서 사용했습니다. Spring 이 많은 편리함을 제공해준다는게 이런 곳에서도 잘 나타나는 것이죠.
개인적으로 바빠지다 보니 점점 글의 내용이 빈약해진다는게 저 스스로도 느껴지네요. 다음 번에는 인코딩에 대한 마지막 글인 MySQL 에서 정보를 가져올 때 인코딩 설정을 어떻게 하는지 간략하게 알아보고 당분간 팁을 작성하는 것은 쉬어야겠네요.
2015년 7월 2일 목요일
인코딩(Encoding)에 대한 이해 - (2) HTML 에서의 캐릭터셋(CharSet) 지정.
웹 개발을 하면서 처음에는 까다롭게 다가오다가 어느 순간 의미도 모른채 그냥 그 설정 그대로 유지하는게 바로 언어에 대한 인코딩이
아닌가 합니다. 특히 정형화된 구조에서 크게 문제가 발생하지 않기 때문에 신경을 쓰지 않다가 새로운 환경으로 이전을 하게
된다거나 할 때 한글이 깨지는 등의 문제가 발생하여 고생을 할 때가 종종 있습니다.
그래서 인코딩에 대한 최소한의 내용을 알려드려 크게 당황하지 않도록 도움을 드리고자 합니다.
이 글은 초보자를 위한 글이므로, 인코딩(Encoding) 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
먼저 살펴볼 것은 HTML 의 캐릭터셋 설정입니다. 아직 웹 프로그램에 익숙하지 않은 분들 중에선 서버와 클라이언트 영역을 혼동하시는 분들이 계신데, 흔히 말하는 웹 프로그램 언어는 서버에서 해석이 되며, HTML 등을 만들어내는 역활을 합니다(물론 동적으로 만들어내는 경우에). 그리고 이렇게 만들어진 HTML 은 사용자의 장비로 전송되어 클라이언트의 브라우져에서 해석되어져 사용자에게 보여지게 됩니다. 이 때 HTML 뿐만 아니라 Javascript, CSS 들도 HTML 에 정의된 내용에 따라 처리되거나 서버에서 읽어오기 때문에 정확한 내용을 읽어오는 것은 중요합니다.
사용자의 브라우져는 HTML 을 해석할 때 스스로 기본적인 캐릭터셋을 가지고 해석을 시도하거나 문서의 내용을 읽어보고 캐릭터셋을 파악할 수 있는지 시도해봅니다. 강제로 지정되어 있다면 그 방식대로 인코딩해서 읽지만, 그렇지 않다면 스스로 인코딩 방식을 변경할 수도 있습니다. 이 때 HTML 내용 중에 <haed> 영역에서 <meta> 로 선언된 부분 중 캐릭터셋에 관련된 설정을 읽어 이를 파악합니다. 주로 다음과 같은 형식으로 되어 있습니다.
<meta http-equiv="content-type" content="text/html; charset=utf-8">
혹은
<meta charset="UTF-8">
전자는 기존의 HTML4, 후자는 HTML5 의 선언방식입니다.
보통 <head> 에서도 가장 먼저 선언을 하는 편이고, 이렇게 선언을 하면(위에서는 utf-8 로 선언을 했습니다) 브라우저는 선언된 방식으로 인코딩을 해서 문서를 해석하려고 할 것입니다.
그럼 외부 링크를 통한 문서 중 Javascript 나 CSS 파일은 어떨까요?
먼저 Javascript 는 HTML 에서 다음과 같이 외부 파일 형태로 읽어올 수 있습니다.
<script type="text/javascript" src="/js/test.js"></script>
이렇게 HTML 에서 외부 파일을 지정하면 다시 서버에 해당 파일을 요청하여 읽어온 뒤 해석을 합니다(type 대신에 language="javascript" 쓰는건 비표준이니까 type 으로 지정하세요). 이 때, 읽어올 파일에 대한 캐릭터셋을 지정할 수 있습니다.
<script type="text/javascript" src="/js/test.js" charset="UTF-8"></script>
눈치채셨겠지만, 동일한 캐릭터셋으로 저장된 파일이라면 굳이 선언을 하지 않아도 됩니다. 하지만, HTML 은 UTF-8 로 저장되었는데, Javascript 파일은 다른 곳에서 가져올 경우 캐릭터셋이 다를 경우 인코딩을 잘못 하여 내용이 깨질 수 있다고 판단되면 charset 애트리뷰트를 명시하여 정확하게 불러오는 것이 좋습니다.
CSS 는 <script> 와 마찬가지로 <link> 의 charset 을 이용할 수도 있지만, 반대로 CSS 파일 제일 상단에 캐릭터셋을 정의할 수도 있습니다.
@charset "UTF-8";
표준 문서에 보면 외부 css 파일을 불러올 때만 사용하고, 파일 제일 상단에 지정하라고 되어 있습니다.
이렇게 하면 정적인 HTML 에서도 인코딩 문제로 인해 글자가 깨지는 문제는 발생하지 않을 것입니다.
팁을 하나 드리면, 흔히 쓰는 이클립스에서는 확장자 별로 캐릭터셋을 UTF-8 로 미리 지정을 해서 새로운 파일을 만들 때 별도로 설정할 필요가 없도록 할 수 있습니다.
윈도우즈용 이클립스 기준으로 CSS 파일을 선언하는 곳은,
Windows > Preferences > General > Content Types
로 이동한 뒤,
Content types: > Text > CSS
을 선택하고 하단의
File associations:
에서 원하는 파일 확장자를 선택한 뒤
Default encoding:
에 UTF-8 을 입력한 뒤 Update 을 누르고 OK 을 눌러서 반영하면 됩니다. 물론 js 파일이나 JSP, HTML, Java 모두 이런 방식으로 인코딩 선언이 가능합니다.
개인적인 일이 좀 바빠서 이번 글은 좀 늦게, 그리고 성의없이(?), 그리고 짧게 끝냈습니다. 죄송합니다. 다음에는 WAS 와 JSP 에서의 설정을 설명드리겠습니다.
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
그래서 인코딩에 대한 최소한의 내용을 알려드려 크게 당황하지 않도록 도움을 드리고자 합니다.
이 글은 초보자를 위한 글이므로, 인코딩(Encoding) 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
먼저 살펴볼 것은 HTML 의 캐릭터셋 설정입니다. 아직 웹 프로그램에 익숙하지 않은 분들 중에선 서버와 클라이언트 영역을 혼동하시는 분들이 계신데, 흔히 말하는 웹 프로그램 언어는 서버에서 해석이 되며, HTML 등을 만들어내는 역활을 합니다(물론 동적으로 만들어내는 경우에). 그리고 이렇게 만들어진 HTML 은 사용자의 장비로 전송되어 클라이언트의 브라우져에서 해석되어져 사용자에게 보여지게 됩니다. 이 때 HTML 뿐만 아니라 Javascript, CSS 들도 HTML 에 정의된 내용에 따라 처리되거나 서버에서 읽어오기 때문에 정확한 내용을 읽어오는 것은 중요합니다.
사용자의 브라우져는 HTML 을 해석할 때 스스로 기본적인 캐릭터셋을 가지고 해석을 시도하거나 문서의 내용을 읽어보고 캐릭터셋을 파악할 수 있는지 시도해봅니다. 강제로 지정되어 있다면 그 방식대로 인코딩해서 읽지만, 그렇지 않다면 스스로 인코딩 방식을 변경할 수도 있습니다. 이 때 HTML 내용 중에 <haed> 영역에서 <meta> 로 선언된 부분 중 캐릭터셋에 관련된 설정을 읽어 이를 파악합니다. 주로 다음과 같은 형식으로 되어 있습니다.
<meta http-equiv="content-type" content="text/html; charset=utf-8">
혹은
<meta charset="UTF-8">
전자는 기존의 HTML4, 후자는 HTML5 의 선언방식입니다.
보통 <head> 에서도 가장 먼저 선언을 하는 편이고, 이렇게 선언을 하면(위에서는 utf-8 로 선언을 했습니다) 브라우저는 선언된 방식으로 인코딩을 해서 문서를 해석하려고 할 것입니다.
그럼 외부 링크를 통한 문서 중 Javascript 나 CSS 파일은 어떨까요?
먼저 Javascript 는 HTML 에서 다음과 같이 외부 파일 형태로 읽어올 수 있습니다.
<script type="text/javascript" src="/js/test.js"></script>
이렇게 HTML 에서 외부 파일을 지정하면 다시 서버에 해당 파일을 요청하여 읽어온 뒤 해석을 합니다(type 대신에 language="javascript" 쓰는건 비표준이니까 type 으로 지정하세요). 이 때, 읽어올 파일에 대한 캐릭터셋을 지정할 수 있습니다.
<script type="text/javascript" src="/js/test.js" charset="UTF-8"></script>
눈치채셨겠지만, 동일한 캐릭터셋으로 저장된 파일이라면 굳이 선언을 하지 않아도 됩니다. 하지만, HTML 은 UTF-8 로 저장되었는데, Javascript 파일은 다른 곳에서 가져올 경우 캐릭터셋이 다를 경우 인코딩을 잘못 하여 내용이 깨질 수 있다고 판단되면 charset 애트리뷰트를 명시하여 정확하게 불러오는 것이 좋습니다.
CSS 는 <script> 와 마찬가지로 <link> 의 charset 을 이용할 수도 있지만, 반대로 CSS 파일 제일 상단에 캐릭터셋을 정의할 수도 있습니다.
@charset "UTF-8";
표준 문서에 보면 외부 css 파일을 불러올 때만 사용하고, 파일 제일 상단에 지정하라고 되어 있습니다.
이렇게 하면 정적인 HTML 에서도 인코딩 문제로 인해 글자가 깨지는 문제는 발생하지 않을 것입니다.
팁을 하나 드리면, 흔히 쓰는 이클립스에서는 확장자 별로 캐릭터셋을 UTF-8 로 미리 지정을 해서 새로운 파일을 만들 때 별도로 설정할 필요가 없도록 할 수 있습니다.
윈도우즈용 이클립스 기준으로 CSS 파일을 선언하는 곳은,
Windows > Preferences > General > Content Types
로 이동한 뒤,
Content types: > Text > CSS
을 선택하고 하단의
File associations:
에서 원하는 파일 확장자를 선택한 뒤
Default encoding:
에 UTF-8 을 입력한 뒤 Update 을 누르고 OK 을 눌러서 반영하면 됩니다. 물론 js 파일이나 JSP, HTML, Java 모두 이런 방식으로 인코딩 선언이 가능합니다.
개인적인 일이 좀 바빠서 이번 글은 좀 늦게, 그리고 성의없이(?), 그리고 짧게 끝냈습니다. 죄송합니다. 다음에는 WAS 와 JSP 에서의 설정을 설명드리겠습니다.
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
2015년 6월 29일 월요일
인코딩(Encoding)에 대한 이해 - (1) ASCII 부터 UTF-8 까지의 변화.
웹 개발을 하면서 처음에는 까다롭게 다가오다가 어느 순간 의미도 모른채 그냥 그 설정 그대로 유지하는게 바로 언어에 대한 인코딩이 아닌가 합니다. 특히 정형화된 구조에서 크게 문제가 발생하지 않기 때문에 신경을 쓰지 않다가 새로운 환경으로 이전을 하게 된다거나 할 때 한글이 깨지는 등의 문제가 발생하여 고생을 할 때가 종종 있습니다.
그래서 인코딩에 대한 최소한의 내용을 알려드려 크게 당황하지 않도록 도움을 드리고자 합니다.
이 글은 초보자를 위한 글이므로, 인코딩(Encoding) 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
일반적으로 한국어를 이용하는 분들이 프로그래밍을 하면서 가장 많이 겪게 되는 인코딩(Encoding)은 ASCII, ISO-8859-1, CP949, MS949, EUC-KR, UTF-8, UTF-16 정도일 것입니다. 이런 인코딩은 무엇을 의미하는 것일까요?
최초의 컴퓨터가 애니악이라는 것은 많은 분들이 잘 아실 겁니다. 저 역시 초등학교(국민학교) 때 애니악, 애드삭, 애드박 등 달달 외어서 전국 컴퓨터 경진대회 출전하고 했던 기억이 나네요. 중요한건 이게 아닙니다만, 어쨌든 초기에는 이런 컴퓨터가 만들어져도 천공카드라고 하는 구멍 뚫린 종이를 이용해서 입력을 했지, 사람이 인식하는 글자로 입력을 한 건 아니었습니다. 그래서 실제 문자에 대한 표준이 나오는데 십수년이 걸렸다고 하네요. 역시나 초등학교 때 BCD, ASCII, EBCDIC 코드에 대해서도 달달 외웠는데, 글자 길이에 따라서 짧은게 더 적은 비트를 이용한다...이렇게 외었더랬습니다. BCD 는 6bit, ASCII 는 7bit, EBCDIC 는 8bit...이렇게요. 가장 많이 사용하게 된 코드는 ASCII 가 되었습니다.
화면 상에 출력될 문자를 7bit 로 나타내게 된 건 영어 문화권의 국가에서 컴퓨터의 발전을 주도했기 때문이었습니다. 7bit 는 2^7 이므로 총 128 개(실제로는 127 개)의 문자를 표현할 수 있었고, 영어 대소문자 각 26 개 뿐만 아니라 화면상에 표시하기 위한(실제로는 beep 라고 하는 경고음까지) 제어문자까지 넣을 수 있는 크기였기 때문입니다.
하지만, 실제 라틴어에서 파생된 많은 언어들은 영어와 비슷하지만 몇가지 다른 문자들을 가지고 있고, 이러한 문자를 다 담는데 127 개로는 부족한 상황이 발생했습니다. 그래서 ISO 표준을 통해 ISO-8859(실제로는 ISO/IEC-8859 던가요) 를 재정하고 이런 추가 문자을 담을 수 있는 집합을 만들어냈습니다. 한 개의 추가 비트를 이용해서 문자의 수를 256 개로 늘려버린 것이죠. 하지만, 이 또한 모든 라틴 계열 문자를 담을 수는 없어서 서브 카테고리라는 것을 통해 문자 집합들을 별도로 관리하게 되었는데, 가장 보편적으로 이용되는 영어권 문자들을 라틴1이라는 영역으로 ISO-8859-1 이라고 정의한 것입니다. 물론 ISO-8859-2 도 있고...16 까지 존재한다고 기억합니다.
문제는, CJK 라고 흔히 불리는 중국/일본/한국어 같은 문자는 이런 문자 집합에 포함되지도 않았고, 포함되기 힘든 상황이었습니다.
먼저 한글입니다. 한글은 세종대왕께서 만드신 아주 과학적으로 창의적인 문자이고, 키보드로 타이핑 할 때에도 아주 효율적인 문자입니다. 하지만, 자소의 조합으로 만들어지다 보니문자를 저장할 때에는 약간의 문제가 발생합니다. "가" 라는 하나의 문자를 표현하기 위해서는 "ㄱ" 와 "ㅏ" 를 각각 저장하고 이를 조합하는 방식과 "가" 라는 글자 자체를 저장하는 방식이 있을 수 있습니다. 이를 각각 조합형과 완성형으로 부릅니다. 초반에는 이 두 진영의 의견이 엇갈렸으나, 국가에서 완성형을 채택합니다. 초기에는 현대 한국어에서만 쓰는 언어들만 코드를 부여해서 제공했는데, 드라마 "똠방각하" 가 방영하면서 문제가 생겼습니다. 정부가 지정한 완성형에서 "똠" 이 포함되지 않아 타이핑을 할 수가 없었던 것입니다. 그래서 확장 완성형이라는 코드도 나오고 표준안에 조합형도 포함시킵니다. 하지만 대세는 완성형으로 흘러가서 현재는 조합형을 거의 쓰지 않습니다. 점점 더 문자 집합을 늘리는데, 이렇게 해서 나오게 된 코드가 KSC 5601 이고 이것이 EUC-KR 입니다. 당연히 일본 쪽도 비슷한 과정을 거치면서 EUC-JP 을 만들었고 중국 역시 별도의 언어 집합을 만듭니다.
Microsoft 는 여기서 좀 더 많은 문자집합을 포함한 코드를 만들어내는데 이것이 CP949 혹은 MS949 입니다.
여기서 문자 코드가 정리되는 듯 했지만, 결국 세계화 시대에 접어들면서 하나의 문서에 여러 언어가 같이 표현될 일이 잦아졌습니다. 그래서 세계의 모든 언어에 코드를 부여해서 사용할 수 있는 문자 집합의 필요성이 대두되었고, 가장 두드러지게 강세를 보였던 두 개의 코드가 나타났습니다. 바로 UTF-16 와 UTF-8 입니다.
얼핏 보기엔 16 비트와 8 비트로 보이고, UTF-16 이 더 큰 문자 집합 같아 보이지만, 사실 그렇진 않습니다. UTF-16 은 16bit 혹은 32bit 로 표현되며, UTF-8 은 8bit~32bit(1byte~4byte, 실제로는 3byte 안에 대부분의 언어가 다 포함되어 있음) 가변 형태로 표현이 됩니다. UTF-16 은 문서 내용 앞에 BOM(Byte Order Mark) 이라고 불리는 값을 입력해서 이 문서가 UTF-16 으로 작성된 문서임을 알립니다. UTF-8 은 저장된 값의 연산을 통해 이 문서가 UTF-8 을 알 수 있도록 구성되어 있습니다. 하지만, MS 에서 UTF-8 에도 BOM 을 앞에 삽입하게 됨으로써(UTF-8 표준에는 BOM 을 넣을 필요도 없고, 넣지 않는 것을 권고하고 있고, 넣지 않는게 표준이라고 분명히 명시하고 있습니다) 메모장 등으로 저장한 UTF-8 은 앞에 BOM 이 강제로 삽입되게 되고, 이를 Java 에서 읽을 경우 BOM 영역 때문에 문서 앞이 깨지고 이상 동작이 발생하는 문제가 생기게 되었습니다. 여기서 분명 말씀드리지만, UTF-8 에서 BOM 은 빼는게 맞고, Java 는 표준에 맞는 행동을 하기 때문에 비표준 방식에서 문제가 나타나는 것일 뿐입니다. 그래서 비표준 문서를 바꾸는게 맞는 방법입니다.
어쨌든 국내에서는 초반에 UTF-16 을 좀 더 선호하는 경향이 있었습니다. UTF-8 에서 한글은 모두 16bit 안에 코드가 부여되어서 한 글자를 표현하는데 16bit 가 소비되었지만, UTF-8 에서는 24bit 영역(3바이트)에 코드가 부여되어 있어서 동일한 내용을 저장하는데 UTF-16 이 더 효율적이었기 때문이었습니다. 그래서 제가 초반에 다루었던 수많은 한국학 관련 XML 데이터들은 모두 UTF-16 으로 작성되었습니다.
하지만, UTF-8 은 UTF-16 보다 나은 장점 하나가 있었습니다. 가변 방식이다 보니, 8bit(1byte) 영역에 정의된 문자들은 UTF-16 보다 적은 공간을 차지한다는 것이었습니다. 이 8bit 영역에 정의된 문자가 바로 ISO-8859-1 에 정의된 영어권 문자들이었습니다. 앞서 말씀드렸듯이 영어권이 가장 큰 힘을 발휘하고 있는게 사실이고, 그 사람들은 자기들이 문서를 작성하는데 왜 공간을 낭비하냐는 생각이 앞섰기 때문에 UTF-8 에 더 힘을 실어 주었습니다. 게다가 UTF-8 의 8bit 영역은 ISO-8859-1 와 코드값이 일치해서 그들의 입맛에 더 맞는 형태였습니다.
그러나 예외는 있습니다. 한자가 특히 큰 문제인데요...한자는 아직도 새롭게 생겨나고 있는 언어입니다. 우리나라도 예외는 아닌데, 특히 조선시대 양반들의 경우 자신의 이름에 쓰이는 한자가 다른 사람과 동일하게 쓰이는 것을 싫어해서 원래 한자에 점 하나를 더 찍거나 선 하나를 더 긋는 식으로 새로운 한자를 만들어내기도 했습니다. 이런 문제 때문에 아직도 신출자라고 하는 형태로 해서 새로운 한자가 추가되고 있으며, 중국과 일본에서도 자신들만의 한자를 계속 만들어내고 있는 형편입니다. 그래서 이런 한자들을 써야하는 곳에서는 별도의 폰트를 만들어서 다운로드해서 쓰게 하고 있습니다.
이렇게 문자코드가 발전을 해왔는데, 사실 현재의 대세는 UTF-8 이 되어버린 상황입니다. 그래서 예전 코드를 관리하는 입장에서라면 문자 코드를 유지하는 것이 좋지만, 새로운 프로젝트에서는 UTF- 8 로 문자 집합을 통일해서 쓰는 것을 제안하는 바입니다.
다음 글에서는 HTML 에서 JSP, Java, DBMS 로 이어지는 문자 코드 설정에 대해서 글을 적어보겠습니다.
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
그래서 인코딩에 대한 최소한의 내용을 알려드려 크게 당황하지 않도록 도움을 드리고자 합니다.
이 글은 초보자를 위한 글이므로, 인코딩(Encoding) 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
일반적으로 한국어를 이용하는 분들이 프로그래밍을 하면서 가장 많이 겪게 되는 인코딩(Encoding)은 ASCII, ISO-8859-1, CP949, MS949, EUC-KR, UTF-8, UTF-16 정도일 것입니다. 이런 인코딩은 무엇을 의미하는 것일까요?
최초의 컴퓨터가 애니악이라는 것은 많은 분들이 잘 아실 겁니다. 저 역시 초등학교(국민학교) 때 애니악, 애드삭, 애드박 등 달달 외어서 전국 컴퓨터 경진대회 출전하고 했던 기억이 나네요. 중요한건 이게 아닙니다만, 어쨌든 초기에는 이런 컴퓨터가 만들어져도 천공카드라고 하는 구멍 뚫린 종이를 이용해서 입력을 했지, 사람이 인식하는 글자로 입력을 한 건 아니었습니다. 그래서 실제 문자에 대한 표준이 나오는데 십수년이 걸렸다고 하네요. 역시나 초등학교 때 BCD, ASCII, EBCDIC 코드에 대해서도 달달 외웠는데, 글자 길이에 따라서 짧은게 더 적은 비트를 이용한다...이렇게 외었더랬습니다. BCD 는 6bit, ASCII 는 7bit, EBCDIC 는 8bit...이렇게요. 가장 많이 사용하게 된 코드는 ASCII 가 되었습니다.
화면 상에 출력될 문자를 7bit 로 나타내게 된 건 영어 문화권의 국가에서 컴퓨터의 발전을 주도했기 때문이었습니다. 7bit 는 2^7 이므로 총 128 개(실제로는 127 개)의 문자를 표현할 수 있었고, 영어 대소문자 각 26 개 뿐만 아니라 화면상에 표시하기 위한(실제로는 beep 라고 하는 경고음까지) 제어문자까지 넣을 수 있는 크기였기 때문입니다.
하지만, 실제 라틴어에서 파생된 많은 언어들은 영어와 비슷하지만 몇가지 다른 문자들을 가지고 있고, 이러한 문자를 다 담는데 127 개로는 부족한 상황이 발생했습니다. 그래서 ISO 표준을 통해 ISO-8859(실제로는 ISO/IEC-8859 던가요) 를 재정하고 이런 추가 문자을 담을 수 있는 집합을 만들어냈습니다. 한 개의 추가 비트를 이용해서 문자의 수를 256 개로 늘려버린 것이죠. 하지만, 이 또한 모든 라틴 계열 문자를 담을 수는 없어서 서브 카테고리라는 것을 통해 문자 집합들을 별도로 관리하게 되었는데, 가장 보편적으로 이용되는 영어권 문자들을 라틴1이라는 영역으로 ISO-8859-1 이라고 정의한 것입니다. 물론 ISO-8859-2 도 있고...16 까지 존재한다고 기억합니다.
문제는, CJK 라고 흔히 불리는 중국/일본/한국어 같은 문자는 이런 문자 집합에 포함되지도 않았고, 포함되기 힘든 상황이었습니다.
먼저 한글입니다. 한글은 세종대왕께서 만드신 아주 과학적으로 창의적인 문자이고, 키보드로 타이핑 할 때에도 아주 효율적인 문자입니다. 하지만, 자소의 조합으로 만들어지다 보니문자를 저장할 때에는 약간의 문제가 발생합니다. "가" 라는 하나의 문자를 표현하기 위해서는 "ㄱ" 와 "ㅏ" 를 각각 저장하고 이를 조합하는 방식과 "가" 라는 글자 자체를 저장하는 방식이 있을 수 있습니다. 이를 각각 조합형과 완성형으로 부릅니다. 초반에는 이 두 진영의 의견이 엇갈렸으나, 국가에서 완성형을 채택합니다. 초기에는 현대 한국어에서만 쓰는 언어들만 코드를 부여해서 제공했는데, 드라마 "똠방각하" 가 방영하면서 문제가 생겼습니다. 정부가 지정한 완성형에서 "똠" 이 포함되지 않아 타이핑을 할 수가 없었던 것입니다. 그래서 확장 완성형이라는 코드도 나오고 표준안에 조합형도 포함시킵니다. 하지만 대세는 완성형으로 흘러가서 현재는 조합형을 거의 쓰지 않습니다. 점점 더 문자 집합을 늘리는데, 이렇게 해서 나오게 된 코드가 KSC 5601 이고 이것이 EUC-KR 입니다. 당연히 일본 쪽도 비슷한 과정을 거치면서 EUC-JP 을 만들었고 중국 역시 별도의 언어 집합을 만듭니다.
Microsoft 는 여기서 좀 더 많은 문자집합을 포함한 코드를 만들어내는데 이것이 CP949 혹은 MS949 입니다.
여기서 문자 코드가 정리되는 듯 했지만, 결국 세계화 시대에 접어들면서 하나의 문서에 여러 언어가 같이 표현될 일이 잦아졌습니다. 그래서 세계의 모든 언어에 코드를 부여해서 사용할 수 있는 문자 집합의 필요성이 대두되었고, 가장 두드러지게 강세를 보였던 두 개의 코드가 나타났습니다. 바로 UTF-16 와 UTF-8 입니다.
얼핏 보기엔 16 비트와 8 비트로 보이고, UTF-16 이 더 큰 문자 집합 같아 보이지만, 사실 그렇진 않습니다. UTF-16 은 16bit 혹은 32bit 로 표현되며, UTF-8 은 8bit~32bit(1byte~4byte, 실제로는 3byte 안에 대부분의 언어가 다 포함되어 있음) 가변 형태로 표현이 됩니다. UTF-16 은 문서 내용 앞에 BOM(Byte Order Mark) 이라고 불리는 값을 입력해서 이 문서가 UTF-16 으로 작성된 문서임을 알립니다. UTF-8 은 저장된 값의 연산을 통해 이 문서가 UTF-8 을 알 수 있도록 구성되어 있습니다. 하지만, MS 에서 UTF-8 에도 BOM 을 앞에 삽입하게 됨으로써(UTF-8 표준에는 BOM 을 넣을 필요도 없고, 넣지 않는 것을 권고하고 있고, 넣지 않는게 표준이라고 분명히 명시하고 있습니다) 메모장 등으로 저장한 UTF-8 은 앞에 BOM 이 강제로 삽입되게 되고, 이를 Java 에서 읽을 경우 BOM 영역 때문에 문서 앞이 깨지고 이상 동작이 발생하는 문제가 생기게 되었습니다. 여기서 분명 말씀드리지만, UTF-8 에서 BOM 은 빼는게 맞고, Java 는 표준에 맞는 행동을 하기 때문에 비표준 방식에서 문제가 나타나는 것일 뿐입니다. 그래서 비표준 문서를 바꾸는게 맞는 방법입니다.
어쨌든 국내에서는 초반에 UTF-16 을 좀 더 선호하는 경향이 있었습니다. UTF-8 에서 한글은 모두 16bit 안에 코드가 부여되어서 한 글자를 표현하는데 16bit 가 소비되었지만, UTF-8 에서는 24bit 영역(3바이트)에 코드가 부여되어 있어서 동일한 내용을 저장하는데 UTF-16 이 더 효율적이었기 때문이었습니다. 그래서 제가 초반에 다루었던 수많은 한국학 관련 XML 데이터들은 모두 UTF-16 으로 작성되었습니다.
하지만, UTF-8 은 UTF-16 보다 나은 장점 하나가 있었습니다. 가변 방식이다 보니, 8bit(1byte) 영역에 정의된 문자들은 UTF-16 보다 적은 공간을 차지한다는 것이었습니다. 이 8bit 영역에 정의된 문자가 바로 ISO-8859-1 에 정의된 영어권 문자들이었습니다. 앞서 말씀드렸듯이 영어권이 가장 큰 힘을 발휘하고 있는게 사실이고, 그 사람들은 자기들이 문서를 작성하는데 왜 공간을 낭비하냐는 생각이 앞섰기 때문에 UTF-8 에 더 힘을 실어 주었습니다. 게다가 UTF-8 의 8bit 영역은 ISO-8859-1 와 코드값이 일치해서 그들의 입맛에 더 맞는 형태였습니다.
그러나 예외는 있습니다. 한자가 특히 큰 문제인데요...한자는 아직도 새롭게 생겨나고 있는 언어입니다. 우리나라도 예외는 아닌데, 특히 조선시대 양반들의 경우 자신의 이름에 쓰이는 한자가 다른 사람과 동일하게 쓰이는 것을 싫어해서 원래 한자에 점 하나를 더 찍거나 선 하나를 더 긋는 식으로 새로운 한자를 만들어내기도 했습니다. 이런 문제 때문에 아직도 신출자라고 하는 형태로 해서 새로운 한자가 추가되고 있으며, 중국과 일본에서도 자신들만의 한자를 계속 만들어내고 있는 형편입니다. 그래서 이런 한자들을 써야하는 곳에서는 별도의 폰트를 만들어서 다운로드해서 쓰게 하고 있습니다.
이렇게 문자코드가 발전을 해왔는데, 사실 현재의 대세는 UTF-8 이 되어버린 상황입니다. 그래서 예전 코드를 관리하는 입장에서라면 문자 코드를 유지하는 것이 좋지만, 새로운 프로젝트에서는 UTF- 8 로 문자 집합을 통일해서 쓰는 것을 제안하는 바입니다.
다음 글에서는 HTML 에서 JSP, Java, DBMS 로 이어지는 문자 코드 설정에 대해서 글을 적어보겠습니다.
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
2015년 6월 26일 금요일
페이징(Paging)에 대한 이해 - (3) LIMIT 와 TOP 을 이용한 게시물 가져오기.
흔히 웹 개발을 처음 하는 개발자가 처음 겪게 되는 난관은 바로 게시판의 페이징(Paging)이 아닐까 합니다. 특히 DBMS 와 연동해서 개발하는 경우가 많은데, 각 DBMS 마다 페이징 방법도 많고 성능 등 신경써야 할 부분이 많이 때문에 어떤 자료를 보고 따라해야 문제가 없을지 판단하기 막막할 때가 있습니다.
그래서 페이징에 대한 원리를 파악해서 가장 기본적인 페이징 구현을 할 수 있는 방법을 알려드리고자 합니다.
이 글은 초보자를 위한 글이므로, 페이징 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
이번에는 MySQL 이나 PostgreSQL 등에서 사용하는 LIMIT 와 MS SQL Server 의 예전 페이징 기법인 TOP 에 대해서 알아보겠습니다.
먼저 LIMIT 는 게시물을 가져오는데 최적의 방법을 제공합니다. 하지만 동일한 LIMIT 라는 이름이라도 사용방법은 사실 좀 다릅니다.
앞선 글에서 ROW NUMBER 을 이용하는 방식에서 설명 드렸듯이 순차적으로 내용을 정렬한 뒤 여기에 번호를 붙여서 번호 범위를 이용해서 게시물을 가져온다고 하였습니다. 하지만, 이렇게 번호를 붙이는 것은 LIMIT 와 같은 기능을 가지고 있지 않을 때의 이야기입니다. LIMIT 에서는 정렬된(order by 된) 게시물의 범위를 지정해 해당 게시물만 가져오는 기능이 있고, 번호를 부여한 다음 그 번호를 이용해 다시 비교를 할 필요가 없기 때문에 ROW NUMBER 보다 속도가 빠릅니다.
MySQL 에서는 다음과 같이 countPage = 10 일 때 3 페이지의 게시물을 가져올 수 있습니다.
select id, name, content, createdate
from board
order by createdate
limit (3 - 1) * 10, 10
MySQL 의 LIMIT 은 두 개의 매개변수를 가지는데, 첫번째 매개변수는 시작 위치, 두번째 매개변수는 가져올 게시물 수 입니다. 그래서 20 이후의 게시물 10 개를 가져오게 됩니다. 주의할 것은 매개변수를 1 개만 가질 수도 있는데, 이 때에는 통상적인 것처럼 첫번째 매개변수만 사용하는 것이 아니라 두번째 매개변수만 사용한다는 것입니다. 그러므로 limit 10 이라고 하면 limit 0, 10 와 동일한 것이 된다는것입니다.
PostgreSQL 의 LIMIT 은 조금 다릅니다.
select id, name, content, createdate
from board
order by createdate
limit 10 offset (3 - 1) * 10
offset 이라는 것이 추가되고, 매개변수의 위치가 바뀌었습니다. MySQL 와 마찬가지로 offset 은 생략 가능합니다. 단지 순서가 다르고 사용법이 조금 다를 뿐, 동일한 기능입니다.
위 예제에서 보셨드셔이 LIMIT 는 게시물을 가져올 때 최적의 성능과 최소한의 쿼리를 이용할 수 있다는 장점이 있습니다.
여담으로, 어떤 게시판 형태들을 보면 화면상에 ID(Index, Sequence) 대신에 화면상에 출력될 순서대로 번호를 보여주는 경우들이 있습니다. 직관적으로 게시물의 갯수 등을 파악할 때 용이합니다. 이 경우 해당 게시물에 마우스를 올려 링크 주소를 보면 화면에 표시되는 번호와 실제 이용에 이용하는 id 가 다른 번호를 가지고 있는 경우가 있습니다.
당연히 ROW NUMBER 을 이용할 때에는 순차적으로 번호를 부여한 뒤 가져오기 때문에 이 값을 이용하면 됩니다. 하지만, LIMIT 의 경우 순차적으로 번호를 부여한 것이 없기 때문에 프로그램에서 총 페이지와 현재 페이지 등을 이용해서 번호를 부여할 수 있습니다. 하지만 MySQL 등에서도 ROW NUMBER 을 부여하는 기능을 흉내낼 수 있는데, 게시물 뿐만 아니라 계층구조(오라클의 connect by 같은)을 만들 때도 이용할 수 있습니다. 방법은 다음과 같습니다.
select @ROWNUM := @ROWNUM + 1 as rnum, id, name, content, createdate
from board, (select @ROWNUM := 0) A
order by createdate
limit (3 - 1) * 10, 10
바로 변수를 이용해서 처리하는 방식입니다. 물론 총 게시물 수를 먼저 조회할 것이기 때문에 초기값을 0 이 아닌 전체 게시물 수로, 1 씩 증가시키는 대신 1 씩 감소를 시키는 형태로 변경하면 역순으로 번호를 발급할 수도 있습니다.
그럼 PostgreSQL 은???
PostgreSQL 에는 ROW NUMBER 발급을 해주는 row_number() 가 있습니다. 게시물 쪽에서는 모든 기능이 다 있는 것은 PostgreSQL 이라고 볼 수도 있겠네요. ^^;;;
오라클이나 MS SQL Server 에서도 최근에 OFFSET FETCH 라는 기능이 추가되어(오라클 12c 이상, MS SQL Server 2012 이상) LIMIT 와 같은 기능을 수행합니다. 하지만 성능 테스트를 진행해보지 못해서...들리는 말에 의하면 MS SQL Server 는 성능이 실망스럽다고 하네요.
마지막으로 TOP 방식에 대해서 알아보겠습니다. 사실 TOP 방식은 그냥 이론적으로만 알고 계시기만 해도 됩니다. 사실 TOP 이라는 키워드는 MS SQL Server 에서 제공하는 것이고, MS SQL Server 의 근간이 되는 데이터베이스도 사실 유사한 기능으로 페이징을 제공합니다.
그런데 MS SQL Server 2005 부터 row_number() 제공하면서 이 TOP 을 이용한 게시물 가져오기는 쓰지 않게 되었습니다. 성능 상의 문제도 있고, 사실 좀 멋진 방법은 아니거든요. 그래서 이번에는 그 중 대표적인 두 가지 방식만 설명하겠습니다.
SQL Server 6, 7 을 거쳐 2000 이 주로 쓰이던 시절까지도 사실 게시물이 몇십만 건씩 쌓여있는 게시판이나 업무 시스템 등은 없다고 해도 무방했습니다. 그래서 초기에 나온 페이징 방법은 그다지 좋지 못한 방법이었습니다. 흔히 NOT IN 방식이라고 표현하는데, ROW NUMBER 방식의 최외각 부등호/MySQL 의 첫번째 매개변수/PostgreSQL 의 offset 에 해당하는 제외 기능(SKIP 이라고 표현하겠습니다)을 구현하기 위해 NOT IN 을 썼습니다. 그런데 이게 성능이 별로 안좋습니다.
원리는 매우 간단합니다. totalCount = 25, countPage = 10, page = 2 인 경우라면 정렬을 한 뒤 10 까지의 게시물을 제외하고 상위 10 개의 게시물을 가져오는 것입니다.
select top 10 id, name, content, createdate
from board
where id not in (
select top (2 - 1) * 10 id
from board
order by createdate)
order by create date
아직도 오래된 글들을 보면 이 방식을 많이 소개하고 있을 겁니다. 문제는 NOT IN 이 성능이 꽤나 나쁘다는 것입니다. 그래서 후에 IN 을 이용한 방식도 나왔습니다만, 저는 주로 TOP 을 두 번 중첩해서 쓰는 방법을 이용했었습니다. 물론 성능은 여전히 안좋습니다.
TOP 을 두 번 이용하는 방식은 LIMIT 와 조금 비슷합니다. order by 을 이용해서 뒤집는 것인데, 인덱스 등을 이용할 수 없기 때문에 성능은 좋지 않습니다.
select top 10 A.id, A.name, A.content, A.createdate
from (
select top 2 * 10 id, name, content, createdate
from board
order by createdate) A
order by createdate desc
2 페이지를 가져오기 위해 1 ~ 2 페이지의 게시물을 가져온 뒤(20개, 2 * 10) 이 결과를 뒤집어서 마지막 10 개(=countPage)을 가져오는 것이죠. 원리상 크게 어렵지는 않습니다. 실제 화면에 출력해 줄 때에는 ResultSet 이나 List 에서 역순으로 꺼내오기만 하면 됩니다.
하지만, 이 쿼리는 여기서 끝내면 안됩니다. 왜냐하면, 위 예제에서 3 페이지, 즉 마지막 페이지를 가져올 때에 문제가 생기기 때문입니다. 총 게시물이 25 개이기 때문에 3 페이지는 21 ~ 25 까지의 5 개의 게시물을 가져와야 합니다. 하지만, 위 쿼리대로라면 top 30 을 해도 실제로는 25 개의 게시물만 가져오기 때문에 외각의 top 10 을 만나면 21 ~ 25 가 아닌 16 ~ 25 사이의 게시물을 가져오게 됩니다. 그래서 중첩된 TOP 을 쓸 경우에는 반드시 외각의 TOP 은 totalCount 을 이용해서 몇 개의 게시물을 가져올지 계산한 뒤 처리해야 합니다.
다음은 Java 에서의 처리 예제입니다.
int totalCount = 25;
int countPage = 10;
int page = 3;
int countTop;
if (page * countPage > totalCount) {
countTop = totalCount - (page - 1) * countPage;
} else {
countTop = countPage;
}
위에서 계산된 countTop 을 이용해서 필요한 만큼만 가져오게 해야 한다는 것이죠.
혹시나 매우 오래된 시스템을 유지보수 해야할 경우 참고하시면 좋겠네요.
이로써 페이징에 대한 소개를 마무리 할 수 있게 되었네요.
초보 분들에게 많은 도움이 되었으면 좋겠네요.
다음에는 페이징 만큼의 이론적인 설명이 아닌, 아주 간단한(그냥 이렇게 해놓고 잊자는 수준의) 인코딩 방법을 설명해볼까 합니다. 제 PC 에 이클립스가 안깔려있어서 잘 될런지는 모르겠네요. 아마 DBMS 는 MySQL 만 설명을 할 것입니다. 오라클도 하고 싶지만, 접근 가능한 오라클 장비가 없어서...
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
2015년 6월 23일 화요일
페이징(Paging)에 대한 이해 - (2) ROW NUMBER 을 이용한 게시물 가져오기.
흔히 웹 개발을 처음 하는 개발자가 처음 겪게 되는 난관은 바로 게시판의 페이징(Paging)이 아닐까 합니다. 특히 DBMS 와 연동해서 개발하는 경우가 많은데, 각 DBMS 마다 페이징 방법도 많고 성능 등 신경써야 할 부분이 많이 때문에 어떤 자료를 보고 따라해야 문제가 없을지 판단하기 막막할 때가 있습니다.
그래서 페이징에 대한 원리를 파악해서 가장 기본적인 페이징 구현을 할 수 있는 방법을 알려드리고자 합니다.
이 글은 초보자를 위한 글이므로, 페이징 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
이번에는 DBMS 에서 ROW NUMBER(오라클의 rownum, SQL Server 의 ROW_NUMBER(), MySQL 의 변수를 이용한 번호 등)을 이용한 게시물 가져오기에 대해서 설명하겠습니다.
ROW NUMBER 는 쉽게 말해 출력되는 결과물에 순차적인 번호를 달아주는 기능입니다. 필요할 때 언제든지 번호를 달아달라고 시스템에 요청을 하면 사용자에게 보내줄 데이터에 번호를 달아주게 되죠. 문제는 이 번호를 달 때 정렬이 되어 있지 않다면 번호를 엉뚱하게 달 수도 있고, 번호를 단다는 것 자체가 시스템에 부하를 줘서 정말 많은 게시물을 가지고 있는 경우 특정 영역의 게시물들을 가져오는데 그 반응이 느려진다는데 있습니다.
DBMS 는 보통 인덱스(Index) 라는 것을 가지고 있습니다. 그래서 1 억 개 이상의 데이터를 가지고 있더라도 인덱스를 이용하면 원하는 자료를 순식간에 가져올 수 있습니다. 하지만, 게시판에서는 특정 번호를 가지고 데이터를 불러올 수 없습니다. 실시간으로 계속 자료가 쌓이다보면 해당 페이지에 보일 게시물의 내용이 지속적으로 바뀌기 때문입니다. 변동이 좀 덜하거나 하면 해당 페이지에 속한 게시물이 어떤 것인지 따로 기입한 뒤 그 정보를 통해 게시물을 가져오면 속도에 대한 문제가 발생하지 않지만, 추가 뿐만 아니라 삭제의 경우 이런 페이징을 어렵게 만드는 요인입니다. 그래서, 화면에 출력되어야 할 순서대로 정렬을 한 뒤 가상의 번호인 ROW NUMBER 을 앞에 달아줘서 원하는 영역의 자료만 가져올 수 있는 방식을 취합니다.
그런데, 일반적인 게시판은 아주 단순한 특징을 하나 가지고 있습니다. 사람들은 일반적으로 페이지 번호가 3 페이지 이상인 것을 거의 읽지 않는다는 것입니다. 즉, 페이지 번호가 낮은 것을 훨씬 더 많이 조회하고 있고, 그 비율이 제 경험상 5 페이지 이하의 페이지 조회율이 95% 이상이라고 생각하고 있습니다. 그러므로 총 페이지수가 1000 페이지인 많은 글이 있는 게시판이라도 1000 페이지를 읽는데 5 초가 걸리더라도 1 페이지를 읽는데 0.01 초 밖에 걸리지 않는다면 사용자들은 그렇게 느리다고 느끼지 않는다는 것입니다. 1 페이지나 1000 페이지나 모두 2 초씩 동일하게 소비되는 것보다 더 빠르고 좋은 게시판으로 사람들은 생각한다는 것이죠. 그래서 이런 특성을 이용한 게시물 가져오기 방식을 많이 쓰게 됩니다.
그럼 테스트 데이터 구조를 정의하겠습니다. 쿼리는 오라클로 하겠습니다. 먼저, board 라는 테이블이 존재하고, id, name, content, createdate 라는 필드가 존재한다고 하겠습니다. 여기서 createdate 는 date 형의 필드이고, id 는 sequence 형태의 number 형, name 은 varchar2, content 는 crob 의 형이라고 가정하겠습니다.
일반적으로 게시판을 검색할 때에는 최근에 작성한 순서대로 게시물을 조회합니다. 그럼 다음과 같은 쿼리를 작성하게 될겁니다.
select id, name, content, createdate
from board
order by createdate
이렇게 하면 최신 순서대로 모든 게시물이 다 출력이 되겠죠. 예전처럼 ResultSet 을 직접 받아와서 처리하면 DBMS 에서 커서를 가지고 대기하다가 데이터를 요청할 때 전송하지만, 요즘 많이 쓰는 MyBatis 등은 데이터를 즉시 받아와서 resultType 이나 resultMap 에 정의된 대로 데이터를 bean 에 저장을 해버립니다. 그래서 모두 받아오면 문제가 발생하겠죠.
그래서 원하는 범위의 게시물을 가져와야 합니다.
총 게시물 수는 25 개, 한 페이지에 보여줄 게시물 수는 10 개, 현재 페이지는 3 페이지라고 가정하겠습니다. 그럼 가져와야할 게시물의 범위는? 딱 봐도 21 ~ 25입니다. 하지만, 아주 다행스럽게도 SQL 의 범위 검색은 범위가 벗어나도 있는데까지만 가져오기 때문에 21 ~ 30 사이의 게시물을 가져오라고 시켜도 알아서 21 ~ 25 까지의 게시물만 가져옵니다. 그래서 프로그램에서는 다음과 같이 시작 게시물 번호와 끝 게시물 번호를 계산해서 쿼리를 만들면 됩니다. 특별히 계산식의 해설은 하지 않겠습니다.
int totalCount = 25;
int countPage = 10;
int page = 3;
int startCount = (page - 1) * countPage + 1; // 21 이 되겠죠
int endCount = page * countPage; // 30 이 될 겁니다.
이렇게 만들어진 startCount 와 endCount 을 이용해서 DBMS 에서 원하는 게시물을 조회하는쿼리를 만들어보겠습니다. 위에서 언급했듯이 오라클의 쿼리입니다. 오라클은 ROW NUMBER 을 rownum 이라는 것으로 제공해줍니다. 사용의 편의를 위해 별칭(alias, as 을 이용해서 붙임)을 사용합니다.
먼저 페이지 번호를 붙여보겠습니다.
select rownum as rnum, id, name, content, createdate
from board
order by createdate
이렇게 해주면...순서대로 출력이 안되는걸 보실 수 있습니다. rownum 은 order by 가 이루어지기 전에 이루어진다는 걸 아실 수 있습니다. 그래서 아래와 같이 번호를 부여해야 합니다. 서브쿼리를 쓰는 거죠.
select rownum as rnum, A.id, A.name, A.content, A.createdate
from (
select id, name, content, createdate
from board
order by createdate) A
이렇게 출력하면 순서대로 출력됩니다. 그럼 우리가 원하는 21 ~ 30 까지의 게시물을 가져오는 쿼리로 확장해보겠습니다.
select rownum as rnum, A.id, A.name, A.content, A.createdate
from (
select id, name, content, createdate
from board
order by createdate) A
where rownum between 21 and 30
-- where rownum >= 21 and rownum <= 30
숫자 범위를 이용해 값을 가져오는 가장 대표적인 방법은 between 정도가 되겠네요. 아주 쉽게 21 ~ 30(실제로는 21 ~ 25) 의 게시물을 가져왔습니다.
끝일까요?
아쉽게도 아닙니다. DBMS 의 내부 동작 원리 상 위와 같이 페이지 내의 게시물을 가져오면 위에서 언급한 "모든 페이지 내 게시물을 가져올 때 2 초가 걸리는" 페이지가 될 수 있습니다. 아니,어쩌면 모든 페이지를 5 초 걸려서 가져올 겁니다.
왜냐하면, 저렇게 범위 검색을 하게 되면 모든 테이블 내용을 정렬한 뒤 1 부터 번호를 쭉 달아서 끝번호까지 번호를 부여한 뒤 21 ~ 30 까지의 데이터를 가져오기 때문입니다. 예제에서는 게시물이 많이 없기 때문에 충분히 빠르지만, 실제 수백, 수천 건의 데이터가 들어있으면 속도 저하가 눈에 보입니다.
그럼 어떻게 해야 할까요?
해결책 중 하나는 다음과 같은 방법입니다.
select X.rnum, X.id, X.name, X.content, X.createdate
from (
select rownum as rnum, A.id, A.name, A.content, A.createdate
from (
select id, name, content, createdate
from board
order by createdate) A
where rownum <= 30) X
where X.rnum >= 21
좀 달라졌죠? 번거롭게 한 번 더 둘러싼 뒤 게시물을 가져옵니다. 무슨 차이가 있을까요? DBMS 는 ROW NUMBER 을 부여하다가 위와 같이 첫번째 조건을 만족하게 되면 최적화를 통해 그 아래 데이터에 대한 정보 수집을 중지합니다. 그래서 30 개까지만 임시 테이블에 저장해둔 뒤 번호를 부여하고 나머지 값들은 버립니다. 그 뒤에 30개 안에서 앞의 20 개를 버리고 21 개째부터 나머지(=30)을 가져오기 때문에 속도가 빠릅니다.
즉, 총 게시물 수가 천만개라면, 정렬 후 천만개 모두를 번호를 매기면서 21 보다 같거나 크고 30 보다 작거나 같은 것을 찾는 것과 정렬 후 천만개 중 30 개까지 번호를 매긴 뒤 그걸 따로 떼서 21 보다 같거나 큰 것만 따로 떼서 사용자에게 제공하는 것의 차이가 발생하기 때문에 속도 차이가 눈에 띄게 나타납니다.
그래서, 만약 현재 자신이 사용중인 페이지 쿼리가 between 을 쓰고 있거나 한 번의 서브쿼리을 이용한 범위 검색을 쓰고 있다면, 두 번의 서브쿼리 형태로 바꾸시길 바랍니다.
추가로, 저번 글에서 말씀드렸던 부분인데...하나의 쿼리에서 총 게시물 수를 ROW_NUMBER() 등을 통해서 위의 페이지 쿼리와 동시에 쓰는 분들이 있습니다. 위와 비슷한 이유로 속도가 느려집니다.
습관적으로 위와 같이 페이징을 할 때에는 전체 게시물 수를 얻는 쿼리 따로, 그리고 페이지 게시물을 가져오는 쿼리를 따로 쓰고, ROW NUMBER 을 이용할 때에는 between 형태를 이용하지 않도록 주의하시면 페이징을 90% 마스터 하시는 겁니다.
물론, content 나 name 은 like 검색을 통해 전후방 일치 검색을 하게 되는데 게시물이 매우 많아지면 어쩔 수 없이 느립니다. 이 때에는 검색엔진의 힘을 빌리는게 가장 좋은데, 검색엔진은 보통 결과로 위에서 설정한 id 와 같은 pk 나 uq 값을 페이징 처리까지 해서 결과로 제공합니다. 그러므로 더 쉬운 게시판 페이징을 할 수 있습니다.
마지막으로, 위와 같은 쿼리를 할 때 또 한가지 주의점을 알려드립니다. 위에서 createdate 는 일부러 날짜형으로 지정을 했습니다. 그런데, 프로그램에서는 이를 변환해서 String 형태로 받는 경우가 더 많습니다. 그래서 DBMS 에 쿼리로 질의를 할 때 변환 함수를 써서 변환을 하는데 이걸 언제하는지 고민하지 않고 개발하는 분들이 계십니다. 아래와 같이 처리하는 거죠.
select X.rnum, X.id, X.name, X.content, X.createdate
from (
select rownum as rnum, A.id, A.name, A.content, A.createdate
from (
select id, name, content, to_char(createdate, 'yyyy-MM-dd') as createdate
from board
order by createdate) A
where rownum <= 30) X
where X.rnum >= 21
하지만, 이렇게 할 경우 모든 게시물에 대한 날짜 정보를 변환을 한 뒤에 번호를 붙여주게 됩니다. 그래서 이런 변환 함수는 해줄 수 있는 가장 마지막 단계에서 해주는게 좋습니다.
select X.rnum, X.id, X.name, X.content, to_char(X.createdate, 'yyyy-MM-dd') as createdate
from (
select rownum as rnum, A.id, A.name, A.content, A.createdate
from (
select id, name, content, createdate
from board
order by createdate) A
where rownum <= 30) X
where X.rnum >= 21
다음에 시간이 좀 남으면 3 편도 연재해볼까 생각합니다. 요즘은 거의 쓸 일이 없는 SQL Server 의 예전 방식인 TOP 을 이용하는 방식이나 MySQL 의 LIMIT 정도가 되겠네요.
초보 분들에게 많은 도움이 되었으면 좋겠네요.
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
2015년 6월 22일 월요일
페이징(Paging)에 대한 이해 - (1) 페이지 번호를 생성하자.
흔히 웹 개발을 처음 하는 개발자가 처음 겪게 되는 난관은 바로 게시판의 페이징(Paging)이 아닐까 합니다. 특히 DBMS 와 연동해서 개발하는 경우가 많은데, 각 DBMS 마다 페이징 방법도 많고 성능 등 신경써야 할 부분이 많이 때문에 어떤 자료를 보고 따라해야 문제가 없을지 판단하기 막막할 때가 있습니다.
그래서 페이징에 대한 원리를 파악해서 가장 기본적인 페이징 구현을 할 수 있는 방법을 알려드리고자 합니다.
이 글은 초보자를 위한 글이므로, 페이징 정도는 우습다는 분은 패스해주시길...^^;;;
------------------------------------------------------------------------------------------------------
먼저, 페이징에 대한 이해를 하기 위해서는 페이징을 하기 위해 각 DBMS 가 어떤 정보를 제공해줘야 하는지 알아야 합니다. DBMS 마다 페이징을 위한 방법이 다른데, 제공하는 정보가 다르기 때문입니다.
모든 DBMS 는 내가 원하는 정보의 총 갯수를 가져오는 기능을 제공합니다. 바로 select count(*) 을 이용하는 것입니다. 이것을 이용해서 게시판 아래 출력되는 페이지 번호를 생성할 수 있습니다.
그렇다면, 해당 페이지의 게시물만 가져오는 방법은 무엇일까요?
오라클은 전통적으로 ROWNUM 을 제공합니다. 출력되는 정보에 순차적인 번호를 부여함으로써 원하는 게시물만 가져오는 방식입니다.
MS SQL Server 는 예전에는 TOP 을 이용했습니다. 출력될 정보 중 상위 몇 개를 가져온 뒤, 그 중 원하는 시작점까지 프로그램 상에서 이동을 해서 그 뒤의 것들을 가져오는 방식입니다. 하지만, 최근에는 오라클과 마찬가지로 ROW_NUMBER() 을 제공함으로써 좀 더 편한 페이징이 가능해졌습니다.
MySQL 이나 PostgreSQL 은 전통적으로 LIMIT 를 이용한 페이징을 제공했습니다. 페이징 할 때 크게 고민할 것 없이 사용이 가능하므로 매우 개발자 친화적이긴 하지만, 두 DBMS 의 LIMIT 는 의미가 약간 다르므로 주의해서 사용해야 합니다.
참고로, 제가 주로 사용하는 NoSQL 제품인 Couchbase 에서는 limit 와 skip 을 통해서 페이징을 가능합니다. 사실 내부적인 동작 원리는 비슷하지만, 어떻게 페이징된 정보를 가져오냐의 차이인 것 같습니다.
이번에는 게시판 하단에 표시되는 페이지 번호에 대해서 알아보겠습니다.
예를 들어 25 개의 게시물이 등록된 게시판(board)이 있다고 하겠습니다. 그럼 다음과 같은 Query 을 통해 총 게시물의 수(totalCount)를 가져올 수 있을 겁니다.
select count(*) as totalCount from board
그럼 프로그램에선 몇 가지 정보를 이용해서 하단의 페이지 번호를 생성할 수 있습니다.
1. 한 페이지에 출력될 게시물 수 (countList)
2. 한 화면에 출력될 페이지 수 (countPage)
3. 현재 페이지 번호 (이하 page)
처음 해야할 것은 총 몇 페이지가 존재하는지 알아내는 것입니다(총 페이지, 이하 totalPage). 이 때 필요한 것은 countList 인데, 이해를 돕기 위해 countList = 10 혹은 countList = 5 라고 가정하고 계산해보겠습니다.
25 / countList
25 / 10 = 2 나머지 5
25 / 5 = 5 나머지 0
우리가 기대하는 수는? 바로 3 와 5 입니다. Java 와 같은 언어에서는 int 형으로 보통 계산을 하기 때문에 25 /10 을 해버리면 2 만 나오게 됩니다. 그렇다고 무조건 1 을 더하게 되면 아래의 countList = 5 와 같은 경우 때문에 정확한 총 페이지 수(totalPage)을 계산할 때 무조건 1 을 더해선 안됩니다(의외로 이렇게 계산해서 마지막 페이지에 게시물 안나오는 곳이 많더군요). 위에서 보듯이 가장 정확하면서 단순한 방법은 나머지가 있을 경우에만 1 을 더해줘야 합니다. Java 로 된 최종 코드는 아래와 같이 되겠죠.
int totalCount = 25; // 물론 실제론 여긴 DBMS 에서 조회해서 들어가야 합니다.
int countList = 10;
int totalPage = totalCount / countList;
if (totalCount % countList > 0) {
totalPage++;
}
혹은 아래와 같이 처리할 수도 있겠죠.
int totalCount = 25;
int countList = 10;
int totalPage = totalCount / countList;
if (totalCount > countList * totalPage) {
totalPage++;
}
이렇게 하면 총 페이지 수를 알 수 있습니다. 이 값을 통해서 한 화면에 출력될 페이지 수가 10 개라고 해도, 3 페이지까지만 존재할 경우 1 에서 3 페이지까지만 하단에 출력해줄 수 있습니다(이런 처리 안해서 없는 페이지를 출력하는 곳도 많더군요).
또한, 잘못된 현재 페이지에 대한 보정도 가능합니다.
현재 페이지 번호가 총 페이지 번호보다 크다면 어떻게 해야 할까요? 현재 페이지를 강제로 총 페이지 번호로 치환하는 것도 방법이 될 것입니다.
int page = 2;
int totalCount = 25;
int countList = 10;
int totalPage = totalCount / countList;
if (totalCount % countList > 0) {
totalPage++;
}
if (totalPage < page) {
page = totalPage;
}
이번에는, 하단에 표시될 페이지 번호들을 어떻게 알아낼 수 있는지 알아보겠습니다. 이번 예제에는 totalCount = 255, countList = 10, countPage = 10 으로 하겠습니다. 그리고 현재 페이지인 page 는 5 와 22 의 두 가지 경우를 생각해보겠습니다. 또한, 페이지 표시 방식은 대한민국에서 가장 많이 쓰는 방식인 10 개의 페이지를 보이는 방식으로 하겠습니다. 이 방식은 현재 페이지 기준으로 앞 뒤 몇 개의 페이지를 보여주는 외국의 방식보다 대한민국에서 더 많이 쓰는 방식입니다.
먼저 page = 5 일 경우입니다. 이 경우 우리가 예상하는 결과는 하단에 1 ~ 10 까지의 페이지 번호가 표시되는 것입니다(countPage = 10 이니까요).
이런 페이지 번호 계산을 하는 방법은 몇 가지가 있습니다만, 가장 간단하고 쉬운 방법은 바로 시작 페이지 번호를 계산해내는 것입니다. page = 5 일 경우 시작 페이지(startPage)는 1, 마지막 페이지(endPage)는 시작 페이지에서 10 페이지(countPage 가 10 이니까요)까지라는 건 금방 이해하시리라 생각합니다. 그럼 page = 5 에서 어떻게 1 페이지를 찾아낼 수 있을까요? 아주 쉽습니다. 그냥 countPage 로 나눠버리면 됩니다(그리고 1 을 더해줘야 해요!). 그럼 마지막 페이지는요? countPage 을 더하면 됩니다. 단, 더한 뒤 1 을 빼주는 작업은 잊으시면 안됩니다. 왜 그런지는 생각해보시면 금방 깨닫게 되실꺼구요.
int page = 5;
int countPage = 10;
int startPage = page / 10 + 1; // 왜 1 을 더할까요?
int endPage = startPage + countPage - 1; // 왜 1 을 뺄까요?
실제 보이는 페이지는 1 ~ 10 까지이지만, 실제 페이지 번호는 0 ~9 까지로 처리하는 경우도 있습니다. 그게 Java 와 같은 언어에선 처리가 더 간단하기 때문인데요...저 역시 시작 페이지는 0 으로 해서 처리한 클래스를 만들어서 씁니다만, 이해를 돕기 위해 여기서는 1 페이지는 1 의 page 번호를 가지도록 하겠습니다.
어쨌든, 저렇게 계산을 하면 startPage = 1, endPage = 10 이라는 결과가 나옵니다.
그럼 화면에 출력할 때에는요?
int page = 5;
int countPage = 10;
int startPage = page / 10 + 1;
int endPage = startPage + countPage - 1;
for (int iCount = startPage; iCount <= endPage; iCount++) {
System.out.print(" " + iCount + " ");
}
이런 식으로 출력하면 페이지 번호가 연달아서 출력이 되겠죠.
그렇다면 page 가 22 인 경우에는요?
먼저 단순히 산수 계산을 해보면 255 개의 게시물이 있을 경우 총 26 페이지가 존재할 것이고, 22 페이지가 있는 곳에는 21 에서 30 페이지 영역일 것입니다. 하지만, 26 페이지까지이기 때문에 단순히 21 페이지에서 countPage 을 더하면 안된다는 것을 대번에 눈치 채셨을 겁니다. 그래서 이 경우에도 마지막 페이지는 총 페이지 수로 대체를 해줘야 합니다.
int page = 22;
int countList = 10;
int countPage = 10;
int totalCount = 255;
int totalPage = totalCount / countList;
if (totalCount % countList > 0) {
totalPage++;
}
if (totalPage < page) {
page = totalPage;
}
int startPage = page / 10 + 1;
int endPage = startPage + countPage - 1;
// 여기서 마지막 페이지를 보정해줍니다.
if (endPage > totalPage) {
endPage = totalPage;
}
// [paging]
// 이 부분은 아래에서 추가로 설명합니다.
for (int iCount = startPage; iCount <= endPage; iCount++) {
System.out.print(" " + iCount + " ");
}
대충 끝이 보이네요. 위에서 [paging] 이라고 표시된 부분에서 페이지 번호를 출력하는데, 출력을 할 때 css 코드를 넣거나 <a> 태그를 이용해서 연결을 하면 페이지 이동이 가능합니다. 이 때 현재 페이지는 굵은 글씨체로 표시하고 <a> 태그를 빼기 위해 아래와 같이 처리도 가능하겠죠.
// [paging]
// 이렇게 개선됩니다.
for (int iCount = startPage; iCount <= endPage; iCount++) {
if (iCount == page) {
System.out.print(" <b>" + iCount + "</b>");
} else {
System.out.print(" " + iCount + " ");
}
}
보통 첫페이지 이동이나 이전 페이지, 다음페이지, 끝페이지 이동 버튼도 추가로 달아줍니다. 그래야 해당 페이지 리스트에 없는 곳으로도 이동이 될테니까요.
첫 페이지는 현재 페이지가 1 페이지가 아닐 때 표시되게 하는 경우도 있고, 시작 페이지가 1페이지가 아닐 때 표시하는 경우도 있습니다. 취향 문제죠. 어쨌든 이동하는 페이지는 항상 page = 1 이 되겠죠. 이전 페이지도 마찬가지입니다. 1 페이지가 아닐 경우 현재 페이지보다 1 페이지 앞으로 이동하도록 page - 1 값을 가지고 이동하게 하면 되죠. 다음페이지는 totalPage 와 비교해서 마찬가지로 표시해주면 됩니다.
int page = 22;
int countList = 10;
int countPage = 10;
int totalCount = 255;
int totalPage = totalCount / countList;
if (totalCount % countList > 0) {
totalPage++;
}
if (totalPage < page) {
page = totalPage;
}
int startPage = page / 10 + 1;
int endPage = startPage + countPage - 1;
if (endPage > totalPage) {
endPage = totalPage;
}
if (startPage > 1) {
System.out.print("<a href=\"?page=1\">처음</a>");
}
if (page > 1) {
System.out.println("<a href=\"?page=" + (page - 1) + "\">이전</a>");
}
for (int iCount = startPage; iCount <= endPage; iCount++) {
if (iCount == page) {
System.out.print(" <b>" + iCount + "</b>");
} else {
System.out.print(" " + iCount + " ");
}
}
if (page < totalPage) {
System.out.println("<a href=\"?page=" + (page + 1) + "\">다음</a>");
}
if (endPage < totalPage) {
System.out.print("<a href=\"?page=" + totalPage + "\">끝</a>");
}
이렇게 처리하면 페이징 표시를 할 수 있게 됩니다. 참 쉽죠~~~잉? (고 밥 로스 아저씨 따라하기)
다음에는 실제 각 DBMS 에서 페이지에 해당하는 글을 읽어오는 방법에 대해서 알아보겠습니다.
그리고, 이 글을 읽으시는 분 중에, DBMS 에서 조회할 때 총 페이지 번호와 게시물을 같이 읽어오면 되지, 왜 두 번에 나눠서 요청을 하느냐...그건 비효율적이다...라고 생각하시는 분은 곧 작성해서 올릴 다음 글을 꼭 읽어주세요. 왜냐하면 잘못된 습관이기 때문입니다.
이 글은 제 개인 블로그(http://zepinos.blogspot.kr)와 okky(http://okky.kr)에만 공개되는 글입니다. 퍼 가는 것은 금해주시고, 링크로 대신해주시기 바랍니다. 당연히 상업적 용도로 이용하시면...저랑 경찰서에서 정모하셔야 합니다. ^^;;;
위에 작성한 코드 등은 실제 컴파일한 것이 아니라 제가 글을 적으면서 키보드 코딩(?...손 코딩의 친구) 한 것이므로, 오류가 있다면 저에게 알려주시면 고맙겠습니다.
2015년 4월 16일 목요일
Vert.x 3 에서 달라지는 점
거창하게 제목을 달았지만, 그리 거창한 것은 아닙니다.
제가 2.x 을 쓰다가 3.x 로 교체를 하고 있는데, 작업 중 느껴지는 바뀐 점들을 정리하는 것입니다.
1. deployWorkerVerticle() 이 사라지고 deployVerticle() 로 통합되었습니다. DeploymentOptions 을 이용해 옵션을 설정해서 deploy 시 옵션 내용을 적용하는 방식입니다.
2. Vertice 대신에 AbstractVerticle 등을 상속받아 사용해야 하며, BusModBase 가 사라졌습니다. BusModBase 의 sendOK() 나 sendError() 등으로 reply() 을 처리했다면 이를 모두 수정해야 합니다.
3. EventBus.registerHandler() 가 EventBus.consumer() 로 변경되었습니다. 사용법은 대동소이합니다.
4. Logger 가 사라졌습니다. 예제에서도 System.out.println() 을 이용하는 걸 봐선, 없어진게 맞을 것 같습니다.
5. Event Loop 에서 처리 시간이 지연될 경우 지속적인 경로 로그 출력이 되고 블록 상태가 풀리는 데도 시간이 좀 걸리는 것 같습니다. 사정상 MyBatis 으로 아주 금방 처리되는 Stored Procedure 을 호출하는 걸 만들어봤는데 정말 짜증나는 상황이 연출되었습니다. 오래 걸릴 코드가 있을 경우 vertx.executeBlocking() 을 이용해 비동기 형태로 처리하게 만들어서 이를 해결할 수도 있고, 그게 아니라면 Worker 로 Verticle 을 Deploy 해서 사용해야 합니다.
6. MySQL 등의 DB 연결 및 질의를 위한 라이브러리가 기본으로 포함되었습니다. 하지만, 기능이 아직은 미약하고, 모두 비동기 방식으로 사용하기 때문에 질의를 순차적으로 여러번 보내야 할 경우 정말 짜증나는 경우가 생길 수 있습니다. 게다가 아직 Stored Procedure 을 제대로 실행하질 못하고, 결과 역시 당연히 못받아옵니다.
7. HttpServer 을 쉽게 사용할 수 있게 해주는 RouteMatcher 가 Router/Route 등으로 변경되었습니다.
8. Vertx.sharedData() 에서 이용할 수 있었던 공유 개체가 ConcurrentMap 등에서 LocalMap 와 ClusterWideMap(AsyncMap) 와 같이 나뉘어졌으며 ClusterWideMap 은 비동기 형태로 가져오며 Lock, Counter 을 이용할 수 있도록 되어 있습니다. 더 이상 Cluster 모드를 위한 외부 라이브러리 이용이 필요 없습니다.
9.EventBus 역시 Cluster 에서 동일하게 사용할 수 있는 기능이 포함되었습니다. EventBus 가 Cluster 에서 모두 등록이 완료될 때 이벤트를 발생해주도록 개선되었습니다.
10. DNS Client 가 추가되었습니다.
11. 내부에서 많이 사용하게 되는 JsonObject 등의 기능이 늘어났습니다. 특히 JsonArray 에서 포지션을 이용한 조회만 제공했던 2.x 버전과 달리 삭제까지 제공하게 되었습니다. stream() 역시 생겼는데, 아쉽게도 내부 개체들은 JsonObject/JsonArray 에서 HashMap/ArrayList 로 바뀌서 저장됩니다.
지금까지 작업하면서 제가 느낀 차이점들입니다. 더 많은 것들이 있지만 일단 큰 것들은 이정도네요. 잘못된 내용이나 지적사항은 언제라도 연락주십시오.
그리고 이 글은 zepinos(zepinos at nate dot com)에게 저작권이 있고 다른 곳으로 퍼갈 수 없습니다.
제가 2.x 을 쓰다가 3.x 로 교체를 하고 있는데, 작업 중 느껴지는 바뀐 점들을 정리하는 것입니다.
1. deployWorkerVerticle() 이 사라지고 deployVerticle() 로 통합되었습니다. DeploymentOptions 을 이용해 옵션을 설정해서 deploy 시 옵션 내용을 적용하는 방식입니다.
2. Vertice 대신에 AbstractVerticle 등을 상속받아 사용해야 하며, BusModBase 가 사라졌습니다. BusModBase 의 sendOK() 나 sendError() 등으로 reply() 을 처리했다면 이를 모두 수정해야 합니다.
3. EventBus.registerHandler() 가 EventBus.consumer() 로 변경되었습니다. 사용법은 대동소이합니다.
4. Logger 가 사라졌습니다. 예제에서도 System.out.println() 을 이용하는 걸 봐선, 없어진게 맞을 것 같습니다.
5. Event Loop 에서 처리 시간이 지연될 경우 지속적인 경로 로그 출력이 되고 블록 상태가 풀리는 데도 시간이 좀 걸리는 것 같습니다. 사정상 MyBatis 으로 아주 금방 처리되는 Stored Procedure 을 호출하는 걸 만들어봤는데 정말 짜증나는 상황이 연출되었습니다. 오래 걸릴 코드가 있을 경우 vertx.executeBlocking() 을 이용해 비동기 형태로 처리하게 만들어서 이를 해결할 수도 있고, 그게 아니라면 Worker 로 Verticle 을 Deploy 해서 사용해야 합니다.
6. MySQL 등의 DB 연결 및 질의를 위한 라이브러리가 기본으로 포함되었습니다. 하지만, 기능이 아직은 미약하고, 모두 비동기 방식으로 사용하기 때문에 질의를 순차적으로 여러번 보내야 할 경우 정말 짜증나는 경우가 생길 수 있습니다. 게다가 아직 Stored Procedure 을 제대로 실행하질 못하고, 결과 역시 당연히 못받아옵니다.
7. HttpServer 을 쉽게 사용할 수 있게 해주는 RouteMatcher 가 Router/Route 등으로 변경되었습니다.
8. Vertx.sharedData() 에서 이용할 수 있었던 공유 개체가 ConcurrentMap 등에서 LocalMap 와 ClusterWideMap(AsyncMap) 와 같이 나뉘어졌으며 ClusterWideMap 은 비동기 형태로 가져오며 Lock, Counter 을 이용할 수 있도록 되어 있습니다. 더 이상 Cluster 모드를 위한 외부 라이브러리 이용이 필요 없습니다.
9.
10. DNS Client 가 추가되었습니다.
11. 내부에서 많이 사용하게 되는 JsonObject 등의 기능이 늘어났습니다. 특히 JsonArray 에서 포지션을 이용한 조회만 제공했던 2.x 버전과 달리 삭제까지 제공하게 되었습니다. stream() 역시 생겼는데, 아쉽게도 내부 개체들은 JsonObject/JsonArray 에서 HashMap/ArrayList 로 바뀌서 저장됩니다.
지금까지 작업하면서 제가 느낀 차이점들입니다. 더 많은 것들이 있지만 일단 큰 것들은 이정도네요. 잘못된 내용이나 지적사항은 언제라도 연락주십시오.
그리고 이 글은 zepinos(zepinos at nate dot com)에게 저작권이 있고 다른 곳으로 퍼갈 수 없습니다.
2015년 2월 23일 월요일
Vert.x with Cluster 이해하기
Vert.x 에 대해서 이야기 해보고자 합니다. 주로 Vert.x 에서 제공해주는 cluster 모드에 대해서 이야기를 해볼까 합니다. (약간의 Vert.x 에 대한 지식이 있어야 이해가 될 겁니다)
Vert.x 는 Server 모드와 Embeded 모드가 있습니다. Server 모드는 Java 등으로 코드를 작성한 뒤 이를 vertx 실행자를 이용해 실행하는 방식입니다. 소스 코드를 그대로 넣어둬야 하기 때문에 직접 서버를 운영하지 않을 때에는 소스의 외부 유출이 문제가 될 수 있습니다. 반대로 Embeded 모드는 기존 코드에 상속을 받아서 구현하며 기존 코드 속에서 돌아가기 때문에 Vert.x 라이브러리만 포함하면 기존 코드 속에서 돌아갑니다.
하지만, 주로 언급할 cluster 는 Server 모드에서만 동작합니다. vertx 실행 시 -cluster 옵션을 주면 작동합니다.
cluster 는 기존에 많이 알려진 clustering 와 별다른 내용을 포함하진 않습니다. Fail-Over, Load Balancing 의 목적으로 구동되며, 이는 내장된 Hazelcast 을 기반으로 동작하기 때문에 cluster.xml (Hazelcast 의 설정파일과 일치) 을 설정하면 자동으로 연동하게 됩니다.
Vert.x 는 기본적으로 비동기 방식을 이용하고 있고, 다른 클래스의 매서드를 실행하는 것도 직접적인 호출 보다는 비동기 방식으로 호출(EventBus)하는 방식을 이용하기 때문에 Java 파일(혹은 Groovy 파일)을 Verticle 이라는 형태로 호출하게 됩니다. 그런데, 현재 정식 버전인 2.1.x 버전에서는 cluster 에서 이러한 Verticle 을 여러 서버의 것을 사용할 수 있도록 분산처리 해주는 형태이지, 공유 데이터(원래 Hazelcast 의 IMDG 로서의 역활)까지 동기화해서 처리해주지 못합니다.
게다가, NetServer 와 같이 클라이언트와의 통신이 1:1 관계가 아닌 상황에서는 더 상황이 복잡해집니다.
A/B/C 세 개의 서버가 cluster 모드로 설정이 되어 있을 경우, 클라이언트는 A 서버에 접속을 하게 됐을 때 A 에서 이를 처리하고 다시 클라이언트에게 메세지를 되돌려 줄 경우에는 문제가 없으나, 이를 EventBus 을 통해 처리를 할 경우 B 혹은 C 서버에서 이를 처리할 수 있습니다. 이 경우 A 서버에 보낸 메세지 처리 순서도 뒤죽박죽이 될 수 있고(먼저 보낸 메세지가 B 서버에서 처리되는 동안 다음 메세지가 A 서버에서 처리되어 먼저 return 될 수 있습니다) B 나 C 서버에서 처리하는 도중 클라이언트에게 직접 메세지를 보내려고 할 경우 메세지를 보낼 수 없는 문제까지 발생하게 됩니다. 클라이언트가 A 서버에 접속하게 되면 Handler ID 라는 고유값이 생성되는데, 이 고유값을 통해 클라이언트에게 메세지를 보낼 수 있습니다. 하지만 이것은 A 서버만 알 수 있는 값이고, B 서버나 C 서버는 이 값을 공유하지도 않고 클라이언트와 연결도 되어 있지 않기 때문에 메세지는 어둠 속(?)에 사라집니다.
저는 이 문제를 해결하기 위해 메세지를 발송하는 EventBus 을 각 서버별로 고유하게 만들고 클라이언트에게 메세지를 보낼 때 클라이언트가 접속한 서버의 해당 EventBus 로 메세지 내용을 보내 전송하도록 구조를 만들어 처리를 하였습니다. 이 부분은 Vert.x 개발자들도 인지하고 있지만, 왜 이런 구조가 필요한지...중요하게 생각 안하는 것 같더군요.
예를 들어보겠습니다. 1:1 채팅방을 만들 경우 처음에 한 사람이 A 서버에 접속했을 경우 A 서버에서 aaa 라는 Handler ID 을 생성한 뒤, 이 aaa 라는 Handler ID 을 Set 이나 Map(편의상 여기서는 Set 을 쓴다고 하겠습니다) 등에 저장을 합니다. 그리고 다른 한 사람이 C 서버에 접속했을 때 C 서버에서 ccc 라는 Handler ID 을 생성한 뒤 Set 을 뒤져 aaa 와 ccc 을 매칭시켜주는 시스템이라고 가정합시다.
그럼 aaa 가 접속을 했을 경우 먼저 Set 을 뒤집니다. Set 에 대기자가 없다면 aaa 에게 접속을 했으나 대기를 해야한다고 알려줘야 합니다. 그리고 기다립니다.
그리고 ccc 가 접속했을 경우 마찬가지로 Set 을 뒤집니다. Set 에 대기자 aaa 가 있으므로 자신에게 접속을 했고 aaa 와 채팅을 할 수 있다고 알려줘야 합니다. 뿐만 아니라 aaa 에게도 ccc 와 채팅을 할 수 있다고 알려줘야 합니다.
aaa 나 ccc 가 접속이 끊어졌을 때도 마찬가지 입니다. aaa 가 정상적으로 접속을 끊더라도, 혹은 비정상적으로 접속이 끊어졌더라도 ccc 에게 메세지를 보내줘야 합니다.
그런데, 위에서 나열한 행위들을 처리하는 것이 A 서버일 수도, C 서버일수도...아니면 엉뚱하게도 B 서버일 수도 있습니다. 왜냐하면 Clustering 의 부하 분산 때문입니다. aaa 의 접속이 끊어졌다는 것은 A 서버에서 이벤트로 받긴 하지만, 일반적으로 이런 메세지를 보내기 위해선 처리해야할 내용이 있을 수 있고, 그런 것을 EventBus 로 보내서 처리한 뒤 거기서 메세지를 보내게 됩니다. 이 때 EventBus 을 통해 A~C 서버 중 한 곳에 무작위로 배치되어 처리하기 때문에 문제가 발생할 수 있습니다. 또한, 하나의 요청에 대해 자신에게 메세지를 되돌려주는건 1회입니다. 자신에게 여러번 메세지를 보내야 할 수도 있고 상대에게도 메세지를 보내줘야 할 경우도 있는데 이런 경우 해당 클라이언트가 접속한 서버를 통해서만 보내야 합니다. 그래서 위에서 언급했듯이 클라이언트에게 메세지를 보내기 위한 특별한 EventBus 을 만들어 둘 필요가 있습니다(제가 고안했습니다. 매우 허접한 형태이니 양해를 바랍니다).
그런데 여기서 문제가 끝나지 않습니다. 3.0.x 버전(아직 M2 까지만 나왔습니다)에서는 기능이 보강이 되었으나 아직 정식 버전인 2.1.x 에서는 해결되지 않은 문제가 있습니다. EventBus 는 상호 사용이 가능하나, 공유 데이터를 다루는 내장 Hazelcast 을 이용한 ConcurrentSet 이나 ConcurrentMap(vertx.sharedData() 을 이용합니다)은 같은 서버의 EventBus 끼리는 데이터를 공유해도 다른 서버에 있는 EventBus 끼리는 데이터를 공유하지 못합니다. 3.0 에서는 이 부분을 해결해놓았으나 이 또한 비동기로만 구현을 해놓아서 원래의 Hazelcast 보다 사용이 불편합니다. 그래서 위의 1:1 채팅방에서 Set 에 aaa 을 넣어놓아도 사실 ccc 는 aaa 을 찾지 못하고 자신도 대기 상태가 되어버리는 문제가 생기게 됩니다.
저는 지금 2.1.5 을 이용중이기 때문에 이 문제를 간단하게 해결하기 위해서 처음 시작되는 Verticle 에 Hazelcast 설정파일(Vert.x 의 cluster.xml)을 다시 읽어 별도의 공유를 생성해서 사용하고 있습니다. 그래서 한 서버에 2 개의 Hazelcast 가 뜨게 되어 있습니다. 원래의 Hazelcast 는 위와 같은 제약이 없으므로 해당 Hazelcast 의 ISet 개체를 통해 쉽게 공유 데이터를 처리할 수 있습니다.
이처럼 Vert.x 의 cluster 는 쉽게 서버 코드를 확장할 수 있게 도와줄 것 같지만, 함정이 숨어 있습니다. 처리하는 매서드를 여러 서버에서 비동기로 처리하기 때문에 서버를 늘리면 늘릴수록 많은 접속(처리시간이 극단적으로 적고 접속량이 많은 경우 유리)을 처리하지만, 잘못 이해할 경우 원인도 모른채 동작이 이상하게 되는 현상을 맞이할 수 있게 됩니다.
이런 부분을 고려하고 Vert.x 을 이용해 프로그래밍을 하신다면 매우 가벼운, Java 로 혹은 Groovy 로 개발할 수 있는 Node,js 을 대체할 수 있는 프로그래밍을 하실 수 있을 겁니다.
그리고 이 글은 zepinos(zepinos at nate dot com)에게 저작권이 있고 다른 곳으로 퍼갈 수 없습니다.
Vert.x 는 Server 모드와 Embeded 모드가 있습니다. Server 모드는 Java 등으로 코드를 작성한 뒤 이를 vertx 실행자를 이용해 실행하는 방식입니다. 소스 코드를 그대로 넣어둬야 하기 때문에 직접 서버를 운영하지 않을 때에는 소스의 외부 유출이 문제가 될 수 있습니다. 반대로 Embeded 모드는 기존 코드에 상속을 받아서 구현하며 기존 코드 속에서 돌아가기 때문에 Vert.x 라이브러리만 포함하면 기존 코드 속에서 돌아갑니다.
하지만, 주로 언급할 cluster 는 Server 모드에서만 동작합니다. vertx 실행 시 -cluster 옵션을 주면 작동합니다.
cluster 는 기존에 많이 알려진 clustering 와 별다른 내용을 포함하진 않습니다. Fail-Over, Load Balancing 의 목적으로 구동되며, 이는 내장된 Hazelcast 을 기반으로 동작하기 때문에 cluster.xml (Hazelcast 의 설정파일과 일치) 을 설정하면 자동으로 연동하게 됩니다.
Vert.x 는 기본적으로 비동기 방식을 이용하고 있고, 다른 클래스의 매서드를 실행하는 것도 직접적인 호출 보다는 비동기 방식으로 호출(EventBus)하는 방식을 이용하기 때문에 Java 파일(혹은 Groovy 파일)을 Verticle 이라는 형태로 호출하게 됩니다. 그런데, 현재 정식 버전인 2.1.x 버전에서는 cluster 에서 이러한 Verticle 을 여러 서버의 것을 사용할 수 있도록 분산처리 해주는 형태이지, 공유 데이터(원래 Hazelcast 의 IMDG 로서의 역활)까지 동기화해서 처리해주지 못합니다.
게다가, NetServer 와 같이 클라이언트와의 통신이 1:1 관계가 아닌 상황에서는 더 상황이 복잡해집니다.
A/B/C 세 개의 서버가 cluster 모드로 설정이 되어 있을 경우, 클라이언트는 A 서버에 접속을 하게 됐을 때 A 에서 이를 처리하고 다시 클라이언트에게 메세지를 되돌려 줄 경우에는 문제가 없으나, 이를 EventBus 을 통해 처리를 할 경우 B 혹은 C 서버에서 이를 처리할 수 있습니다. 이 경우 A 서버에 보낸 메세지 처리 순서도 뒤죽박죽이 될 수 있고(먼저 보낸 메세지가 B 서버에서 처리되는 동안 다음 메세지가 A 서버에서 처리되어 먼저 return 될 수 있습니다) B 나 C 서버에서 처리하는 도중 클라이언트에게 직접 메세지를 보내려고 할 경우 메세지를 보낼 수 없는 문제까지 발생하게 됩니다. 클라이언트가 A 서버에 접속하게 되면 Handler ID 라는 고유값이 생성되는데, 이 고유값을 통해 클라이언트에게 메세지를 보낼 수 있습니다. 하지만 이것은 A 서버만 알 수 있는 값이고, B 서버나 C 서버는 이 값을 공유하지도 않고 클라이언트와 연결도 되어 있지 않기 때문에 메세지는 어둠 속(?)에 사라집니다.
저는 이 문제를 해결하기 위해 메세지를 발송하는 EventBus 을 각 서버별로 고유하게 만들고 클라이언트에게 메세지를 보낼 때 클라이언트가 접속한 서버의 해당 EventBus 로 메세지 내용을 보내 전송하도록 구조를 만들어 처리를 하였습니다. 이 부분은 Vert.x 개발자들도 인지하고 있지만, 왜 이런 구조가 필요한지...중요하게 생각 안하는 것 같더군요.
예를 들어보겠습니다. 1:1 채팅방을 만들 경우 처음에 한 사람이 A 서버에 접속했을 경우 A 서버에서 aaa 라는 Handler ID 을 생성한 뒤, 이 aaa 라는 Handler ID 을 Set 이나 Map(편의상 여기서는 Set 을 쓴다고 하겠습니다) 등에 저장을 합니다. 그리고 다른 한 사람이 C 서버에 접속했을 때 C 서버에서 ccc 라는 Handler ID 을 생성한 뒤 Set 을 뒤져 aaa 와 ccc 을 매칭시켜주는 시스템이라고 가정합시다.
그럼 aaa 가 접속을 했을 경우 먼저 Set 을 뒤집니다. Set 에 대기자가 없다면 aaa 에게 접속을 했으나 대기를 해야한다고 알려줘야 합니다. 그리고 기다립니다.
그리고 ccc 가 접속했을 경우 마찬가지로 Set 을 뒤집니다. Set 에 대기자 aaa 가 있으므로 자신에게 접속을 했고 aaa 와 채팅을 할 수 있다고 알려줘야 합니다. 뿐만 아니라 aaa 에게도 ccc 와 채팅을 할 수 있다고 알려줘야 합니다.
aaa 나 ccc 가 접속이 끊어졌을 때도 마찬가지 입니다. aaa 가 정상적으로 접속을 끊더라도, 혹은 비정상적으로 접속이 끊어졌더라도 ccc 에게 메세지를 보내줘야 합니다.
그런데, 위에서 나열한 행위들을 처리하는 것이 A 서버일 수도, C 서버일수도...아니면 엉뚱하게도 B 서버일 수도 있습니다. 왜냐하면 Clustering 의 부하 분산 때문입니다. aaa 의 접속이 끊어졌다는 것은 A 서버에서 이벤트로 받긴 하지만, 일반적으로 이런 메세지를 보내기 위해선 처리해야할 내용이 있을 수 있고, 그런 것을 EventBus 로 보내서 처리한 뒤 거기서 메세지를 보내게 됩니다. 이 때 EventBus 을 통해 A~C 서버 중 한 곳에 무작위로 배치되어 처리하기 때문에 문제가 발생할 수 있습니다. 또한, 하나의 요청에 대해 자신에게 메세지를 되돌려주는건 1회입니다. 자신에게 여러번 메세지를 보내야 할 수도 있고 상대에게도 메세지를 보내줘야 할 경우도 있는데 이런 경우 해당 클라이언트가 접속한 서버를 통해서만 보내야 합니다. 그래서 위에서 언급했듯이 클라이언트에게 메세지를 보내기 위한 특별한 EventBus 을 만들어 둘 필요가 있습니다(제가 고안했습니다. 매우 허접한 형태이니 양해를 바랍니다).
그런데 여기서 문제가 끝나지 않습니다. 3.0.x 버전(아직 M2 까지만 나왔습니다)에서는 기능이 보강이 되었으나 아직 정식 버전인 2.1.x 에서는 해결되지 않은 문제가 있습니다. EventBus 는 상호 사용이 가능하나, 공유 데이터를 다루는 내장 Hazelcast 을 이용한 ConcurrentSet 이나 ConcurrentMap(vertx.sharedData() 을 이용합니다)은 같은 서버의 EventBus 끼리는 데이터를 공유해도 다른 서버에 있는 EventBus 끼리는 데이터를 공유하지 못합니다. 3.0 에서는 이 부분을 해결해놓았으나 이 또한 비동기로만 구현을 해놓아서 원래의 Hazelcast 보다 사용이 불편합니다. 그래서 위의 1:1 채팅방에서 Set 에 aaa 을 넣어놓아도 사실 ccc 는 aaa 을 찾지 못하고 자신도 대기 상태가 되어버리는 문제가 생기게 됩니다.
저는 지금 2.1.5 을 이용중이기 때문에 이 문제를 간단하게 해결하기 위해서 처음 시작되는 Verticle 에 Hazelcast 설정파일(Vert.x 의 cluster.xml)을 다시 읽어 별도의 공유를 생성해서 사용하고 있습니다. 그래서 한 서버에 2 개의 Hazelcast 가 뜨게 되어 있습니다. 원래의 Hazelcast 는 위와 같은 제약이 없으므로 해당 Hazelcast 의 ISet 개체를 통해 쉽게 공유 데이터를 처리할 수 있습니다.
이처럼 Vert.x 의 cluster 는 쉽게 서버 코드를 확장할 수 있게 도와줄 것 같지만, 함정이 숨어 있습니다. 처리하는 매서드를 여러 서버에서 비동기로 처리하기 때문에 서버를 늘리면 늘릴수록 많은 접속(처리시간이 극단적으로 적고 접속량이 많은 경우 유리)을 처리하지만, 잘못 이해할 경우 원인도 모른채 동작이 이상하게 되는 현상을 맞이할 수 있게 됩니다.
이런 부분을 고려하고 Vert.x 을 이용해 프로그래밍을 하신다면 매우 가벼운, Java 로 혹은 Groovy 로 개발할 수 있는 Node,js 을 대체할 수 있는 프로그래밍을 하실 수 있을 겁니다.
그리고 이 글은 zepinos(zepinos at nate dot com)에게 저작권이 있고 다른 곳으로 퍼갈 수 없습니다.
2015년 2월 9일 월요일
CentOS(RHEL 등) 에서 최신버전의 MySQL 설치
현재 가장 많이 사용하는 CentOS 6 에는 MySQL 5.1 이 기본 제공됩니다. 특성상 마이너 업그레이드가 진행되어도 MySQL 의 마이너 버전 업그레이드는 이루어지지 않습니다.
그럼, rpm 을 가져와서 설치하거나 소스 설치를 해야만 하는 걸까요?
그건 아닙니다. 친절하게도 yum repository 을 제공합니다.
아래 링크에서 각 메이저 버전에 맞는 repository rpm 을 제공합니다.
http://dev.mysql.com/downloads/repo/yum/
CentOS 6 (RHEL 6 등) 을 위한 파일은 아래와 같습니다.
http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
이를 설치하기 위해서는
yum install http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
와 같이 바로 설치를 하면 됩니다. 이후 yum update 후 다시 mysql-server 을 설치하면 최신 버전의 MySQL 을 설치할 수 있습니다.
여담으로, MariaDB 대신에 MySQL 을 설치하는 이유는 MySQL Workbench 때문입니다. MariaDB 가 버전을 10.x 로 올려버리는 바람에 Workbench 에서 알 수 없는 버전이라고 하며 몇가지 기능을 사용할 수 없게 해버리기 때문입니다.
그럼, rpm 을 가져와서 설치하거나 소스 설치를 해야만 하는 걸까요?
그건 아닙니다. 친절하게도 yum repository 을 제공합니다.
아래 링크에서 각 메이저 버전에 맞는 repository rpm 을 제공합니다.
http://dev.mysql.com/downloads/repo/yum/
CentOS 6 (RHEL 6 등) 을 위한 파일은 아래와 같습니다.
http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
이를 설치하기 위해서는
yum install http://dev.mysql.com/get/mysql-community-release-el6-5.noarch.rpm
와 같이 바로 설치를 하면 됩니다. 이후 yum update 후 다시 mysql-server 을 설치하면 최신 버전의 MySQL 을 설치할 수 있습니다.
여담으로, MariaDB 대신에 MySQL 을 설치하는 이유는 MySQL Workbench 때문입니다. MariaDB 가 버전을 10.x 로 올려버리는 바람에 Workbench 에서 알 수 없는 버전이라고 하며 몇가지 기능을 사용할 수 없게 해버리기 때문입니다.
AWS 의 Elastic IP 초기 5 개 제한 상향하기
AWS 에서 EC2 을 신청한 뒤 이를 고정IP 로 만들어두기 위해서 Elastic IP 을 신청하여 EC2 Instanse 와 연결합니다. 그런데, 고정IP 로 모두 발급하기엔 IP 자원이 부족하다는 이유로 초기에는 5 개의 IP 만 발급받을 수 있습니다.
더 많은 IP 을 신청하기 위해서는 별도의 신청 절차를 거쳐야 합니다.
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html
위 공식 문서에 나와있는데...제일 아래쪽에 있으니(게다가 영어) 사람들이 잘 모르더군요.
https://console.aws.amazon.com/support/home#/case/create?issueType=service-limit-increase&limitType=service-code-elastic-ips-ec2-classic
위 링크에 접속해서 상향 신청을 해야 합니다. 그냥 해주진 않고 필요하다고 판단할 때만 주는 것 같네요.
더 많은 IP 을 신청하기 위해서는 별도의 신청 절차를 거쳐야 합니다.
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html
위 공식 문서에 나와있는데...제일 아래쪽에 있으니(게다가 영어) 사람들이 잘 모르더군요.
https://console.aws.amazon.com/support/home#/case/create?issueType=service-limit-increase&limitType=service-code-elastic-ips-ec2-classic
위 링크에 접속해서 상향 신청을 해야 합니다. 그냥 해주진 않고 필요하다고 판단할 때만 주는 것 같네요.
CentOS 에서 Locale, TimeZone 변경
Locale 변경
/etc/sysconfig/i18n 파일을 열어서 수정
TimeZone 변경
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
으로 타임존 설정을 복제하거나 심볼릭 링크
vi /etc/sysconfig/clock
에서
ZONE="Asia/Seoul"
UTC=False
로 변경
2015년 1월 27일 화요일
logback 에서 설정파일 위치를 소스에서 재지정하기
간혹 소스 상에서 강제로 다른 설정 파일을 읽어야 할 때가 있을 수 있습니다.
이 경우 아래와 같이 설정파일 위치를 재정의할 수 있습니다.
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();
try {
configurator.doConfigure("파일위치");
} catch (JoranException e) {
e.printStackTrace();
}
log4j 는 매우 간단하게 처리할 수 있습니다.
PropertyConfigurator.configure("파일위치");
or
DOMConfigurator.configure("파일위치");
당연하게도(?) PropertyConfigurator 을 이용할 때에도 xml 파일을 지정하면 잘 읽어옵니다. DOMConfigurator의 경우는 안해봐서...
이 경우 아래와 같이 설정파일 위치를 재정의할 수 있습니다.
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();
try {
configurator.doConfigure("파일위치");
} catch (JoranException e) {
e.printStackTrace();
}
log4j 는 매우 간단하게 처리할 수 있습니다.
PropertyConfigurator.configure("파일위치");
or
DOMConfigurator.configure("파일위치");
당연하게도(?) PropertyConfigurator 을 이용할 때에도 xml 파일을 지정하면 잘 읽어옵니다. DOMConfigurator의 경우는 안해봐서...
피드 구독하기:
글 (Atom)