Struts/Properties_인코딩문제

Struts의 MessageResources에서의 한글 문제 해결법. JDK의 ResourceBundle을 쓰건, Struts의 MessageResources를 쓰건 자바의 Properties 파일을 이용하는 이상 인코딩 문제는 피해갈 수가 없습니다. 실제 대부분의 프로퍼티 파일들은 각 시스템의 기본 인코딩으로 작성되는 반면 자바의 Properties는 무조건 ISO-8859-1 인코딩으로만 인식하고 읽기 때문에 이를 기반으로 한 대부분의 클래스들 역시 인코딩의 문제를 피해갈 수 없습니다.(ISO-8859-1로 인식할 수 없는 문자에 대해서는 유니코드로 인식합니다.) 따라서 원죄는 Properties에 있는 것이고 근본적인 해결책은 Properties에 현재의 load, store를 빨리 deprecated시키고 Reader, Writer 혹은 nio를 통해 설정파일에 입출력할 수 있는 메쏘드가 추가되는 것입니다.

그러나, 현실적으로 프로퍼티를 사용하는 곳은 너무나 많고 이를 모두 수정하기는 불가능에 가까운 일입니다. 따라서, 프로퍼티에서 읽을 때 인코딩 변환을 해주는 방법이 가장 현실적인 대안이라고 할 수 있겠습니다. 그렇다면 차선책으로 가장 좋은 것은 Struts에서 이 점을 반영한 패치를 내놓는 것입니다. 이건 동아시아권 개발자들이 참여해야할 부분이라고 생각됩니다. 그러나, 그렇다고 패치가 나오기까지 무작정 기다릴 수는 없겠죠. 그 패치가 다른 나라 개발자들의 동의를 얻을 수 있을지도 미지수이고요. (이 문제는 우리가 유니코드만 쓴다면 아무 문제가 안되는 것입니다. 사실 저 역시 우리도 어서 UTF-16이든 UTF-8이든 유니코드 기반의 인코딩으로 모든 소스들을 전환해야한다고 생각합니다.) 결국 마지막 남은 방법은 우리 개발자들의 몫이겠죠.

다행스럽게도 Struts의 MessageResources 클래스는 어느 정도 추상화가 되어 있습니다. 실제 구현하는 클래스는PropertyMessageResources입니다. 이놈의 코드를 살펴보면, 어떤 로케일로 getMessage를 호출했을 때 해당하는 로케일로 읽어둔 메세지가 없으면 그 때 해당 로케일의 프로퍼티 파일을 찾아서 읽게 되어 있습니다. 근데, 그냥 읽어서 Properties 객체로 갖고 있는 것이 아니라 새로운 HashMap 객체를 생성해서 이곳에 담습니다. (이 개발자도 Properties에서 Hashtable을 사용하는 것은 못마땅했던 모양입니다.) 덕분에 우리는 큰 퍼포먼스 저하 없이 쉽게 인코딩 변환할 할 방법을 찾을 수 있게 되었죠. HashMap으로 옮겨 담는 부분에서 Configuration 등에서 읽어온 인코딩으로 변환을 시켜주면 됩니다. 사실 이 작업을 getMessage를 오버라이드해서 해줄 것인지 HashMap에 옮겨 담는 과정에서 해줄 것인지 고민하긴 했습니다만 결국 중요한 것은 프로퍼티 파일 작성자의 의도가 담긴 객체로 남아 있는 것이라 생각해서 HashMap에 옮겨 담을 때 인코딩 변환을 하도록 구현했습니다.

아직도 문제는 남았습니다. 그렇다면 이 과정으로 Struts 소스를 고쳐서 새로 컴파일하고 사용하는 것이 바람직할까요? 제 생각엔 No입니다. 개발자 자신이 유지보수하는 것이 아닌 패키지를 제공 받아 사용할 때 이를 함부로 수정해서 사용하는 것은 많은 문제를 낳습니다. 당장 Struts 버전업할 때마다 새로 수정, 컴파일, 패키징 과정을 거쳐야하죠. 다행스럽게도 Struts의 개발자들은 이런 부분에 대해 세심한 배려를 해두었습니다. Factory 클래스를 교체할 수 있게 한 것이죠. 이런 배려는 여기 뿐 아니라 Jakarta 프로젝트 전반에 걸쳐 나타나고 있죠.(사실 Abstract Factory 메쏘드 패턴을 사용할 때 당연히 따라오는 장점이기도 합니다.) 따라서 우리는 PropertyMessageResourcesPropertyMessageResourcesFactory를 상속 받아서 위에서 설명한 부분을 오버라이드하고 이 팩토리를 struts-config.xml에 설정해주면 됩니다. 다음과 유사하게 설정하면 됩니다.

  <message-resources parameter="messages" factory="com.hangame.jwdf.util.NativePropertyMessageResourcesFactory"/>

상속 받아 추가된 클래스는 다음과 유사하게 작성하면 됩니다.

NativePropertyMessageResourcesFactory.java

package com.hangame.jwdf.util;

import org.apache.struts.util.MessageResources;
import org.apache.struts.util.PropertyMessageResourcesFactory;


public class NativePropertyMessageResourcesFactory extends PropertyMessageResourcesFactory {
        
        public NativePropertyMessageResourcesFactory() {
                super();
        }

        public MessageResources createResources(String config) {
                return new NativePropertyMessageResources(this, config, this.returnNull);
        }

}

NativePropertyMessageResources.java

package com.hangame.jwdf.util;

import net.javaservice.jdf.util.CharConversion;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.util.MessageResourcesFactory;
import org.apache.struts.util.PropertyMessageResources;

import com.hangame.jwdf.ExceptionHandler;
import com.hangame.jwdf.config.Configuration;
import com.hangame.jwdf.config.ConfigurationException;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

import java.util.Iterator;
import java.util.Locale;
import java.util.Properties;


public class NativePropertyMessageResources extends PropertyMessageResources {
        Log log = LogFactory.getLog(NativePropertyMessageResources.class);

        /**
         *
         */
        public NativePropertyMessageResources(MessageResourcesFactory factory, String config) {
                super(factory, config);
        }

        /**
         *
         */
        public NativePropertyMessageResources(MessageResourcesFactory factory, String config, boolean returnNull) {
                super(factory, config, returnNull);
        }

        /**
         * Load the messages associated with the specified Locale key.  For this
         * implementation, the config property should contain a fully
         * qualified package and resource name, separated by periods, of a series
         * of property resources to be loaded from the class loader that created
         * this PropertyMessageResources instance.  This is exactly the same name
         * format you would use when utilizing the
         * java.util.PropertyResourceBundle class.
         *
         * @param localeKey Locale key for the messages to be retrieved
         */
        protected synchronized void loadLocale(String localeKey) {
                if (log.isTraceEnabled()) {
                        log.trace("loadLocale(" + localeKey + ")");
                }

                // Have we already attempted to load messages for this locale?
                if (locales.get(localeKey) != null) {
                        return;
                }

                locales.put(localeKey, localeKey);

                // Set up to load the property resource for this locale key, if we can
                String name = config.replace('.', '/');

                if (localeKey.length() > 0) {
                        name += ("_" + localeKey);
                }

                name += ".properties";

                InputStream is = null;
                Properties props = new Properties();

                // Load the specified property resource
                if (log.isTraceEnabled()) {
                        log.trace("  Loading resource '" + name + "'");
                }

                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

                if (classLoader == null) {
                        classLoader = this.getClass().getClassLoader();
                }

                is = classLoader.getResourceAsStream(name);

                if (is != null) {
                        try {
                                props.load(is);
                        } catch (IOException e) {
                                log.error("loadLocale()", e);
                        } finally {
                                try {
                                        is.close();
                                } catch (IOException e) {
                                        log.error("loadLocale()", e);
                                }
                        }
                }

                if (log.isTraceEnabled()) {
                        log.trace("  Loading resource completed");
                }

                // Copy the corresponding values into our cache
                if (props.size() < 1) {
                        return;
                }

                synchronized (messages) {
                        Iterator names = props.keySet().iterator();

                        while (names.hasNext()) {
                                String key = (String) names.next();

                                if (log.isTraceEnabled()) {
                                        log.trace("  Saving message key '" + messageKey(localeKey, key));
                                }

                                String convertedMessage = props.getProperty(key);

                                try {
                                        Configuration conf = Configuration.getInstance();
                                        convertedMessage = CharConversion.convert(props.getProperty(key), "ISO-8859-1", conf.get("native.encoding"));
                                } catch (UnsupportedEncodingException e) {
                                        log.fatal("Invalid Encoding..");
                                } catch (ConfigurationException e) {
                                        log.fatal("Configuration Error..");
                                }

                                messages.put(messageKey(localeKey, key), convertedMessage);
                        }
                }
        }
        
        
        /**
         *
         */
        public String getMessage(String key) {
                String message = super.getMessage(key);
                try {
                        Configuration conf = Configuration.getInstance();
                        Locale locale = new Locale(conf.get("native.locale.language"), conf.get("native.locale.country"));
                        message = getMessage(locale, key);
                } catch (ConfigurationException e) {
                        ExceptionHandler.handle(e);
                }
                return message;
        }
}

사실 이건 임시 방편에 불과할지도 모릅니다. 자바에서 properties라는 편리한 방법을 제시하고는 있지만 이런 것도 XML로 처리하는 게 더 좋을 수도 있죠. 그리고 properties를 쓰기로 한다면 그렇게 정한 이상 properties의 인코딩 방식에 따르는 것이 더 편할 것도 같구요. 어쨋든 이 정도의 방법으로도 간단하게 해결은 됩니다.


[자바분류]