[Java] 사용자 정의 이벤트 추가하기

컴포넌트는 시스템을 구성하는 하나의 부품으로 자신을 운용하는 시스템이나.. 또는 자신과 관계를 가지는 다른 컴포넌트와 상호 통신을 위한 효율적인 방안으로 이벤트라는 개념을 가지고 있습니다. 자바(Java)에서 어떤 컴포넌트를 개발하였고.. 이 컴포넌트에 사용자 정의 이벤트를 추가하는 방법에 대한 설명입니다. 이벤트를 추가하기 위해 새롭게 정의해야 할 클래스는 총 2가지입니다.

  • 이벤트에 대한 정보를 담고 있으며 EventObject를 상속받는 클래스
  • 이벤트에 대해 실행해야할 코드를 정의할 수 있는 방법을 제공하며 EventListener를 확장하는 인터페이스

먼저 EventObject를 상속받으며.. 이벤트에 대한 정보(이벤트를 발생시킨 객체 등)를 담고 있는 클래스에 대한 예를 들어보겠습니다.

import java.util.EventObject;

public class CustomEvent extends EventObject {
    public CustomEvent(Object source) {
        super(source);
    }
}

이벤트를 발생시킨 객체에 대한 정보를 위해 생성자에서 Object 타입의 source를 인자로 받습니다. 만약 이벤트에 대한 다른 정보가 필요하다면 이 클래스에 추가하면 됩니다.

다음으로 EventListener 인터페이스를 확장하며 이벤트에 대해 실행해야할 코드를 정의할 수 있는 길을 열어주는 인터페이스에 대한 예를 들어 보겠습니다.

import java.util.EventListener;

public interface CustomEventListener extends EventListener {
    void customEvent(CustomEvent event);
}

이 인터페이스의 매서드인 customEvent에 이벤트가 발생했을때 어떤 코드를 실행할지를 정의하게 됩니다. 이렇게 사용자 정의 이벤트를 추가하기 위해 필요한 2개의 클래스를 정의했으므로 이제.. 컴포넌트에 이벤트를 코드를 살펴보겠습니다. 컴포넌트의 클래스는 CustomComponent이라고 하고 이벤트에 관련된 코드만을 살펴보면 다음과 같습니다.

public class CustomComponent {
    .... 이벤트에 관련된 코드만 봅시다!

    protected EventListenerList listenerList = new EventListenerList();
 
    public void addCustomEventListener(CustomEventListener listener) {
        listenerList.add(CustomEventListener.class, listener);
    }
 
    public void removeCustomEventListener(CustomEventListener listener) {
        listenerList.remove(CustomEventListener.class, listener);
    }
 
    private void fireCustomEvent(CustomEvent event) {
        Object[] listeners = listenerList.getListenerList();

        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i] == CustomEventListener.class) {
                ((CustomEventListener)listeners[i+1]).customEvent(event);
            }
        }
    }
 
    .... 이벤트에 관련된 코드만 봅시다!
}

자바는 하나의 이벤트에 대해서도 여러개의 이벤트 처리 코드(핸들러)를 담을 수 있습니다. 이 이벤트 핸들러를 담을 수 있는 컨테이너가 필요한데.. 그 컨테이너가 바로 4번 코드에서 EventListenerList 타입의 listenerList 변수입니다. 그리고 가장 앞서 언급한 새로운 이벤트를 추가하고 제거 하는 매서드가 6번과 10번 코드의 addCustomEventListener과 removeCustomEventListener입니다. 그리고 14번 코드인 fireCustomEvent 매서드는 이벤트가 발생해야할 적당한 시점에서 이벤트를 발생을 위해 ‘호출’ 해주는 매서드입니다. 이 매서드가 private로 선언된 이유는 이벤트가 발생하는 시점은 컴포넌트 안에서 정의하는 것이고 외부에서 그 시점을 컨트롤 해서는 않되기 때문입니다.

이상으로 자바에서 사용자 정의 이벤트(Custom Event)를 정의하는 방법에 대해 설명드렸습니다. 하지만 여기서 EventListenerList 클래스는 자바의 swing 패키지에 정의된 클래스로 swing은 안드로이드에서는 제공하지 않는 패키지입니다. 해서 안드로이드에서는 EventListenerList 클래스를 직접 개발자가 정의해 주어야 합니다. EventListenerList 클래스는 아래 다운로드를 통해 받으시기 바랍니다.

왜 안드로이드에서 EventListenerList를 제공하지 않는지에 대한 고민해 해보시기 바랍니다. 만약 제가 안드로이드에서 어떤 컴포넌트를 개발할때 새로운 이벤트를 정의해야할 필요가 있다면.. EventListenerList를 사용하지 않을 겁니다. 이에 대한 이야기는 다음 기회에 말씀드리로 하겠습니다.

[GIS] 자바 API를 이용한 ArcSDE 9.2 연동

환경은 ArcSDE 9.2이고..  ArcSDE 서비스 팩은 설치하지 않았으며 오라클은 10g 버전입니다. 그리고 ArcSDE에 대한 자바 API는 ArcSDE 9.2 서비스팩6에 해당합니다. 참고로 자바 API를 구성하는 jar 파일은 다음과 같으며 모두 반드시.. 필요합니다.

  • jsde92_sdk.jar
  • jpe92_sdk.jar
  • icu4j_3_2.jar

네.. 이제 ArcSDE와 연동하기 위한 자바 API에 대해 살펴보겠습니다.. 먼저 연결에 대한 코드입니다..

import java.util.*;
import com.esri.sde.sdk.client.*;

public class MainEntry {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        SeConnection conn = null;
        String server = "192.168.0.18";
        int service = 5151;
        String database = "";
        String user = "na_gis";
        String password = "na_gis";

        try {
            conn = new SeConnection(server, service, database, user, password);
            Vector layerList = conn.getLayers();
            System.out.println(layerList.size() + " layers existed... ");

            for(int i=0; i

위의 코드는 연결뿐만 아니라 연결된 ArcSDE 서버가 관리하고 있는 레이어 개수와 이름을 화면에 출력합니다.

다음은 레이어를 구성하는 필드 정보를 얻어 오는 코드입니다.

import java.util.*;
import com.esri.sde.sdk.client.*;

public class MainEntry {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        SeConnection conn = null;
        String server = "192.168.0.18";
        int service = 5151;
        String database = "";
        String user = "na_gis";
        String password = "na_gis";

        try {
            conn = new SeConnection(server, service, database, user, password);

            Vector tableList = conn.getTables(SeDefs.SE_SELECT_PRIVILEGE);
            System.out.println(tableList.size() + " table existed... ");
   
            for(int i=0; i

필드 정보는 레이어 개념이 아닌 테이블 개념으로 가져와야 합니다. 레이어는 테이블 + 도형 데이터 쯤으로.. 생각하면 됩니다.

다음은 테이블의 특정 필드(들)의 값을 얻어오는 코드입니다.

import java.util.*;
import com.esri.sde.sdk.client.*;

public class MainEntry {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        SeConnection conn = null;
        String server = "192.168.0.18";
        int service = 5151;
        String database = "";
        String user = "na_gis";
        String password = "na_gis";

        try {
            conn = new SeConnection(server, service, database, user, password);
            Vector tableList = conn.getTables(SeDefs.SE_SELECT_PRIVILEGE);

            SeTable tbl = (SeTable)tableList.get(89);

            SeSqlConstruct sqlConstruct = 
                new SeSqlConstruct(tbl.getQualifiedName());

            String[] cols = new String[1];
            cols[0] = "IST_YMD";

            SeQuery query = new SeQuery(conn, cols, sqlConstruct);
            query.prepareQuery();
            query.execute();

            SeRow row;
            while((row = query.fetch()) != null)
            {
                String value = row.getString(0);
                System.out.println(value);
            }
        } catch(SeException e) {
            e.printStackTrace();
        }
    }
}

얻어온 테이블 리스트(Vector tableList)에서 인덱스 번호 89에 해당하는 테이블의 IST_YMD 필드에 대한 값을 뽑아 내 화면에 표시합니다. 만약 SQL의 Where 구문을 지정하고자 한다면 SeSqlConstruct sqlConstruct 객체의 매서드 setWhere에 구문을 지정하면 됩니다. 예를 들면.. 만약 필드 ADM_NAM에 대해서 '목'으로 시작하는 조건의 경우는 다음과 같습니다.

    ....

    sqlConstruct.setWhere("ADM_NAM LIKE '목%'");

    ....

다음은 레이어에서 도형에 대한 좌표를 표시하는 코드입니다.

import java.util.*;
import com.esri.sde.sdk.client.*;

public class MainEntry {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        SeConnection conn = null;
        String server = "192.168.0.18";
        int service = 5151;
        String database = "";
        String user = "na_gis";
        String password = "na_gis";

        try {
            conn = new SeConnection(server, service, database, user, password);
            Vector layerList = conn.getLayers();

            SeLayer lyr = (SeLayer)layerList.get(4);
            SeSqlConstruct sqlConstruct =
                new SeSqlConstruct(lyr.getQualifiedName());

            String[] cols = new String[3];
            cols[0] = "BD_NM";
            cols[1] = "BD_MGT_SN";
            cols[2] = "SHAPE";

            SeQuery query = new SeQuery(conn, cols, sqlConstruct);
            query.prepareQuery();
            query.execute();

            SeRow row;
            while((row = query.fetch()) != null)
            {
                SeShape shp = row.getShape(2);

                double coordinates[][][] = shp.getAllCoords();
                System.out.println("coordinates.length = " + coordinates.length);
                System.out.println("coordinates[0].length = " + coordinates[0].length);
                System.out.println("coordinates[0][0].length = " + coordinates[0][0].length);

                double partCoords[] = coordinates[0][0];
                for(int i=0; i

얻어온 레이어 리스트(Vector layerList)에서 인덱스 4번에 해당하는 레이어에 대해서 필드 BD_NM, BD_MGT_SN 그리고 좌표에 대한 값이 담긴 SHPAE 필드를 지정해 쿼리를 실행합니다. 위의 코드는 지정된 레이어에 대한 모든 레코드를 읽어 오게 됩니다. 여기에 제한을 두어 지정된 MBR에 포함되거나 교차하는 레코드만을 얻어오는 코드는 다음과 같습니다.

import java.util.*;
import com.esri.sde.sdk.client.*; public class MainEntry {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        SeConnection conn = null;
        String server = "192.168.0.18";
        int service = 5151;
        String database = "";
        String user = "na_gis";
        String password = "na_gis";

        try {
            conn = new SeConnection(server, service, database, user, password);
            Vector layerList = conn.getLayers();

            SeLayer lyr = (SeLayer)layerList.get(4);
            SeSqlConstruct sqlConstruct = 
                new SeSqlConstruct(lyr.getQualifiedName());

            String[] cols = new String[3];
            cols[0] = "BD_NM";
            cols[1] = "BD_MGT_SN";
            cols[2] = "SHAPE";

            SeQuery query = new SeQuery(conn, cols, sqlConstruct);
            query.prepareQuery();

            // Add New Codes
            SeShape filterShape = new SeShape(lyr.getCoordRef());
            SeExtent ext = new SeExtent(148574, 146403, 148587, 146412);
            filterShape.generateRectangle(ext); 

            SeShapeFilter filter = new SeShapeFilter(lyr.getQualifiedName(), 
                lyr.getSpatialColumn(), filterShape, SeShapeFilter.METHOD_AI);

            SeShapeFilter[] filters = new SeShapeFilter[1];
            filters[0] = filter;

            query.setSpatialConstraints(SeQuery.SE_SPATIAL_FIRST, 
                false, filters);
            // At Here!

            query.execute();
   
            SeRow row;
            while((row = query.fetch()) != null)
            {
                SeShape shp = row.getShape(2);

                double coordinates[][][] = shp.getAllCoords();
                System.out.println(
                    "coordinates.length = " + coordinates.length);
                System.out.println(
                    "coordinates[0].length = " + coordinates[0].length);
                System.out.println(
                    "coordinates[0][0].length = " + coordinates[0][0].length);

                double partCoords[] = coordinates[0][0];
                for(int i=0; i

코드 번호 29~42에 해당하는 새로운 코드가 추가되었습니다.

완전하 SELECT 구문에 대한 SQL 문으로 Row을 읽어 오는 코드는 다음과 같습니다.

import java.util.*;   
import com.esri.sde.sdk.client.*; 

public class TestArcSDEQuery {
    public static void main(String[] args) {
        SeConnection conn = null;   
        String server = "222.237.78.208";   
        int service = 5151;   
        String database = "";   
        String user = "na_gis";   
        String password = "na_gis";   

        try {
            conn = new SeConnection(server, service, database, user, password); 
         
            SeQuery query = new SeQuery(conn);   
            query.prepareSql("SELECT * FROM CC_META");
            query.execute();   
  
            SeRow row;
            while((row = query.fetch()) != null)   
            {
                int cntFields = query.getNumColumns();
                SeColumnDefinition[] tableDef = row.getColumns();
             
                for(int iField=0; iField
	

[Java] @Override 어노테이션의 사용

자바 언어에서 클래스를 작성할때 그 행위를 파생 클래스에 특화시키기 위해 부모 클래스의 매서드를 오버라이드(Override)하는 경우가 있습니다. 예를 들어서.. 안드로이드라는 모바일 개발 플랫폼에서 모바일 애뮬레이터의 Menu 키를 누르게 되면 Activity라는 클래스의 onCreateOptionsMenu 매서드가 호출됩니다. 즉, Activity 클래스를 상속받아 자신만의 메뉴를 만들기 위해 상속받은 클래스는 onCreateOptionsMenu 매서드를 오버라이드해야 한다는 것입니다. 아래처럼요..

public class MenuTest extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    public boolean onCreateOptionMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

         menu.add(0, 1, 0, "Menu1");
         menu.add(0, 2, 0, "Menu2");

         return true;
     }
}

자.. 이제 실행하고 Menu 키를 누르면 메뉴가 짠~하고 나타나길 기대합니다! 하지만 나타나지 않습니다.. 이유는 MenuTest라는 클래스에 Override했다고 생각하는 onCreateOptionsMenu가 Override되지 않았기 때문입니다. 분명히 위의 코드에서 Override했다고요? 눈을 크게 뜨고 보시기 바랍니다.. 네.. 철자가 틀렸습니다! 우리가 Override 했다고 생각한 것은.. 사실 새로운 onCreateOptionMenu 매서스를 추가한 것입니다.. 즉 철자가 틀렸습니다.. onCreateOptionMenu가 아니고 onCreateOptionsMenu입니다..

의도는 분명했으나 개발자도 사람인지라 철자에 대한 실수를 했습니다.. 바로 이러한 실수를 방지할 목적으로 자바에서는 @Override 어노테이션을 지원하게 되었습니다.. 이제 다시 위의 코드에 이 @Override 어노테이션을 사용해 보면 아래와 같습니다..

public class MenuTest extends Activity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public boolean onCreateOptionMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

         menu.add(0, 1, 0, "Menu1");
         menu.add(0, 2, 0, "Menu2");

         return true;
     }
}

컴파일 해보면 에러가 발생합니다.. 에러 내용은 onCreateOptionMenu는 Override될만한 매서드가 아니라는 내용을 개발자에게 전달합니다. 여기서 개발자는 자신의 실수를 인지하고 해결책을 마련합니다. 즉, 틀린 철자를 고쳐서 말입니다.

자바라는 언어가 앞서 예를 든 개발자의 실수를 방지할 수 없으므로.. 비록 차선책이라고 생각되기는 하지만… 바로 @Override라는 어노테이션을 제공하게 되었습니다. 이 어노테이션을 제대로 활용하기 위해서는.. 일괄적으로 개발자 스스로가 Override 되는 모든 매서드에 대해서 바로 이 @Override를 붙여 놓는 것입니다. 이것은 분명히 명확하고 옳바른 습관입니다.

[Java] 파생 클래스가 아니라.. 딱 내 클래스인가?

뭐.. 제목이 저래.. 라고 생각하시는 분도 분명 계실테고.. 저 역시 뭐 제목이 이래.. 라고 생각하고 있습니다.. 먼저 아래와 같은 클래스 계층이 있다고 칩시다..

사용자 삽입 이미지
Geometry를 부모로 하는 세개의 자식 클래스입니다.. 예를 들어 아래처럼 코딩을 했습니다..

Geometry g = new Rectangle();

이제 이제 g 객체가 Circle이냐.. Rectangle이냐.. 아니면 Triangle이냐.. 아니면.. Geometry이냐를 어떻게 알 수 있을까요? 뭐 이런거야.. 자바에는 편리한 instanceof가 있으니 아래처럼 하면되지라며.. 코딩합니다..

Geometry g = new Rectangle();

if(g instanceof Rectangle) {
    System.out.println("Type is Rectangle");
}

if(g instanceof Circle) {
    System.out.println("Type is Circle");
}

if(g instanceof Triangle) {
    System.out.println("Type is Triangle");
}

if(g instanceof Geometry) {
    System.out.println("Type is Geometry");
}

자바를 잘아신다면.. 어.. 이거 아닌데.. 라고 바로 하실테지만.. 저는 단지.. 그냥 냄새가 좀 나는데.. 는 정도였답니다.. 여튼, 실행해보면 2개의 조건문에서 true를 만나게 됩니다.. 3번 코드의 if 문과 15번 코드의 if 문.. 즉 g는 Rectangle이면서 동시에 Geometry 타입입니다.. 당연한 결과이지요.. 하지만 원하는 것은 딱… “나는 나다!”라는 것이죠.. 즉, 위의 경우에는 g는 Geometry가 아니라 Rectangle다! 라는 것을 판별해야 한다는 것입니다..

자바는 C/C++ 언어가 가지지 못한 강력한 기능이 있습니다.. 실행시간에서의 타입 정보(RTTI)인데요.. 이 RTTI를 위해 자바는 모든 객체에 대해서 자신의 타입에 대한 정보를 담고 있습니다.. 이를 이용해 우리가 원하는 바를 얻을 수 있는 코드는 아래와 같습니다..

Geometry g = new Rectangle();

if(g.getClass() == Rectangle.class) {
    System.out.println("Type is Rectangle");
}

if(g.getClass() == Circle.class) {
    System.out.println("Type is Circle");
}

if(g.getClass() == Triangle.class) {
    System.out.println("Type is Triangle");
}

if(g.getClass() == Geometry.class) {
    System.out.println("Type is Geometry");
}

이제 정확히.. 오직 3번 코드에 대한 if문에서만 true인, “나는 나다!”라는 결과를 얻을 수 있게 됩니다..

[Java] 정적 초기화 블럭(static initialization block)

클래스에서 정적 변수나 매서드는 클래스 타입에 대한 인스턴스화 없이도 호출하거나 참조할 수 있는 요긴한 녀석들인데요.. 이 정적 변수를 사용하기 전에 미리 초기화해 놓을 필요가 있는 경우가… 대부분입니다. 뭐.. 간단히 클래스 안에서 정적 변수를 선언할때 값을 지정해 버리면 될 일이지만… 즉, 아래처럼요..

class IAMABOY {
    static int a = 100;
}

뭐.. 간단하죠? 하지만 여기에 더해서 추가적으로 자바에서는 정적 초기화 블럭이라는 방법을 제공합니다.. 가끔 사용하는 방법인지라.. 잊고.. 필요해서 사용할라치면 기억에서 가물가물.. 가물치가 되는지라.. 정리를 한번 해보렵니다.. 뭐.. 예시 코드 한방 날리면..

class IAMABOY {
    static int a = 100;

    static {
        for(int i=0; i<100; i++) {
            a = i;
        }
    }
}

즉.. 정적 초기화 블럭은 클래스의 인스턴스화에 상관없이 딱 한번 호출되는 절차적인 코드들로 이루어진 블럭입니다.. 요놈.. 요놈.. 가만히 보니.. 단위 테스트 기능 구현에 적용하면 딱... 이겠구나.. 싶어집니다..