주요글: 도커 시작하기
반응형
Properties 클래스를 사용하여 다양한 종류의 어플리케이션에서 손쉽게 사용할 수 있는 범용적인 Configuration 클래스를 구현해본다.

설정 시스템은 왜 필요한가?

대부분의 어플리케이션은 올바르게 실행하기 위해서 몇 가지 설정을 해 주어야 한다. 예를 들어, 웹 서버를 개발할 경우 다음과 같은 질문에 답할 수 있어야 한다.

  • 몇 번 포트를 사용할 것인가? 80 포트? 또는 8080 포트? 아니면 그 외 다른 포트?
  • 웹 문서의 루트는 어느 디렉토리를 사용할 것인가?
  • 기본 문서를 index.html로 할 것인가? 또는 index.htm으로 할 것인가?
물론, 이 세 가지 질문 이외에 다른 많은 질문들이 떠오를 것이다. 여기서 이러한 질문을 던지는 주체는 바로 웹 서버 프로그램이다. 왜냐면, 웹 서버를 실행하기 위해서는 위와 관련된 정보를 반드시 알고 있어야 하기 때문이다. 그렇다면, 여러분은 웹 서버 프로그램에 위 질문에 대한 답을 제공할 수 있어야 한다. 가장 첫번째로 사용할 수 있는 방법은 다음과 같이 프로그램 코드에 직접 위 질문과 관련된 부분을 삽입하는 것이다.

public void run() {
   server = new ServerSocket(80);
   while(true) {
      socket = server.accept();
      new ProcessClient(socket).start();
   }
   .....
}

class ProcessClient implement Thread {
   private String documentRoot = "/user/madvirus/webroot";
   private String[] defaultDocument = new String[] { "index.html", "index.htm" };
   
   public void run() {
      ...
   }
}

이와 같은 방법의 장점은 프로그래밍하기가 매우 쉽다는 점이다. 하지만, 단점이 장점보다는 더 크다. 가장 큰 단점은, 포트 번호나 문서의 루트 디렉토리와 같은 설정 정보를 변경하기 위해서는 소스 코드를 재컴파일해야 한다는 점이다. 여기서 발생되는 또 다른 문제점은 개발시와 배포시에 사용되는 환경이 다르기 때문에, 배포시에 다시 한번 더 소스 코드를 재컴파일해야 하는 과정을 밟아야 한다는 점이다. 이것뿐만이 아니다. 일단 배포가 완전히 이루어졌다고 해도, 실제 어플리케이션을 사용하는 도중에 설정 값을 바꿔야 할 때가 있다. 이러한 경우, 최종 사용자들은 설정할 수 있는 방법이 없으므로, 개발자가 일일이 변경을 해 주어야 한다. 이는 매우 귀찮은 일이 될 수 있다.

이러한 단점을 없애기 위해서 많이 사용되는 방법중의 하나가 바로 설정 파일을 작성하는 것이다. 예를 들어, 아파치 웹 서버의 경우 다음과 같은 형태의 설정 파일을 사용하고 있다.

Port 80
ServerAdmin root@localhost

# ServerRoot: The directory the server's config, error, and log files
# are kept in.
ServerRoot /etc/httpd

# ErrorLog: The location of the error log file. If this does not start
# with /, ServerRoot is prepended to it.
ErrorLog logs/error_log

여기서, 아파치 웹 서버 관리자는 단순히 설정 파일의 특정한 값을 변경하고, 웹 서버를 재가동함으로써 변경된 값을 적용할 수 있다. 예를 들어, 포트 번호를 8080으로 변경하고자 할 경우 위의 Port 다음에 있는 80을 8080으로 변경한 후, 웹 서버를 재가동하면 된다. 여기서 '#'은 주석을 의미하며, 아파치 웹 서버는 '#'으로 시작되는 줄은 분석하지 않는다.

소스 코드에 직접적으로 입력하는 것에 비해서 상당히 진보된 형태임을 알 수 있다. 이 글에서는 java.util 패키지의 Properties 클래스를 사용하여 위와 비슷한 형태의 설정 파일로부터 설정 정보를 읽어오는 클래스인 Configuration 클래스를 작성할 것이다. 이 클래스를 사용함으로써 여러분은 좀 더 손쉽게 어플리케이션에 '설정가능한(configurable) 환경'을 제공할 수 있을 것이다.

설정파일의 형태

먼저 살펴볼 내용은 어플리케이션에서 사용할 설정 파일이다. Configuration 클래스가 인식할 수 있는 설정파일의 형태는 다음과 같다.

# 주석으로 처리되는 줄
key1=value1
key2=value2
.....

각각의 프로퍼티는 'key1=value1'의 형태로 저장된다. 여기서 등호(=)의 왼쪽에 있는 key1은 프로퍼티의 키에 해당하며 value1은 프로퍼티의 값에 해당한다. 아파치 웹 서버의 설정 파일과 마찬가지로 '#'으로 시작되는 줄은 주석으로 처리한다. 웹 서버를 위한 설정 파일의 경우 다음과 같은 형태를 지닐 것이다.

# 웹 서버가 사용할 포트 번호
webserver.port=8080

# 문서의 루트 디렉토리
webserver.root_directory=/home/madvirus/webroot/

# 기본적으로 읽어올 문서
webserver.default_document=index.html,index.htm

단지 웹 서버 뿐만 아니라 여러 곳에서 이러한 형태의 설정 파일을 손쉽게 사용할 수 있을 것이다. 예를 들면, 데이터베이스 커넥션 풀에서 사용할 JDBC 드라이버의 종류라든가 최대로 열수 있는 Connection의 개수 등을 이 설정 파일에서 지정할 수 있을 것이다. 위에서 예로든 설정 파일의 webserver.port=8080 부분을 보면 프로퍼티의 값이 문자열 뿐만 아니라 숫자가 될 수 있다는 것을 알 수 있다. 따라서, 우리가 작성할 Configuration 클래스는 String 타입 뿐만 아니라, int, double, boolean과 같은 타입을 처리할 수 있어야 한다.

java.util.Properties 클래스와 Config 인터페이스

이제 Configuration 클래스에서 핵심적인 역할을 하는 java.util 패키지에 있는 Properties 클래스에 대해서 알아보도록 하자. Properties 클래스는 프로퍼티의 지속적인(persistent) 집합을 나타내며, 프로퍼티의 목록을 스트림으로 저장되거나 스트림으로부터 읽어올 수 있다. java.util.Hashtable 클래스를 상속하고 있기 때문에 각각의 프로퍼티들은 <키, 값>의 형태로 저장되며, 또한 Hashtable의 put() 메소드를 사용하여 손쉽게 프로퍼티를 추가하거나 변경할 수 있다. (하지만, 프로퍼티의 값을 추가하거나 변경하는 것이 그리 좋은 방법은 아니다).

Properties 클래스에서 사용할 메소드는 getProperty()이며, 다음과 같은 두 가지 형태가 존재한다.

public String getProperty(String key)
public String getProperty(String key, String defaultValue)

첫번째 메소드는 key를 키값으로 갖는 프로퍼티가 있을 경우 그 값을 리턴하고, 없다면 null을 리턴한다. 두번째 메소드는 key를 키값으로 갖는 프로퍼티가 있을 경우 그 값을 리턴하고, 없다면 두번째 파리미터로 넘겨준 defaultValue를 리턴한다. getProperty() 메소드 이외에 다른 메소드에 대해 알고 싶다면 Java API 문서를 참조하기 바란다.

Config 인터페이스, AbstractConfiguration 클래스

앞에서 이 글의 목적은 다양한 어플리케이션에서 범용적으로 사용될 수 있는 설정 클래스를 작성하는 것이다. 범용적인 설정 클래스를 작성한다는 것은 여러 종류의 형태로 설정 정보를 입력 받을 수 있어야 한다는 것을 의미한다. 예를 들어, 로컬 파일이 아닌 RMI, HTTP, Corba와 같이 원격에 있는 자원을 사용하여 설정 내용을 읽어올 수 있어야 한다. 그렇다면 Configuration 클래스에서 이 모든 것을 다 처리할 것인가? 답은 '아니오'이다. Configuration 클래스는 단지 로컬에 있는 파일로부터 프로퍼티를 읽어오며, 원격으로부터 프로퍼티를 읽어오는 것은 RMIConfiguration, HTTPConfiguration 그리고 CorbaConfiguration과 같은 별도의 클래스에서 책임을 진다. 이들 클래스들은 모두 다른 자원으로부터 프로퍼티를 읽어오는 것이 다를 뿐 그들의 역할은 모두 같다. 이처럼 여러 개의 클래스들이 공통된 형태의 작업을 하는 경우에 주로 사용되는 것이 있다. 바로 상속이다. 이 절의 제목에서 사용된 AbstractConfig 클래스는 모든 종류의 Configuration 클래스들이 상속받는 추상 클래스이며, Config 인터페이스를 구현하고 있다. 즉, 다음과 같은 형태의 구조를 지닌다.


먼저 Config 인터페이스를 정의해보자. Config 인터페이스는 String, int, long, double, float 형의 프로퍼티의 값을 구해주는 메소드를 선언하고 있다. Config 인터페이스의 소스 코드는 다음과 같다.

package javacan.config;

public interface Config {
   public java.util.Properties getProperties();
   public String getString(String key);
   public int getInt(String key);
   public double getDouble(String key);
   public boolean getBoolean(String key);
}

Config 인터페이스는 보는 것 처럼 간단하다. 단순히 필요한 타입의 프로퍼티 값을 구해주는 네 개의 getXXX(key) 메소드를 정의하고 있다. Configuration 클래스의 사용자들은 Configuration, HTTPConfiguration, RMIConfiguration과 같은 클래스들의 구체적인 구현을 알 필요 없이 단지 Config 인터페이스를 통해서 필요한 프로퍼티의 값을 읽어오기만 하면 된다. 이제 AbstractConfiguration 클래스의 소스를 살펴보자.

package javacan.config;

import java.util.Properties;

public abstract class AbstractConfiguration implements Config {
   /**
    * 모든 속성값을 저장한다..
    */
   protected Properties props = null;
   
   public AbstractConfiguration() {
      // do nothing;
   }
   
   /**
    * 모든 속성 이름을 구한다.
    */
   public Properties getProperties() {
      return props;
   }
   
   /**
    * String 타입 속성값을 읽어온다.
    */
   public String getString(String key) {
      String value = null;
      value = props.getProperty(key);
      
      if (value == null) throw new IllegalArgumentException("Illegal String key : "+key);
      
      return value;
   }
   
   /**
    * int 타입 속성값을 읽어온다.
    */
   public int getInt(String key) {
      int value = 0;
      try {
         value = Integer.parseInt( props.getProperty(key) );
      } catch(Exception ex) {
         throw new IllegalArgumentException("Illegal int key : "+key);
      }
      return value;
   }
   
   /**
    * double 타입 속성값을 읽어온다.
    */
   public double getDouble(String key) {
      double value = 0.0;
      try {
         value = Double.valueOf( props.getProperty(key) ).doubleValue();
      } catch(Exception ex) {
         throw new IllegalArgumentException("Illegal double key : "+key);
      }
      return value;
   }
   
   /**
    * boolean 타입 속성값을 읽어온다.
    */
   public boolean getBoolean(String key) {
      boolean value = false;
      try {
         value = Boolean.valueOf(props.getProperty(key)).booleanValue();
      } catch(Exception ex) {
         throw new IllegalArgumentException("Illegal boolean key : "+key);
      }
      return value;
   }
}

AbstractConfiguration 클래스는 Config 인터페이스의 각 메소드를 구현하고 있다. 또한, 각각의 프로퍼티는 멤버 변수인 props에 저장되어 있다고 가정한다. props는 Properties 클래스이며 각각의 getXXX(key) 메소드는 내부적으로 이 Properties를 사용한다. 여기서 주의할 점은 getXXX()에 throws 구문을 통해서 예외를 던지지 않고 RuntimeException의 한 종류인 IllegalArgumentException을 발생한다는 것이다. 이는 자바에서는 상위 클래스에 있는 메소드를 오버라이딩(overriding) 할 경우 throws 구문을 변경할 수 없기 때문에 그렇게 한 것이다. AbstractConfiguration 클래스의 소스 코드를 보면 AbstractConfiguration 클래스는 자체적으로 Properties 클래스를 초기화하지 않는다는 것을 알 수 있다. 이러한 프로퍼티의 초기화는 AbstractConfiguration 클래스를 상속한 Configuration이나 HTTPConfiguration과 같이 실제로 특정 자원(예를 들면, 로컬 파일)으로부터 프로퍼티 목록을 읽어오는 클래스에서 이루어진다.

Configuration 클래스의 구현

Configuration 클래스는 로컬 파일로부터 설정 정보를 읽어와서 Properties 객체를 초기화한다. 로컬 파일을 읽어오는 방법에는 여러 가지가 있을 수 있으나, 여기서는 CLASSPATH에 있는 파일로부터 설정 정보를 읽어올 것이다. CLASSPATH에 있는 파일을 읽어오기 위해서는 ClassLoader 클래스의 getSystemResource() 메소드를 사용하면 된다. 예를 들어, CLASSPATH에 있는 javacan.properties 파일을 읽고자 한다면, 다음과 같은 코드를 사용하면 된다.

java.net.URL dbURL = ClassLoader.getSystemResource("javacan.properties");
File fileName = new File(dbURL.getFile());

예를 들어, 현재 환경변수 CLASSPATH의 값이 다음과 같다고 해 보자.

CLASSPATH=/home/madvirus/config/

이 경우 javacan.properties 파일은 /home/madvirus/config 디렉토리에 위치하면 된다.

ClassLoader.getSystemResource() 메소드는 현재 사용되는 클래스로더에 따라 다른 형태로 구현될 수 있다. 자바2에서 기본적으로 사용되는 클래스로더의 경우, getSystemResource() 메소드는 CLASSPATH로부터 자원을 읽어온다. 이제 CLASSPATH에 있는 javacan.properties 파일을 읽어올 수 있게 되었다. 이제 남은 것은 그 파일로부터 프로퍼티 목록을 읽어와서 Properties 클래스의 인스턴스를 생성하는 것이다. 이에 대한 설명은 Configuration 클래스의 소스 코드를 본 이후에 계속하도록 하자.

package javacan.config;

import java.io.*;

public class Configuration extends AbstractConfiguration {
   
   /**
    * 설정 파일의 이름을 저장한다.
    */
   private String configFileName;
   
   public Configuration() throws ConfigurationException {
      initialize();
   }
   
   /**
    * 필요한 초기화를 한다.
    */
   private void initialize() throws ConfigurationException {
      java.net.URL dbURL = ClassLoader.getSystemResource("javacan.properties");   
      if (dbURL == null) {
         File defaultFile = new File(System.getProperty("user.home"), "javacan.properties");
         configFileName = System.getProperty("javacan.config.file", defaultFile.getAbsolutePath());
      } else {
         File fileName = new File(dbURL.getFile());
         configFileName = fileName.getAbsolutePath();
      }
      
      try {
         File configFile = new File(configFileName);
         if ( ! configFile.canRead() ) 
            throw new ConfigurationException( "Can't open configuration file: " + configFileName );
         
         props = new java.util.Properties();
         FileInputStream fin = new FileInputStream(configFile);
         props.load(new BufferedInputStream(fin));
         fin.close();
      } catch(Exception ex) {
         throw new ConfigurationException("Can't load configuration file: "+configFileName);
      }
   }
}

Configuration 클래스의 소스를 보면, 단지 생성자와 initialize() 메소드만 정의되어 있다. initialize() 메소드는 CLASSPATH에 있는 javacan.properties 파일로부터 프로퍼티 정보를 읽어온 후 상위 클래스의 멤버인 props에 저장한다. 여기서 눈여겨 볼 부분은 바로 Properties의 load() 메소드를 사용하는 부분이다. Properties 클래스의 load() 메소드는 파라미터로 넘겨준 InputStream으로부터 자동으로 프로퍼티 목록을 만들어낸다. 따라서 개발자가 파일을 분석한 후 일일이 각각의 프로퍼티를 Properties에 추가해줄 필요가 없다.

이제 마지막으로 ConfigurationException 클래스에 대해서 알아보자. 이 클래스는 이름에서도 알 수 있듯이 예외 클래스이며, 다음과 같이 간단하게 정의되어 있다.

package javacan.config;

public class ConfigurationException extends Exception {

   public ConfigurationException() {
      super();
   }

   public ConfigurationException(String s) {
      super(s);
   }
}

테스트

이제 모든 "설정가능한" 어플리케이션을 만들기 위한 모든 준비가 끝났다. Configuration 클래스의 사용법은 다음과 같이 매우 쉽다.

Config conf = new Configuration();
int val1 = conf.getInt("port");
String email = conf.getString("admin_email");

실제로 Configuration 클래스가 올바르게 동작하는 지 테스트하기 위해서는 프로퍼티 목록을 저장하고 있는 설정 파일이 필요하다. 테스트에서 사용할 설정 파일 javacan.properties는 다음과 같다.

webserver.hostname=www.javacan.com
webserver.document_root=/home/madvirus/webroot
webserver.port=8080

이 파일은 CLASSPATH에 위치해야 한다. 예를 들어, 이 파일이 /home/madvirus/conf/ 디렉토리에 있다면, CLASSPATH에 /home/madvirus/conf 디렉토리를 추가해주어야 한다. 이제 테스트를 위한 Test 클래스를 작성해보자. Test 클래스는 다음과 같다.

import javacan.config.*;

public class Test {

   public static void main(String[] args) {
      try {
         Config conf = new Configuration();
         System.out.println("webserver.hostname:"+conf.getString("webserver.hostname") );
         System.out.println("webserver.document_root:"+conf.getString("webserver.document_root") );
         System.out.println("webserver.port:"+conf.getInt("webserver.port") );
      } catch(ConfigurationException ex) {
         ex.printStackTrace();
      }
   }
}

Test 클래스를 실행하면 다음과 같이 결과가 출력될 것이다.

[webmaster@server madvirus]$ java Test
webserver.hostname:www.javacan.com
webserver.document_root:/home/madvirus/webroot
webserver.port:8080
[webmaster@server madvirus]$

문제점

Configuration 클래스가 사용하기 쉽다는 장점은 있으나, 몇 가지 문제점을 갖고 있다.

첫번째 문제점은 Configuration 클래스의 인스턴스를 생성할 때 마다 매번 설정 파일을 읽어온다는 점이다. 실제로 설정 파일이 변경되는 경우는 극히 드물며, 따라서 설정 내용이 필요할 때 마다, 매번 설정 파일을 읽어온다는 것은 비효율적이다.

두번째 문제점은 여러 개의 파일로부터 설정 정보를 읽어올 수 없다는 점이다. 이는 Configuration 클래스를 조금만 변경하면 손쉽게 개선할 수 있을 것이다. 단, 여러 파일로부터 설정 정보를 읽어올 경우 서로 다른 파일에 있는 프로퍼티가 같은 키값을 가질수도 있기 때문에, 키값이 겹치지 않도록 하기 위한 별도의 방법을 사용해야 한다.

세번째 문제점은 설정 파일만으로는 각 프로퍼티 간의 관계를 알 수 없다는 점이다. 물론, 엄밀히 말해서 이것이 문제점이 되지는 않는다. 현재 나와 있는 대부분의 어플리케이션의 설정 파일은 여기서 작성한 설정 파일과 비슷한 형태로 되어있으며, 현재까지 이러한 형태의 설정 파일이 문제가 된 적은 거의 없다. 하지만, 각 프로퍼티 간의 관계를 명시적으로 표시할 수 있다면 설정 파일을 관리하는 것이 지금보다 훨씬 더 간단하고 수월해질 것이다.

네번째 문제점은 세번째 문제점과 유사한 것으로 프로퍼티들의 집합이라는 개념이 존재하지 않는다는 것이다. 예를 들어, 채팅서버와 웹 서버의 프로퍼티를 하나의 설정 파일에 저장할 수 있으며, 이러한 경우에 채팅서버와 관련된 프로퍼티와 웹 서버와 관련된 프로퍼티를 별도의 집합으로 지정할 수 있다면, 훨씬 더 명료하게 설정 파일을 관리할 수 있을 것이다.

이러한 문제점 중에 몇 가지는 XML을 이용하여 손쉽게 해결할 수 있다. 나중에 기회가 된다면 XML을 이용한 설정에 대해서 알아볼 것이다.

결론

이 글에서는 java.util.Properties 클래스를 이용하여 설정 파일로부터 손쉽게 프로퍼티를 읽어오는 것에 대해 알아보았다. Configuration 클래스는 CLASSPATH에 있는 자원으로부터 프로퍼티를 읽어 매우 손쉽게 어플리케이션의 여러 프로퍼티를 설정할 수 있도록 해준다. 비록 몇 가지 문제점이 있긴 하지만, 여러분 스스로 조금만 노력한다면 그러한 문제를 어렵지 않게 해결할 수 있을 것이다.

관련링크:

+ Recent posts