JAVA SE의 정규표현식

개발 이야기/Java | 2007. 5. 30. 10:27
Posted by 시반

정규 expression 또는 regex 지원은 버전 1.4 이후 자바 플랫폼의 일부가 되어 왔다. java.util.regex 패키지에서 발견되는 regex 클래스는 펄 언어가 제공하는 것과 유사한 패턴 매칭을 지원하지만 자바 언어 구문 및 클래스를 사용한다.

 

패키지 전체는 Pattern, MatcherPatternSyntaxException의 3가지 클래스로 제한된다. 버전 1.5에서는 MatchResult 인터페이스가 소개되었다.

 

두 클래스 PatternMatcher를 함께 사용한다.

Pattern 클래스를 사용하여 정규 표현식을 정의한 다음, Matcher 클래스를 사용하여 입력 소스에 대해 패턴을 검사한다. 표현식에서 패턴에 구문 오류가 있으면 예외가 발생한다.

 

두 클래스 모두 구성자를 가지지 않는다. 대신, 정규 표현식을 컴파일하여 패턴을 얻은 다음 반환된 Pattern에게 일부 입력 소스를 기반으로 해당 Matcher를 요청한다.

 

Pattern pattern = Pattern.compile( <regular expression> );
Matcher matcher = pattern.matcher( <input source> );

 

Matcher를 얻었으면 일반적으로 입력 소스를 처리하여 포함된 모든 매칭을 찾는다.

find() 메소드를 사용하여 입력 소스에서 패턴의 매칭을 찾는다. find()에 대한 각 호출은 마지막 호출 위치에서 계속되거나 첫 번째 호출 위치 0에서 계속된다. 그런 다음 매칭되는 항목이 group() 메소드에 의해 반환된다.

 

while (matcher.find()) {
   System.out.printf"Found: \"%s\" from %d to %d.%n",
       matcher.group(), matcher.start(), matcher.end());
}

다음 코드는 기본적인 정규 표현식 프로그램을 보여 주며 사용자가 정규 표현식과 비교 대상 문자열을 입력하도록 메시지를 표시한다.

 

import java.util.regex.*;

public class Regex {

   public static void main(String args[]) {
      Console console = System.console();

      // Get regular expression
      String regex = console.readLine("%nEnter expression: ");
      Pattern pattern = Pattern.compile(regex);

      // Get source
      String source = console.readLine("Enter input source: ");
      Matcher matcher = pattern.matcher(source);

      // Show matches
      while (matcher.find()) {
         System.out.printf("Found: \"%s\" from %d to %d.%n",
             matcher.group(), matcher.start(), matcher.end());
      }
   }
}

그러면 정규 표현식의 모양은 정확하게 어떠한가?

Pattern 클래스는 보다 세부적인 사항을 제공하지만 기본적으로 정규 표현식은 다른 문자 시퀀스와 일치시킬 문자 시퀀스이다.

예를 들어, "Hello, World" 문자열에서 두 개의 L자("ll") 문자열 리터럴 패턴을 찾을 수 있다. 앞의 프로그램은 시작 위치 2와 끝 위치 4에서 "ll" 패턴을 찾을 것이다. 끝 위치는 일치된 문자열 패턴의 끝 이후에 다음 문자의 위치이다.

"ll" 같은 패턴 문자열은 입력 소스에서 문자적으로 위치하는 지점만을 보고하므로 그리 흥미롭지 않다.

정규 표현식 패턴은 특수 메타 문자를 포함할 수 있다. 메타 문자는 정규 표현식에서 강력한 매칭 기능을 제공한다. 정규 표현식에서는 "([{\^-$|]})?*+."의 15문자를 메타 문자로 사용할 수 있다.일부 메타 문자는 문자 그룹을 나타낸다.

예를 들어, 대괄호([ 및 ])를 사용하면 대괄호 안의 문자 중 하나가 텍스트에서 발견되는 경우 매칭이 성공하는 일련의 문자를 지정할 수 있다. 예를 들어, "co[cl]a" 패턴은 coca 및 cola라는 단어와 매칭된다. []는 단일 문자를 매칭하는 데만 사용되므로 cocla는 매칭되지 않는다.

몇 가지 매칭을 해 보고 문제가 없으면 곧 수량자에 대해 자세히 살펴보자.

개별 문자의 매칭 이외에 대괄호 문자([ 및 ])를 사용하여 [j-z]로 지정된 j-z의 문자처럼 일정 범위의 문자를 매칭할 수 있다. 이러한 문자 범위는 "foo[j-z]"처럼 문자열 리터럴과 결합할 수도 있다. 여기서 fool을 찾으면 매칭이 성공하고 food를 찾으면 매칭이 실패한다. ljz 사이의 범위 안에 있지만 d는 그렇지 않기 때문이다.

 ^ 문자를 사용하여 문자열 리터럴 또는 문자 범위의 제외를 나타낼 수도 있다.

"foo[^j-z]" 패턴은 foo로 시작하고 j에서 z 사이의 문자로 끝나지 않는 단어를 찾는다.

따라서 이번에는 food라는 문자열이 매칭에 성공한다.

[a-zA-Z]처럼 여러 범위를 결합하여 a에서 z 사이의 소문자와 대문자를 나타낼 수도 있다.

정규 표현식을 처음 학습할 때는 문자열 리터럴이 유용하지만 정규 표현식에서 대부분의 사람들이 사용하는 보다 일반적인 요소는 미리 정의된 문자 클래스이다.

여기서 메타 문자 .\가 사용된다. 마침표(.)는 임의 문자를 나타내는 데 사용된다.

따라서 정규 표현식 ".oney"는 money 및 honey와 매칭되며 oney로 끝나는 5자의 어느 단어와도 매칭된다.

반면에 \는 다른 문자와 함께 사용되어 전체 문자 집합을 나타낸다.

예를 들어, 숫자 집합을 나타내기 위해 [0-9]를 사용할 수 있지만 \d를 사용할 수도 있다.

숫자가 아닌 문자 집합을 나타내기 위해 [^0-9]를 사용할 수도 있다.

또는 \D의 미리 정의된 문자 클래스 문자열을 사용할 수 있다.

이러한 모든 문자 클래스 문자열은 모두 기억하기가 어려우므로 패턴 클래스에 대한 자바 플랫폼 문서에 정의되어 있다.

 

다음은 미리 정의된 특수 문자 클래스의 하위 집합이다.

* \s -- whitespace
* \S -- non-whitespace
* \w -- word character [a-zA-Z0-9]
* \W -- non-word character
* \p{Punct} -- punctuation
* \p{Lower} -- lowercase [a-z]
* \p{Upper} -- uppercase [A-Z]

 

미리 정의된 문자열과 관련하여 지적해야 할 사항은 즉각 눈에 띄지 않는다.

위의 Regex 프로그램에 이러한 문자열 중 하나를 사용하려면 표시된 대로 입력한다.

\s는 공백과 매칭된다. 하지만 자바 소스 파일에서 정규 표현식을 하드 코딩하려면 \ 문자가 특별하게 취급된다는 것을 기억해야 한다.

소스에서 이 문자열을 다음과 같이 이스케이프해야 한다.

String regexString = "\\s";

여기서 \\는 문자열에서 하나의 백슬래시를 나타낸다.

 

다른 문자열 리터럴을 나타내기 위한 기타 특수 문자열은 다음과 같다.

* \t -- tab
* \n -- newline
* \r -- carriage return
* \xhh -- hex character 0xhh
* \uhhhh -- hex character 0xhhhh

 

수량자는 정규 표현식을 더욱 흥미롭게 만드는데 문자 클래스 같은 기타 표현식과 결합될 때는 특히 그렇다. 예를 들어, a-z에서 3자의 문자열을 매칭하기 위해 "[a-z][a-z][a-z]" 패턴을 사용할 수도 있지만 그럴 필요가 없다. 문자열을 반복하는 대신 패턴 다음에 수량자를 추가하면 된다.

이 예제의 경우, "[a-z][a-z][a-z]"":[a-z]{3}"으로 나타낼 수 있다. 특정 수량에 대해 숫자가 {} 괄호 안에 들어간다.

?, * 또는 + 문자를 사용하여 0번 또는 한 번, 0번 이상, 한 번 이상을 각각 나타낼 수도 있다.

[a-z]? 패턴은 a-z의 문자와 0번 또는 한 번 매칭된다.

[a-z]* 패턴은 a-z의 문자와 0번 이상 매칭된다.

[a-z]+ 패턴은 a-z의 문자와 한 번 이상 매칭된다.

수량자는 주의해서 사용한다. 0번 매칭을 허용하는 수량자에는 특별한 주의를 기울여야 한다.

괄호 기호({})를 수량자로 사용할 때는 범위를 지정해야 한다.

{3}은 정확히 3번을 의미하지만 {3,}은 적어도 3번을 의미한다. 수량자 {3, 5}3번에서 5번까지의 패턴과 매칭된다.

정규 표현식에는 여기서 살펴본 것보다 훨씬 많은 내용이 있다.

특정 상황에 맞는 정규 표현식을 사용하는 것이 중요하다.

앞의 Regex 프로그램을 사용하여 몇 가지 표현식을 시험해 보고 기대했던 결과가 나오는지 확인해 본다.

여러 가지 수량자를 사용하여 각 차이가 어떻게 나오는지 이해할 수 있도록 한다.

일반적으로 수량자는 가능한 매칭에 대해 최대 수의 문자를 포함하려고 한다.

 

정규 표현식에 대한 자세한 내용을 살펴보려면 자바 온라인 자습서의 정규 표현식 편을 참고한다.

또한 패턴 클래스에 대한 내용은 javadoc을 참고한다.

 

[본문스크랩] JAVA FTP 프로그램 

개발 이야기/Java | 2006. 12. 19. 10:51
Posted by 시반

JAVA 로 FTP Client를 만드시려는 분들께 도움이 될만한 자료 같습니다.
발췌:http://blog.empas.com/juxtapose/7388152
//------------------------------------------------
// ftp 프로그램 :  Ftp.java
// 이 프로그램은 ftp 서버에 접속하여 파일을 전송한다.
// 사용법 :  java  Ftp  서버주소
// 사용예 :  java Ftp  netlab.woosong.ac.kr
//------------------------------------------------

// 라이브러리의 이용
import java.net.*;
import java.io.*;

// Ftp 클래스
public class Ftp {
    // 소켓의 준비
    Socket ctrlSocket;//    제어용 소켓
    public PrintWriter ctrlOutput;//  제어 출력용 스트림
    public BufferedReader ctrlInput;//  제어 입력용 스트림

    final int CTRLPORT = 21 ;//   ftp 제어용 포트

    // openConnection  메소드
    // 주소와 포트 번호로부터 소켓을 만들고 제어용 스트림을 작성한다.
    public void openConnection(String host)
        throws IOException,UnknownHostException
    {
        ctrlSocket = new Socket(host, CTRLPORT);
        ctrlOutput = new PrintWriter(ctrlSocket.getOutputStream());
        ctrlInput
          = new BufferedReader(new InputStreamReader(ctrlSocket.getInputStream()));
    }

    // closeConnection  메소드
    // 제어용 소켓을 닫는다.
    public void closeConnection()
        throws IOException
    {
        ctrlSocket.close() ;
    }


    // showMenu 메소드
    // Ftp의 명령 메뉴를 출력한다.
    public void showMenu()
    {
        System.out.println(">Command?") ;
        System.out.print("2 ls") ;
        System.out.print("    3 cd") ;
        System.out.print("    4 get") ;
        System.out.print("    5 put") ;
        System.out.print("    6 ascii") ;
        System.out.print("    7 binary") ;
        System.out.println("    9 quit") ;
    }

    // getCommand 메소드
    // 이용자가 지정한 명령 번호를 읽어 처리한다.
    public String getCommand()
    {
       String buf = "" ;
       BufferedReader lineread
         = new BufferedReader(new InputStreamReader(System.in)) ;
       
       while(buf.length() != 1){// 1 문자의 입력을 받을 때까지 반복한다.
         try{
           buf = lineread.readLine() ;
         }catch(Exception e)
         {
          e.printStackTrace();
          System.exit(1);
         }
       }
       return (buf) ;
     }

    // doLogin 메소드
    // ftp 서버에 로그-인 한다.
    public void doLogin()
    {
       String loginName = "" ;
       String password = "" ;
       BufferedReader lineread
          = new BufferedReader(new InputStreamReader(System.in)) ;

       try{
            System.out.println("로그인 이름을 입력하세요 : ") ;
            loginName = lineread.readLine() ;
            // USER 명령에 의한 로그인
            ctrlOutput.println("USER " + loginName) ;
            ctrlOutput.flush() ;
            // PASS 명령에 의한 패스워드의 입력
            System.out.println("패스워드를 입력하세요 : ") ;
            password = lineread.readLine() ;
            ctrlOutput.println("PASS " + password) ;
            ctrlOutput.flush() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // doQuit 메소드
    // ftp 서버로부터 로그 아웃한다
    public void doQuit()
    {
       try{
            ctrlOutput.println("QUIT ") ;//  QUIT 명령의 송신
            ctrlOutput.flush() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // doCd  메소드
    // 디렉토리를 변경한다.
    public void doCd()
    {
       String dirName = "" ;
       BufferedReader lineread
          = new BufferedReader(new InputStreamReader(System.in)) ;

       try{
            System.out.println("디렉토리 이름을 입력하세요 : ") ;
            dirName = lineread.readLine() ;
            ctrlOutput.println("CWD " + dirName) ;// CWD 명령
            ctrlOutput.flush() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // doLs 메소드
    // 디렉토리 정보를 얻는다.
    public void doLs()
    {
       try{
            int n ;
            byte[] buff = new byte[1024] ;

            // 데이터용 연결(connection)을 만든다.
            Socket dataSocket = dataConnection("LIST") ;
            //  데이터를 읽어 처리하는 스트림을 사용한다.
            BufferedInputStream dataInput
              = new BufferedInputStream(dataSocket.getInputStream()) ;
            //  디렉토리 정보를 읽고 처리한다.
            while((n = dataInput.read(buff)) > 0){
                System.out.write(buff,0,n)  ;
            }
            dataSocket.close() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // dataConnection 메소드
    // 서버와의 데이터 교환용 소켓을 만든다.
    // 또한, 서버에게 port 명령으로 포트를 알린다.
    public Socket dataConnection(String ctrlcmd)
    {
       String cmd = "PORT " ; //PORT 명령으로 송신할 데이터 저장 변수
       int i ;
       Socket dataSocket = null ;//  데이터 전송용 소켓
       try{
             //  자신의 주소를 얻는다.
             byte[] address = InetAddress.getLocalHost().getAddress() ;
             //  적당한 포트 번호의 서버 소켓을 만든다.
             ServerSocket serverDataSocket = new ServerSocket(0,1) ;
             // PORT 명령용의 송신 데이터를 이용한다.
             for(i = 0; i < 4; ++i)
                 cmd = cmd +  (address[i] & 0xff) + "," ;
             cmd = cmd + (((serverDataSocket.getLocalPort()) / 256) & 0xff)
                       + ","
                       + (serverDataSocket.getLocalPort() & 0xff) ;
             // PORT 명령을 제어용 스트림을 통해 전송한다.
             ctrlOutput.println(cmd) ;
             ctrlOutput.flush() ;
             // 처리 대상 명령 (LIST, RETR, STOR)을 서버로 보낸다.
             ctrlOutput.println(ctrlcmd) ;
             ctrlOutput.flush() ;
             // 서버로부터 접속을 받는다.
             dataSocket = serverDataSocket.accept() ;
             serverDataSocket.close() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
       return  dataSocket ;
    }

    // doAscii 메소드
    // 텍스트 전송 모드로 셋팅한다.
    public void doAscii()
    {
       try{
            ctrlOutput.println("TYPE A") ;// A 모드
            ctrlOutput.flush() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // doBinary 메소드
    // 이진 전송 모드로 셋팅한다.
    public void doBinary()
    {
       try{
            ctrlOutput.println("TYPE I") ;// I 모드
            ctrlOutput.flush() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // doGet 메소드
    // 서버상의 파일을 가져온다.
    public void doGet()
    {
       String fileName = "" ;
       BufferedReader lineread
          = new BufferedReader(new InputStreamReader(System.in)) ;

       try{
            int n ;
            byte[] buff = new byte[1024] ;
            // 서버상의 파일의 이름을 지정한다.
            System.out.println("파일 이름을 입력하세요 : ") ;
            fileName = lineread.readLine() ;
            // 클라이언트상에 수신용 파일을 준비한다.
            FileOutputStream outfile =  new FileOutputStream(fileName) ;
            // 파일 전송용 데이터 스트림을 작성한다.
            Socket dataSocket = dataConnection("RETR " + fileName) ;
            BufferedInputStream dataInput
              = new BufferedInputStream(dataSocket.getInputStream()) ;
            // 서버로부터 데이터를 받아 파일로 저장한다.
            while((n = dataInput.read(buff)) > 0){
                outfile.write(buff,0,n) ;
            }
            dataSocket.close() ;
            outfile.close() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }

    // doPut 메소드
    // 서버에 파일을 전송한다.
    public void doPut()
    {
       String fileName = "" ;
       BufferedReader lineread
          = new BufferedReader(new InputStreamReader(System.in)) ;

       try{
            int n ;
            byte[] buff = new byte[1024] ;
            FileInputStream sendfile = null ;

            // 파일 이름을 지정한다.
            System.out.println("파일명을 입력하세요 : ") ;
            fileName = lineread.readLine() ;
            // 클라이언트상의 파일을 읽어 보낼 준비를 한다.
            try{
                sendfile =  new FileInputStream(fileName) ;
            }catch(Exception e){
                System.out.println("&#44594;?&#44541;&#44625;&#44437;&#44423;&#44511;&#44495;&#44455;&#44522;") ;
                return ;
            }

            // 전송용 데이터 스트림을 사용한다.
            Socket dataSocket = dataConnection("STOR " + fileName) ;
            OutputStream outstr =  dataSocket.getOutputStream() ;
            // 파일을 읽어 네트워크를 경유하여 서버로 보낸다.
            while((n = sendfile.read(buff)) > 0){
                outstr.write(buff,0,n) ;
            }
            dataSocket.close() ;
            sendfile.close() ;
       }catch(Exception e)
       {
            e.printStackTrace();
            System.exit(1);
       }
    }


    // execCommand 메소드
    // 명령에 대응하는 각 처리를 호출한다.
    public boolean execCommand(String command)
    {
          boolean cont = true ;

          switch(Integer.parseInt(command)){
          case 2 : //  서버의 디렉토리 표시 처리
              doLs() ;
              break ;
          case 3 : //  서버의 작업 디렉토리 변경 표시
              doCd() ;
              break ;
          case 4 : //  서버로부터의 파일 얻기 처리
              doGet() ;
              break ;
          case 5 : //  서버로 파일 전송 처리
              doPut() ;
              break ;
          case 6 : //  텍스트 전송 모드
              doAscii() ;
              break ;
          case 7 : //  바이너리 전송 모드
              doBinary() ;
              break ;
          case 9 : //  처리 종료
              doQuit() ;
              cont = false ;
              break ;
          default : // 그 이외의 입력 처리
              System.out.println("번호를 선택하세요 : ") ;
          }
          return(cont) ;
    }

    // main_proc 메소드
    // Ftp의 명령 메뉴를 출력하여 해당되는 처리를 호출한다.
    public void main_proc()
        throws IOException
    {
         boolean cont = true ;
        try {
            //  로그인 처리를 한다.
            doLogin() ;
            while(cont){
                //  메뉴를 출력한다.
                showMenu() ;
                //  명령을 받아서 처리한다.
                 cont = execCommand(getCommand()) ;
             }
        }
        catch(Exception e){
            System.err.print(e);
            System.exit(1);
        }
    }

    // getMsgs 메소드
    // 제어 스트림의 수신 슬롯을 개시한다.
    public void getMsgs(){
        try {
            CtrlListen listener = new CtrlListen(ctrlInput) ;
            Thread listenerthread = new Thread(listener) ;
            listenerthread.start() ;
        }catch(Exception e){
            e.printStackTrace() ;
            System.exit(1) ;
        }
    }

    // main 메소드
    // TCP 연결(connection)을 열어서 처리를 개시한다.
    public static void main(String[] arg){
        try {
            Ftp f = null;

            if(arg.length < 1){
              System.out.println("usage: java Ftp <host name>") ;
              return ;
            }
            f = new Ftp();
            f.openConnection(arg[0]); //  제어용 연결의 설정
            f.getMsgs() ;             //  수신 슬롯의 개시
            f.main_proc();            // ftp 처리
            f.closeConnection() ;     //  연결 닫기
            System.exit(0) ;          //  프로그램 종료
        }catch(Exception e){
            e.printStackTrace();
            System.exit(1);
        }
    }
}

// CtrlListen 클래스
class CtrlListen implements Runnable{
        BufferedReader ctrlInput = null ;
        //  constructor 읽고 처리하기 위한 상대방 지정
        public CtrlListen(BufferedReader in){
            ctrlInput = in ;
        }

        public void run(){
            while(true){
                try{ //  
                    System.out.println(ctrlInput.readLine()) ;
                } catch (Exception e){
                    e.printStackTrace() ;
                    System.exit(1) ;
                }
            }
        }
}

 

'개발 이야기 > Java' 카테고리의 다른 글

jxl을 통한 엑셀 저장하기  (0) 2008.01.16
JAVA API Chm파일 다운로드 링크  (0) 2007.12.21
[JBoss 보안] DataSource 패스워드 암호화  (0) 2007.06.26
[java] 예약어 enum  (0) 2007.06.04
JAVA SE의 정규표현식  (0) 2007.05.30
 

[JavaSE]문자열 오브젝트 길이 & 비교법

기타 | 2006. 9. 12. 09:49
Posted by 시반

 

문자열 오브젝트의 길이는?


여러분의 텍스트 스트링의 길이는 얼마나 되는가? 사용자 입력이 데이터 필드 길이 제한 조건에 부합하는지 확인하기 위해선 그 답이 필요하다. 데이터베이스의 텍스트 필드는 입력을 일정한 길이로 제한하는 경우가 많으므로 텍스트를 제출하기 전에 길이를 확인할 필요가 있다. 이유야 어쨌든, 누구든지 이따금씩 텍스트 필드의 길이를 알아야 할 때가 있다. 많은 프로그래머들이 String 오브젝트의 length 메소드를 이용하여 그 정보를 얻으며, 대개의 경우 length 메소드는 올바른 솔루션을 제공한다. 하지만 이것이 String 오브젝트의 길이를 결정하는 유일한 방법은 아니며, 언제나 올바른 것도 아니다.

자바 플랫폼에서 텍스트 길이를 측정하는 데 세 개 이상의 일반적인 방법이 있다.

  1. char 코드 유닛의 수
  2. 문자 또는 코드 포인트의 수
  3. 바이트의 수

char 유닛 수 세기

자바 플랫폼은 유니코드 표준 을 사용하여 문자를 정의하는데, 이 유니코드 표준은 한때 고정 폭(U0000~UFFFF 범위 내의 16비트 값)으로 문자를 정의했다. U 접두사는 유효한 유니코드 문자 값을 16진수로 표시한다. 자바 언어는 편의상 char 타입에 대해 고정 폭 표준을 채택했고, 따라서 char 값은 어떠한 16비트 유니코드 문자라도 표현이 가능했다.

대부분의 프로그래머는 length 메소드에 대해 잘 알고 있고 있을 것이다. 아래의 코드는 예제 문자열에서 char 값의 수를 세는데, 예제 String 오브젝트에는 자바 언어의 \u 표기법으로 정의되는 몇 개의 단순 문자와 기타의 문자가 포함되어 있다는 점에 유의하기 바란다. \u 표기법은 16비트 char 값을 16진수로 정의하며 유니코드 표준에서 사용되는 U 표기법과 유사하다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
System.out.printf("char count: %d\n", charCount);

length 메소드는 String 오브젝트에서 char 값의 수를 센다. 이 예제 코드는 다음을 출력한다.

char count: 7

문자 유닛 수 세기

유니코드 버전 4.0이 UFFFF 이상 되는 상당히 큰 수의 새로운 문자를 정의하면서부터 16비트 char 타입은 더 이상 모든 문자를 표현할 수 없게 되었다. 따라서 자바 플랫폼 J2SE 5.0버전부터는 새로운 유니코드 문자를 16비트 char 값의 쌍(pair)으로 표현하며, 이를 surrogate pair 라고 부른다. 2개의 char 유닛이 U10000~U10FFFF 범위 내의 유니코드 문자를 대리 표현(surrogate representation)하는데, 이 새로운 범위의 문자를 supplementary characters 라고 한다.

여전히 단일 char 값으로 UFFFF까지의 유니코드 값을 표현할 수는 있지만, char 대리 쌍(surrogate pair)만이 추가 문자(supplementary characters)를 표현할 수 있다. 쌍의 최초값 또는 high 값은 UD800~UDBFF 범위에, 그리고 마지막값 또는 low 값은 UDC00~UDFFF 범위에 속한다. 유니코드 표준은 이 두 범위를 대리 쌍을 위한 특수 용도로 배정하고 있으며, 표준은 또한 UFFFF를 벗어나는 대리 쌍과 문자 값 사이의 매핑을 위한 알고리즘을 정의한다. 프로그래머는 대리 쌍을 이용하여 유니코드 표준 내의 어떠한 문자라도 표현할 수 있는데, 이런 특수 용도의 16비트 유닛을 UTF-16 이라고 부르며, 자바 플랫폼은 UTF-16을 사용하여 유니코드 문자를 표현한다. char 타입은 이제 UTF-16 코드 유닛으로, 반드시 완전한 유니코드 문자(코드 포인트)가 아니어도 된다.

length 메소드는 단지 char 유닛만을 세기 때문에 추가 문자는 셀 수 없다. 다행히도 J2SE 5.0 API에는 codePointCount(int beginIndex, int endIndex) 라는 새로운 String 메소드가 추가되었다. 이 메소드는 두 인덱스 사이에 얼마나 많은 유니코드 포인트(문자)가 있는지 알려주는데, 인덱스 값은 코드 유닛 또는 char 위치를 참조한다. 식 endIndex - beginIndex 의 값은 length 메소드가 제공하는 값과 동일하며, 이 둘의 차이는 codePointCount 메소드가 반환하는 값과 항상 동일하지는 않다. 텍스트에 대리 쌍이 포함되어 있는 경우에는 길이 수가 확실히 달라진다. 대리 쌍은 하나 또는 두 개의 char 유닛으로 된 단일 문자 코드 포인트를 정의한다.

하나의 문자열에 얼마나 많은 유니코드 문자 코드 포인트가 있는지 알아보려면 codePointCount 메소드를 사용하도록 한다.

private String testString = "abcd\u5B66\uD800\uDF30";
int charCount = testString.length();
int characterCount = testString.codePointCount(0, charCount);
System.out.printf("character count: %d\n", characterCount);

이 예제는 다음 내용을 출력한다.

character count: 6

testString 변수에는 두 개의 흥미로운 문자가 포함되어 있는데, 그것은 '배움(learning)'이라는 뜻의 일본어 문자와 GOTHIC LETTER AHSA 라는 이름의 문자이다. 일본어 문자는 유니코드 코드 포인트 U5B66을 사용하며, 이는 동일한 16진수 char 값 \u5B66을 가진다. 고딕체 글자의 코드 포인트는 U10330이고, UTF-16에서 고딕체 글자는 대리 쌍 \uD800\uDF30이다. 이 쌍은 단일 유니코드 코드 포인트를 표현하므로 전체 문자열의 문자 코드 포인트 수는 7이 아니라 6이 된다.

바이트 수 세기

String 안에는 얼마나 많은 바이트가 있을까? 그 대답은 사용되는 바이트 기반 문자 세트에 의해 결정된다. 바이트 수를 알아보려는 이유 중 하나는 데이터베이스의 문자열 길이 제한 조건을 충족하기 위해서이다. getBytes 메소드는 유니코드 문자를 바이트 기반 인코딩으로 변환하며, 이는 byte[] 를 반환한다. UTF-8 은 바이트 기반 인코딩의 일종이지만, 모든 유니코드 코드 포인트를 정확하게 표현할 수 있다는 점에서 대부분의 다른 바이트 기반 인코딩과는 구별된다.

다음 코드는 텍스트를 byte 값의 배열로 변환한다.

byte[] utf8 = null;
int byteCount = 0;
try {
  utf8 = str.getBytes("UTF-8");
  byteCount = utf8.length;
} catch (UnsupportedEncodingException ex) {
  ex.printStackTrace();

System.out.printf("UTF-8 Byte Count: %d\n", byteCount);

타깃 문자 세트가 생성될 바이트의 수를 결정한다. UTF-8 인코딩은 단일 유니코드 코드 포인트를 1~4개의 8비트 코드 유닛(=1 바이트)으로 변환한다. 문자 a , b , c , d 는 모두 4 바이트만을 요구하는 한편, 일본어 문자(한국어도 같음)는 3 바이트로 변환된다. 또한 고딕체 글자는 4 바이트를 차지한다. 결국 다음의 결과가 나오는 것을 알 수 있다.

UTF-8 Byte Count: 11


그림 1. 무엇을 세느냐에 따라 문자열의 길이가 달라진다.

요약

추가 문자를 사용하지 않으면 length codePointCount 의 반환 값 간에는 차이점이 없다고 할 수 있다. 하지만 UFFFF 범위를 넘어서는 문자를 사용할 경우에는 길이를 결정하는 다양한 방법에 관해 알아야 할 필요가 있다. 예컨대 미국에서 한국으로 제품을 발송할 경우에는 십중팔구 length codePointCount 가 서로 다른 값을 반환하는 상황이 발생한다. 데이터베이스의 문자 세트 인코딩과 일부 직렬화 포맷은 UTF-8을 최선의 방법으로 권장되고 있는데, 이 경우에도 역시 텍스트 길이는 달라진다. 결국 길이를 어떻게 활용할 것인지에 따라 다양한 측정 옵션을 사용할 수 있다.

추가 정보

본 테크 팁 주제에 관한 자세한 내용을 보려면 다음 리소스를 참조할 것(영문)

 

 

문자열 오브젝트 비교법


String 오브젝트는 다양한 방법으로 비교가 가능하며 그 결과 또한 종종 차이가 날 수 있다. 결과의 정확성은 주로 어떤 유형의 비교가 요구되느냐에 의해 좌우된다. 일반적인 비교 기법으로는 다음과 같은 것들이 있다.

  • == 연산자에 의한 비교.
  • String 오브젝트의 equals 메소드에 의한 비교.
  • String 오브젝트의 compareTo 메소드에 의한 비교.
  • Collator 오브젝트에 의한 비교.

== 연산자에 의한 비교

== 연산자는 String 오브젝트 레퍼런스에 사용되는데, 2개의 String 변수가 메모리 내의 동일한 오브젝트를 가리킬 경우 비교값으로 true를 리턴한다 . 그렇지 않은 경우, 텍스트가 동일한 문자 값을 가지는지 여부에 관계없이 비교값으로 false 를 리턴한다. == 연산자는 실제 char 데이터를 비교하지 않는데, 이 점을 간과할 경우 작업자는 그 후의 코드에 'The strings are unequal(문자열이 동일하지 않다)' 라고 나타나는 것에 당황해 할 수도 있다.

String name1 = "Michèle";
String name2 = new String("Michèle");
if (name1 == name2) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The strings are unequal.");

자바 플랫폼은 문자열 리터럴 및 상수를 위한 내부 풀을 생성한다. 정확하게 동일한 char 값을 가지는 문자열 리터럴과 상수는 풀 내에 단 한번 존재하게 된다. 또한 String 리터럴 및 상수를 동일 char 값과 비교하면 그 결과가 항상 동일하다는 것을 알 수 있다.

equals 메소드에 의한 비교

equals 메소드는 두 문자열의 실제 char 내용을 비교하는데, 이 메소드는 2개의 String 오브젝트가 동일한 값을 가지는 char 데이터를 보유할 경우 true 를 반환한다. 다음의 코드 예제에서는 'The strings are equal(문자열이 동일하다)' 이 출력된다.

String name1 = "Michèle";
String name2 = new String("Michèle");
if (name1.equals(name2) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The strings are unequal.");

compareTo 메소드에 의한 비교

compareTo 메소드는 equals 메소드와 유사하게 char 값을 비교한다. 아울러, 이 메소드는 자체 String 오브젝트가 인수 문자열에 선행할 경우 음의 정수를 반환하고, 문자열이 동일할 경우에는 0을 반환한다. 반면에 오브젝트가 인수 문자열 뒤에 올 경우에는 양의 정수를 반환한다. compareTo 메소드는 cat hat 에 선행한다는 것을 나타낸다. 이 비교에서 알아두어야 할 가장 중요한 점은 메소드가 char 값을 축어적으로 비교한다는 점인데, 가령 메소드는 cat 의 'c' 값이 hat 의 'h'보다 적은 숫자 값을 가진다고 판단한다.

String w1 = "cat";
String w2 = "hat";
int comparison = w1.compareTo(w2);
if (comparison < 0) {
  System.out.printf("%s < %s\n", w1, w2);
} else {
  System.out.printf("%s < %s\n", w2, w1);

위의 코드 예제는 compareTo 메소드의 동작을 보여주며 cat < hat 을 출력한다. 우리가 예상한 결과이긴 한데, 그렇다면 이 경우 어떤 결함과 문제점이 생길 수 있는가?

오류 산출

사전을 이용할 때처럼 텍스트를 자연어로서 비교하려 하면 문제가 발생하게 된다. String 클래스는 자연어의 관점에서 텍스트를 비교할 능력을 가지고 있지 않은데, 이 클래스의 equals compareTo 메소드는 문자열의 개별 char 값을 비교한다. name1 의 인덱스 n 에 해당하는 char 값과 name2 의 인덱스 n 에 해당하는 char 값이 양 문자열의 모든 n 에 대해서 동일할 경우 equals 메소드는 true 값을 리턴한다.

동일한 compareTo 메소드에 대해 cat Hat 의 비교를 요구할 경우 혼란스런 결과가 산출된다. 일반적으로 사람들은 대문자 여부와 관계없이 cat Hat 보다 앞선다는 것을 알고 있다. 하지만, compareTo 메소드는 Hat < cat 으로 표시하는데, 이유인 즉 유니코드 문자표에서는 대문자가 소문자에 선행하기 때문에 이런 결과가 나오는 것이다. ASCII 문자표에서도 같은 순서가 적용된다. 따라서 분명한 것은 애플리케이션 사용자에게 정렬된 텍스트를 제시하고자 할 경우 이런 순서가 항상 바람직한 것은 아니라는 점이다.

문자열의 동일성을 판단하려고 할 때에도 잠재적인 문제점이 나타날 수 있다. 즉, 텍스트는 복수의 내부 표현을 가질 수 있는데, 가령 'Michèle'이라는 불어 이름에는 유니코드 문자 시퀀스 M i c h è l e 이 포함된다. 하지만, 시퀀스 M i c h e ` l e 을 사용할 수도 있다. 두 번째는 '시퀀스 조합'('e' '`')을 사용하여 'è'를 표시하는 경우이다. 유니코드를 이해하는 그래픽 시스템은 이 두 표현의 내부 문자 시퀀스가 약간 다르더라도 동일하게 보이도록 디스플레이할 것이고, String 오브젝트의 단순한(simplistic) equals 메소드는 이 두 문자열이 다른 텍스트를 가지는 것으로 표시한다. 즉, 이들은 사전적으로(lexicographically) 동일한 것이 아니라 언어학적으로(linguistically) 동일한 것이다.

아래의 코드 단편으로는 'The strings are unequal.'이 나타난다. equals compareTo 메소드 모두 이들 문자열의 언어학적 동일성을 이해하지 못한다.

String name1 = "Michèle";
String name2 = "Miche\u0300le"; //U0300 is the COMBINING GRAVE ACCENT
if (name1.equals(name2)) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The strings are unequal.");

이름들의 목록을 정렬하려고 할 경우, 문자열의 compareTo 메소드는 거의 십중팔구 잘못된 결과를 표시하게 된다. 특정 이름을 검색하고자 할 경우, 사용자가 시퀀스 조합을 입력하거나 데이터베이스가 사용자의 입력과 다르게 데이터를 정규화(normalize)하면 이번에도 equals 메소드는 엉뚱한 결과를 표시한다. 문제는, 자연어 정렬 또는 검색을 수행할 때마다 문자열의 단순한 비교가 잘못된 결과를 표시한다는 것인데, 이런 연산을 수행하려면 단순한 char 값 비교보다는 더 강력한 무언가가 필요하다.

Collator 사용하기

java.text.Collator 클래스는 자연어 비교 기능을 제공하는데, 자연어 비교는 주로 특정 서체의 문자들에 대한 동일성과 순서를 결정하는 지역 고유의(locale-specific) 규칙에 의존한다.

Collator 오브젝트는 사전에서 'cat'이 'Hat'보다 먼저 나온다는 사실을 이해하므로 collator 비교를 이용할 경우 다음 코드는 cat < Hat 을 출력하게 된다.

Collator collator = Collator.getInstance(new Locale("en", "US"));
int comparison = collator.compare("cat", "Hat");
if (comparison < 0) {
  System.out.printf("%s < %s\n", "cat", "Hat");
} else {
  System.out.printf("%s < %s\n", "Hat", "cat" );

collator는 경우에 따라(대개는 자연어 처리가 중요한 경우) 문자 시퀀스 M i c h è l e M i c h e ` l e 과 동일하다는 것을 알고 있다.

다음 비교에서는 Collator 오브젝트가 사용되는데, 이는 시퀀스 조합을 인식하여 두 문자열이 동일한 것으로 판단한다. 따라서 ' The strings are equal.'이라고 나타난다.

Collator collator = Collator.getInstance(Locale.US);
String name1 = "Michèle";
String name2 = "Miche\u0300le";
int comparison = collator.compare(name1, name2);
if (comparison == 0) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The string are unequal.");

Collator 오브젝트는 심지어 몇 단계로 구분하여 문자 차이를 이해할 수 있다. 가령 e d 는 2개의 서로 다른 글자인데, 이런 차이는 '1차적' 차이이다. 한편 글자 e è 도 역시 다르지만, 그 차이는 '2차적' 특성을 띤다 할 수 있다. 또한 Collator 인스턴스 구성 방식에 따라 단어 'Michèle'과 'Michele'을 동일하게 간주할 수도 있다. 다음 코드는 'The strings are equal.' 을 출력한다.

Collator collator = Collator.getInstance(Locale.US);
collator.setStrength(Collator.PRIMARY);
int comparison = collator.compare("Michèle", "Michele");
if (comparison == 0) {
  System.out.println("The strings are equal.");
} else {
  System.out.println("The string are unequal.");

요약

어떤 경우에 equals 메소드가 == 연산자보다 적절한지 한 번 생각보기 바란다. 또한, 텍스트에 순서를 매겨야 할 경우 Collator 오브젝트의 자연어 비교가 필요한지 여부에 대해서도 생각해보라. 다양하고 미묘한 비교의 차이점을 살펴보고 나면, 경우에 따라 잘못된 API를 사용해 왔다는 사실을 발견하게 될지도 모른다. 이렇게 차이점을 알게 되고 나면 애플리케이션과 고객을 위해 올바른 선택을 하는 데 큰 도움이 될 수 있을 것이다.

추가 정보

본 테크 팁의 주제에 관한 자세한 내용을 보려면 다음 리소스를 참조할 것(영문)

 

출처 : JavaSE 테크팁 아카이브

'기타' 카테고리의 다른 글

VMWare로 Fedora6 설치하기 02  (0) 2007.03.10
VMWare로 Fedora Core6 설치하기 01  (0) 2007.03.10
CHM 파일이 열리지 않을 때  (0) 2006.11.09
자바 2.0의 시대  (0) 2006.08.25
window update 정품인증 회피(3월1일 이후)  (0) 2006.05.08
 

자바 2.0의 시대

기타 | 2006. 8. 25. 09:44
Posted by 시반
2006 월드컵의 열기가 한창인 6월, 나는 평소와 다름없이 컴퓨터 앞에 앉아 이달 특집의 문을 열 서문을 쓰고 있다. 자바로 일을 시작한 지도 벌써 7년. 그동안의 변화를 돌이켜 보면, 과거의 꿈은 현실이 되었음을 느낀다. 늘 함께 하여 낯설지 않지만, 옛 모습을 떠올려 보면 자바도 참 많이 달라졌음을 느끼게 된다. 이것은 단순한 진화가 아닌 패러다임의 이동이다. 서서히 드러나던 자바의 새로운 패러다임, 자바 2.0을 말해보자.

말을 만드는 것은 쉬우면서도 어려운 일이다. 웹2.0도 그렇다. 누가 감히 웹이라는 일반화된 명사에 버전번호를 붙일 생각을 했겠는가? 그러나 한번 물꼬가 트이면 일반화까지는 일사천리다. 더욱이 1990년대 중반부터 대중화에 성공한 인터넷 관련 기술에 있어 2000년대 중반은 확실히 전환점으로 삼기 충분했고, 거기에 2.0이라는 접미사는 훌륭한 조합이었다. 우연인지는 몰라도 올해 많은 자바 기술이 큰 변화를 겪었다. 자바 웹 서비스를 이루는 새로운 표준들이 대거 등장하여 필자는 그것들을 한 데 묶어 자바 웹 서비스 2.0이라고 부르기 시작했다 (weblogs.java.net/blog/iasandcb/archive/2006/03/java_web_servic.html 참조). 그런데 이번에는 또 다른 새로운 이름이 필요한 때가 되었다. 작년과 올해에 거쳐 자바의 모든 면에서 새로운 국면을 맞게 된 탓이다. 그래서, 나는 다시 과감히 자바에 ‘2.0’이라는 버전을 붙여보고자 한다. 1부에서는 자바 2.0이 자바 1.0과 어떻게 다른 지와 자바 2.0 시대에 개발자들이 주목해야 할 것은 무엇인지에 대해 알아보자.

새 이름을 얻은 자바 플랫폼

자바의 이름이 바뀌었다. J2ME, J2SE, J2EE, 자바가 실험실 수준을 벗어나 본격적으로 주목을 받던 1990년대 말부터 쓰이기 시작하여 격변의 IT계에서는 보기 드물게 오랜 수명을 누린 이름이다. 마이크로소프트(이하 MS)의 윈도우만 봐도 윈도우 95, 98, 2000, 2003이라는 연도 기반 이름과 더불어 중간중간 ME, XP, 이제는 비스타(Vista)까지 쓰이며 버전 번호라는 일반적인 명명법과는 거리가 먼 행보를 거듭해 왔다. 서비스팩이라는 수단으로 밋밋한 이름에 힘을 주는 센스도 선보였다.

그에 비하면 자바는 J2SE만 보더라도 1.1에서 1.2로 현대화된 이후로 1.4와 1.5에 이르면서 상당히 큰 변화가 있었음에도 계속 1.x의 버전 번호를 고수해왔다. 개인적으로 J2SE에서의 2라는 숫자는 매우 제한적이라고 생각했다. 도대체 언제까지 자바2라고 할 것인지 궁금하기까지 했다.  J2SE 1.4는 코어 플랫폼에 있어 가장 많고 중요한 API 추가가 있었고, J2SE 1.5는 자바 언어 자체에 심대한 발전이 있었다. 이미 이때부터 2라는 테두리로 묶기에는 커져버렸던 것이 아닐까? 그래서 필자는 J2SE 1.4부터는 1.4대신 4.0을 써야 하는 것이 아닌가하는 생각이 들 정도였다.
이런 작은 단위의 버전 업이 안정감을 나타낼 수는 있겠지만 동시에 더딘 성장새를 시사할 수도 있는 탓이다. 실제로는 대폭 변했는데도 버전 번호를 낮춰 별로 변한 것이 없다는 식의 태도는 오히려 점점 더 많은 문제를 해결해야 하는 범용 소프트웨어에 있어 답답하다는 인상을 주기에 충분했다.

J2EE도 상황은 마찬가지여서 1.2로 시작하여 1.3에서 EJB 2.0으로 크게 도약한 분산 컴포넌트 기술은 1.4에서 웹 서비스 지원의 추가로 날개를 달았지만 여전히 1.x라는 빈약한 버전을 가지고 있었다.

그래서, J2SE 1.6과 J2EE 1.5부터는 J2라는 약자를 버리고 자바라는 완전한 이름으로 돌아옴과 동시에 1.6대신 6, 1.5대신 5라는 과감한 버전 업그레이드를 단행하게 되었다. 아직도 여전히 J2SE와 J2EE라는 말은 자바 플랫폼의 대명사로 많이 쓰이고 있지만, 새 술을 새 부대에 담듯이 자바SE 6와 자바EE 5는 그 새로운 이름과 함께 ‘2’라는 굴레를 벗어나 도약의 로드맵을 사용자에게 제시하는 첫걸음을 디디게 된다.

한편, 함께 이름을 바꾼 자바 ME는 버전 번호의 큰 변경은 없지만 내실에 있어서는 큰 변화를 더했다. 제약이 많던 CLDC(connected Limited Device Configuration)에서 거의 PC급인 CDC(connected Device Configuration)로 빠른 이전 현상을 보이며 새로운 이름에 걸 맞는 실질적인 변화가 일어나고 있다.

오픈 플랫폼의 가치

자바2.0 시대의 개막이 웹2.0 시대의 개막과 가장 유사한 점은 바로 열린 플랫폼(Open Platform)에선 찾을 수 있다.  MS라는 한 기업이 내부적인 절차에 의해 개발하는 방식에 비해, 자바는 매우 일찍부터 많은 개발사 혹은 개발자들과 함께 시장을 키워왔다. 축구 종주국은 영국이지만 월드컵은 세계인의 축제로 발전한 것처럼 자바도 종가인 썬마이크로시스템즈(이하 썬)뿐 아니라 써드 파티들도 함께 성공을 거두는 좋은 자원이 되고 있다. 이런 기술 주도와 사업 성공의 독립성은 전체 시장의 활성화에 매우 중요하다. 닌텐도는 매번 우수한 게임기 하드웨어를 내놓고 거기에 최적화된 게임 소프트로 팬들을 열광시키지만, 저조한 써드 파티들의 활약으로 매번 위험성을 지적 받고 있다. 독차지하기 보다는 서로 나누면서 자라는 것은 자연스럽게 건전한 경쟁으로 이어지고, 그 과정에서 공정한 경쟁의 장을 마련해주는 선순환이 이루어진 셈이다.

하지만, JCP의 표준화가 아무리 투명한 절차로 진행되더라도, 자바는 여전히 100% 오픈이라는 평가를 받기 어려웠다. 이는 오픈 소스의 대명사인 리눅스와 비교해보면 더욱 확연히 드러난다. 리눅스라는 OS적 오픈 플랫폼에 이은 자바라는 애플리케이션적 오픈 플랫폼의 등장이 장애로 여겨지기도 했다. 물론, 여기에는 두 가지 측면이 있다. 하나는 썬이라는 한 회사가 자바 기술에서 차지하는 비중이며, 또 하나는 그 동안 표준을 강제하며 변종을 억제해 온 통제 시장 구조이다.

그럼에도 불구하고 이제 용단을 내릴 때가 온 것으로 보인다. 먼저 자바EE 5의 참조 구현체(Reference Implementation, 이하 RI)가 글래스피시(GlassFish)라는 오픈 소스 프로젝트로 진행되어 왔다. 사용상의 라이선스도 CDDL(Common Development and Deployment License)로 OSI(Open Source Initiative)가 인증한 라이선스이며 GPL(GNU Public License)보다도 쓰기가 편하다. 또한 자바SE 6의 RI도 머스탱(Mustang)이라는 오픈 소스 프로젝트로 공개되어 있다. 아직 라이선스는 매우 제한적이며 실제 사용도 어렵지만, 이 또한 올해 자바원 컨퍼런스에서 썬의 CEO인 조나단 슈왈츠가 조만간 글래스피시처럼 될 것이라고 공식 발표했다.

이와 같이 자바가 리눅스처럼 오픈 소스로 풀린다는 것은 어떤 의미를 가지는 것일까?  글래스피시 프로젝트를 이끌고 있는 사람들 중 한 명인 에드와르도(Eduardo Pelegri-Llopart)는 “RI는 그 동안 장난 수준의 구현(Toy Implementation)으로 여겨져서, 그저 한번 설치하고 간단한 애플리케이션을 돌리는 수준으로 쓰여 왔지만 그것은 오해”라며 잘라 말한다 (http://weblogs.java.net/blog/pelegri/archive/2006/06/what_is_a_jcp_r.html).


그런 선입견을 극복하기 위해 RI를 제품 수준의 품질로 끌어 올리려는 노력이 부단히 이루어져 왔다. 톰캣(Tomcat)의 경우도 초기에는 그야말로 써블릿과 JSP에 관심 있는 사람들에게 학습과 시험의 도구 정도로만 여겨져 왔다. ‘왜 이것을 실제 서비스에 쓸 수 없는가?’라는 각성으로부터 실질적인 기능들이 추가되고 많은 버그들이 고쳐져서 (다음과 같은 포탈에서 쓰일 정도로) 많은 사람들이 실제 서비스에 채택하기에 이른 것이다. 일단 RI가 건실해지고 저변을 넓히면, 다음은 다양한 배포판이 나오며 사용자들을 즐겁게 한다. 즉 RI가 오픈 소스가 되어도 천하를 통일하여 독점하게 된다면 이 역시 아무 소용이 없다. 탄탄한 기초가 창조적인 응용을 낳는 것이 가장 바람직함은 자바의 발전에도 예외가 없는 셈이다.

잘 알려진 것처럼 구글은 자체 개조한 리눅스를 이용하여 서비스를 하고 있다. 그렇다면, 다음은 무엇일까? 구글은 자바에 깊이 관여하고 있다. RI에 기반하여 자체 자바EE 구현과 SE의 구현이 가능할 것이다. 구글뿐만 아니라 플랫폼을 이용하여 서비스를 제공하는 회사라면 누구라도 할 수 있는 일이다. 그렇다면 그 동안 썬이 혼자서 짊어져 오던 자바에 대한 투자가 더 많은 회사들로 확산되어 중복은 피하고 특성은 강화하는 효율적인 집단 개발이 실현되는 것이다.

사용자 작성 코드(User Created Code)

자바 2.0이 웹 2.0과 유사한 또 한 가지 측면은 사용자가 창조의 중심에 선다는 점에서 찾아볼 수 있다. 웹 2.0에서 사용자 작성 컨텐츠(User Created Contents)를 전면에 내세우듯이, 자바 2.0도 개발자 개개인의 블로그를 통해 지식과 코드를 나누는 것이 일상화되어 가고 있다. 더불어 과거(90년대 이전)와 비교해 보면 2006년 현재의 개발 환경은 그야말로 천국이나 다름없다. 게다가 돈을 들여 툴을 살 필요도 없다. 일인당 생산성은 현저히 올라가고, 필요한 지식의 습득과 달성을 돕는 인프라도 급속도로 증가되어왔다. 당장 개발자 개인이 쓰는 개발용 컴퓨터의 사양만 봐도 전에는 하드 디스크드라이브에서나 쓸법하던 용량이 지금은 메인 메모리 용량이 되어버렸다. 노트북이 개발자에게 크게 어필하게 되면서 ‘어디서나 코딩’이 가능해졌다. 굳이 일 중독이 아니더라도 분위기 좋은 카페나 햇살 따스한 야외에서 최고의 창의적 무드를 코드로 승화시켜 볼 수도 있다.

사실 자신이 만든 코드를 올린다는 것은 웹상의 프로젝트에 참여하는 식의 형식적이며 커뮤니티적인 통로로만 행해지는 것으로 생각되어왔다. 사실 블로그에 자료를 올리듯이 코드를 올린다고 생각하면 그리 대단한 과정을 거치지 않아도 충분하다. 대체로 이런 짧은 코드는 하나의 완성된 애플리케이션이기보다는 기능과 아이디어에 집중된 것이다. 이런 코드의 설명에 글이나 그림으로 살을 붙이면, 코드는 문서화라는 까다로운 작업을 할 필요 없이 훌륭히 완성된다.

기존의 웹이 HTML 작성과 퍼블리싱이라는 다소 거창한 작업을 통해 콘텐츠를 공개했던 것에 반해(특히 HTML은 웹 디자인이라는 요소까지 맞물려 비전문가에게 한계를 심어주었다) 웹 2.0은 글쓰기 플랫폼으로서(예를 들어 태터툴즈와 같은 설치형 블로그) 겉보기와 올리기라는 작업을 줄이고 창작 자체에 집중할 수 있도록 하는 점도 최근 자바가 추구하는 비즈니스 로직 집중과 일맥상통한다.

즉 인프라가 점점 더 두터워지고, 아이디어 공개와 교류가 실시간으로 이루어질 수 있는 공간이 탄생하면서, 자바 2.0은 단순히 프로그래밍 언어가 아니라 알고리즘과 로직의 플랫폼으로 자리매김할 것이다. 스트럿츠와 스프링으로 이어지는 프레임워크의 흐름은 이와 같은 움직임을 대변하고 있다. 그 동안 컴포넌트 컴포넌트 노래를 불렀지만, 마치 자동차 부품처럼 규격화된 컴포넌트가 아닌, 인간의 ‘생각’을 컴포넌트화한다면, 꼭 인터페이스를 맞추는 고정관념을 탈피하여 자유롭고 유연한 재활용의 세계가 펼쳐지는 것이다. 더욱 동적인 시스템을 수용할 수 있는 프레임워크, 그리고 동적일 수밖에 없는 인간의 사고와 요구에 이제 새로운 자바가 부응하기 시작하는 모습이다.

컨버전스(Convergence)


하드웨어 이야기이긴 하지만, 울트라 모바일 PC(이하 UMPC)는 컨버전스의 미래를 보여준다. MP3 플레이어, PMP, PDA, DMB, 네비게이션, 컴퓨터가 합체된 작고 휴대하기 좋은 기기. 여기에 휴대폰같은 통신 기기까지 가미되면 어디서나 인터넷에 접속하며 화상 통화도 즐길 수 있다. 필자가 늘 꿈꾸던 컴퓨팅 환경도 바로 그런 것이었다. 필자가 평소에 들고 다니는 것은 지갑, 휴대폰, 그리고 회사 출입 카드와 집 열쇠다. 지갑 안은 더 가관이다. 각종 카드와 현금, 명함, 메모 등이 빼곡하다. 휴대폰은 DMB폰이라서 크고 무거운 데다가 출입 카드나 열쇠를 잊고 나오기라도 하면 정말 낭패다. 봄이나 가을 겨울에야 겉옷을 입는다지만, 여름에는 넣을 주머니도 부족하다. 왜 이런 것들이 하나로 합쳐지지 않을까?

아주 극단적으로, 위에서 말한 모든 기능을 가진 기기를 생각해보자. 대략 휴대폰보다는 약간 크지만, UMPC처럼 크진 않고 현재의 휴대폰보다는 넓은 화면과 고운 해상도를 가져 MP3 플레이어, PMP, PDA, DMB, 네비게이션으로 부족함이 없도록 한다. 내부에는 고성능 임베디드 CPU를 탑재하여 멀티태스킹 OS를 지원하고, 그 위에 자바SE급의 자바를 올려 웬만한 자바 애플리케이션은 수정 없이 돌릴 수 있게 한다. 여기에 제 3세대 급의 무선 이동 통신이 가능하여 고속 인터넷 접속을 지원한다. 모든 카드는 이 기기에 특별히 할당된 카드 메모리에 기억되어, 아무리 많은 카드라도 모조리 그 특징과 함께 수록된다. SKT, KTF, LGT 멤버십 카드를 다 가지고 있다면 세 장의 카드를 따로따로 가지고 다닐 필요 없이 이 기기로 제시하고 싶은 카드를 선택할 수 있게 하는 것이다. 명함도 실제 종이 명함이 아니라 이 기기로 주고받고, 모든 출입 통제는 이 기기에 부여된 권한으로 이루어지게 한다.

어떤가? 이 정체 모를 기기, 잃어버리면 끝이지만 그야말로 만능이고 무거운 노트북을 들고 다닐 필요도 없다. 무선 인터넷을 몰래 쓰기 위해 거리를 배회할 필요도 없다. 그런데도 작고 가벼워 늘 들고 다닐 수 있다. 전에는 기능 하나의 기능만 가진 기기조차도 들고 다니기 어려울 만큼 무거웠을 텐데 말이다.

하드웨어 이야기를 장황하게 늘어놓은 이유는 자바에도 바로 이런 현상이 나타나고 있기 때문이다. 개별적으로 존재하던 많은 기술들이 자바EE와 자바SE라는 우산 아래로 모여들고 있다.

<그림 3>을 과거 J2EE 1.4와 비교해보면 점점 더 포함되는 JSR이 많아짐을 느낄 수 있다. 한편으로는 비대해지는 듯 보이지만(실제로 플랫폼 자체의 배포판 크기도 늘고 있다), 자바를 지탱해주는 하드웨어의 발전(CPU, 메모리, 하드 디스크, IO 버스 대역폭 등)에 비하면 오히려 하드웨어를 제대로 활용하기 위한 방향이라고 할 수 있다.

특히 자바SE 6의 포용력은 경이롭다. 자바EE 5에도 포함된 XML과 웹 서비스 관련 기술(JSR 109과 JAXR을 제외한 JAXP, StAX, JAXB, SAAJ, JAX-WS)을 모두 포함하고 있다. 심지어 자바 DB라는 100% 자바 기반 RDBMS까지 내장하게 되었다. 앞에서 예로 들었던 당장은 비현실적인 컨버전스가 자바에서는 올해 가을이면 현실로 다가오는 셈이다. 이제 Java SE 6 하나만 설치하면 네트워크, XML 처리, 그래픽, 그리고 DB의 저장까지 가능하게 되는 것이다.

그렇다면 어떤 일이 가능해지는 것일까? 사용자가 XML을 주면 그것으로부터 일정 정보를 뽑아 DB에 유지하는 프로그램을 짜야 한다고 생각해보자. 전에는 자바SE뿐만 아니라, XML 처리를 쉽게 하기 위해 JAXB도 따로 깔고(단순히 라이브러리뿐만 아니라 스키마를 처리하는 툴도 필요하다), MySQL과 같은 DBMS와 거기에 맞는 JDBC 드라이버도 구해 넣어야 했다. 그런데 이제 그런 인프라 구축 과정이 일체 필요 없게 되는 것이다. 여기에 자바 퍼시스턴스(Java Persistence) API까지 가미되면, XML 처리와 DB 처리에 XML 이해와 SQL 쿼리가 전혀 필요 없는 자바 지향적 프로그래밍까지 가능해진다. 달리 말하면, 자바 2.0의 컨버전스는 단순히 API의 수집이 아니라 개발 방식에 있어서도 통합을 의미한다고 할 수 있다.   

한편, 자바ME의 컨버전스는 고성능으로 치닫고 있다. 노키아를 위시한 메이저 휴대폰 메이커들이 기존의 CLDC에서 보다 다양한 기능을 지원하는 CDC로 이전하고 있다. 거기에 많은 부가 패키지를 올려 자바SE에 뒤지지 않는 실행 환경을 제공하기 시작했다. 이는 휴대폰을 구성하는 하드웨어의 폭발적인 발전과 맞물려 있는데, 조만간 CDC조차도 뛰어넘는 자바SE급의 자바 환경이 휴대폰에 올라 갈 지도 모를 일이다.

자바의 활약이 기대되는 매우 흥미로운 곳이 하나 더 있다. 바로 최근에 출시된 블루레이 디스크(Blue-ray Disk, 이하 BD)이다. BD는 DVD처럼 많은 콘텐츠를 섭렵할 수 있도록 다양한 메뉴 시스템을 제공해야 한다. 바로 이 메뉴 시스템을 확장하여 콘텐츠를 운영하는 애플리케이션을 작성하고 실행하는 플랫폼으로 자바가 쓰이게 되었다. 모든 BD 플레이어는 자바를 지원할 것이며, 자바는 단순한 인터렉티브 메뉴 개발뿐 아니라 BD 컨텐츠를 활용한 멀티미디어 애플리케이션에 능히 쓰일 수 있다. 더욱이 BD 전용 플레이어가 네트워크 연결을 지원하거나 PC에서 BD를 보는 경우 자바의 네트워크 기능까지 활용한다면 그 응용성은 상상하기조차 힘들어 질 것이다.

언어적 중립성


자바 2.0의 마지막 특징은 타 언어와의 혼용 지원에 있다. 이 말을 듣고 벌써 ‘닷넷에서 이미 한 거잖아’라고 생각했다면 맞다 바로 그것이다. 그런데 재미있는 것은 닷넷의 다중 언어 지원이 이미 MS가 공들인 프로그래밍 언어들을 한 데 모은 것인 반면, 자바와 함께 하고 싶어 하는 언어들은 자바와는 성질이 다른(특히 동적 스크립트) 언어가 많다는 사실이다. 이와 관련한 JSR인 JSR 223 Scripting for Java Platform이 자바SE 6에 탑재되어 있어, 자바스크립트는 기본으로 지원되고, 그 밖에 그루비(Groovy), 파이썬(Python), 루비(Ruby), PHP등 많은 스크립트 언어를 자바 코드 안에서 실행할 수 있고 그 반대도 가능하다. 자바가 주가 되는 측면에서 바라본다면, 전체 코드에서 일부 코드를 자신이 좋아하는 스크립트 언어로 작성해도 된다. 또 이미 많이 나와 있는 스크립트들을 라이브러리처럼 불러 쓸 수 있다. 그야말로 전에는 생각하기 어려웠던 놀라운 일이다.

자바는 프로그래밍 언어이기도 하지만 자바 바이트 코드를 실행하는 버추얼 머신이기도 하다. 바로 이 점에서 ‘자바 실행 파일을 꼭 자바로 짜야 하는가?’라는 의문을 낳게 되었다. 이미 닷넷이 CLR(Common Language Runtime)을 통해 보여준 것처럼, 실행 환경과 개발 환경의 분리는 새로운 것도 아니지만 그 동안 자바에 천착한 이들에게는 생소하고 또 두려울 수도 있다. 하지만 자바는 다른 모든 언어가 그렇듯이 언어로서 모든 목적에 100% 부합할 수 없다. 개발에 쓸 언어의 선택은 사용자의 몫으로 돌리고, 자바2.0은 실행 환경으로서 더욱 넓어지는 길을 운명으로 삼으려 한다. 이는 자바SE 7 코드명 돌핀(Dolphin)에서 구제화 될 예정이며, 자바에 있어 일대 전환점이 될 것이다.

꿈이 자라면 현실이 된다.

꿈은 이루어진다. 2002년 월드컵 구호가 아니다. 자바에 대한 많은 꿈들이 현실로 이루어졌고, 또 많은 꿈들이 실현의 후보로 뛰고 있다. 좀 더 나은 소프트웨어 개발을 향해 우리는 한걸음 한걸음 나아가고 있으며, 그것이 개발자가 할 수 있는 좋은 세상 만들기의 지름길이기도 하다. 자바 2.0은 자바를 통해 그 세상을 열어 가려는 사람들의 염원의 결정체이다. 꿈을 꾸는 당신이 가장 소중하다.

(box)-----------------------------------------------------------------------------------
머스탱과 글래스피시, 과연 내 맘대로 고쳐 쓸 수 있을까?

머스탱과 글래스피시는 각각 자바SE 6와 자바EE 5의 RI를 오픈 소스로 구현한 것이다. 소스가 있으니 내려 받아 빌드도 가능해야 하겠지만 그 복잡한 코드만큼이나 빌드는 만만치 않다.

머스탱은 현재 솔라리스, 리눅스, 윈도우 세 가지 플랫폼을 지원한다. 세 OS를 빼고 개인이 가장 많이 쓴다고 할 수 있는 Mac OS를 지원하지 않는 것이 아쉽지만, 최근에 맥이 인텔로 CPU를 이주하면서 전보다 훨씬 포팅 속도가 빨라진 것은 반가운 일이다. 사실 맥용 자바SE 플랫폼은 애플이 작업하고 있으나 아직 오픈 소스가 아니어서 아쉬움이 남는 부분이기도 하다. 글래스피시처럼 머스탱도 맥을 공식 지원하기를 기대해본다.

머스탱을 윈도우에서 빌드하려면 비주얼 스튜디오(이하 VS) 2003 프로페셔널이 필요하다. VS 프로는 한두 푼 하는 툴이 아닌 탓에 개인이 정식으로 구입하기에는 장벽이 높다. MS에서는 VS 익스프레스 에디션(이하 VSE)이라는 것을 무료로 배포하고 있다. 그래서, 이 익스프레스 에디션의 최신 버전인 VSE 2005 C++를 머스탱 빌드에 쓰려고 하는 시도가 몇몇 열혈지사들을 통해 이루어지고 있다. 조만간 썬에서 머스탱 빌드에 공식적으로 VSE 2005를 지원하면 윈도우 사용자에게 큰 도움이 될 것이다.

이런 사정으로 인해 개인적인 머스탱 개발은 리눅스가 가장 쓰기 편해 보인다. 실제 리눅스를 쓰면 특별히 컴파일러나 추가 패키지를 깔 필요가 거의 없다. 그런데 산 넘어 산인 것이 웬만한 PC에서도 몇 시간씩 걸치는 완전 빌드 과정이다. 이것은 글래스피시도 마찬가지여서, 필자가 쓰는 초강력 파워맥(CPU 코어 4개, 메모리 8기가)에서도 2시간이나 걸릴 정도이다. 물론 모든 모듈을 다 빌드해야할 필요성은 드물겠지만, 이토록 엄청난 규모의 프로젝트를 파악하고 자기 것으로 만드는 데에는 많은 시간과 자원의 투자가 필요한 상황이다.

하지만, 여기에도 반전이 있어 PC의 하드웨어는 눈부시게 발전하고 있다. 64비트, 멀티 코어, SSD(Solid State Disk) 등의 기술이 속속 개인 사용자에게 다가가고 있다. 이제 바야흐로 맞춤형 자바의 시대가 다가오는 것이다.

-----------------------------------------------------------------------------------------

(Box)-----------------------------------------------------------------------------------
Java SE 7와 Java EE 6을 구성할 신기술들


자바 개발자들의 꿈을 현실에 한걸음 다가서도록 만들어 줄 자바SE 7과 자바EE 6의 정확한 로드맵은 아직 나오지 않았지만, 다음과 같은 기술들이 새로이 추가될 것으로 관측되고 있다. 관심 있는 많은 자바 연구자와 개발자의 참여 바란다.

● Java SE 7
 - JSR 277 Java Module System
 - JSR 292 Supporting Dynamically Typed Languages on the Java Platform
 - JSR 294 Improved Modularity Support in the Java Programming Language
 - JSR 295 Beans Binding
 - JSR 296 Swing Application Framework

● Java EE 6
 - JSR 208 Java Business Integration (JBI)
 - JSR 225 XQuery API for Java (XQJ)
 - JSR 235 Service Data Objects (SDO)
 - JSR 283 Content Repository for Java Technology API (JCR) 2.0
 - JSR 286 Portlet Specification 2.0
 - JSR 299 Web Beans

 

출처 : 마소

 
블로그 이미지

시반

시반(詩伴)이란 함께 시를 짓는 벗이란 뜻을 가지고 있습니다. 함께 나눌수 있는 그런 공간이길 바라며...

카테고리

분류 전체보기 (233)
개발 이야기 (73)
WEB2.0 (57)
DB2 (24)
MySQL (6)
오라클 (26)
기타 (44)
취미 (0)
잡담 (2)