dynamicJarLoader.zip


라이브러리에 대한 동적 로딩은 사용자의 요구가 다양하게 변화하는 어플리케이션을 위한 기술이다. Java에서는 URLClassLoader라는 라이브러리 클래스를 통해서 이 기능을 지원하고 있다.

이를 이용할 상황을 하나 생각해 보도록 하자.

화면에 도형을 그리는 어플리케이션을 만들어 사용자에게 배포하였다고 가정하자. 이미 도형에 대한 클래스 모델링이 잘 되어 있기 때문에 Shape이라는 인터페이스는 이미 배포되어 있다. 그리고 몇몇 도형들 역시 배포된 상태다. 소프트웨어 내부에서는 이미 배포된 jar 파일을 통해 도형들을 생성해서 사용하고 있다.

이 상황에서 고객이 새로운 도형을 만들어 달라고 요청해 왔다. 필요한 도형은 Circle과 Triangle이다. 이 파일들은 개발자에 의해서 개발되었고 사용자에게 배포되었다. 이 때 이미 동작 중인 어플리케이션은 종료할 수 없다. 이런 경우에 어플리케이션을 중단시키지 않아도 클래스들을 로드할 수 있는 동적 로더가 활용될 수 있다.

아래 클래스 다이어그램을 보자.

위에서 설명했듯이 Shape 인터페이스는 이미 배포된 상태이다. 여기서 사용자가 원하는 Circle과 Triangle을 각각의 이름으로 된 jar 파일을 배포하였다.

여기서 사용자가 어플리케이션을 종료하지 않고 이들 jar 파일들을 로드하려면 동적 Jar 로드 기능이 탑재되어 있어야 한다. 이를 구현한 것이 DynamicJarLoader 클래스이다.


DynamicJarLoader 클래스 설명

이 클래스는 객체 생성자를 통해서 Jar 파일이 들어 있는 폴더 경로(String jarPath)를 입력 받는다.

이후 load(jarFileName : String) 메소드를 통해서 Jar 파일 이름을 입력해주면 라이브러리가 로드된다.

특히 DynamicJarLoader 클래스는 내부에 있는 loaderMap을 통해서 여러 이름의 라이브러리를 동시에 로드할 수 있다. 따라서 이 객체 하나 만으로도 여러 라이브러리 파일을 로드하고, 로드된 라이브러리들을 통해서 객체를 생성할 수 있다. 리턴되는 boolean 값은 라이브러리 로드가 성공했는지 여부를 알려준다.


unload(jarFileName : String) 메소드는 이미 로드된 라이브러리를 메모리에서 내리거나, 새로운 버전의 라이브러리를 열기 위해서 기존에 이미 열려진 라이브러리를 닫아야 할 경우에 호출하는 메소드이다.


이미 라이브러리가 로드되어 있다면 열린 라이브러리를 통해서 객체를 새로 생성할 수 있어야 한다. newInstance() 메소드는 클래스 이름(className)을 통해서 객체를 로드할 수 있도록 구현된 메소드이다. newInstance() 메소드는 두 개가 있는데, 하나는 className만으로 객체를 생성하도록 하고, 나머지 하나는 jar 파일 이름까지 입력해서 보다 정확한 객체를 생성하도록 한다.


첨부파일

맨 위쪽에 첨부된 첨부파일은 shape.jar / circle.jar / triangle.jar 파일과 DynamicJarLoader 클래스, 그리고 이들 라이브러리 동적 로딩을 테스트해 볼 수 있는  Use 클래스를 포함하고 있는 eclipse 프로젝트 파일이다.(JDK 1.7 이상을 사용하기 바란다.)


아래는 DynamicJarLoader 클래스의 소스이다.

public class DynamicJarLoader {

    private String jarPath;

    private Map<String, URLClassLoader> loaderMap = new HashMap<String, URLClassLoader>();

    public DynamicJarLoader(String jarPath){

        this.jarPath = jarPath;

        this.jarPath.replaceAll("\\\\", "/");

        if(this.jarPath.endsWith("/") == false) this.jarPath = this.jarPath + "/";

    }

    public boolean load(String jarFileName){

        if(loaderMap.containsKey(jarFileName) == true) unload(jarFileName);

        String jarFilePath = jarPath + jarFileName;

        File jarFile = new File(jarFilePath);

        try {

            URL classURL = new URL("jar:" + jarFile.toURI().toURL() + "!/");

            URLClassLoader classLoader = new URLClassLoader(new URL [] {classURL});

            loaderMap.put(jarFileName, classLoader);

            return true;

        } catch (MalformedURLException e) {

            return false;

        }

    }

    public boolean unload(String jarFileName){

        URLClassLoader loader = loaderMap.get(jarFileName);

        if(loader == null) return true;

        try {

            loader.close();

            return true;

        } catch (IOException e) {

            return false;

        }

        finally{

            loaderMap.remove(jarFileName);

        }

    }

    public Object newInstance(String jarFileName, String className){

        URLClassLoader loader = loaderMap.get(jarFileName);

        if(loader == null) return true;

        try {

            Class<?> clazz = loader.loadClass(className);

            return clazz.newInstance();

        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {

            return null;

        }

    }

    public Object newInstance(String className){

        for(String each : loaderMap.keySet()){

            Object object = newInstance(each, className);

            if(object != null) return object;

        }

        return null;

    }

}


Posted by 이세영2
,

Java에서는 다음과 같이 가변 길이 파라메터를 지원한다. 이 가변길이 파라메터는 재밌는 특성들이 있는데 이를 활용하는 방법을 알아보자.


우선 기본적인 사용법 부터 알아보도록 한다.

public static void target(String...strings){

    String[] array = strings; // 배열화 된다.

    for(String str : strings) System.out.println(str);

}

이 함수의 사용법은 아래와 같다.

target("A", "B", "C", "D");

target();

실행 결과는 아래와 같다.

A

B

C

D


이와 같이 0~* 개의 동일한 타입을 파라메터로 받을 수 있다. 만약 또 다른 파라메터와 함께 사용하고 싶다면 가변길이 파라메터는 맨 뒤에 위치 시켜야 한다.


자 여기서부터 재밌는 실험을 해보도록 하자. 혹시 가변길이 파라메터로 받은 인자를 가변길이 파라메터 함수에 다시 넣을 수 있을까? 코드는 아래와 같다.

public static void wrap(String...strings){

    target(strings); // 받은 가변 인자 파라메터를 그대로 넘김

}

      

public static void target(String...strings){

    String[] array = strings; // 배열화 된다.

    for(String str : strings) System.out.println(str);

}

다음과 같이 실행해 본다.

wrap("A", "B", "C", "D");

wrap();

실행 결과는 위와 같다. 즉, 가변인자 파라메터로 받은 인자를 다시 가변인자 파라메터로 넘기는 것이 가능하다.

재밌는 것은 가변길이 파라메터에 집어 넣은 파라메터들이 무엇으로 변환 되는지이다. target() 함수 두번째 줄에 보면 가변길이 파라메터들이 배열로 변환됨을 알 수 있다. 이것이 의미하는 바는 무엇인가? String...strings와 같은 가변인자 파라메터 형식은 여러 String 객체가 나열된 형식("a", "b", "c"와 같은)도 인자로 인정해 주고, String[] 형식 즉, 배열 한 개도 인자로 인정해 준다는 의미이다. 따라서 아래와 같은 사용도 가능하다.

String[] array = new String[]{"a", "b"};

target(array);

결과는 예상할 수 있을 것이다.


이 특성을 이용하면 몇가지 유틸리티 성 함수를 만들어 볼 수 있다. 우선 여러 개별 객체를 받아 배열로 변환해 주는 toArray(String...strings)와 같은 함수를 만들어 볼 수 있겠다.

public static String[] toArray(String...strings){

    return strings;

}


또한 객체의 배열을 인자로 받는 함수를 이용하기 쉽게 만들 수도 있다. 만약 제공된 API가 다음과 같은 모양이라고 가정하자.

public void api(Object[]);

이런 경우 가장 손쉽게 사용하는 방법은 api(new Object{......})와 같은 형식이다. 하지만 위의 toArray() 함수와 같이 한번 가변인자 파라메터로 통과 시켜 주면 더 간편하게 api() 함수를 사용할 수 있다.

Posted by 이세영2
,

Reflection을 잘 활용할 줄 알면 Java 프로그래밍에서 코드의 범용성을 극대화 할 수 있다.


이 글에서는 그 중에서 Reflection의 특성을 이해하고 코드의 범용성을 높일 수 있는 범용 toString() 함수를 만들어 보도록 하겠다. toString() 함수는 프로그래밍 시 디버깅 용도로 상당한 활용도가 있는 함수이다.


범용 toString() 이란?

기본적으로 Java에서는 모든 클래스가 toString() 함수를 지원한다. 따라서 라이브러리에 선언되어 있는 클래스들은 .toString() 함수만 호출하면 해당 클래스 내부에 있는 값들을 출력해 낼 수 있다. 하지만 사용자가 직접 선언한 클래스는 toString() 함수 호출만 가지고 내부 변수에 가지고 있는 값들을 확인하기가 어렵다.

예를 들어 다음과 같은 클래스를 선언했다고 하자.

class TestClass{

    short s = 1;

    int i = 10;

    long l = 20;

   

    String name = "nameString";

   

    Long L = new Long(100);

   

    List<String> list = new ArrayList<String>();

    Map<String, String> map = new HashMap<String, String>();

   

    public TestClass(){

        list.add("string1");

        list.add("string2");

        list.add("string3");

        map.put("key1", "value1");

        map.put("key2", "value2");

        map.put("key3", "value3");

    }

}


그리고 아래처럼 toString() 함수를 호출해보면

TestClass tc = new TestClass();

System.out.println(tc.toString());

출력 결과는 다음과 같다.

MyPattern.reflection.TestClass@15db9742

패키지 경로에 따라서 조금씩 다를 수는 있지만 기본적으로는 내부에 있는 변수들의 값이 나오지는 않는다. 그래서 새로 클래스를 정의할 때마다 toString() 함수를 재정의(override) 할 필요가 있다. 하지만 Java 프로그래밍을 하면서 엄청나게 많은 클래스들을 만들텐데 새로 만드는 클래스마다 각 클래스에 필요한 toString() 함수를 재정의 하기는 너무 힘든 일이다.

이 때 Reflection을 이용하여 toString() 함수를 만들지 않고도 사용할 수 있는 것이 바로 범용 toString() 함수이다.


일단 원형을 만들어 보자. 재정의를 이용하지 않으면서 범용으로 사용하기 위해서는 별도의 클래스로 만들 필요가 있다. 그래서 ToString이라는 클래스를 하나 만들도록 하겠다.


class ToString

public class ToString {

    public static String toString(Object object){

        Field []fields = object.getClass().getDeclaredFields();

        String str = "{";

        for(Field field : fields){

            field.setAccessible(true);

        }

        return str + "} ";

    }

}

일단 내부에 toString() 함수가 선언되어 있지만 아직 동작하지는 않는 상태이다.

우선 인터페이스를 살펴 보자. toString() 함수는 Object를 매개변수로 받는다. 이 매개 변수에는 우리가 출력해 내기 원하는 객체를 집어 넣을 것이다. 범용이므로 당연히 Object 클래스 타입으로 집어 넣는다.

내부에 보면 Fields 변수가 선언되어 있고, object.getClass().getDeclaredFields() 함수를 호출하여 Field[]를 가지고 오게 되어 있다. getClass()는 Reflection에 활용되는 클래스 객체를 가지고 오는 함수이다. 즉, object 객체는 .class 객체를 가지고 있는데, 객체의 본래 타입에 따라서 .class 객체는 모두 다르다. getDeclaredFields() 함수는 클래스 객체로부터 선언된 모든 Field 객체들을 배열 형태로 가져오는 함수다. .class 객체가 객체의 본래 타입에 따라 다르기 때문에 getDeclaredFields() 함수를 통해 나오게 되는 Field[]도 모두 다르다.

이제 Field[]들을 모두 얻었으니 이를 이용만 하면 된다. for문 안을 보면 각 Field 객체에 대해 setAccessible(true) 함수가 호출되는 것을 볼 수 있다. 만약 Field가 public이 아닌 경우 캡슐화로 인하여 그 값에 접근하는 것이 불가능한데 Reflection은 이 함수를 통해서 접근이 가능하도록 만들 수 있다.(이 부분은 아직도 이슈가 많은 부분이라 프로그래머 마다 찬반 의견이 다르다. 하지만 유용성에 대해서는 아무도 부인하지는 못할 것이다.)

자 일단 Fileld 객체에 대한 접근도 가능해졌다. 그러면 이제 출력을 해 볼 시간이다. 우선 Field 객체의 출력 포맷을 다음과 같이 정해 보겠다.

[type] [field name] : [value] 

만약 short s = 1 을 출력한다면 short s : 1 과 같이 출력될 것이다.

그러면 type, fieldName, value를 가지고 올 수 있는 Field 객체의 API를 알아 보아야 한다.

String getType() : type에 대한 클래스 객체(Class object)를 반환하는 함수 

String getName() : 필드명을 String 객체로 반환하는 함수

Object get(object) : Field가 값으로 가지고 있는 객체를 반환하는 함수

이를 통해서 간단히 문자열을 만들어 보면 다음과 같다.

str += field.getType().toString() + " " + field.getName() + ":" + field.get(object) + " ";

이렇게 하면 되는데, 예외 처리 때문에 try-catch문이 필요할 것이다. 이것은 있다가 한꺼번에 처리하도록 하고, 우선 한가지만 짚어 보고 넘어가자.

field.getType()의 경우 타입을 반환하긴 하는데 primitive 타입이 아닌 경우 타입이 장황하게 출력될 수 있다. 예를 들어 Long 타입인 경우 "class java.lang.Long"라고 출력된다. 이것이 너무 장황하다면 짧게 줄일 필요가 있겠는데 이는 아래와 같이 하면 된다.

String type = field.getType().toString().substring(field.getType().toString().lastIndexOf(".") + 1);

type 만을 문자열로 뽑아 내는 구문이다. 어려운 것이 아니고 문자열 중에서 마지막 "."이 나오는 index를 찾고, 그 index 다음번(+ 1) 문자부터 시작하는 substring()을 뽑아 내도록 한 것이다. 이렇게 하면 type을 짧게 출력해 낼 수 있다.


이것을 코드에 적용시켜 보면 다음과 같다.

public class ToString {   

    public static String toString(Object object){

        Field []fields = object.getClass().getDeclaredFields();

        String str = "{";

        for(Field field : fields){

            field.setAccessible(true);

   

            try {

                String type 

                  field.getType().toString().

                  substring(field.getType().toString().lastIndexOf(".") + 1);


                  str += type + " " + field.getName() + ":" + field.get(object) + " ";   


            } catch (IllegalArgumentException e) {

            } catch (IllegalAccessException e) {

            }

    }

}


이렇게 해서 맨 처음 선언한 TestClass 객체를 넣어 출력을 해보면 아래와 같이 나오게 될 것이다.

    TestClass tc = new TestClass();

    System.out.println(ToString.toString(tc));


출력결과 :


{short s:1 int i:10 long l:20 String name:nameString Long L:100 List list:[string1, string2, string3] Map map:{key1=value1, key2=value2, key3=value3} } 

위와 같이 원하는 대로 출력이 됨을 알 수가 있다. 특히 Map이나 List도 출력이 가능하므로 이 정도 만으로도 상당히 유용하게 사용할 수 있다.


하지만 우리는 많은 경우에 어떤 객체를 선언할 때 자신이 만든 맴버 객체를 선언하여 사용할 경우가 많다. 이런 경우에는 안타깝게도 이 코드는 소용이 없다. 그 이유가 무엇일까? field.get(object)를 통해 나오는 것은 변수에 할당되어 있는 객체이다. 즉 우리가 만든 맴버 객체가 이 함수를 통해 나오게 된다. String 클래스에서 문자열 변환을 할 때 자동으로 객체의 toString() 함수를 호출하게 되어 있는데, 우리가 만든 객체는 toString() 함수를 재정의 하지 않았다! 따라서 그냥 출력하면 이상한 값이 나오게 될 것이다.


그렇다면 이대로 만족하고 사용해야 할까? 당연히 그렇지 않다. 자, 상황을 정확히 알기 위해 다음의 함수를 만들어 실행해 보도록 하자. 지금 중요한 것은 우리가 만든 객체는 primitive  타입이나 라이브러리에서 제공하는 객체가 아니다. 따라서 우리가 만든 객체임을 확인하고, 그 때에는 재귀적으로 ToString.toString(Object object) 함수를 호출하도록 한다면 문제가 해결될 것이다. 다음과 같은 논리이다.

ToString.toString(Object 우리가만든객체){

    ........ if(field.getType() == 내부에선언된우리가만든객체) toString(내부에선언된우리가만든객체);

}


그렇다 필요한 것은 getType() 함수를 통해서 타입을 확인해 보면 금방 알 수 있다. 그러면 클래스 객체가 어떻게 나오는지를 확인하기 위해 다음과 같은 코드를 동작시켜 보도록 하자. 좀 길긴 하지만 복사해서 붙여 넣고 실행해 보면 된다.

public class ToString {

    public static String toTypeString(Object object){

        Field []fields = object.getClass().getDeclaredFields();

        String str = "";

        for(Field field : fields){

            field.setAccessible(true);

   

            str += field.getType() + " " + field.getName() + "\n";

        }

        return str;

    }


    public static void main(String[] args) {

        AllTypes types = new AllTypes();

        System.out.println(ToString.toTypeString(types));

    }

}

class InnerClass{

    int a = 100;

    long b = 200;

    String name = "innerClass";

}


class AllTypes{

    short s = 1;

    int i = 1;

    long l = 1;

    Short S = new Short((short)0);

    Integer I = new Integer(0);

    Long L = new Long(0);

   

    InnerClass inner = new InnerClass();

   

    List<String> list = new ArrayList<String>();

    Map<String, String> map = new HashMap<String, String>();

}

ToString.toTypeString() 함수를 일단 보면 각 Field 객체에 대해 getType()과 getName()을 호출하여 문자열 화 하는 것을 알 수 있다.

우리가 출력하고자 하는 것은 AllType 클래스이다. 이 클래스는 대부분의 primitive 타입과 Map 타입, List 타입이 선언되어 있어서 toTypeString() 함수를 통해 그 타입을 출력해 볼 수 있다. 중요한 것은 InnerClass 부분이다. 이것이 어떻게 출력되는지를 확인해야 한다. 위의 코드를 실행하면 아래와 같이 출력된다.

short s

int i

long l

class java.lang.Short S

class java.lang.Integer I

class java.lang.Long L

class MyPattern.reflection.InnerClass inner

interface java.util.List list

interface java.util.Map map


눈여겨 볼 것은 볼드체로 되어 있는 부분이다. InnerClass는 우리가 만든 것인데 이것은 class 로 시작해서 패키지 경로.InnerClass 타입명으로 나오는 것으로 되어 있다.

그러면 이런 형태로 우리가 만든 클래스를 구분해 내 볼 수 있을 것이다.

우리가 정의한 객체는 if( 클래스명이 class로 시작 하면서 class java.lang. 으로 시작하지 않는 경우 ) 에 해당된다.

이를 코드에 반영하여 전체 코드를 살펴 보면 다음과 같다.


최종 결과물

public class ToString {  

    public static String toString(Object object){

        Field []fields = object.getClass().getDeclaredFields();

        String str = "{";

        for(Field field : fields){

            field.setAccessible(true);

   

            try {

                String type =               

                    field.getType().toString().

                    substring(field.getType().toString().lastIndexOf(".") + 1);


                if(field.getType().toString().startsWith("class ") &&           

                   !field.getType().toString().startsWith("class java.lang.")){


                   str += type + " " + field.getName() + 

                          toString(field.get(object)) + " ";

                }

                else{

                   str += type + " " + field.getName() + ":"

                          field.get(object) + " ";   

                }

            } catch (IllegalArgumentException e) {

            } catch (IllegalAccessException e) {

            }

        }

        return str + "} ";

    }

   

    public static void main(String[] args) {

        AllTypes types = new AllTypes();

        System.out.println(ToString.toString(types));

    }

}

AllTypes 클래스의 정의는 위에서 가져와서 실행해 보면 된다. 실행 결과를 보면 다음과 같다.


실행 결과

{short s:1 int i:1 long l:1 Short S:0 Integer I:0 Long L:0 InnerClass inner{int a:100 long b:200 String name:innerClass }  List list:[] Map map:{} } 

이제 InnerClass 내부의 정보도 모두 출력되고 있음을 알 수 있다.


약간 덧붙이자면, 일단 field가 null인 경우 동작하지 않을 수 있으므로 null 체크도 들어가는게 좋다.

그리고 배열의 경우 위의 경우에서 모두 벗어나기 때문에 제대로 출력이 안될 수 있다. 이 부분은 필요시 직접 처리하기 바란다.

만일 ToString 클래스를 사용해야 하는 것이 불편하고, toString()을 재정의 해서 사용하고 싶다고 할 때에도 이 ToString 클래스는 유용하다. 클래스마다 toString() 함수를 재정의 할 때 다음과 같이 동일하게 해주면 된다.


toString()함수를 재정의 하고자 할 때

@Override

public String toString() {

    return ToString.toString(this);

}

이렇게 하면 역시 클래스별로 toString() 함수를 재정의 할 때마다 다른 코드를 작성할 필요가 없어진다.


Posted by 이세영2
,

C에서는 enum이 바로 숫자이기 때문에 배열의 인덱스로 자주 활용이 된다. 하지만 JAVA에서는 이것이 명시적이지 않아서 잘 모르는 경우가 많이 있다. 여기서는 JAVA에서 enum 타입을 배열의 인덱스로 활용하는 방법과 더불어 C언어의 enum보다 나은 점도 함께 알아보도록 하겠다.


해결하고자 하는 문제

다음과 같은 클래스가 있다고 하자. 대략 동서남북으로의 거리를 나타내는 클래스라고 생각하면 되겠다.

class Distance{

    private int east;

    private int west;

    private int north;

    private int south;

    public void setEast(int east){ this.east = east; }

    public void setWest(int west){ this.west = west; }

    public void setNorth(int north){ this.north = north; }

    public void setSouth(int south){ this.south = south; }

    public int getEast() { return east; }

    public int getWest() { return west; }

    public int getNorth() { return north; }

    public int getSouth() { return south; }

}

클래스 선언 상으로 보면 큰 문제는 없어보이지만 다만 마음에 걸리는 것은 중복이 많다는 점이다.

그래서 잘 들여다 보면 모두 같은 타입(int)의 변수를 선언하고 있고, 모두 getter와 setter를 제공하고 있음을 알 수 있다. 그리고 동서남북을 나타내는 변수들이므로 서로 연관성도 있어 보인다. 이런 경우에는 enum 타입을 하나 선언해서 동서남북을 가리키는 서브 타입을 만들고 이것을 인덱스로 활용하는 방법을 사용하면 중복이 제거된다.

다음과 같은 과정을 통해서 클래스를 수정해 볼 수 있다. 우선 방향을 나타내는 enum을 선언한다. Direction이라는 클래스명을 사용하도록 하겠다.

방향을 나타내는 enum Direction

enum Direction{

    EAST,

    WEST,

    NORTH,

    SOUTH;

}

이제 enum을 선언했으니 Distance 클래스에서 이를 이용하면서 코드를 간결하게 바꿔 보도록 하자. 바꾸는 방법은 우선 각 방향별로 선언되었던 변수를 하나의 배열로 선언하도록 한다. 방향이 4개니까 4개짜리 배열이 되겠다. 그런 다음 getter와 setter가 Direction을 매개 변수로 받도록 수정한다. 이제 중요한 부분이 남았다. enum 타입인 Direction  하위 타입을 어떻게 인덱스 숫자로 바꾸느냐는 것인데, JAVA에서는 ordinal() 이라는 함수를 통해 숫자로 바꿀 수 있다. int ordinal() 함수는 해당 서브 enum 타입의 정의 순서에 따른 숫자를 리턴한다. 가령 EAST.ordinal() = 0, WEST.ordinal() = 1......와 같은 방식이다. 이것은 배열의 인덱스 값과 같은 값이다. 따라서 전혀 문제 없이 사용할 수 있다.

class Distance 수정

class Distance{

    private int distance[] = new int[4];

    public int get(Direction direction){

        return distance[direction.ordinal()]; 

    }

    public void set(Direction direction, int value){ 

        distance[direction.ordinal()] = value; 

    }

}

이것으로 일단 원하는 대로 수정을 완료 하긴 하였다. 하지만 조금만 더 깊이 들어가 보자. 우리는 이미 동서남북이 4방향이라는 것을 알고 있고, 이에 따라 distance 변수의 배열 길이를 4로 지정했다. 하지만 만약 새로운 방향이 추가 된다면 어떻게 될 것인가? 물론 사용시에 예외가 발생하게 되겠지만 보다 근본적으로 이 문제를 해결할 방법이 있다. JAVA의 enum 타입은 기본적으로 static final이다. 따라서 컴파일 타임에 이미 enum 하위 타입의 개수도 정해진다. enum 하위 타입의 개수를 알아내는 방법은 보통 Type.values().length 를 이용한다. .values() 함수는 enum 하위 타입들을 배열화하여 리턴해주는 함수이다. .length는 잘 알다시피 배열의 길이를 나타내는 배열 객체의 변수이다. 그리고 이들은 앞서 말한 바와 같이 컴파일 타임에 결정되기 때문에 배열의 개수 초기화에도 사용할 수 있다.


values().length 를 이용한 배열 개수 초기화

class Distance{

    private int distance[] = new int[Direction.values().length]; // 이부분이 수정되었다.

    public int get(Direction direction){ 

        return distance[direction.ordinal()]; 

    }

    public void set(Direction direction, int value){ 

        distance[direction.ordinal()] = value; 

    }

}

이렇게 하면 Direction에 NORTH_EAST, SOUTH_EAST 등 새로운 하위 타입이 추가될 때마다 Direction.values().length 값도 함께 바뀌게 되고, 따라서 방향의 추가/삭제 시에도 배열의 길이에 의한 문제점이 발생하지 않게 된다.


완성된 코드를 모두 합쳐 보면 다음과 같다.


최종 결과물

enum Direction{

    EAST,

    WEST,

    NORTH,

    SOUTH;

}

class Distance{

    private int distance[] = new int[Direction.values().length];

    public int get(Direction direction){ 

        return distance[direction.ordinal()]; 

    }

    public void set(Direction direction, int value){ 

        distance[direction.ordinal()] = value; 

    }

}


enum Direction과 Distance 클래스를 혼합하여 사용하는 방법은 아래와 같다.


main() 함수

public static void main(String[] args) {

    Distance distance = new Distance();

    distance.set(Direction.EAST, 100);

    distance.set(Direction.WEST, 200);

    distance.set(Direction.NORTH, 300);

    distance.set(Direction.SOUTH, 400);

    System.out.println(Direction.EAST + " " + distance.get(Direction.EAST));

    System.out.println(Direction.WEST + " " + distance.get(Direction.WEST));

    System.out.println(Direction.NORTH + " " + distance.get(Direction.NORTH));

    System.out.println(Direction.SOUTH + " " + distance.get(Direction.SOUTH));

}

출력되는 결과물은 다음과 같다.


출력 결과

EAST 100

WEST 200

NORTH 300

SOUTH 400


원하던 값이 잘 출력됨을 알 수 있다.


이 과정이 시사하는 바가 있는데 이것을 한번 정리해 보면 다음과 같다.

1. 유사한 코드가 반복적으로 나타나는 경우에는 반복을 줄일 수 있는 방법을 생각해 봐야 한다.

2. 유사성을 표현할 수 있는 enum이나 상위 클래스를 선언할 수 있는지를 살펴 보아야 한다.

3. 변수들이 반복적으로 선언된 경우라면 enum과 배열을, if문이나 switch 문이 있을 경우에는 클래스를 이용한다.(클래스를 이용하는 방법에는 상태/전략 패턴이 있다.)

4. 배열을 이용할 경우 추가적인 타입 선언에 대해서도 고려 해야 한다.

Posted by 이세영2
,

enum의 활용법

4.JAVA 2016. 8. 13. 18:34

C언어에서 enum은 단순히 상수형 변수 역할에 지나지 않았다. 하지만 Java에서는 매우 다른 특성들을 지니고 있다. 이 특성들 중에는 특별한 것들도 있어서 기존과는 다른 여러 방식으로 enum을 활용할 수 있다.


먼저 enum의 실제 타입부터 알아보자.


enum의 실제 타입

enum Type{ // abstract class

    ADD,    // public static final class ADD extends Type{}

    SUB;    // public static final class SUB extends Type{}

}


이처럼 기본적으로 enum은 추상 클래스이다. 그리고 그 하위에 선언된 각 열거형 변수는 실제로는 변수가 아니고 enum의 타입을 상속 받은 하위 클래스이다. 이 하위 클래스는 외부에 노출되어 있고 생성할 필요가 없으며 런타임에 교체가 불가능하므로 public static final 타입을 갖는다.




enum은 기본적으로 추상 클래스이기는 하나 다른 클래스로부터 상속을 받지는 못한다. 하지만 interface는 상속을 받을 수 있다. 따라서 다음과 같은 형태로 구현이 가능하다.


enum에 인터페이스 상속 받기

interface Interface{

    public void api();

}

enum Type implements Interface{

    ADD{

        public void api(){ System.out.println("ADD api"); }

    },

    SUB{

        public void api(){ System.out.println("SUB api"); }

    };

}


이 인터페이스 상속 방법은 생각보다 강력하다. 이 특성을 이용해서 디자인 패턴에 나오는 수많은 패턴들을 enum을 통해 구현할 수 있다. enum이 public static final 이라는 점은 Singleton과 유사한 특성을 지니고 있다. 따라서 중복 생성이 안되면서도 일반 클래스 기능을 할 수도 있고, 필요한 때에는 enum의 특성을 활용할 수도 있다. 이 강력한 특성 때문에 Singleton 패턴, Abstract Factory 패턴, Factory Method 패턴, State 패턴, Strategy 패턴, Visitor 등 다양한 패턴의 enum 버전이 있다. 이들에 대해서는 디자인 패턴 항목에서 다룰 예정이다.




추상 클래스라면 함수를 선언할 수도 있어야 한다. 아래와 같이 함수 선언이 가능하다.


enum에서 함수 선언하기

enum Type{

    ADD,

    SUB;

    public void api(){ System.out.println("api()"); }

}




추상 클래스이기 때문에 추상 메소드의 선언도 가능하다. interface를 상속 받을 수 있다는 점에서도 이 점은 유추해 낼 수 있다.


enum에서 추상 메소드 선언하기

enum Type{

    ADD{

        public void api(){ System.out.println("ADD api"); }

    },

    SUB{

        public void api(){ System.out.println("SUB api"); }

    };

    abstract public void api();

}




같은 맥락에서 static 메소드 역시 가능하다. 실용적인 예제를 위해서 하위 타입의 개수를 알아내는 함수로 해보자.


enum에서 static 메소드 선언하기(하위 타입의 개수 알아내기)

public static int size(){ return values().length; }


보통은 Type.values().length를 통해 꺼내오기도 하지만 가독성 면에서 Type.size()를 호출하는 것이 더 나아 보인다.




클래스라면 생성자도 선언할 수 있어야 할 것이다. 생성자 선언은 아래와 같다.


enum에서 생성자 선언하기(enum 클래스를 인덱스로 활용하기)

enum Type{

    ADD(0),// 생성자에게 필요한 인자는 () 안에 넣는다.

    SUB(1); //

    int value;

    private Type(int value){ this.value = value; }   // 이것이 생성자.

    public int value(){ return value; }

}


역시 활용성을 위해서 enum 클래스를 인덱스로 활용할 수 있도록 value 라는 int 형 값을 생성시에 인자로 받도록 했다. Java의 enum은 클래스이기 때문에 C언어에서 처럼 값으로 활용할 수가 없다. 대신에 이처럼 생성자를 통해 인자로 받은 값을 가지고 있다가 값이 필요한 경우 value() 함수를 호출함으로써 값으로도 사용이 가능하다. 이 활용법은 "Effective Java" 라는 책에 수록된 내용이다. 생성자가 private인 것에 주목해야 한다. enum은 외부에서 생성이 불가능하기 때문에 생성자는 항상 private으로 선언해 주어야 한다.




다음은 문자열로 enum을 알아내는 함수이다. enum이 클래스라는 것은 이미 이야기 하였다. 따라서 하위 타입들도 모두 toString() 함수를 가지고 있는데, 그 결과 값은 기본적으로 자신의 선언된 이름과 같다. ADD.toString()은 "ADD" 값이 결과값이다. 이러한 특성은 매우 유용한데, 특히 데이터베이스에 저장할 때 그렇다. DB의 가독성 측면에서 문자열을 활용한 경우에 문자열을 입력받아 타입을 리턴하도록 하면 코드 상에서 문자열 대신 enum을 활용할 수 있으므로 코딩이 편리해진다.


enum 에서 문자열로 enum의 타입을 알아내기

public static Type getTypeByString(String str){

    for(Type each : values()){

        if(each.toString().equals(str)) return each;

    }

    return null;

}



enum 타입이 제공하는 기본 함수로 enum의 순서를 알 수 있는 함수가 있다.


public int ordinal();


위 ordinal() 이라는 함수인데 이 함수는 선언된 enum의 하위 타입이 몇 번째 순서로 선언되었는지를 알 수 있다.(순서의 시작 값은 0이다.) 가령 위에서 선언한 ADD 타입의 경우 0이 리턴되고, SUB의 경우 1이 리턴된다. 이 특성을 이용하면 enum을 인덱스로도 활용이 가능하다.



Java에서의 enum은 열거형의 특성과 클래스의 특성을 함께 가지고 있다는 장점이 있다. toString() 함수를 가지고 있다는 것만으로도 디버깅을 얼마나 쉽게 만들어 주는지 모른다. 그 밖에도 데이터 베이스와의 연동, switch-case 문에 대한 활용, 인터페이스 상속을 활용한 디자인 패턴 등 다양한 곳에 활용할 수 있다.

Posted by 이세영2
,