헤르메스 LIFE

[DeveloperWorks] 평범한 Java 도구에 대해 모르고 있던 5가지 사항 본문

Core Java

[DeveloperWorks] 평범한 Java 도구에 대해 모르고 있던 5가지 사항

헤르메스의날개 2010. 12. 30. 09:20
728x90

원문 : http://www.ibm.com/developerworks/kr/library/j-5things12/index.html

구문 분석, 시간 제어 및 소리와 같은 평범한 기능을 처리하는 Java 도구

요약: 몇몇 Java™ 도구는 범주가 정해져 있지 않으며 이러한 도구는 주로 "작업 상황"을 기준으로 수집합니다. 이번에 소개할 대상은 5가지 도구로 구성된 콜렉션으로 사용할 필요성이 없어지는 경우에도 계속해서 사용하고 싶은 도구로 구성되어 있습니다.


수년전 고등학교에 다니고 있었을 당시, 필자는 소설 작가가 되고자 하는 마음에 Writer's Digest라고 하는 잡지를 구독했었다. 한 가지 기억나는 것은 "보관하기에는 너무 작은 도구"에 관한 컬럼이었는데, 이 컬럼에는 분류하기 어려운 잡다한 물건으로 가득 찬 주방의 잡동사니 서랍을 컬럼니스트가 어떻게 묘사해야 하는지 그 방법이 기술되어 있었다. 이 말은 언제나 필자의 뇌리에 있었으며 적어도 한동안은 이 시리즈의 최종 컬럼이 될 이 기사의 주제를 설명하는 데 적절한 것처럼 보였다.

Java 플랫폼은 이러한 "작지만 유용한 도구" 즉, 대부분의 Java 개발자는 결코 알지도 못하고 사용하지도 않는 유용한 명령행 도구와 라이브러리로 가득 차있다. 이러한 도구와 라이브러리는 대부분 필자가 이제까지 5가지 사항 시리즈에서 다루었던 프로그래밍 카테고리에 잘 맞지 않지만 일단은 사용해 보기로 하자. 그렇지만 일부 도구는 여전히 사용되지 못하고 어딘가에 보관되어 있을 수밖에 없다.

1. StAX

대부분의 Java 개발자에게 XML이 처음 소개된 밀레니엄 전환기에는 XML 파일을 구문 분석하는 방식이 기본적으로 두 가지가 있었다. 본질적으로 SAX 구문 분석기는 일련의 콜백 메소드를 통해 개발자에게 대응하는 이벤트로 구성된 거대한 상태 시스템이다. DOM 구문 분석기는 전체 XML 문서를 메모리로 가져와서 일련의 개별 오브젝트로 분할하며 이러한 오브젝트는 서로 링크되어 트리를 형성한다. 이 트리는 해당 문서의 전체 XML 정보 세트를 나타낸다. 이러한 두 가지 구문 분석기에는 각기 단점이 있다. SAX는 사용하기에는 레벨이 너무 낮으며 DOM은 비용이 너무 많이 들어간다. 특히, 대용량 XML 파일의 경우에 그러하며 이 경우에는 전체 트리로 인해 매우 많은 힙 호그(heap hog)가 발생한다.

다행히도 Java 개발자는 XML 파일을 구문 분석하는 세 번째 방법을 찾아냈으며 이 방법에서는 문서 스트림에서 한 번에 하나씩 문서를 가져와서 시험해 보고 처리하거나 버릴 수 있는 있는 일련의 "노드"로 문서를 모델링한다. 이러한 "노드"로 구성된 "스트림"은 SAX와 DOM을 절충한 것이며 StAX(Streaming API for XML)라고 한다. (동일한 이름으로 통하는 원래의 SAX 구문 분석기와 새로운 API를 구분하기 위해 약어를 사용했다.) StAX 구문 분석기는 나중에 JDK에 합쳐졌으며 javax.xml.stream 패키지에 존재한다.

StAX를 사용하는 방법은 매우 간단하다. XMLEventReader를 인스턴스화하고 이 인스턴스로 잘 구성된 XML 파일을 가리킨 다음, while 루프를 사용하여 한 번에 하나씩 노드를 "가져"와서 시험한다. 다시 말해서 Listing 1에 있는 Ant 빌드 스크립트를 사용하면 모든 대상을 표시할 수 있다.


Listing 1. StAX를 사용하여 대상 가리키기
import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.*;
import javax.xml.stream.util.*;

public class Targets
{
    public static void main(String[] args)
        throws Exception
    {
        for (String arg : args)
        {
            XMLEventReader xsr = 
                XMLInputFactory.newInstance()
                    .createXMLEventReader(new FileReader(arg));
            while (xsr.hasNext())
            {
                XMLEvent evt = xsr.nextEvent();
                switch (evt.getEventType())
                {
                    case XMLEvent.START_ELEMENT:
                    {
                        StartElement se = evt.asStartElement();
                        if (se.getName().getLocalPart().equals("target"))
                        {
                            Attribute targetName = 
                                se.getAttributeByName(new QName("name"));
                            // Found a target!
                            System.out.println(targetName.getValue());
                        }
                        break;
                    }
                    // Ignore everything else
                }
            }
        }
    }
}

StAX 구문 분석기를 통해 현존하는 모든 SAX 및 DOM 코드를 바꿀 수는 없다. 그러나 이 구문 분석기를 이용하면 특정 태스크를 더욱 쉽게 처리할 수 있다는 점은 분명하다. 이 도구는 XML 문서의 전체 트리 구조를 필요로 하지 않는 태스크에 특히 유용하다.

또한, 작업을 하기에는 이벤트 오브젝트의 레벨이 여전히 너무 높지만 StAX의 XMLStreamReader에는 더욱 낮은 레벨의 API가 있다. 그리고 XMLStreamReader만큼 유용하지는 않겠지만 StAX에는 XML 출력을 처리하는 XMLStreamWriterXMLEventWriter 클래스가 있다.


2. ServiceLoader

Java 개발자들은 컴포넌트를 사용하는 데 필요한 정보와 컴포넌트를 작성하는 데 필요한 정보를 서로 분리하기를 원한다. 이렇게 하려면 일반적으로 컴포넌트가 수행할 수 있는 조치를 나타내는 인터페이스를 작성한 다음, 일종의 매개자를 사용하여 컴포넌트 인스턴스를 작성해야 한다. 대부분의 개발자들은 Spring 프레임워크를 사용하여 이러한 작업을 수행하지만 Spring 컨테이너보다 훨씬 더 경량인 방법이 있다.

java.utilServiceLoader 클래스는 JAR 파일에 포함된 구성 파일을 읽어서 인터페이스 구현을 찾은 다음, 이 구현을 선택한 오브젝트의 목록으로 사용한다. 다시 말해서 해당 태스크를 수행할 개인용 서번트 컴포넌트가 필요한 경우에는 Listing 2에 있는 코드를 사용하여 컴포넌트를 얻을 수 있다.


Listing 2. IPersonalServant
public interface IPersonalServant
{
    // Process a file of commands to the servant
    public void process(java.io.File f)
        throws java.io.IOException;
    public boolean can(String command);
}

can() 메소드를 이용하면 제공된 개인용 서번트 구현이 해당 요구사항을 충족시키는지 판단할 수 있다. Listing 3에 있는 ServiceLoader는 본질적으로 이러한 요구사항에 맞는 IPersonalServant로 구성된 일종의 목록이다.


Listing 3. IPersonalServant가 유용할까?
import java.io.*;
import java.util.*;

public class Servant
{
    public static void main(String[] args)
        throws IOException
    {
        ServiceLoader<IPersonalServant> servantLoader = 
            ServiceLoader.load(IPersonalServant.class);

        IPersonalServant i = null;
        for (IPersonalServant ii : servantLoader)
            if (ii.can("fetch tea"))
                i = ii;

        if (i == null)
            throw new IllegalArgumentException("No suitable servant found");
        
        for (String arg : args)
        {
            i.process(new File(arg));
        }
    }
}

이러한 인터페이스를 구현하면 Listing 4와 같이 된다.


Listing 4. Jeeves를 이용한 IPersonalServant 구현
import java.io.*;

public class Jeeves
    implements IPersonalServant
{
    public void process(File f)
    {
        System.out.println("Very good, sir.");
    }
    public boolean can(String cmd)
    {
        if (cmd.equals("fetch tea"))
            return true;
        else
            return false;
    }
}

이제는 ServiceLoader에서 인식할 수 있게 이러한 구현이 포함된 JAR 파일을 구성하기만 하면 된다. 그러나 이러한 작업은 까다롭다. JDK에서는 JAR 파일에 완전한 인터페이스 클래스와 이름이 같은 텍스트 파일이(이 경우에는 META-INF/services/IPersonalServant) 포함된 META-INF/services 디렉토리가 있어야 한다. 이 인터페이스 클래스 이름의 내용은 Listing 5와 같으며 한 줄에 하나씩 구현 이름이 표시된다.


Listing 5. META-INF/services/IPersonalServant
Jeeves   # comments are OK

다행히도 Ant 빌드 시스템(1.7.0 이후 버전)에는 이러한 작업을 비교적 쉽게 할 수 있게 도움을 주는 Listing 6과 같은 서비스가 jar 태스크에 연결되어 있다.


Listing 6. Ant로 빌드한 IPersonalServant
   <target name="serviceloader" depends="build">
        <jar destfile="misc.jar" basedir="./classes">
            <service type="IPersonalServant">
                <provider classname="Jeeves" />
            </service>
        </jar>
    </target>

IPersonalServant를 호출하여 명령을 실행하도록 요청하는 작업은 간단하다. 그러나 이러한 명령을 구문 분석하여 실행하는 작업은 까다롭다. 다음으로 유용한 도구는 Scanner이다.


3. Scanner

구문 분석기를 빌드하는 데 도움이 되는 Java 유틸리티는 다양하다. 그리고 현존하는 함수형 언어로 구문 분석기 함수 라이브러리(구문 분석기 컴비네이터)를 성공적으로 빌드한 사례가 다소 있다. 그러나 쉼표로 분리된 값으로 된 파일이나 공백으로 구분된 텍스트 파일을 구문 분석해야 하는 경우에는 어떻게 해야 할까? 대부분의 유틸리티는 이러한 목적으로 사용하기에는 기능이 너무 과하고 String.split() 함수는 여전히 기능이 부족하다. (정규식에 대해서라면 "문제점이 있어서 이 문제점을 해결하기 위해 정규식을 사용했더니 문제점이 두 가지로 늘어났다"라는 옛말을 기억하도록 하자.)

이러한 경우에는 Java 플랫폼의 Scanner 클래스가 최상의 선택이 될 수 있다. Scanner는 경량 텍스트 구문 분석기로 사용되며 구조화된 개별 텍스트를 강력하게 유형이 지정된 부분으로 가져오는 데 필요한 비교적 간단한 API를 제공한다. Listing 7과 같은 텍스트 파일에서 DSL 형태의 일련의 명령(Terry Pratchett의 소설 Discworld에서 따온)을 배열하는 경우를 생각해보자.


Listing 7. Igor용 태스크
fetch 1 head
fetch 3 eye
fetch 1 foot
attach foot to head
attach eye to head
admire

Listing 8에 표시된 바와 같이 Igor라고 하는 개인용 서번트에서는 Scanner를 사용하여 이러한 일련의 명령을 쉽게 구문 분석할 수 있다.


Listing 8. Scanner로 구문 분석한 Igor용 태스크
 import java.io.*;
import java.util.*;

public class Igor
    implements IPersonalServant
{
    public boolean can(String cmd)
    {
        if (cmd.equals("fetch body parts"))
            return true;
        if (cmd.equals("attach body parts"))
            return true;
        else
            return false;
    }
    public void process(File commandFile)
        throws FileNotFoundException
    {
        Scanner scanner = new Scanner(commandFile);
        // Commands come in a verb/number/noun or verb form
        while (scanner.hasNext())
        {
            String verb = scanner.next();
            if (verb.equals("fetch"))
            {
                int num = scanner.nextInt();
                String type = scanner.next();
                fetch (num, type);
            }
            else if (verb.equals("attach"))
            {
                String item = scanner.next();
                String to = scanner.next();
                String target = scanner.next();
                attach(item, target);
            }
            else if (verb.equals("admire"))
            {
                admire();
            }
            else
            {
                System.out.println("I don't know how to " 
                    + verb + ", marthter.");
            }
        }
    }
    
    public void fetch(int number, String type)
    {
        if (parts.get(type) == null)
        {
            System.out.println("Fetching " + number + " " 
                + type + (number > 1 ? "s" : "") + ", marthter!");
            parts.put(type, number);
        }
        else
        {
            System.out.println("Fetching " + number + " more " 
                + type + (number > 1 ? "s" : "") + ", marthter!");
            Integer currentTotal = parts.get(type);
            parts.put(type, currentTotal + number);
        }
        System.out.println("We now have " + parts.toString());
    }
    
    public void attach(String item, String target)
    {
        System.out.println("Attaching the " + item + " to the " +
            target + ", marthter!");
    }
    
    public void admire()
    {
        System.out.println("It'th quite the creathion, marthter");
    }
    
    private Map<String, Integer> parts = new HashMap<String, Integer>();
}

ServantLoaderIgor를 등록했다고 가정하면 Listing 9과 같이 앞서 살펴본 Servant 코드를 재사용하도록 하거나 더욱 적합하도록 can() 호출을 변경하는 작업은 간단하다.


Listing 9. Igor용 작업
import java.io.*;
import java.util.*;

public class Servant
{
    public static void main(String[] args)
        throws IOException
    {
        ServiceLoader<IPersonalServant> servantLoader = 
            ServiceLoader.load(IPersonalServant.class);

        IPersonalServant i = null;
        for (IPersonalServant ii : servantLoader)
            if (ii.can("fetch body parts"))
                i = ii;

        if (i == null)
            throw new IllegalArgumentException("No suitable servant found");
        
        for (String arg : args)
        {
            i.process(new File(arg));
        }
    }
}

분명히 실제 DSL 구현은 단지 표준 출력 스트림에 인쇄하는 기능 이상의 역할을 한다. 어떤 부분이 어떤 파트에 첨부되는지 추적하고 Igor를 신뢰할 수 있게 하는 작업은 독자의 몫으로 남겨둔다.


4. Timer

java.util.TimerTimerTask 클래스는 일회성이나 주기적 지연을 기반으로 태스크를 실행할 수 있는 편리하고 비교적 간단한 방법을 제공한다.


Listing 10. 나중에 실행하기
import java.util.*;

public class Later
{
    public static void main(String[] args)
    {
        Timer t = new Timer("TimerThread");
        t.schedule(new TimerTask() {
            public void run() {
                System.out.println("This is later");
                System.exit(0);
            }
        }, 1 * 1000);
        System.out.println("Exiting main()");
    }
}

Timerschedule() 오버로드가 많다. 그리고 특정 태스크가 한 번 수행되는지 아니면 반복해서 수행되는지 표시하면서 TimerTask 인스턴스가 실행되도록 한다. 본질적으로 TimerTask(사실상 Timer가 TimerTask를 구현함)는 실행 가능하지만, TimerTask는 두 개의 추가 메소드 즉, 작업을 강제 종료하는 cancel() 메소드와 작업을 실행할 때 근사값을 리턴하는 scheduledExecutionTime() 메소드와 함께 제공된다.

그러나 Timer는 비디먼 스레드를 작성하여 백그라운드에서 태스크를 실행한다. 따라서 Listing 10에서 필자는 System.exit() 메소드를 호출하여 VM을 강제로 종료했다. 아마도 장기간 실행하는 프로그램에서는 부울 매개변수를 취하는 생성자를 사용하여 디먼의 상태를 나타내는 디먼 스레드로 Timer를 가장 많이 사용하므로 이러한 프로그램에서는 VM의 상태를 계속해서 유지할 필요가 없다.

이 클래스에는 그다지 특이한 사항이 없지만 백그라운드 태스크를 실행하는 의도를 이 클래스에서 더욱 분명하게 확인할 수 있다. 또한, 이 클래스는 몇 줄의 스레드 코드를 저장할 수 있으며 전체 java.util.concurrent 패키지로 들어가기에는 준비가 덜 된 개발자를 위해 경량 ScheduledExecutorService로 역할을 한다.


5. JavaSound

서버측 애플리케이션에서는 이 클래스를 자주 사용하지 않지만 소리는 관리자에게 유용한 "수동적인" 감각으로 역할을 할 수 있으며 잘못된 동작을 표시할 수 있는 좋은 도구가 된다. 이 클래스는 나중에야 Java 플랫폼에 도입되었지만 결국에는 JavaSound API 덕택에 이 클래스가 Java의 코어 런타임 라이브러리가 되었으며 javax.sound * 패키지(MIDI 파일용 패키지 하나와 유비쿼터스 .WAV 파일 형식과 같은 샘플링된 오디오 파일용 패키지 하나)에 포함되었다.

JavaSound의 "hello world"는 Listing 11에 있는 것과 같은 클립을 재생하는 것이다.


Listing 11. 카사블랑카여, 다시 한 번(Play it again, Sam)
public static void playClip(String audioFile)
{
    try
    {
        AudioInputStream audioInputStream =
            AudioSystem.getAudioInputStream(
                this.getClass().getResourceAsStream(audioFile));
        DataLine.Info info = 
            new DataLine.Info( Clip.class, audioInputStream.getFormat() );
        Clip clip = (Clip) AudioSystem.getLine(info);
        clip.addLineListener(new LineListener() {
                public void update(LineEvent e) {
                    if (e.getType() == LineEvent.Type.STOP) {
                        synchronized(clip) {
                            clip.notify();
                        }
                    }
                }
            });
        clip.open(audioInputStream);        
        
        clip.setFramePosition(0);

        clip.start();
        synchronized (clip) {
            clip.wait();
        }
        clip.drain();
        clip.close();
    }
    catch (Exception ex)
    {
        ex.printStackTrace();
    }
}

이 코드는 대부분 매우 간단하다. 다시 말하면 적어도 JavaSound API를 사용할 때 만큼이나 간단하다. 첫 번째 단계에서는 재생할 파일과 관련된 AudioInputStream을 작성한다. 가능한 이 메소드를 컨텍스트에 무관하도록 유지하기 위해 클래스를 로드한 ClassLoaderInputStream으로서 파일을 캡처한다. (소리 파일에 대한 정확한 경로가 지정된 시간보다 빨리 알려지면 AudioSystem파일이나 문자열을 취하기도 한다.) 이 작업이 완료되면 DataLine.Info 오브젝트를 AudioSystem에 제공하여 Clip을 얻는다. 이렇게 하는 것이 오디오 클립을 재생할 수 있는 가장 쉬운 방법이다. (다른 방법에서는 오디오 클립을 제어할 수 있는 더욱 많은 기능(예: SourceDataLine 얻기)을 제공하지만 이러한 기능은 오디오 클립을 단순히 "재생"하는 데는 그다지 필요하지 않다.)

여기서 부터는 AudioInputStream과 관련된 open() 메소드를 호출하는 것만큼이나 간단하다. (이러한 경우는 다음 섹션에서 살펴볼 버그가 발생하지 않는 경우에 해당한다.) 재생을 시작하려면 start() 메소드를 호출하고 재생이 끝날 때까지 대기하려면 drain() 메소드를 호출하며 오디오를 종료하려면 close() 메소드를 호출한다. 재생은 별도의 스레드로 처리된다. 따라서 stop() 메소드를 호출하면 재생이 중지되고 start() 메소드를 연속해서 호출하면 일시정지된 위치에서 다시 재생된다. 시작 지점을 다시 설정하려면 setFramePosition(0)을 사용한다.

소리가 나지 않는 경우

JDK 5 릴리스에는 약간 버그가 있다. 일부 플랫폼에서 짧은 오디오 클립을 재생하면 코드는 제대로 실행되는 것처럼 보이지만 소리가 나지 않는다. 미디어 플레이어는 분명히 이러한 짧은 오디오 클립을 대상으로 실행해야 하는 것보다 빠르게 STOP 이벤트를 실행한다. (버그 페이지의 링크는 참고자료 섹션을 참조한다.)

이 버그는 "수정하지 않는" 것으로 결정되었지만 이 버그가 발생하지 않도록 하는 방법은 매우 간단하다. LineListener를 등록하여 STOP 이벤트를 청취하도록 한 후, 이벤트가 실행되면 오디오 클립 오브젝트를 대상으로 notifyAll() 메소드를 호출한다. 그런 다음, "호출자" 코드에서 wait() 메소드를 호출하여 오디오 클립이 끝나서 notifyAll() 메소드가 호출될 때까지 대기한다. 버그가 없는 플랫폼에서는 이러한 단계를 수행하지 않아도 되지만 Windows®와 일부 Linux® 배포판에서는 이러한 단계를 수행했을 때와 그렇지 않았을 때의 차이점이 존재한다.


결론

이제까지 5가지 도구를 살펴보았다. 대부분의 개발자들이 여기서 살펴본 도구를 잘 알고 있다고 생각하지만 이러한 전문적인 기사를 통해 이러한 도구를 소개함으로써 많은 개발자들이 이러한 도구의 장점을 활용할 수 있을 것으로 기대하며 또한 사용하지 않고 잊혀진 도구들을 다시 기억해 본다는 점에서도 가치가 있다고 생각한다.

필자는 다른 기고자들이 그들의 전문 분야에 협력할 수 있는 기회를 제공하기 위해 이 시리즈를 잠깐 중단할 계획이다. 그러나 이 시리즈를 통해서나 혹은 다른 중요 분야를 탐구하는 새로운 5가지 사항 기사를 통해 다시 돌아올 계획이니 기대하기 바란다. 그때까지 계속해서 Java 플랫폼을 탐구하여 프로그램의 생산성을 높여줄 숨겨진 보물을 발견하기 바란다.


참고자료

교육

  • 모르고 있던 5가지 사항: 평범한 Java 기술을 유용한 프로그래밍 팁으로 바꿔주는 이 시리즈를 통해 그동안 몰랐던 Java 플랫폼의 많은 기능을 확인하자.

  • "Scheduling recurring tasks in Java applications"(Tom White, developerWorks, 2003년 11월): 더욱 유연한 스케줄링이 가능하도록 TimerTimerTask가 일반화한 스케줄링 프레임워크를 확인하자.

  • "Simple Dependency Injection with ServiceLoader in JDK 6"(Tim Boudreau, Java.net, 2008년 8월): JDK 6에 맞게 빌드되고 유형 변환 오류에 민감하지 않은 간단한 IOC 프레임워크로서 ServiceLoader를 사용하는 방법과 관련된 자세한 정보가 있다.

  • javax.sound의 "소리가 않나는" 문제는 Sun/Oracle 버그 데이터베이스의 버그 목록에 올라있다 .

  • developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.

제품 및 기술 얻기

  • StAX(Streaming API for XML)는 애플리케이션을 오고 가는 XML 데이터를 스트림할 수 있게 도움을 주는 표준 XML 처리 API이다.

토론

  • My developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.

필자소개

Ted Neward는 글로벌 컨설팅 업체인 ThoughtWorks의 컨설턴트이자 Neward & Associates의 회장으로 Java, .NET, XML 서비스 및 기타 플랫폼에 대한 컨설팅, 조언, 교육 및 강연을 한다. 워싱턴 주의 시애틀 근교에 살고 있다.

728x90