SSISO Community

시소당

자바 동적 메소드 호출

이 자바 코드는 두 가지 동적 메소드 호출 방법을 다룹니다.
첫 번째는 전통적(?)인 방법으로, 맵 컨테이너를 이용한 방법입니다.
두 번째는 자바의 리플렉션 기능을 이용한 것입니다.

자바가 C보다 강력하게 동적 메소드 호출을 지원하긴 하지만, 스크립트 언어의 그것을 따라잡는 것은 아직 멀었다는 생각이 듭니다. 스크립트 언어에서 대부분 지원되는 eval이라는 함수의 강력함은 써 본 사람은 결코 잊을 수 없지요.

less..

Source code : Commander.java
package com.codns.wbstory.tutor.java.Commander;

import java.util.HashMap;

public class Commander {
    private HashMap<String, AbstractCommand> dispatchTable;
       
    public Commander()
    {
        dispatchTable = new HashMap<String, AbstractCommand>();

        dispatchTable.put("Command1", new Command1());
        dispatchTable.put("Command2", new Command2());
    }
   
    public void run(String[] args)
    {
        if(args.length != 2) return;
       
        String messagetype = args[0];
       
        AbstractCommand cmd = dispatchTable.get(messagetype);
        if(cmd instanceof AbstractCommand)  // get에 실패하면 null을 반환하므로...
        {
            cmd.doCommand(args[1]);
        }
    }
   
    public static void main(String[] args)
    {
        Commander instance = new Commander();
        String[] cmd = new String[2];
       
        cmd[0] = "Command1";
        cmd[1] = "A";
        instance.run(cmd);

        cmd[0] = "Command1";
        cmd[1] = "B";
        instance.run(cmd);

        cmd[0] = "Command2";
        cmd[1] = "A";
        instance.run(cmd);

        cmd[0] = "Command2";
        cmd[1] = "B";
        instance.run(cmd);
    }
}

메인 클래스입니다. main 메소드도 여기에 정의되어 있습니다.
컨 테이너로 HashMap을 사용했지만, HashSet등 다른 것을 사용할 수도 있습니다. 해싱 비용이 아깝다고 생각된다면(명령어로 숫자가 들어온다거나) 좀 변형해서 ArrayList등을 사용할 수도 있지요. 물론 ArrayList를 사용해서 숫자로 호출한다면 보다 전통적인  switch-case 해결책이 더 나을 수도 있습니다.

Source Code : AbstractCommand.java
package com.codns.wbstory.tutor.java.Commander;

public interface AbstractCommand {
    void doCommand(String option);
}

커맨드 패턴의 전형적인 모습이지요. 커맨드 패턴을 위한 인터페이스를 정의하고 있습니다.

Source Code : Command1.java
package com.codns.wbstory.tutor.java.Commander;

public class Command1 implements AbstractCommand {
    public void A()
    {
        System.out.println("Command1 class, method A");
    }

    public void B()
    {
        System.out.println("Command1 class, method B");
    }
   
    public void doCommand(String option)
    {
        try {
            this.getClass().getMethod(option, (Class<Command1>[])null).invoke(this,(Object[])null);
        } catch (Exception e) {
            e.printStackTrace();
        }           
    }
}

Source Code : Command2.java
package com.codns.wbstory.tutor.java.Commander;

public class Command2 implements AbstractCommand {
    public void A()
    {
        System.out.println("Command2 class, method A");
    }

    public void B()
    {
        System.out.println("Command2 class, method B");
    }
   
    public void doCommand(String option)
    {
        try {
            this.getClass().getMethod(option, (Class<Command2>[])null).invoke(this,(Object[])null);
        } catch (Exception e) {
            e.printStackTrace();
        }           
    }
}

이것은 메인 클래스인 Commander가 맵을 사용하여 호출하는 클래스들입니다.
cmd변수의 0번 요소의 문자열 값이 어떤 클래스를 사용할지를 결정합니다.

cmd[0]의 값에 의해 선택된 클래스의 내부로 진입하면 아래 코드

this.getClass().getMethod(option, (Class<Command2>[])null).invoke(this,(Object[])null);

가 cmd변수의 1번 요소 문자열에 따라 invoke합니다.
전체적인 흐름은 다음과 같습니다.

  1. cmd의 0번 요소에 따라 클래스를 선택.
  2. 선택된 클래스의 doCommand 메소드에 cmd의 1번 요소를 넘겨주며 호출
  3. doCommand메소드는 받은 인자값과 같은 이름을 가진 메소드를 호출
두 방법의 장단점은 다음과 같습니다.

map을 사용한 방법은 여러 개의 클래스를 사용한 확장에 용이합니다. 나중에 클래스가 추가될 수도 있는 경우, 각각의 클래스는 AbstractCommand 인터페이스를 구현하기만 하면 되기 때문입니다.

반면, invoke 방법은 한 클래스 내에서 메소드가 추가되는 경우에 유용합니다. 위의 map을 사용한 방법은 클래스 단위에서 이루어지므로, 한 클래스의 멤버 변수를 조작하는 여러 개의 메소드를 동적 호출하려면 한계를 드러냅니다. abstract 클래스를 상속시켜서 해결하는 방법이 있기는 하지만 굉장히 지저분하지요. 물론 반대의 경우 즉 클래스를 넘나드는 동적 호출의 경우에는 invoke쪽이 굉장히 지저분해집니다.

자바 API를 좀 들여다보시면 invoke 되는 대상 메소드에 매개변수를 전달해 줄 수도 있지만, 그 정도로 복잡한 프로그램을 짜게 되었다면 디자인에 결함이 있는지를 의심해 보아야 할 것입니다. invoke에 매개변수까지 넣어 준다면, [명령어-옵션-옵션의 인자] 정도의 깊이인데, 이건 명령어 해석기라기보다는 컴파일러에 가깝지요. 이 정도의 깊이가 초기 디자인에서 발견된다면, 과감하게 lex/yacc를 사용하여 컴파일러를 제작하는 것이 현명합니다.
결정이 힘들다면, 해석하려는 명령어가 재귀를 가지고 있는지, 재귀가 없다고 하더라도 파스 트리가 3레벨 이상 깊어질 가능성이 있는지 확인해 보세요. 이 물음에 '그렇다'라고 대답할 수 있다면, 다음으로 '명령어가 정규표현식으로 분해가 가능한가' 에 질문해 보세요.
lex/yacc는 토큰을 보고 파싱을 합니다. 문자열의 위치에 따라 특별한 의미가 부여되는(바코드같이) 명령어를 해석하는 해석기를 제작한다면 lex/yacc를 사용할 수가 없습니다.
(제가 Cargo2000 프로젝트를 수행하면서 두 달이나 삽질하게 된 원인 아니 원흉(!)이었습니다. 데이터가 위치 기반 파싱을 요구하는데 두 달이나 토큰 기반의 lex가지고 씨름을 했으니 원...)

참 고로, '명령어가 재귀를 가지고 있는가' 와 '명령어가 토큰 기반이 아닌 위치기반 파싱을 요구하는가' 란 질문에 모두 '예'가 나왔다면, 이건 두말할 것 없이 데이터 포맷 디자인이 잘못된 것입니다. 하이브리드 방식(기본적으로 토큰을 쓰면서 각각의 세그먼트는 위치기반 파싱)이라면 모를까 그것도 아니라면 논리적으로 말이 안 되는 포맷이지요. 스크램블러도 아니고..

제 개인적인 생각으로는, 이런 동적 메소드 호출을 자주 사용하게 된다면, 그리고 개발 플랫폼이 자바라면, Jython이나 JRuby 같은 동적 타입 언어의 자바 구현을 사용하여 클래스 파일을 만들고 그것을 메인 프로젝트에 링크하는 것이 좋다고 생각합니다. 요즘같이 언어 풍년의 시대에 한 가지 언어를 고집할 이유는 사실 없지요. 물론 C 기반의 개발 환경(메인 모듈이 C언어인 환경)에서 이런 다양한 언어 구현을 섞기는 힘듭니다. 하지만 적어도 자바는 그 가능성을 활짝 열어놓고 있으니 시도해 보는 것도 나쁘지 않을 것입니다.

출처 : 기술자의 길, 예술가의 길

1378 view

4.0 stars