반응형

J2SE 5.0에는 새로운 언어 기능이 다수 도입되었는데, 여기에는 제네릭(Generics)과 향상된 루프문(Enhanced for Loop)이 포함된다. 제네릭(GENERIC)향상된 루프문에 대해서는 이미 이전 테크 팁에서 살펴본 바 있다. J2SE 5.0에 추가된 또 다른 중요한 기능으로 주석을 들 수 있으며, 본 팁에서는 J2SE 5.0에 내장된 주석을 살펴보기로 한다.

먼저, 주석이란 무엇인가? JSR 175: A Metadata Facility for the Java Programming Language의 일부로 정의된 주석은 메타데이터를 프로그램 엘리먼트(클래스, 인터페이스, 메소드 등)에 연결하는 방법을 제시해준다. 주석은 해당 엘리먼트에 대해 생성된 바이트코드를 변경하지 않는 추가 수식자(modifier)라고 할 수 있다.

메타데이터를 소스 코드에 도입한다는 개념은 J2SE 5.0에서 처음 등장한 것은 아니다. 사용자가 @deprecated 태그를 메소드의 javadoc 코멘트에 추가하면 컴파일러는 이를 메소드에 관한 메타데이터로 취급하는데, 이런 기능은 J2SE의 1.0 버전부터 포함되었다. 초기 버전의 플랫폼의 경우 이미 Systemgetenv() 메소드(1.1 addendum까지는 Java Language Specification에 포함되어 있지 않았음)와 더불어 구(deprecated) 메소드가 포함되어 있었지만, 현재로서는--적어도 구문의 @ 부분에 관한 한--개념은 거의 동일하다고 볼 수 있다. 단, 위치에는 변동 사항이 생겼다는 점을 알아두기 바란다--주석 태그가 코멘트가 아니라 소스에 포함된다. 여기서 주안점은 주석이 선언적 프로그래밍 모델을 체계적으로 지원하기 위한 방법이라는 사실이다.

이제 J2SE 5.0에 포함된 최초의 주석, @Deprecated부터 살펴보도록 하자. 우선 여기서는 대문자 D에 유의할 필요가 있다. 기능적으로 볼 때, 소스 내의 @Deprecated는 클래스 또는 메소드와 연결된 javadoc 내의 @deprecated와 동일하게 작동한다. 메소드에 @Deprecated 태그를 플래그하면 해당 메소드나 클래스 사용 시 사용자에게 경고 메시지를 보내도록 컴파일러를 환기시키는 효과가 있다.

아래의 Main 클래스는 @Deprecated 주석과 @deprecated 코멘트가 플래그된 deprecatedMethod()라는 이름의 메소드를 가진다.

  public class Main {

/**
* @deprecated Out of date. Use System.foobar() instead.
*/

@Deprecated
public static void deprecatedMethod() {
System.out.println("Don't call me");
}
}

주석이 붙은 클래스도 주석이 붙지 않은 경우와 동일한 방식으로 컴파일한다.

> javac Main.java

예상대로 Main.class가 생성된다.

deprecated 메소드를 사용하면 javadoc 내의 @deprecated 태그를 사용할 때와 마찬가지로 컴파일 시간 경고가 생성된다. 관련 예는 다음과 같다.

  public class User {
public static void main(String args[]) {
Main.deprecatedMethod();
}
}

아래의 클래스를 컴파일하면,

> javac User.java

deprecated 메소드를 사용하는 데 대한 경고 메시지가 다음과 같이 표시된다.

  Note: User.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

컴파일 라인에 -Xlint를 추가하면 무엇이 잘못되었는지 구체적으로 표시할 수 있다.

> javac -Xlint:deprecation User.java

User.java:3: warning: [deprecation] deprecatedMethod() in
Main has been deprecated
Main.deprecatedMethod();
^

1 warning

@deprecated 코멘트에서 @Deprecated 주석으로 변경되더라도 시스템에는 아무런 변화가 생기지 않으며, 단지 작업 수행 방식에 약간의 변화가 발생하는 것 뿐이다. 하지만 J2SE 5.0 플랫폼에 새로 추가된 다른 두 개의 주석, @Override@SuppressWarnings의 경우에는 플랫폼에 새로운 기능을 부여할 수 있다.

@Override 주석은 메소드 선언과 함께 사용할 수 있는데, 이름에서 알 수 있듯이 @Override 주석을 사용하여 수퍼클래스의 메소드를 오버라이드하도록 되어 있는 메소드를 플래그한다. 이 주석은 왜 사용하는 것일까? 오류를 더 신속하게 잡아내기 위해서이다. 아마도 여러분은 메소드를 오버라이드하려다가 메소드 이름의 철자를 틀리거나, 잘못된 인자를 지정하거나, 다른 리턴 타입을 설정했던 적이 무수히 많았을 것이다. 즉, 원래의 의도는 기존의 메소드를 오버라이드하는 것이었는데 그만 새로운 메소드를 정의해 버리는 결과가 종종 발생하게 되는 것이다. @Override를 이용하면 그나마 클래스에서 문제점을 빨리 발견할 수가 있다.

  public class Overrode {
@Override
public int hashcode() {
return 0;
}

@Override
public boolean equals(Object o) {
return true;
}
}

여기서 문제는 메소드 이름이 hashcode가 아니라 hashCode여야 한다는 점이다. 메소드 선언이 훨씬 더 큰 클래스 정의를 위해 소스에 묻혀 버린다고 가정해보자. 최초의 @Override 주석이 없다면, hashCode() 메소드(모두 소문자가 아니라, 대문자로 시작되는 두개의 영단어를 붙여 써서)가 호출되는 대신 상위 Object 클래스의 기본값 동작을 얻고 있다는 사실을 알아차리는 데 얼마나 많은 시간이 걸리겠는가? 하지만 이제는 @Override 주석 덕분에, 클래스 컴파일 시 컴파일 시간 오류가 생성되어 문제점을 경고할 수 있다.

> javac Overrode.java

Overrode.java:2: method does not override a method from its
superclass
@Override
^
1 error

이런 특성의 오류를 좀더 빨리 발견할 수만 있다면 수정에 드는 수고와 비용을 획기적으로 절감할 수 있을 것이다. hashCode() 메소드의 경우 절대로 상수를 리턴해서는 안된다는 점에 유의할 것. hashCode()equals()의 올바른 사용법에 관한 자세한 설명을 보려면 Joshua Bloch가 저술한 Effective Java Programming Language Guide(영문)의 8항을 참조할 것.

J2SE 5.0에 새로 추가된 주석의 마지막 @SuppressWarnings는 그 중에서도 가장 흥미롭다고 할 수 있다. 이 주석은 일반적으로 경고하는 내용을 경고하지 말도록 컴파일러에게 지시하는데, 경고는 일종의 범주에 속하므로 주석에 대해 어떤 종류의 경고를 금지할 것인지 지시해야 한다. javac 컴파일러는 all, deprecation, unchecked, fallthrough, path, serial, finally 등 7개의 금지 옵션을 정의한다. (언어 스펙은 이 중에서 두 가지-- deprecation과 unchecked--만을 정의함.)

예시를 위해 fallthrough 옵션 금지사항에 대해 살펴보기로 한다. 먼저 아래의 클래스를 사용해보자. 클래스에는 switch 문의 각 케이스에 대한 break 문이 빠져 있다는 점에 유의할 것.

  public class Fall {
public static void main(String args[]) {
int i = args.length;
switch (i) {
case 0: System.out.println("0");
case 1: System.out.println("1");
case 2: System.out.println("2");
case 3: System.out.println("3");
default: System.out.println("Default");
}
}
}

javac으로 클래스를 컴파일하면 단순히 .class 파일만 생성되고 경고는 표시되지 않는다는 것을 알 수 있다.

javac Fall.java

컴파일러가 fall through하는(즉, 하나 이상의 break 문이 빠져 있는) switch 문에 관해 경고하기를 원할 경우에는 -Xlint:fallthrough 옵션으로 컴파일한다.

javac -Xlint:fallthrough Fall.java

그러면 아래의 경고문이 생성된다.

Fall.java:6: warning: [fallthrough] possible fall-through into case
case 1: System.out.println("1");
^
Fall.java:7: warning: [fallthrough] possible fall-through into case
case 2: System.out.println("2");
^
Fall.java:8: warning: [fallthrough] possible fall-through into case
case 3: System.out.println("3");
^
Fall.java:9: warning: [fallthrough] possible fall-through into case
default : System.out.println("Default");
^
4 warnings

그러나 switch 문에 각 케이스에 대한 break 문이 빠져 있다는 사실을 무시하고 싶은 경우에는 어떻게 해야 할까? 바로 이 때 @SuppressWarnings 주석이 필요하다. main() 메소드 선언 앞에 다음 행을 추가하면,

@SuppressWarnings("fallthrough")

-Xlint:fallthrough 옵션으로 클래스를 컴파일할 경우,

   javac -Xlint:fallthrough Fall.java

.class 파일만 생성되고 경고문은 표시되지 않는다.

@SuppressWarnings 주석은 또한 컬렉션 엘리먼트의 데이터 유형을 지정하지 않고 컬렉션을 사용할 경우에 표시되는 것과 같은 그 밖의 경고를 금지하는 데도 사용될 수 있다. 그러나 단순히 컴파일 시간 경고를 피하기 위해서 @SuppressWarnings 주석을 사용해서는 안 된다. 제네릭을 염두에 두지 않고 구축된 라이브러리를 사용할 때처럼, 금지되지 않은 경고가 불가피한 경우에 이 주석을 사용하도록 한다.

이것으로 내장 주석 기능에 관한 설명은 마무리하고, 마지막으로 (인자를 포함한) 주석은 통상적으로 하나의 행에서 독자적으로 지정된다는 점에 유의하기 바란다.

J2SE 5.0에 이미 정의되어 있는 주석을 사용하는 것 보다 사용자가 직접 주석을 정의하는 경우에는 더 많은 기능들을 활용할 수 있다. 주석 정의에 관한 자세한 내용은 Annotations(영문)를 참조하기 바란다.

<출처 - http://kr.sun.com/developers/techtips/2006/c0713.html >

반응형
* 강좌를 시작하기전에.....

JDBC를 이용하여 DB를 엑세스해서 프로그래밍을 하는 방법의 기초에 대해서 강의합니다.
따라서 이번 강좌는 JDBC를 처음 사용하고 하는 분들에게 적합한 강좌입니다.


* JDBC 프로그래밍의 5 단계

  1. DriverManager에 해당 DBMS Driver를 등록
  2. 해당 Driver로 부터 Connection 객체 획득
  3. Connection 객체로부터 Statement 객체 획득
  4. Statement의 method를 이용하여 SQL실행
  5. ResultSet 으로 받아서 처리(executeUpdate 의 경우엔 제외)
  6. 객체 close() (ResultSet, Statement, Connection)
그럼 각 단계에 대해서 자세히 알아 보도록 하겠습니다.


* 1. DriverManager에 해당 DBMS Driver를 등록

Driver를 DriverManager에 등록하는 방법은 3가지가 있습니다.
Class.forName() 메소드를 이용하는 방법, Driver객체를 생성하는 방법, registerDriver() 메소드를 이용하는 방법 이렇게 3가지의 방법이 있지만 가장 일반적으로 사용하고 쉬운 방법인 Class.forName() 메소드를 이용하는 방법에 대해서만 설명하도록 하겠습니다.

해당 JDBC 드라이버를 등록하려면 당연히 JDBC 드라이버가 필요합니다. 해당 데이터베이스의 드라이버 파일을 먼저 클래스 패스에 잡으시기 바랍니다. 오라클의 경우는 www.oracle.com 에서 다운로드 받을수도 있고 오라클 설치시 같이 설치 되므로 오라클 디렉토리를 잘 살펴 보기 바랍니다.

다음으로 mySQL의 경우는 http://sourceforge.net/project/showfiles.php?group_id=15923으로 가셔서 다운받으시기 바랍니다.

이제 JDBC 드라이버를 DriverManager에 등록하는 방법에 대해서 알아 보겠습니다.

Class.forName("org.gjt.mm.mysql.Driver"); -> mySQL 의 경우
Class.forName("oracle.jdbc.driver.OracleDriver"); -> Oracle thin 드라이버의 경우

Class 는 java.lang 패키지에 있는 클래스 입니다. 자세한 내용이 궁금하신 분은 api문서를 참고 하시구요... forName 메소드에 parameter로 쓰인 String은 당연히 DBMS의 종류에 따라 틀려지겠지요? 어째든 위의 메소를 실행시키고나면 자동으로 드라이버 객체가 생성이되고, 자신을 DriverManager에 등록하는 처리가 이루어지게 됩니다.


* 2. 해당 Driver로 부터 Connection 객체 획득

Connection이라는 것은 DBMS에 연결된 session을 의미 합니다. Connection 객체를 획득하는 방법은 DriverManager의 getConnection() 메소드를 이용하면 되는데 그 형식은 다음과 같습니다.

Connection con=DriverManager.getConnection(String jdbcURL,String id,String password);
			
getConnection 메소드는 몇가지로 오버로딩되어 있으니 궁금하신분은 API문서를 참고 하십시요..

다음과 같은 형식으로 사용합니다.


String url="jdbc:mysql://localhost/dbname";  // dbname에는 사용하는 database 이름 

Connection con=DriverManager(url,"root","1234");  // root 계정, 패스워드는 1234 


* 3. Connection 객체로 부터 Statement 객체 획득

하나의 Connection으로 부터 여러개의 Statement의 생성이 가능합니다. 이후에 Statement로 부터 SQL을 실행하게 됩니다. 아래의 그림을 보시면 이해가 되리라 생각됩니다.



Connection con=DriverManager.getConnection("jdbc:mysql://localhost/dbname","root","password");
  
Statement st1=con.createStatement();

ResultSet rs1=st1.executeQuery("select * form names");

Statement st2=con.createStatement();

ResultSet rs2=st2.executeQuery("select * form users");

Statement st3=con.createStatement(); 

ResultSet rs3=st3.executeQuery("select * form emails");
따라서 위와 같이 사용이 가능하죠. Statement에는 3가지 종류가 있는데 그중 첫번째가 지금 사용한 Statement 입니다. 두번째로 PreparedStatement 가 있고 마지막으로 CallableStatement가 있다. 그중 CallableStatement는 저장 프로시져 (stored procedure)를 호출하는데 사용되는 것인데, 여기에서는 초급강좌인 만큼 다루지 않습니다.
PreparedStatement는 다음과 같이 사용합니다.

Connection con=DriverManager.getConnection("jdbc:mysql://localhost/dbname","root","password");
  
PreparedStatement ps=con.prepareStatement("update juso set si=? where zip=?); // 밑줄친 부분 주의

ps.setString(1,"Seoul");  // 첫번재 ? 이 1번이 됩니다.

ps.setString(2,request.getParameter("zip")); // 두번째 " ? "

// setString 말고도, setDate, setArray, setCharacterStream등 여러가지가 있습니다.

ps.executeUpdate();
위와같은 쿼리문(값이 변하는)을 Statement로 처리한다면 PreparedStatement 문에 비해서 String 객체를 더 많이 생성해야 해야 하고, PreparedStatement사용시에는 DB에서의 처리시에도(특히 오라클에서) 쿼리문이 캐싱되어 보다 높은 퍼포먼스를 제공하게 됩니다.


* 4. Statement의 메소드를 이용해서 SQL문의 실행

Statement 클래스에서는 쿼리문의 실행을 위해서 몇가지 메소드를 제공하는데, executeQuery()와 executeUpdate() 두개의 메소드만 알면 충분히 활용이 가능합니다.

ResultSet rs=stmt.executeQuery("select * from users");
			
위와 같은 방식으로 사용되며 executeQuery()는 "select"문을 사용할때에만 사용되는 것이죠. 실행 결과는 ResultSet에 저장되며 저장된 정보의 처리 방법은 아래 에서 다루도록 하겠습니다.
또다른 메소드인 executeUpdate()는 select 외에 update,delete,insert 등의 쿼리문을 사용할때 이용되어 집니다. 다음과 같이 사용하시면 됩니다.
stmt.executeUpdate("insert into users(id,password) values('ab','bc')");
			


* 5. ResultSet으로 받아 서 처리하기

위에서 설명했다 시피 executeQuery()를 실행하면 ResultSet 타입의 객체를 반환합니다. 이 객체는 실행된 쿼리문의 결과 같을 가지고 있는데 ResultSet의 몇가지 메소드를 이용하면 ResultSet에 저장된 정보를 사용할수 있습니다. select쿼리를 실행 했으므로 하나이상의 row를 지니고 있는데.. 현재 row에서 다음 row로 넘기려면 rs.next() 와 같이 사용하면 됩니다.(첫번째 row도 next()를 사용해야 합니다.)

각 row가 선택이 되었으면 각컬럼의 데이터를 불러 오면 되는데, 두가지의 방법이 있습니다.
첫번째방법은

String name=rs.getString(1); (name에 해당하는 항목의 첫번재 컬럼에 있을경우)

이와같이 데이터의 컬럼 위치로 검색합니다. getString의 String 에는 Int,Double, 등과 같은 자료형을 사용하면 해당 자료형에 맞는 데이터를 리턴하여 줍니다. (getString(), getInt(), getDouble() 등등....)
또다른 방법은 컬럼의 이름을 직접 서술하는 방법인데, 다음과 같이 사용됩니다.

String name=rs.getString("name");

이러게 하면 rs ResultSet에 저장되 었던 name컬럼의 데이터가 name에 저장됩니다.


* 6. 객체 close() 하기 (ResultSet, Statement, Connection)

Java는 메모리 관리를 따로 하지 않아도 자체적으로 garbage collection 기능이 있어 사용되지 않는 객체는 자동적으로 처리된다. 그러나, Sun은 JDBC 드라이버와 같이 외부 지원되는 드라이버에 대해서는 생성된 객체를 코드 내에서 소멸시키도록 권장하고 있다. JDBC API는 이를 위해 close() 메쏘드를 제공하는데, close() 메쏘드를 사용해야 할 클래스는 Connection, Statement, PreparedStatement, ResultSet 등이다. 따라서, 데이터베이스에 접속하여 SQL 구문을 모두 수행했다면 다음처럼 close() 메쏘드를 사용하여 객체를 모두 소멸시키도록 한다. - webdox

rs.close();
st.close();
con.close();

위와 같이 사용하며, con (Connection) 과 st (Statement or PreparedStatement)는 반드시 close 되어야 합니다. 위에서 설명한 바와 같이 Statement 와 ResultSet 은 1:1 대응 이기 때문에 ResultSet 은 Statement 가 소멸되면 자동으로 소멸됩니다. (아예 처리를 하지 말라는 얘기는 아닙니다. -안전을 위해서)


* 그리고....

대부분의 Servlet/JSP 웹 애플리케이션 에서는 Connection 을 Pool 로서 관리를 하는데, 이는 매 요청시 마다 데이터베이스에 접속하고 해제하는 일을 수행하는데 있어 발생하는 부하를 줄여 주고, 일정 갯수 이상의 커넥션수를 넘지 않도록 유지시켜 주기 위해서 입니다.
이러한 Connection Pool 은 몇가지 공개된것도 있고 직접 제작할수도 있으나, 여기 에서는 생략하도록 하겠습니다.

<출처- http://www.aboutjsp.com/lec/jdbc.jsp >

반응형

자바에서 각 이벤트는 하나의 이벤트 타입에 속하게 됩니다.

사용자가 JButton 컴포넌트를 클릭할 때 발생하는 actionPerformed 이벤트는 ActionEvent라고 하는 이벤트 타입에

속합니다. 여기에서 이벤트 타입은 EventObject 클래스에서 파생되는 클래스로 정의됩니다.

그리고 각 이벤트 타입에 속하는 모든 이벤트는 리스너 인터페이스의 멤버가 됩니다.

예를 들어 ActionEvent 이벤트 타입에 속하는 actionPerformed 이벤트는 ActionListener라고 하는 리스너

인터페이스의 멤버가 되는 것이지요.

 

결국 하나의 이벤트 타입에 대하여 하나의 리스터 인터페이스가 있는 셈입니다.

 

Interface

Class

ActionListener

ActionEvent

AdjustmentListener

AdjustmentEvent

AWTEventListener

AWTEvenetListenerProxy

ComponentListener

ComponentAdapter

ContainerListener

ComponentEvent

FocusListener

ContainerAdapter

HierachyBoundsListener

ContainerEvent

HierachyListener

FocusAdapter

InputMethodListener

FocusEvent

ItemListener

HierachyBoundsAdapter

KeyListener

HierachyEvent

MouseListener

InputEvent

MouseMotionListener

InputMethodEvent

MouseWheelListener

InvocationEvent

TextListener

ItemEvent

WindowFocusListener

KeyAdapter

WindowListener

KeyEvent

WindowStateListener

MouseAdapter

 

MouseEvent

 

MouseMotionAdapter

 

MouseWheelEvent

 

PaintEvent

 

TextEvent

 

WindowAdapter

 

WindowEvent

 

 

하나의 리스너 인터페이스에 여러 메소드 즉, 여러 이벤트가 포함될 수 있습니다.

예를 들어 창과 관련된 이벤트에서는 windowActivated, windowClosed, windowClosing, windowDeactivated,

windowDeiconified windowIconified, windowOpened 등이 있으며, 이들 이벤트는 모두 windowListener라고 하는

리스너 인터페이스의 멤버가 됩니다.

 

이러한 리스너 인터페이스를 구현하는 클래스를 어댑터(Adapter)라고 하며, 어댑터의 인스턴스를 리스너라고 합니다.

그리고 리스너 인터페이스를 구현하고 있는 리스너를 이벤트 소스에 등록시켜야 합니다.

이벤트 소스란 이벤트를 발생시키는 객체입니다.

public class MyPanel extends Jpanel implements ActionListener{

public void actionPerformed(ActionEvent e){

JoptionPane.showMessageDialog(

this,

getSource().ToString() + “ 를 클릭했습니다.”,

“actionPerformed 이벤트”,

JoptionPane.OK_OPTION);

}

public MyPanel(){

jButton = new Jbutton(“클릭하세요.”);

jButton.addActionListener(this);

}

}

 

public class MyPanel extends Panel{

ActionAdapter actionAdapter = new ActionAdapter();

public MyPanel(){

jButton = new Jbutton(“클릭하세요.”);

jButton.addActionListener(actionAdapter);

}

}

 

class WindowClosing extends WindowAdapter{

    public void windowClosing(WindowEvent e){

        System.exit(0);

    }

}

 

class MyFrame extends JFrame{

    public MyFrame(){

        WindowClosing listener = new WindowClosing();

        addWindowListener(listener);

    }

}

 

class MyFrame extends JFrame{

    public MyFrame(){

        addWindowListener(new WindowAdapter(){

            public void windowClosing(WindowEvent e){

                System.exit(0);

            }

        });

    }

}

 

* “예전에 누가 Adapter를 사용하면 리스너에 있는 method()를 모두 구현하지 않아도

되며 필요한 method()구현하면 된다고 했었는데, 사실은 Adapter 클래스를

상속받아서 새로운 리스너를 구현하는 것입니.”

반응형

Method Categories and Interface

 

Category

Interface Name

Methods

Action

ActionListener

actionPerformed(ActionEvent)

Item

ItemListener

itemStateChanged(ItemEvent)

Mouse

MouseListener

mousePressed(MouseEvent)

mouseReleased(MouseEvent)

mouseEnterd(MouseEvent)

mouseExited(MouseEvent)

mouseClicked(MouseEvent)

Mouse Motion

MouseMotionListener

mouseDragged(MouseEvent)

mouseMoved(MouseEvent)

Key

KeyListener

keyPressed(KeyEvent)

keyReleased(KeyEvent)

keyTyped(KeyEvent)

Focus

FocusListener

focusGained(FocusEvent)

focusLost(FocusEvent)

Adjustment

AdjustmentListener

adjustmentValueChanged(AdjusmentEvent)

Component

ComponenetListener

componentMoved(ComponentEvent)

componentHidden(ComponentEvent)

componentResized(ComponentEvent)

componentShown(ComponentEvent)

Window

WindowListener

windowClosing(WindowEvent)

windowOpened(WindowEvent)

windowIconified(WindowEvent)

windowDeiconified(WindowEvent)

windowClosed(WindowEvent)

windowActivated(WindowEvent)

windowDeactivated(WindowEvent)

Container

ContainerListener

componentAdded(ContainerEvent)

componentRemoved(ContainerEvent)

Text

TextListener

textValueChanged(TextEvent)

 

 

 

 

 

AWT Components

 

Component Type

Description

Button

A named rectangular box used for receiving mouse clicks

Canvas

A panel used for drawing

Checkbox

A component allowing the user to select an item

ChekboxMenuItem

A checkbox within a menu

Choice

A pull-down static list of items

Component

The parent of all AWT components, except menu components

Container

The parent of all AWT containers

Dialog

The base class of all modal dialog boxes

Frame

The base class of all GUI windows with window manager controls

Label

A text string component

List

A component that containers a dynamic set of items

Menu

An element under the menu bar, which containers a set of menu items

MenuItem

An item within a menu

Panel

A basic container class used most often to create complex layouts

Scrollbar

A component that allows a user to “select from a range of values”

ScrollPane

A container class that implements automatic horizontal and vertical

Scrolling for a single child component

TextArea

A component that allows the user to enter a block of text

TextField

A component that allows the user to enter a single line of text

Window

The base class of all GUI windows, without window manager controls

  

Component Events

 

Component Type

Act

Adj

Cmp

Cnt

Foc

Itm

Key

Mou

MM

Text

Win

Button

V

 

V

 

V

 

V

V

V

 

 

Canvas

 

 

V

 

V

 

V

V

V

 

 

Checkbox

 

 

V

 

V

V

V

V

V

 

 

CheckboxMenuItem

 

 

 

 

 

V

 

 

 

 

 

Choice

 

 

V

 

V

V

V

V

V

 

 

Component

 

 

V

 

V

 

V

V

V

 

 

Container

 

 

V

V

V

 

V

V

V

 

 

Dialog

 

 

V

V

V

 

V

V

V

 

V

Frame

 

 

V

V

V

 

V

V

V

 

V

Label

 

 

V

 

V

 

V

V

V

 

 

List

V

 

V

 

V

V

V

V

V

 

 

MenuItem

V

 

 

 

 

 

 

 

 

 

 

Panel

 

 

V

V

V

 

V

V

V

 

 

Scrollbar

 

V

V

 

V

 

V

V

V

 

 

ScrollPane

 

 

V

V

V

 

V

V

V

 

 

TextArea

 

 

V

 

V

 

V

V

V

V

 

TextField

V

 

V

 

V

 

V

V

V

V

 

Window

 

 

V

V

V

 

V

V

V

 

V

 

Act – ActionListener

Adj – AdjustmentListener

Cmp – ComponentListener

Cnt – ContainerListener

Foc – FocusListener

Itm – ItemListener

Key – KeyListener

Mou – MouseListener

MM – MouseMotionListener

Text – TextListener

Win – WindowListener

 

반응형
IBM 소프트웨어 엔지니어 (마음은 게이머)인 Scott Clee가 테트리스 게임 모델을 재사용 가능한 자바 빈 컴포넌트로 포장하는 간단한 방법을 소개한다. 일단 게임의 구성 요소들이 자바 객체들로 나누어지면 완전한 게임 모델 빈을 형성하도록 재조립될 수 있고, 실제로 어떤 테트리스 GUI에도 결합될 수 있다. forum에서 이 글에 대한 여러분의 생각을 저자와 다른 독자들과 공유하기 바란다.

-참조 : http://www.ibm.com/developerworks/kr/library/j-tetris/

테트리스 게임의 구성 요소들을 자바 객체로 나누어 재사용 가능한 자바 게임 컴포넌트로 만드는 방법

한 친구가 자신은 새로운 프로그래밍 언어를 배울 때마다 그 언어를 사용해 테트리스 게임을 작성하는데 도전한다고 말한 적이 있다. 이러한 전통하에, 내가 처음 자바 언어를 사용한 프로그래밍 법을 배웠을 때 나도 같은 시도를 해 보기로 했다. 나의 첫번째 시도는 하나의 완전한 게임이긴 했지만 매우 간단하고 흉한 것이었다. 시간이 가고 내가 자바 설계와 개발에 더 많은 경험을 얻음에 따라 나는 GUI에서 게임 모델을 분리시켜 (Swing 컴포넌트와 유사하게) 테트리스 빈을 만들 수 있다는 사실을 알았다. 그래서 나는 그것을 해 보기 시작했다.

이 글에서 나는 테트리스 빈을 구축하고 구현하는 방법을 안내하겠다.

테트리스는 자바 객체로 표현될 수 있는 몇 개의 구성 요소를 가지고 있다.:

  • The Tetris pieces 테트리스 조각
  • 조각을 가지고 있는 테트리스 판
  • 판 위의 조각 제어, 점수 관리 등을 하는 게임

이 요소들 각각을 좀 더 자세히 살펴보자.

  • 각 요소는 정확히 네 개의 블록으로 구성되어 있다.
  • 조각 내의 각 블럭은 테트리스 판 내에 (x.y) 좌표를 가지고 있다.
  • 각 조각은 0, 2, 4의 회전율을 가지고 있다.
  • 각 조각은 L, J, S, O, I, Z, 혹은 T 모양을 지닐 수 있다.


그림 1. 각 테트리스 조각에 네 요소가 필요함 : 블록, (x,y) 좌표, 회전율 및 모양
Four elements of each Tetris piece
테트리스 빈으로 무엇을 할 수 있을까?

다음은 테트리스 빈을 만든 후 내가 수행한 몇 가지 일들이다.
  • 빈의 두 인스턴스를 연결시켜 대결 게임을 만들다.
  • Netris라는 이름을 가진, 최초의 Psion Netpad용 테트리스 게임을 만들다
  • 빈을 애플릿에 통합시켜 브라우저와 호환되는 테트리스 게임을 만들다.

첫번째 두 아이템은 매우 간단한 시스템을 사용해 구현될 수 있다. 각 조각에 대해 중앙 블록을 선택하고 이 블록의 (x,y) 좌표를 저장하면, 조각 내의 나머지 블록들을 이 블록을 중심으로 한 상대 좌표로 저장할 수 있다. 이 방식은 중앙 조각에 대한 상대 블록으로 모양을 저장함으로써 조각의 어떤 형태도 기술할 수 있게 해준다. 중앙 지점은 java.awt.Point로 저장될 수 있고 상대 좌표는 java.awt.Point 배열에 저장될 수 있다. 좌표 계산을 쉽게 하기 위해, 중앙 조각을 (0,0) 상대 좌표를 가진 블록으로 저장할 수 있다.

이 시스템은 또한 조각의 회전을 계산할 때 더 쉽다. 간단한 행렬 조작을 사용하여 단순히 y좌표를 x 좌표로 바꾸고 x 좌표는 y 좌표의 마이너스 값으로 바꾸면 조각을 시계방향으로 90도 회전시킬 수 있다. 우리는 중앙 지점을 중심으로 상대 좌표를 사용하고 있기 때문에, 여기에서도 마찬가지로 할 수 있다:



temp = x;
x = -y;
y = temp;

여러분은 또한 시계방향 회전을 3번 적용하면 조각을 시계 반대 방향으로 90도 회전시킬 수 있다. (믿지 못하겠다면 한 번 해보기 바란다.)

마지막으로, 모든 조각이 동일한 회전율을 가지는 것은 아니므로, 이 문제를 보충하기 위해 회전 기법을 체크해 보아야 할 것이다.

우리는 모든 유형의 조각을 표시하는데 동일한 TetrisPiece 클래스를 사용하기 때문에 이들을 구분할 방법이 필요하다. 이를 위해 우리는 몇 개의 정적인 int 생성자를 사용하여 다른 유형들을 표시하고 지역 변수가 조각의 유형을 저장하도록 한다. 다음은 이 생성자들 중 하나의 예이다:



public static final int L_PIECE = 0;

조각을 테트리스 판 위에서 이동시킬 것이므로 이를 위한 이동 메소드를 제공해야 한다. 몇 가지 이동 (이미 가능한 한 제일 오른쪽 끝에 와 있는데 다시 오른쪽으로 가려는 시도)은 불법적일 수 있다. 따라서 우리는 모든 이동 요청을 확인할 필요가 있다. 우리는 참조를 저장할 테트리스 판에서 이것을 구현할 것이다. 따라서 우리 클래스의 생성자는 여기에서 두 가지 매개 변수를 가질 것이다: 첫번째는 만들어진 조각의 유형이고, 두번째는 테트리스판에 대한 참조이다. 생성자에서 우리는 initalizeBlocks()이라는 private 유틸리티 메소드를 호출할 것인데, 이 메소드는 조각의 상대 좌표값을 각각의 조각 유형에 설정할 것이다.

이동이 합법적인지를 체크하는 간단한 방법은 판에서 조각을 떼내어 원하는 방향으로 이동시킨 후 맞는지 보는 것이다. 맞으면 조각을 보드의 새 위치에 둔다. 그렇지 않으면 이동을 취소하고 원래 있던 자리에 다시 둔다. 반환되는 값은 그 결과에 따라 true (이동이 맞으면)나 false(이동이 맞지 않으면)가 될 것이다.

좀 더 주의를 기울여야 할 이동의 한 유형은 조각이 떨어지는 경우이다. 떨어진다는 것은 조각이 판의 제일 아래쪽으로 바로 내려간다는 의미이다. 이를 위해 우리는 더 이상 움직일 수 없을 때가지 조각을 아래로 계속 이동시키는 while 루프가 필요하다. 그러면 조각은 그 위치에 배치될 것이다.

조각에 적용될 수 있는 다양한 이동들을 구별하기 위해 우리는 다음 예제에 나타난 것과 같이 몇 가지 추가적인 static int 생성자를 사용할 것이다.:



public static final int LEFT = 10;

조각이 맞는지 보기 위해 나중에 willFit() 메소드가 TetrisBoard 클래스에서 구현된 것이다.

마지막으로 TetrisPiece 클래스를 포장하기 위해 우리는 중앙 지점과 상대 좌표와 같은 몇 가지 변수에 대한 getters와 setters, 그리고 무작위 유형의 TetrisPiece 인스턴스를 반환할 getRandomPiece()라는 정적인 메소드를 가진다.

TetrisPiece 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

테트리스 판은 빈 블록과 색깔 있는 블록을 가지고 있는 2D 격자판으로 생각할 수 있다. 다양한 유형의 테트리스 조각들이 int 생성자에 의해 구별되기 때문에, 우리가 해야 할 일은 빈 블록의 값을 정의하는 것 뿐이고 우리는 판을 2D int 배열로 저장할 수 있다. 이 방식을 사용하면 판 내의 빈 블록은 다음에 의해 표시될 것이다.:



public static final int EMPTY_BLOCK = -1;

유연성을 유지하기 위해 판의 크기를 가변적으로 하겠지만 이것을 생성자 내에 정의할 것이다. 따라서 생성자는 열과 행의 수를 나타내는 두 ints를 받아들일 것이다. 그리고 나서 2D 배열 내의 모든 값을 기본적으로 빈 블록으로 만드는 resetBoard() 메소드를 호출할 것이다.

조각들이 판에 추가되고 제거되기 때문에 우리는 addPiece()removePiece()메소드를 제공한다. addPiece() 메소드는 TetrisPiece()를 취하고 판에서 이 메소드가 차지하는 모든 위치의 값을 자신의 유형으로 설정함으로써 작동한다. removePiece() 메소드는 판의 값이 빈 블록의 값으로 설정된다는 점을 제외하면 비슷하다.

판에 변화가 있을 때 사용자가 알 수 있도록 하기 위해, 조각이 추가되거나 이동되었을 때 BoardEvent가 구동될 것이다. 이 이벤트를 듣는 클래스들에 대해 우리는 BoardListener 인터페이스가 필요한데, 이벤트가 구동되었을 때 이 인터페이스의 boardChange()메소드가 호출된다. 이 이벤트들은 화면 수정이 필요할 때 통지되도록 테트리스 판 GUI에 의해 사용될 수 있다. listener를 저장하기 위해 우리는 java.util.Vector를 사용할 것이고, listener를 추가/삭제하고 이벤트를 구동시키기 위한 관련 메소드를 제공할 것이다.

때때로 여러분이 조각을 추가하고 삭제할 때 BoardEvents를 구동시키는 것이 부적절할 수 있다. 조각이 떨어져야 할 때 (이 이동은 조각을 떨어뜨리기 위해 while 루프를 사용한다는 것을 기억하라)를 예로 들 수 있다. 이 경우 조각이 바닥에 부딪쳤을 때만 이벤트가 필요하다. 이를 용이하게 하기 위해 우리는 boolean 매개변수를 취하도록 addPiece() 메소드를 만들어 값이 true일 경우에만 이벤트가 구동되도록 할 것이다.

테트리스 게임의 중요 요소 중 하나는 한 행이 완료되면 그 행은 사라지고 그 위의 모든 행들이 내려온다는 것이다. 이를 위해 우리는 삭제될 행의 지수를 매개변수로 취하는 removeRow() 메소드를 제공할 것이다. 행이 없어진 후에 BoardEvent가 구동될 것이다.

private 변수들에 접근하기 위해 필요한 getter와 setter들 외에도, 우리는 하나의 메소드가 더 필요하다. 앞에서 설명한 willFit()가 그것이다. 이 메소드는 TetrisPiece를 매개변수로 취해 그 조각이 판에 맞는지 결정하기 위한 boolean 값을 돌려준다. 맞는다는 것은 그 조각이 판의 경계 안에 있고 판에서 그 조각이 맞춰질 곳에 있는 블록의 값이 비어 있다고 설정되어 있음을 의미한다. 이런 경우 true 값이 반환된다.

이제 TetrisBoard 클래스가 완성되었다.이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드받을 수 있다.

100 피트 벽을 가진 테트리스?

어느날 나는 이 빈을 타워 블록의 점등 시스템에 연결시키고 빌딩의 측면을 따라 내려가면서 테트리스 게임을 하고 싶어졌다. 나는 누군가가 이렇게 했다는 것을 신문에서 읽은 후 내내 이것을 하고 싶어해 왔다.

이제 테트리스 게임에서 사용되는 두 개의 주 컴포넌트를 만들었으므로, 이들을 모아 게임 로직을 만들면 된다.

게임의 흐름을 제어하기 위한 좋은 방법은 이것을 java.lang.Thread를 확장하는 내부 클래스에 내장시키는 것이다. 이 방식의 한 가지 장점은 게임 속도를 제어하기 위해 스레드 sleep 호출을 추가할 수 있다는 것이다. 또 다른 장점은 현재 주 애플리케이션 스레드가 자유롭기 때문에 하나의 GUI가 첨부될 때 색칠 문제가 없어진다는 것이다. 이 문제는 주 스레드가 계속 묶여 있어 색칠할 시간이 없을 때 때때로 발생할 수 있다.

스레드 내의 로직은 run() 메소드 내의 while 루프 속에 구현될 것이다. 루프는 계속해서 조각을 만들어 내고 조각을 더 이상 맞출 수 없을 때까지 이들을 게임판으로 떨어뜨릴 것이다. 이 때 fPlaying이라는 지역 boolean 변수가 false로 설정되어 루프를 끝내고 GameEvent를 구동시켜 게임이 종료되었음을 표시할 것이다.

while 루프 내에 fPaused의 boolean 값을 체크하는 if 절이 있다. 이 값이 true로 설정되었을 경우 루프는 계속 실행되겠지만 모든 게임 로직이 무시되어 종료되는 느낌을 줄 것이다. Boolean이 false로 다시 바뀌면 게임이 계속될 것이다.

우리는 한 번에 하나씩 떨어지는 조각에만 관심을 가지고 있으므로, 여기에 대한 참조를 저장할 fCurrPiece라는 변수를 만들 것이다. 이 변수가 null 값으로 설정되면 이전의 조각이 더 이상 아래로 내려갈 수 없으며 판의 최종 위치에 도착했음을 의미한다. 이 때 우리는 새 조각을 만들어 판의 맨 위 중앙에 둔다. fCurrPiece 변수가 null값이 아닌 모든 경우에 우리가 해야 할 일은 그 것을 한 위치로 떨어뜨리고 주어진 시간 동안 스레드를 휴면 상태로 만드는 것이다.

한 조각이 더 이상 움직일 수 없게 되었을 때 우리는 행이 완성되었는지 보아야 한다. 이를 위한 손쉬운 방법은 한 쌍의 중첩 for 루프를 사용하는 것이다. 이 for 루프의 바깥 쪽 루프는 행의 지수를 따라 작업하며, 안쪽의 루프는 지수 전체에 걸쳐 확인 작업을 수행한다. 만일 우리가 완성된 행을 발견하면, TetrisBoard 클래스에 구현된 removeRow() 메소드를 호출하며 완성된 행의 지수를 전달할 수 있다. 이제 제거된 행 위의 모든 행들이 하나씩 내려올 것이기 때문에 우리는 이들을 다시 체크해야 할 것이다. 여러 행을 한번에 완성하도록 장려하기 위해 우리는 완성된 행의 개수를 저장하고 각각 더 높은 점수를 줄 것이다.

테트리스 게임의 또 다른 주요 요소는 더 많은 행이 완성될수록 조각이 더 빨리 내려온다는 것이다. 이 기능은 지금까지 완성된 행의 개수를 체크하고 이에 따라 스레드의 휴면 주기를 점차 감소시켜가는 방식으로 구현될 수 있다.

GameThread 내부 클래스를 만들기 위해 필요한 것은 이것이 전부지만, 구현해야 할 또 다른 내부 클래스가 있다. 다수의 이벤트가 구동될 것이고 이들은 listerners가 저장되도록 요구할 것이므로, 이들을 모두 한 장소에 두는 것이 좋을 것이다. 우리는 EventHandler 내부 클래스를 사용하여 이를 수행할 것이다.

EnvetnHandler 내부 클래스
이 클래스는 우리가 구동하는 이벤트에 관심이 있는 listener들에 대한 참조를 저장할 것이다. listener들에 대한 addremove 메소드를 제공할 뿐 아니라 이벤트들을 구동하기 위한 유틸리티 메소드도 있을 것이다.

이 클래스는 다음 유형의 이벤트들을 다룬다. :

  • GameEvent : 게임이 시작되거나 멈출 때마다 구동된다. 게임 START 혹은 END를 표시하기 위한 값을 가지고 있다.

  • BoardEvent : 게임판에 변경 사항이 있을 때 구동된다. EventHandler 클래스에서 추가/삭제 listener 호출이 TetrisBoard 클래스로 전달된다.

  • ScoreEvent: 점수가 바뀔 때 구동된다.

구동될 수 있는 많은 다른 유형의 이벤트들이 있지만, 간편성을 위해 나는 위에서 설명한 이벤트들만 사용하였다. 우리가 구현할 수 있는 다른 이벤트에는 LineEvent가 있는데, 한 행, 혹은 여러 행이 완성되었을 때 구동되고 화면 애니메이션을 일으키는데 사용될 수 있다.

TetrisGame 마무리하기

이제 내부 클래스들을 완성하였으므로 TetrisGame 클래스의 나머지 부분을 설명해야 한다. 모든 자바 빈과 마찬가지로 우리는 매개 변수 없는 생성자가 필요하다. 이 생성자에서 우리는 EventHandler 클래스와 TetrisBoard 클래스의 인스턴스를 만들 것이다. TetrisBoard 클래스 10x20이라는 기본 사이즈를 가질 것이다.

게임 상태를 제어하기 위해 우리는 개시, 중지, 일시 중지 메소드를 사용할 것이다. startGame() 메소드는 모든 게임 변수를 리셋하고 ScoreEvent (이제 0으로 리셋됨)와 GameEvent (START라는 매개 변수 유형을 가짐)를 구동시킬 것이다. 또한 GameThread를 생성하여 개시할 것이다. stopGame() 메소드는 fPlaying 변수를 false로 바꾸어 GameThread가 끝나도록 하고 END라는 매개변수 유형으로 GameEvent를 구동시킨다. setPause() 메소드는 한 게임을 일시 중지시키는 역할만 한다.

필요한 모든 getters와 setters와 별도로, 구현할 메소드가 하나 더 있는데, move() 메소드가 그것이다. 이 메소드는 이동 방향을 매개변수로 취하는데, 이것은 TetrisPiece 클래스에서 나오는 생성자이다. move() 메소드는 게임이 진행중이며 일시 중지 상태가 아니라고 가정하고 이동하려고 시도한다. 그 이동이 아래로 떨어지는 요청인데 성공하지 못한다면 fCurrPiece가 null 값으로 설정되고 조각은 게임판 내의 현재 위치에 남아 있을 것이다. 그러면 GameThread는 새로운 조각을 생성한다.

TetrisGame에 대해서는 이게 전부이다. 이 클래스를 포함한 완성된 소스를 참고 자료에서 다운로드 받을 수 있다.

이제 완벽함을 위해 우리는 BeanInfo 클래스를 만들고 이 클래스들을 적절한 파일들로 채울 수 있지만, 여기에서는 그럴 필요가 없다. 우리가 필요한 것은 우리의 빈을 테스트할 간단한 GUI이고 참고 자료에 하나가 제공되고 있다. 이것은 테트리스 게임판을 그리기 위해 한 개의 내부 클래스를 사용하고 키 조작을 조정하기 위한 몇 가지 로직을 포함하고 있는 간단한 클래스이다. GUI는 javax.swing.JFrame에 표시되며, 선택적으로 java.applet.Applet이 될 수도 있다.

우리의 테트리스 빈을 테스트하기 위해 소스 파일을 푸는데, 디렉토리 구조를 그대로 두기 바란다. (왜냐하면 내가 빈 클래스들이 TetrisBean 디렉토리에 있도록 이들을 TetrisBean 패키지에 두었기 때문에 때문이다.) 여러분이 소스 파일을 푼 경로를 자바 클래스 경로에 추가하고 파일을 컴파일한다. 이제 여러분이 해야 할 일은 "java Scottris"를 실행시키는 것 뿐이다.

나는 이러한 테트리스 빈을 구현할 수 있는 많은 방법이 있다는 것을 알고 있다. 내가 이 글에서 소개한 것은 아주 간단한 방법이며, 이것이 여러분의 창조력에 불을 붙일 수 있기를 바란다. 나는 여러분이 이것을 자유롭게 개선시키기 바란다.




위로


참고자료




위로


필자소개

Photo of Scott Clee

Scott Clee는 현재 IBM의 CICS 제품에 대한 FV Tester로 일하고 있다. 4년간 자바 프로그래머로 일했으며 자바와 관련된 재미있는 프로젝트를 취미 삼아 수행하는 것을 즐긴다.





위로
반응형

저자 JZ Ventures사의 사장 겸 대표 컨설턴트인 John Zukowski

이 아티클의 영문 원본은
http://java.sun.com/mailers/techtips/corejava/2007/tt0907.html#2
에서 볼 수 있습니다.

이 팁은 Java SE 6을 사용하여 작성되었습니다. 이번 및 향후 테크팁을 사용하기 위해 Java Platform, Standard Edition 6 Development Kit(JDK 6)을 Java SE 다운로드 페이지에서 다운로드할 수 있습니다.

선호 설정(Preferences) API는 표준 플랫폼 1.4버전에 도입된 직후 2003년 7월 15일자 영문기사  선호 설정API에서 맨 처음 다뤄진 바 있다.

이 기사에서는 사용자별 선호 설정을 가져오고 설정하는 방법에 대해 설명했다. 선호 설정 API는 사용자별 설정을 가져오고 설정하는 것에 국한되지 않는다. 시스템 선호 설정, 선호 설정 가져오기 및 내보내기, 선호 설정과 연결된 이벤트 알림도 있다. 선호 설정을 저장하기 위한 사용자 정의 위치를 제공하는 방법도 있다. 언급한 처음 세 옵션에 대해 여기서 설명한다. 사용자 정의 기본 선호 팩토리 만들기는 이후의 팁에서 다루기로 한다.

시스템 선호 설정

선호 설정 API는 서로 다른 두 가지 선호 설정 집합을 제공한다. 첫 번째 집합은 개별 사용자용으로서, 동일한 시스템의 여러 사용자가 서로 다른 설정을 정의할 수 있게 한다. 이를 사용자 선호 설정이라고 한다. 동일한 시스템을 공유하는 각 사용자는 자신의 고유한 값 집합을 선호 설정의 그룹과 연결할 수 있다. 사용자 비밀번호, 시작 디렉토리 등이 그 예이다. 한 시스템의 모든 사용자가 동일한 비밀번호와 홈 디렉토리를 갖는 것은 바람직하지 않다. 독자들도 그렇게 생각하리라 기대한다.

또 다른 선호 설정 형태는 시스템 유형이다. 한 시스템의 모든 사용자가 동일한 시스템 선호 설정 집합을 공유한다. 예를 들어, 설치된 프린터의 위치는 일반적으로 시스템 선호 설정이다. 굳이 사용자별로 다른 프린터 집합을 설치할 필요는 없다. 동일한 시스템을 사용하는 사람이라면 그 시스템을 기준으로 구별되는 모든 프린터를 알고 있을 것이다.

시스템 선호 설정의 또 다른 예로 게임 고득점이 있다. 전체 고득점은 오로지 하나만 존재해야 한다. 여기서 시스템 선호 설정이 사용될 수 있다. 이전 팁에서 userNodeForPackge() 및 그에 이어 userRoot()가 사용자 기본 설정 노드를 가져오는 데 어떻게 사용되는지 확인했다면, 다음 예제에서는 systemNodeForPackage() 또는 루트용 systemRoot()를 사용하여 시스템 기본 선호 트리 중 알맞은 부분을 가져오는 방법을 보여 준다. 알맞은 기본 선호 노드를 가져오는 메소드 호출과 달리 API 사용법은 동일하다.

이번 예제는 간단한 게임이므로 여기서는 게임 용어를 엄격하지 않게 사용하기로 한다. 이 게임에서는 0 ~ 99 범위에서 임의의 수를 선택한다. 그 수가 이전에 저장된 값보다 높으면 "high score"를 업데이트한다. 또한 이 예제에서는 현재의 고득점을 보여 준다. 선호 설정 API 사용법은 간단한 편이다. 여기서는 getSavedHighScore()를 사용하여 저장된 값을 가져오고, 아직 저장된 고득점이 없으면 기본값인 -1을 제공하며, updateHighScore(int value)를 사용하여 새로운 고득점을 저장한다. HIGH_SCORE 키는 새로운 선호 설정 API 액세스에서 공유하는 상수이다.

private static int getSavedHighScore() {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    return systemNode.getInt(HIGH_SCORE, -1);
  }

  private static void updateHighScore(int value) {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    systemNode.putInt(HIGH_SCORE, value);
 }



전체 프로그램은 다음과 같이 된다.

import java.util.*;
import java.util.prefs.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class High {
  static JLabel highScore = new JLabel();
  static JLabel score = new JLabel();
  static Random random = new Random(new Date().getTime());
  private static final String HIGH_SCORE = "High.highScore";

  public static void main (String args[]) {
    /* -- Uncomment these lines to clear saved score
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    systemNode.remove(HIGH_SCORE);
    */

    EventQueue.invokeLater(
      new Runnable() {
        public void run() {
          JFrame frame = new JFrame("High Score");
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          updateHighScoreLabel(getSavedHighScore());
          frame.add(highScore, BorderLayout.NORTH);
          frame.add(score, BorderLayout.CENTER);
          JButton button = new JButton("Play");
          ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              int next = random.nextInt(100);
              score.setText(Integer.toString(next));
              int old = getSavedHighScore();
              if (next > old) {
                Toolkit.getDefaultToolkit().beep();
                updateHighScore(next);
                updateHighScoreLabel(next);
              }
            }
          };
          button.addActionListener(listener);
          frame.add(button, BorderLayout.SOUTH);
          frame.setSize(200, 200);
          frame.setVisible(true);
        }
      }
    );
  }

  private static void updateHighScoreLabel(int value) {
    if (value == -1) {
      highScore.setText("");
    } else {
      highScore.setText(Integer.toString(value));
    }
  }

  private static int getSavedHighScore() {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    return systemNode.getInt(HIGH_SCORE, -1);
  }

  private static void updateHighScore(int value) {
    Preferences systemNode = Preferences.systemNodeForPackage(High.class);
    systemNode.putInt(HIGH_SCORE, value);
 }
}


그리고 몇 번의 실행 후 화면은 다음과 같이 된다. 61점이 그다지 높은 점수가 아니더라도 고득점이 될 가능성은 있다.

High Score 61













서로 다른 사용자로 애플리케이션을 실행해 보고 모두 동일한 고득점이 적용되는지 확인할 수 있다.
가져오기 및 내보내기

어떤 사용자나 시스템에서 다른 사용자 또는 다른 시스템으로 선호 설정을 전송하려는 경우, 해당 사용자/시스템에서 선호 설정을 내보낸 다음 다른 사용자/시스템으로 이를 가져올 수 있다. 선호 설정을 내보내기할 때 XML 형식의 문서로 내보내며, 독자들이 굳이 알 필요는 없지만 이 문서의 DTD는 http://java.sun.com/dtd/preferences.dtd에서 지정한다. exportSubtree() 메소드를 사용하여 하위 트리 전체를 내보내거나 exportNode() 메소드를 사용하여 단일 노드만 내보낼 수 있다. 두 메소드 모두 OutputStream 인수를 받아 저장 위치를 지정한다. XML 문서는 UTF-8 문자 인코딩 방식이다. 그런 다음 importPreferences() 메소드를 통해 데이터 가져오기가 이루어지는데, 이 메소드는 InputStream 인수를 받는다. API 측면에서 보면 시스템 노드/트리 가져오기와 사용자 노드 가져오기 사이에는 아무런 차이가 없다.

이전 예제에 몇 줄의 코드를 추가하면 새로 업데이트된 고득점을 high.xml 파일로 내보낸다. 추가된 코드 중에는 파일을 저장하고 예외를 처리하는 새로운 스레드를 시작하는 작업이 상당 부분을 차지한다. 단일 노드를 내보내는 부분은 단 세 줄이다.

    Thread runner = new Thread(new Runnable() {
      public void run() {
        try {
          FileOutputStream fis = new FileOutputStream("high.xml");
          systemNode.exportNode(fis);
          fis.close();
        } catch (Exception e) {
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
        }
      }
    });
    runner.start();


내보내기할 때 파일은 다음과 같이 된다.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE preferences SYSTEM "http://java.sun.com/dtd/preferences.dtd">
<preferences EXTERNAL_XML_VERSION="1.0">
  <root type="system">
    <map/>
    <node name="<unnamed>">
      <map>
        <entry key="High.highScore" value="95"/>
      </map>
    </node>
  </root>
</preferences>

루트 요소에 "system"이라고 말하는 유형 속성이 있다. 이는 그것이 노드의 유형임을 나타낸다. 또한 노드에는 "<unnamed>"라는 값의 이름 속성이 있다. High 클래스는 패키지에 포함되어 있지 않으므로 명명되지 않은 시스템 노드 영역에서 작업해야 한다. 이 항목 속성은 현재의 고득점 값(여기서는 95)을 제공하는데, 실제 값은 다를 수 있다.

이 예제에서는 import 코드를 포함시키지 않겠지만, 가져오기를 수행하려면 선호 설정에 대한 정적 메소드를 호출하고 알맞은 입력 스트림을 전달하면 된다.

FileInputStream fis = new FileInputStream("high.xml");
  Preferences.importPreferences(fis);
  fis.close();



XML 파일은 선호 설정이 시스템 또는 사용자 유형인지 여부에 대한 정보를 포함하므로 가져오기 호출에서 이 정보를 명시적으로 포함할 필요는 없다. 발생 가능한 일반적인 IOExceptions 외에도 가져오기 호출은 파일 형식이 잘못된 경우 InvalidPreferencesFormatException을 throw한다. 또한 내보낼 데이터를 백업 저장소로부터 올바르게 읽을 수 없다면 BackingStoreException을 throw하기도 한다.

이벤트 알림

원래 버전의 High 게임은 고득점 선호 설정을 업데이트한 다음 화면의 레이블을 업데이트하도록 명시적으로 호출한다. 이 작업을 수행하는 더 좋은 방법은 선호 설정 노드에 수신기를 추가하는 것인데, 그러면 값이 변경될 때 레이블의 값 업데이트가 자동으로 트리거될 수 있다. 따라서 고득점이 여러 위치로부터 업데이트되더라도 업데이트된 값을 저장한 다음 레이블을 업데이트하는 코드를 추가할 필요가 없다.

다음 두 줄은

 updateHighScore(next);
  updateHighScoreLabel(next);

알맞은 수신기를 추가하여 한 줄로 만들 수 있다.

updateHighScore(next);

그러한 작업에 꼭 알맞은 PreferenceChangeListener 및 그와 연결된 PreferenceChangeEvent가 있다. 수신기는 연결된 노드의 모든 변경 사항을 알게 되므로, 다음과 같이 어떤 키-값 쌍이 수정되었는지 확인해야 한다.

  PreferenceChangeListener changeListener =
        new PreferenceChangeListener() {

      public void preferenceChange(PreferenceChangeEvent e) {
        if (HIGH_SCORE.equals(e.getKey())) {
          String newValue = e.getNewValue();
          int value = Integer.valueOf(newValue);
          updateHighScoreLabel(value);
        }
      }
    };
    systemNode.addPreferenceChangeListener(changeListener);

PreferenceChangeEvent에는 세 가지 중요한 등록 정보가 있다. key, new value 그리고 node 자체이다. 그러나 new value가 선호 설정의 모든 편의 메소드를 포함하지는 않는다. 예를 들어, 값을 int로 검색할 수 없다. 그 대신 값을 수동으로 변환해야 한다. 수정된 High 클래스는 다음과 같이 된다.


import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.prefs.*;
import javax.swing.*;

public class High {
  static JLabel highScore = new JLabel();
  static JLabel score = new JLabel();
  static Random random = new Random(new Date().getTime());
  private static final String HIGH_SCORE = "High.highScore";
  static Preferences systemNode =
  Preferences.systemNodeForPackage(High.class);

  public static void main (String args[]) {
    /* -- Uncomment these lines to clear saved score
    systemNode.remove(HIGH_SCORE);
    */

    PreferenceChangeListener changeListener =
        new PreferenceChangeListener() {

      public void preferenceChange(PreferenceChangeEvent e) {
        if (HIGH_SCORE.equals(e.getKey())) {
          String newValue = e.getNewValue();
          int value = Integer.valueOf(newValue);
          updateHighScoreLabel(value);
        }
      }
    };
    systemNode.addPreferenceChangeListener(changeListener);

    EventQueue.invokeLater(
      new Runnable() {
        public void run() {
          JFrame frame = new JFrame("High Score");
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          updateHighScoreLabel(getSavedHighScore());
          frame.add(highScore, BorderLayout.NORTH);
          frame.add(score, BorderLayout.CENTER);
          JButton button = new JButton("Play");
          ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
              int next = random.nextInt(100);
              score.setText(Integer.toString(next));
              int old = getSavedHighScore();
              if (next > old) {
                Toolkit.getDefaultToolkit().beep();
                updateHighScore(next);
              }
            }
          };
          button.addActionListener(listener);
          frame.add(button, BorderLayout.SOUTH);
          frame.setSize(200, 200);
          frame.setVisible(true);
        }
      }
    );
  }

  private static void updateHighScoreLabel(int value) {
    if (value == -1) {
      highScore.setText("");
    } else {
      highScore.setText(Integer.toString(value));
    }
  }

  private static int getSavedHighScore() {
    return systemNode.getInt(HIGH_SCORE, -1);
  }

  private static void updateHighScore(int value) {
    systemNode.putInt(HIGH_SCORE, value);
    // Save XML in separate thread
    Thread runner = new Thread(new Runnable() {
      public void run() {
        try {
          FileOutputStream fis = new FileOutputStream("high.xml");
          systemNode.exportNode(fis);
          fis.close();
        } catch (Exception e) {
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
          Toolkit.getDefaultToolkit().beep();
        }
      }
    });
    runner.start();
  }
}



PreferenceChangeListener/Event 클래스 쌍 외에도 선호 설정 변경을 알리기 위한 NodeChangeListenerNodeChangeEvent 콤보가 있다. 그러나 이는 특정 노드의 값을 변경하는 것이 아니라 알림 노드를 추가하고 제거하는 용도이다. 물론 선호 설정 뷰어와 같은 것을 작성하려면 노드가 나타나는지 여부 및 그 시점을 알 필요가 있으므로 이 클래스도 관심사가 될 수 있다.

전체 선호 설정 API는 애플리케이션의 수명이 끝나더라도 데이터베이스 시스템에 의존할 필요 없이 데이터를 저장하는 매우 편리한 방법이 될 수 있다. API에 대한 자세한 내용은 Sir, What is Your Preference? 영문기사를 참조한다.

반응형

쓰레딩과 동기화


쓰레딩과 동기화는 Java Language Specification (JLS)에 명시되어 있듯, 자바 프로그래밍 언어의 핵심 기능이다. RTSJ는 JLS의 핵심 기능들을 수 많은 방식으로 확장한다. (참고자료) 예를 들어, RTSJ는 일반 자바 쓰레드 보다 더욱 엄격한 스케줄링 정책을 따라야 하는 새로운 실시간(RT) 쓰레드 유형을 도입했다. 또 다른 예로, 우선 순위 상속(priority inheritance)이 있는데, 이는 잠금이 경쟁할 때 잠금 동기화가 관리되는 방법을 정의한 잠금 정책이다.

우선 순위와 우선 순위 큐의 관리를 이해하는 것이 RTSJ의 쓰레딩과 동기화 변화를 이해하는데 유용하다. 우선 순위 역시 RT 애플리케이션에는 중요한 장치이다. 이 글에서는 쓰레드 우선 순위와 우선 순위 큐가 관리되는 방법을 논하면서 RTSJ 쓰레딩과 동기화 측면을 설명하도록 하겠다. IBM WebSphere® Real Time을 사용하여 구현하는 것을 포함하여, RT 애플리케이션을 개발, 전개, 실행할 때 고려해야 하는 사항들도 설명한다. (참고자료).

일반 자바 쓰레드 이해하기

JLS에서 정의된 쓰레드는 일반 자바 쓰레드(regular Java threads)라고 한다. 일반 자바 쓰레드는 1부터 10까지 정수 우선 순위를 가진 java.lang.Thread 클래스의 인스턴스이다. 많은 실행 플랫폼을 수용하기 위해 JLS는 일반 자바 쓰레드의 우선 순위 구현, 스케줄링, 관리 방식에 많은 유연성을 가져왔다.

WebSphere Real Time을 포함하여, 리눅스 기반 WebSphere VM은 리눅스® OS가 제공하는 네이티브 쓰레딩 서비스를 사용한다. 리눅스 쓰레딩과 동기화를 이해함으로써 자바 쓰레딩과 동기화를 이해할 수 있다.

리눅스 쓰레딩과 동기화

리눅스 OS는 그 동안 다양한 사용자 레벨 쓰레딩 구현을 제공했다. Native POSIX Thread Library (NPTL)는 리눅스용 최신 전략적 쓰레딩 방향이고 WebSphere VM에서 사용된다. (참고자료) 전 구현에 비해 NPTL이 가진 장점은 POSIX 호환성과 더 나은 성능을 보인다는 점이다. POSIX 서버는 libpthread.so 동적 라이브러리와 기반 리눅스 커널 지원을 통해 런타임에서 사용할 수 있다. 리눅스 커널은 쓰레드 우선 순위 레벨 같은 정적 컨트롤과 시스템에서 실행되는 쓰레드의 동적인 조건을 기반으로 쓰레드 스케줄링을 수행한다.

POSIX에서는 다양한 스케줄링 정책과 우선 순위를 사용하여 POSIX 쓰레드(pthreads)를 만들어서 다양한 애플리케이션 필요를 채울 수 있다. 다음은 스케줄링 정책들이다.

  • SCHED_OTHER
  • SCHED_FIFO
  • SCHED_RR

SCHED_OTHER 정책은 프로그램 개발 툴, 오피스 애플리케이션, 웹 브라우저 같은 전통적인 사용자 태스크에 사용된다. SCHED_RRSCHED_FIFO는 보다 결정론적이며 시의성을 요구하는 애플리케이션에 사용된다. SCHED_RRSCHED_FIFO의 주요한 차이는 SCHED_RR 은 쓰레드의 실행에 타임 슬라이스 방식을 취하는 반면, SCHED_FIFO는 그렇지 않다는 점이다. SCHED_OTHERSCHED_FIFO 정책은 WebSphere Real Time에 의해 사용되며, 아래에 자세히 설명해 놓았다. (WebSphere Real Time이 사용하지 않는 SCHED_RR 정책에 대해서는 다루지 않겠다.

POSIX는 pthread_mutex 데이터 유형을 통해 잠금과 동기화 지원을 제공한다. pthread_mutex는 다양한 잠금 정책들로 만들어 질 수 있다. 잠금 정책은 한 개 이상의 쓰레드가 같은 잠금을 동시에 얻어야 할 때 실행 방식에 영향을 준다. 표준 리눅스 버전들은 하나의 기본 정책을 지원하지만, RT 리눅스 버전은 우선 순위 상속 잠금 정책도 지원한다. 동기화 개요 섹션에서 우선 순위 상속 정책에 대해 자세히 설명하겠다.

리눅스 스케줄링과 잠금에는 first in, first out (FIFO) 큐 관리가 포함된다.

일반 자바 쓰레드에서의 쓰레드 스케줄링

RTSJ에서는 일반 자바 쓰레드의 작동이 JLS에 정의된 것과 같다고 명시하고 있다. WebSphere Real Time에서, 일반 자바 쓰레드는 리눅스의 POSIX SCHED_OTHER 스케줄링 정책을 사용하여 구현된다. SCHED_OTHER 정책은 결정론을 필요로 하는 태스크 보다는 컴파일러와 워드 프로세서 같은 애플리케이션에 적용된다.

2.6 리눅스 커널에서, SCHED_OTHER 정책은 40 개의 우선 순위 레벨을 지원한다. 이러한 40 개의 우선 순위 레벨은 프로세서 기반으로 관리된다.

  • 리눅스는 캐시 성능 때문에 같은 프로세서에 하나의 쓰레드를 실행한다.
  • 쓰레드 스케줄링은 시스템 당 잠금 대신에 프로세서 당 잠금을 주로 사용한다.

필요할 경우, 리눅스는 하나의 프로세서에서 다른 프로세서로 쓰레드를 이동하여 워크로드의 균형을 맞춘다.

40개의 우선 순위 레벨에서, 리눅스는 활성 큐(active queue)와 종료 큐(expiration queue)를 관리한다. 각 큐에는 연결된 쓰레드 리스트가 포함되어 있거나 비어있다. 활성 큐와 종료 큐는 효율성, 로드 밸런싱, 기타 목적에 쓰인다. 이론상으로는, 시스템을 각 40 개의 우선 순위에 대해 실행 큐(run-queue)라고 하는 하나의 FIFO 큐를 관리하는 것으로 볼 수 있다. 쓰레드는 가장 높은 우선 순위를 가진 비어있지 않은 실행 큐부터 내보낸다. 이 쓰레드는 큐에서 제거되고 time quantum 또는 time slice라는 기간 동안 실행된다. 실행 쓰레드가 time quantum을 종료하면, 실행 큐의 끝에 배치되고 새로운 time quantum이 할당된다. 큐의 헤드에서 파견하고 종료된 쓰레드를 큐의 끝에 배치함으로써, 우선 순위 내에서 round-robin 방식으로 실행한다.

쓰레드에 주어진 time quantum은 쓰레드의 할당된 우선 순위에 기반한다. 높은 할당 우선 순위를 가진 쓰레드에게는 더 오랜 실행 time quantum이 주어진다. 쓰레드가 CPU를 차지하지 않도록 하기 위해, 리눅스는 쓰레드가 I/O 바운드 인지 또는 CPU 바운드인지에 따라서 쓰레드의 우선 순위를 높이거나 낮춘다. 쓰레드는 yielding (Thread.yield() 호출)에 의해 time slice를 자발적으로 포기하거나, 블로킹에 의해서 컨트롤을 포기한다. 이 시점에서 쓰레드는 이벤트 발생을 기다린다. 이 같은 이벤트는 잠금 해제를 통해 실행될 수 있다.

WebSphere Real Time의 VM은 40개의 SCHED_OTHER 리눅스 쓰레드 우선 순위 범위에서 10 개의 일반 자바 쓰레드 우선 순위는 명확히 할당하지 않는다. 모든 일반 자바 쓰레드는, 자바 우선 순위와 관계 없이, 기본 리눅스 우선 순위로 할당된다. 기본 리눅스 우선 순위는 40개의 SCHED_OTHER 우선 순위 범위의 중간에 있다. 기본을 사용함으로써, 일반 자바 쓰레드는 리눅스의 동적인 우선 순위 조정에 관계 없이, 실행 큐에 있는 모든 일반 자바 쓰레드가 실행되도록 한다. 이것은 RT 쓰레드를 실행하는 시스템을 실행하거나 실행하지 않는 일반 자바 쓰레드들만을 가진 시스템으로 가정한다.

WebSphere Real Time의 VM과 비 RT 버전의 WebSphere VM은 일반 자바 쓰레드에 SCHED_OTHER 정책과 기본 우선 순위 할당을 사용한다. 같은 정책을 사용함으로써, 두 개의 JVM들은 비슷하지만 동일하지는 않은 쓰레드 스케줄링과 동기화 특성을 갖추게 된다. RTSJ와 RT Metronome 가비지 컬렉터(참고자료 보기)의 도입으로 인한 WebSphere Real Time 클래스 라이브러리의 변경, JVM의 변경, JIT 컴파일러의 변경 때문에 두 JVM에서 동일한 타이밍과 성능 특성으로 애플리케이션이 실행될 수 없도록 한다. IBM WebSphere Real Time 테스팅 시, 타이밍 차이는 다른 JVM에서 수년 동안 잘 실행되었던 테스트 프로그램에서 경쟁 조건(다시 말해서, 버그)을 해결했다.




일반 자바 쓰레드를 사용한 코드 예제

Listing 1은 두 개의 쓰레드에 의해서 5초 간격으로 얼마나 많은 루프의 반복이 실행될 수 있는지를 보여주고 있다.


Listing 1. 일반 자바 쓰레드

                
class myThreadClass extends java.lang.Thread {
   volatile static boolean Stop = false;

   // Primordial thread executes main()
   public static void main(String args[]) throws InterruptedException {

      // Create and start 2 threads
      myThreadClass thread1 = new myThreadClass();
      thread1.setPriority(4);    // 1st thread at 4th non-RT priority
      myThreadClass thread2 = new myThreadClass();
      thread2.setPriority(6);    // 2nd thread at 6th non-RT priority
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      Thread.sleep(5*1000);
      Stop = true;
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

Listing 1의 프로그램에는 일반 자바 쓰레드들인 세 개의 사용자 쓰레드가 있다.

  • 기본 쓰레드
    • 프로세스 시작 시 생성된 메인 쓰레드이며, main() 메소드를 실행한다.
    • main()은 두 개의 일반 자바 쓰레드를 생성한다. 한 쓰레드는 우선 순위 4이고, 또 다른 쓰레드는 우선 순위 6이다.
    • 메인 쓰레드는 Thread.sleep()을 호출함으로써 5초 동안 수면(sleep)하는 것으로 차단을 수행한다.
    • 5초의 수면 후에, 이 쓰레드는 다른 두 개의 쓰레드 종료를 명령한다.
  • 우선 순위 4의 쓰레드
    • 이 쓰레드는 기본 쓰레드에 의해 생성되며, for 루프를 포함하고 있는 run() 메소드를 실행한다.
    • 이 쓰레드는,
      1. 각 루프 반복 시 카운트를 늘린다.
      2. Thread.yield()를 호출함으로써 타임 슬라이스를 자발적으로 포기한다.
      3. 메인 쓰레드가 요청될 때 종료한다. 종료 직전에, 쓰레드는 루프 카운트를 프린트 한다.
  • 우선 순위 6의 쓰레드: 이 쓰레드는 우선 순위 4 쓰레드와 같은 액션을 수행한다.

이 프로그램이 유니프로세서(uniprocessor) 또는 로딩되지 않은 멀티프로세서 시스템에서 실행된다면, 각 쓰레드는 for 루프 반복 카운트에 가까운 수를 프린트 한다. 프린트 내용은 다음과 같다.

Created thread
Created thread
Thread terminates. Loop count is 540084
Thread terminates. Loop count is 540083

Thread.yield()에 대한 호출이 제거되면, 두 개의 쓰레드에 대한 루프 카운트들은 거의 비슷하지만 동일하지는 않다. 두 쓰레드 모두 SCHED_OTHER 정책의 같은 기본 우선 순위가 할당되었기 때문에, 두 쓰레드에는 같은 타임 슬라이스가 주어진다. 이 쓰레드는 동일한 코드를 실행하기 때문에 비슷한 동적인 우선 순위 조정을 해야 하고 같은 실행 큐에서 round-robin 방식으로 실행되어야 한다. 하지만, 우선 순위 4의 쓰레드가 첫 번째로 실행되기 때문에, 5초 실행 간격에서 더 많은 부분을 차지하고 더 높은 루프 카운트로 출력되는 것이다.




RT 쓰레드 이해하기

RT 쓰레드는 javax.realtime.RealtimeThread의 인스턴스이다. RTSJ는 이 스팩이 RT 쓰레드에 최소한 28 개의 우선 순위를 제공해야 한다고 요구하고 있다. 이러한 우선 순위들을 실시간 우선 순위(real-time priorities)라고 한다. RT 우선 순위 범위의 시작 값에 대해서는 10보다 커야 한다고 스팩에서는 지정하고 있다. 이는 일반 자바 쓰레드에 주어진 가장 높은 우선 순위 값이다. 애플리케이션 코드는 새로운 PriorityScheduler 클래스의 getPriorityMin()getPriorityMax()를 사용하여 가용 RT 우선 순위 값의 범위를 정한다.

RT 쓰레드의 동기

JLS의 쓰레드 스케줄링은 정확하지 않고 단 10 개의 우선 순위 값을 제공한다. POSIX SCHED_OTHER는 다양한 애플리케이션의 필요를 채우고 있다. 하지만 SCHED_OTHER 정책에도 바람직하지 못한 특성들이 있다. 동적인 우선 순위 조정과 타임 슬라이스가 예견하지 못한 시간에 발생할 수 있다. 40개의 SCHED_OTHER 우선 순위도 충분하지 않으며, 이러한 우선 순위 범위는 이미 일반 자바 쓰레드와를 가진 애플리케이션과 동적 우선 순위 조정은 당연한 것이다. JVM 역시 가비지 컬렉션(GC) 같은 특별한 목적에 내부 쓰레드용 우선 순위를 필요로 한다.

결정론의 부족, 더 많은 우선 순위 레벨의 필요, 기존 애플리케이션과의 호환성 등이 새로운 스케줄링 기능을 제공하는 확장을 만드는 동기가 되었다. javax.realtime 패키지의 클래스들은 RTSJ에서 설명한 대로, 이러한 기능들을 제공한다. WebSphere Real Time에서, Linux SCHED_FIFO 스케줄링 정책은 RTSJ 스케줄링 필요를 다루고 있다.

RT 자바 쓰레드에 대한 쓰레드 스케줄링

WebSphere Real Time에서, 28개의 RT 자바 우선 순위들이 지원되고 11에서 38까지의 범위를 갖고 있다. PriorityScheduler 클래스의 API는 이러한 범위를 검색하는데 사용된다. 이 섹션에서는 RTSJ에서 설명하고 있는 쓰레드 스케줄링을 상세히 설명하고, RTSJ의 요구 사항들에 부합하는 Linux SCHED_FIFO 정책을 설명하도록 하겠다.

RTSJ는 RT 우선 순위들이 각 RT 우선 순위들을 위해 개별 큐를 관리하는 런타임 시스템에 의해 논리적으로 구현되고 있는 것으로 간주한다. 쓰레드 스케줄러는 비어있지 않은 가장 높은 우선 순위 큐의 헤드에서 파견된다. 어떤 쓰레드도 RT 우선 순위용 큐에 없다면, 일반 자바 쓰레드가 파견된다. (일반 자바 쓰레드에 대한 쓰레드 스케줄링).

RT 우선 순위로 파견된 쓰레드는 차단될 때까지 실행될 수 있고, yielding에 의해서 컨트롤을 자발적으로 포기하거나, 더 높은 RT 우선 순위를 가진 쓰레드로 선점된다. 자발적으로 양보하는 RT 우선 순위를 가진 쓰레드는 큐의 뒤쪽에 배치된다. RTSJ 역시 이 같은 스케줄링이 일정한 시간으로 수행되어야 함을 명시하고 있고 실행 중인 RT 쓰레드의 수 같은 요소에 기반하여 달라질 수 없다고 명시하고 있다. RTSJ의 1.02 버전은 이러한 규칙을 유니프로세서 시스템에 적용하고 있다. RTSJ에는 멀티프로세서 시스템에 스케줄링이 어떻게 발생하는지에 대해서는 언급되어 있지 않다.

리눅스는 모든 RTSJ 스케줄링 요구 사항에 SCHED_FIFO 정책을 제공한다. SCHED_FIFO 정책은 사용자 태스크가 아닌 RT용이다. SCHED_FIFOSCHED_OTHER 정책과 달리 99 개의 우선 순위 레벨을 제공한다. SCHED_FIFO는 쓰레드에 타임 슬라이스를 수행하지 않는다. 또한, SCHED_FIFO FIFO 정책은 우선 순위 상속 잠금 정책을 제외하고는 RT 쓰레드의 우선순위를 동적으로 조정하지 않는다. (동기화 개요 섹션) 우선 순위 상속 때문에 우선 순위 조정은 RTSJ에 필요한 것이다.

리눅스는 RT 쓰레드와 일반 자바 쓰레드에 일정한 시간 스케줄링을 제공한다. 멀티프로세서 시스템에서, 리눅스는 가용 프로세서로 파견된 RT 쓰레드의 하나의 글로벌 큐의 작동을 모방한다. 이는 RTSJ의 원리와 흡사하지만, 일반 자바 쓰레드에 사용되는 SCHED_OTHER 정책과는 다르다.

RT 쓰레드를 사용하는 문제가 많은 코드 예제

Listing 2는 Listing 1의 코드를 수정하여 일반 자바 쓰레드 대신 RT 쓰레드를 만든다. java.lang.Thread 대신 java.realtime.RealtimeThread를 사용한다는 것부터 다르다. 첫 번째 쓰레드는 4의 RT 우선 순위 레벨로 생성되고, 두 번째 쓰레드는 6의 RT 우선 순위 레벨로 생성된다. 이는 getPriorityMin() 메소드에 의해 결정된 것이다.


Listing 2. RT 쓰레드

                
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
   volatile static boolean Stop = false;

   // Primordial thread executes main()
   public static void main(String args[]) throws InterruptedException {

      // Create and start 2 threads
      myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
      // want 1st thread at 4th real-time priority
      thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
      myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
      // want 2nd thread at 6th real-time priority
      thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      Thread.sleep(5*1000);
      Stop = true;
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         // Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

Listing 2의 수정된 코드는 몇 가지 문제를 갖고 있다. 프로그램이 유니프로세서 환경에서 실행되면, 절대로 종료되지 않고 다음을 프린트 한다.

Created thread

이 결과는 RT 쓰레드 스케줄링 작동에 의해 설명될 수 있다. 기본 쓰레드는 일반 자바 쓰레드로 남아있고, 비 RT(SCHED_OTHER) 정책으로 실행된다. 기본 쓰레드가 첫 번째의 RT 쓰레드를 시작하자마자, RT 쓰레드는 기본 쓰레드를 선점하고, RT 쓰레드는 무한정 실행된다. Time quantum에 의해 한정되지 않았고 쓰레드 차단도 수행하지 않기 때문이다. 기본 쓰레드가 선점된 후에는 실행될 수 없기 때문에 두 번째 RT 쓰레드를 시작할 수 없다. Thread.yield()는 기본 쓰레드를 실행하는데 어떤 영향도 줄 수 없다. yielding은 논리적으로 RT 쓰레드를 실행 큐의 끝에 배치하지만, 이 쓰레드 스케줄러는 같은 쓰레드를 다시 파견한다. 이것이 가장 높은 우선 순위를 가지고 실행 큐의 앞에 있는 쓰레드이기 때문이다.

프로그램은 또한 Two-processor 시스템에서는 실패한다. 다음이 프린트 된다.

Created thread
Created thread

기본 쓰레드는 두 개의 RT 쓰레드를 만들 수 있다. 하지만, 두 번째 쓰레드를 만든 후에, 기본 쓰레드는 선점되고 쓰레드에게 종료를 명령할 수 없다. 두 개의 RT 쓰레드는 Two-processor 시스템에서 실행되고 절대로 차단하지 않기 때문이다.

프로그램은 끝까지 실행되고 세 개 이상의 프로세서를 가진 시스템에서 결과를 만들어 낸다.

단일 프로세서에서 실행되는 RT 코드 예제

Listing 3은 유니프로세서 시스템에서 올바르게 실행되도록 수정된 코드 모습이다. main() 메소드의 로직은 RT 우선 순위 8을 가진 "주" RT 쓰레드로 옮겨갔다. 이 우선 순위는 주 RT 쓰레드가 만든 다른 두 개의 RT 쓰레드의 우선 순위 보다 더 높다. 가장 높은 우선 순위를 지닌다는 것은 주 RT 쓰레드가 두 개의 RT 쓰레드를 성공적으로 생성할 수 있고, 5초 수면에서 깨어났을 때 주 RT 쓰레드가 현재 실행되는 쓰레드를 선점할 수 있다는 것을 의미한다.


Listing 3. 수정된 RT 쓰레드 예제

                
import javax.realtime.*;
class myRealtimeThreadClass extends javax.realtime.RealtimeThread {
   volatile static boolean Stop = false;

   static class myRealtimeStartup extends javax.realtime.RealtimeThread {

   public void run() {
      // Create and start 2 threads
      myRealtimeThreadClass thread1 = new myRealtimeThreadClass();
      // want 1st thread at 4th real-time priority
      thread1.setPriority(PriorityScheduler.getMinPriority(null)+ 4);
      myRealtimeThreadClass thread2 = new myRealtimeThreadClass();
      // want 1st thread at 6th real-time priority
      thread2.setPriority(PriorityScheduler.getMinPriority(null)+ 6);
      thread1.start();           // start 1st thread to execute run()
      thread2.start();           // start 2nd thread to execute run()

      // Sleep for 5 seconds, then tell the threads to terminate
      try {
                        Thread.sleep(5*1000);
      } catch (InterruptedException e) {
      }
      myRealtimeThreadClass.Stop = true;
      }
   }

   // Primordial thread creates real-time startup thread
   public static void main(String args[]) {
      myRealtimeStartup startThr = new myRealtimeStartup();
      startThr.setPriority(PriorityScheduler.getMinPriority(null)+ 8);
      startThr.start();
   }

   public void run() { // Created threads execute this method
      System.out.println("Created thread");
      int count = 0;
      for (;Stop != true;) {    // continue until asked to stop
         count++;
         // Thread.yield();   // yield to other thread
      }
      System.out.println("Thread terminates. Loop count is " + count);
   }
}

이 프로그램이 유니프로세서에서 실행되면, 다음과 같이 프린트 된다.

Created thread
Thread terminates. Loop count is 32767955
Created thread
Thread terminates. Loop count is 0

이 프로그램의 결과는 모든 쓰레드가 실행된 후 종료하지만, 두 쓰레드 중 단 하나만 for 루프를 반복한다는 것을 나타내고 있다. 이러한 결과는 RT 쓰레드의 우선 순위를 따져보면 이해될 수 있다. 주 RT 쓰레드는 Thread.sleep() 호출에 의해 차단될 때까지 실행된다. 주 RT 쓰레드는 두 개의 RT 쓰레드를 만들지만, RT 우선 순위 6의 두 번째 RT 쓰레드만이 주 RT 쓰레드가 수면하고 있는 동안 실행될 수 있다. 이 쓰레드는 주 RT 쓰레드가 종료되면, 우선 순위 6의 쓰레드는 실행 및 종료될 수 있다. 그리고 0외의 값으로 루프 카운트를 출력한다. 이 쓰레드가 종료된 후에, RT 우선 순위 4의 쓰레드가 실행될 수 있지만, 종료되어야 하는 명령을 받았기 때문에 for 루프를 우회한다. 이 쓰레드는 종료하기 전에 루프 카운트 값, 0을 출력한다.




위로


RT 애플리케이션을 위한 쓰레딩 고려 사항

이 섹션에서는 애플리케이션들을 포팅하여 RT 쓰레드를 사용하거나 RT 쓰레딩을 활용하는 새로운 애플리케이션을 작성할 때 고려해야 할 사항들을 설명한다.

RT 쓰레드의 새로운 확장

RTSJ는 RT 쓰레드를 만들어서 특정 시간 또는 적절한 시간에 시작하도록 하는 장치를 지정하고 있다. 지정된 시간 간격이나 주기로 특정 로직을 실행하는 쓰레드를 만들 수 있다. 로직이 지정된 기간 내에 완료되지 못할 때 AsynchronousEventHandler (AEH)를 실행하는 쓰레드를 정의할 수 있다. 또한, 쓰레드가 소비할 수 있는 메모리 유형과 양에 대한 한계도 정의할 수 있다. 쓰레드가 제한량 보다 더 많은 것을 소비할 때 OutOfMemoryError가 던져지는 방식이다. 이러한 장치들은 RT 쓰레드에서만 사용할 수 있고 일반 자바 쓰레드에서는 사용할 수 없다. RTSJ에는 이러한 장치들에 대한 자세한 정보가 수록되어 있다.

Thread.interrupt()과 보류(pending) 예외

Thread.interrupt() 작동은 RT 쓰레드를 위해 확장된다. 이 API는 JLS에서 기술된 것처럼 차단된 쓰레드를 인터럽트 한다. 이러한 예외는 사용자가 Throws AsynchronouslyInterruptedException문을 메소드 선언에 추가함으로써, 인터럽트 가능한 것으로 명확하게 표시했던 메소드에서도 발생할 수 있다. 이 예외는 사용자가 예외를 명확하게 처리해야 한다는 점에서, 이 쓰레드에는 성가신 존재이다. 그렇지 않으면, 쓰레드에 속한 채로 남아있는데, 이를 보류(pending)라고 한다. 사용자가 예외를 제거하지 않으면, 쓰레드는 골치 아픈 예외가 여전히 걸린 채로 종료될 수 있다. 이러한 에러는 RT 쓰레드 고유의 풀링 폼을 수행하는 애플리케이션이 아닌,"정상적인" 방식으로 종료한다면 비교적 양호한 것이다. 다시 말해서, 쓰레드는 InterruptedException 예외가 있는 상태로 풀로 리턴 될 수 있다. 이 경우, 쓰레드 풀링을 수행하는 코드는 예외를 명확히 제거한다. 그렇지 않으면, 예외는 풀링된 쓰레드가 다시 할당될 때 예외가 생길 것이다.

기본 쓰레드와 애플리케이션 디스패치 로직

기본 쓰레드는 언제나 RT 쓰레드가 아닌 일반 자바 쓰레드이다. 첫 번째 RT 쓰레드는 늘 일반 자바 쓰레드에 의해 생성된다. 이 RT 쓰레드는 RT 쓰레드와 일반 자바 쓰레드를 동시에 실행하는 프로세서가 불충분하다면 일반 자바 쓰레드를 즉각 선점한다. 선점으로 인해 일반 자바 쓰레드는 더 이상의 RT 쓰레드나 기타 로직을 만들 수 없어서 애플리케이션은 초기화된 상태가 된다.

높은 우선 순위 RT 쓰레드에서 애플리케이션 초기화를 수행함으로써 이런 문제를 해결할 수 있다. 이 기술은 고유한 쓰레드 풀링과 쓰레드 디스패치를 수행하는 애플리케이션이나 라이브러리에 필요하다. 쓰레드-디스패치 로직은 높은 우선 순위 또는 높은 우선 순위 쓰레드에서 실행되어야 한다. 쓰레드-풀링 로직을 실행하기에 적합한 우선 순위를 선택하면 쓰레드 인큐잉(enqueueing)과 디큐잉(dequeueing)에서 생기는 문제를 피할 수 있다.

폭주(Runaway) 쓰레드

일반 자바 쓰레드는 time quantum으로 실행되고, CPU 사용에 기반하여 스케줄러가 수행하는 동적 우선 순위 조정은 모든 일반 자바 쓰레드가 모두 실행될 수 있도록 한다. 반대로, RT 쓰레드는 time quantum에 의해 바운드 되지 않고, 쓰레드 스케줄러는 CPU 사용에 기반하여 어떤 형태의 동적 우선 순위 조정도 수행하지 않는다. 일반 자바 쓰레드와 RT 쓰레드간 스케줄링 정책에 있어서의 차이점은 폭주 RT 쓰레드가 발생할 수 있는 기회를 만들어 낸다. 폭주 RT 쓰레드는 시스템을 제어할 수 있고, 다른 애플리케이션들이 실행되지 못하게 하며, 사용자들이 시스템에 로그인 하지 못하게 한다.

개발과 테스팅 동안, 폭주 쓰레드의 영향력을 줄이는데 도움이 되었던 한 가지 기술은 프로세스가 사용할 수 있는 CPU 양에 한계를 설정하는 것이다. 리눅스에서, CPU 소비량을 제한하면 CPU 한계에 다다를 때 폭주 쓰레드는 죽는다. 또한, 시스템 상태를 감시하거나 시스템 로그인을 제공하는 프로그램들은 높은 RT 우선 순위로 실행되어 프로그램이 문제가 많은 쓰레드들을 선점할 수 있도록 한다.

자바 우선 순위와 OS 우선 순위 매핑하기

리눅스에서, POSIX SCHED_FIFO 정책은 1부터 99까지의 정수 범위에서 99의 RT 우선 순위를 제공한다. 이러한 시스템 범위에서, 우선 순위 11부터 89까지는 WebSphere VM 에 의해 사용되고, 이 범위의 일부는 28 RTSJ 우선 순위를 구현하는데 사용된다. 28 RT 자바 우선 순위를 POSIX 시스템 우선 순위로 매핑하는 것은 IBM WebSphere Real Time 문서에 설명되어 있다. 애플리케이션 코드는 이러한 매핑에 의존해서는 안되고, 자바 레벨에서 28 RT 우선 순위의 순서에 의존해야 한다. 이렇게 하면 JVM은 이 범위를 재 매핑하고, 앞으로의 WebSphere Real Time 릴리스에서는 이 부분이 향상될 것이다.

애플리케이션은 WebSphere Real Time에서 사용되었던 것 보다 높거나 낮은 RT 우선 순위를 요할 때 SCHED_FIFO 우선 순위 1 또는 우선 순위 90을 사용하여 데몬이나 기타 RT 프로세스를 구현할 수 있다.

JNI AttachThread()

Java Native Interface (JNI)에서는 JNI AttachThread() API를 사용하여 C 코드에서 생성된 쓰레드를 JVM에 붙일 수 있지만, RTSJ는 RT 쓰레드를 붙이기 위한 JNI 인터페이스를 변경하거나 제공하지 않는다. 따라서, 애플리케이션은 JVM에 붙일 POSIX RT 쓰레드를 C 코드로 만들지 않도록 해야 한다. 대신, 이 같은 RT 쓰레드는 자바 언어에서 생성되어야 한다.

분기된 프로세스와 RT 우선 순위

쓰레드는 또 다른 프로세스를 분기할 수 있다. 리눅스에서, 분기된 프로세스의 기본 쓰레드는 부모 쓰레드의 우선 순위를 상속 받는다. 분기된 프로세스가 JVM이라면, JVM의 기본 쓰레드는 RT 우선 순위로 생성된다. 이는 일반 자바 쓰레드(기본 쓰레드)의 순서를 위반한 것이다. 이러한 상황을 피하려면, JVM은 기본 쓰레드가 비 RT 우선 순위를 갖게끔 한다. (SCHED_OTHER 정책)

Thread.yield()

Thread.yield()는 같은 우선 순위에 있는 쓰레드에만 실행을 양보하고 더 높거나 낮은 우선 순위를 가진 쓰레드에는 절대로 양보하지 않는다. 같은 우선 순위를 가진 쓰레드에 대한 Yielding은 Thread.yield()가 한 개 이상의 RT 우선 순위를 사용하는 RT 애플리케이션에서 사용될 때 문제를 일으킨다는 것을 시사한다. 꼭 필요한 것이 아니라면 Thread.yield()를 사용하지 않는 것이 좋다.

NoHeapRealtimeThreads

javax.realtime.NoHeapRealtimeThread (NHRT)는 javax.realtime.RealtimeThread의 하위 클래스인 RTSJ의 새로운 쓰레드 유형이다. NHRT는 RT 쓰레드와 같은 스케줄링 특성을 갖고 있다. 단, NHRT는 GC에 의해 선점되지 않고, NHRT는 자바 힙을 읽고 쓸 수 없다. NHRT는 RTSJ의 중요한 측면이기 때문에 다음 기술자료에서 자세히 다루도록 하겠다.

AsynchronousEventHandlers

AsynchronousEventHandler (AEH) 역시 새로운 것이고 이벤트가 발생할 때 실행되는 RT 쓰레드이다. 예를 들어, AEH를 설정하여 지정된 시간 또는 알맞은 시간에 실행할 수 있다. AEH 역시 RT 쓰레드와 같은 스케줄링 특성을 갖고 있고 힙과 힙이 아닌 곳에 적용된다.




위로


동기화 개요

많은 자바 애플리케이션들은 자바 쓰레딩 기능을 직접 사용하거나, 개발 중인 애플리케이션들은 한 개 이상의 쓰레드를 포함하고 있는 라이브러리를 사용한다. 멀티쓰레디드 프로그래밍에서 중요한 것은 프로그램을 올바르게 실행하는 것이고, 여러 쓰레드들이 실행되는 시스템에서 쓰레드 보안을 확립하는 것이다. 안전한 프로그램 쓰레드를 만들기 위해서는 잠금 또는 최소한의 머신 연산 같은 동기화 명령들을 사용하여 한 개 이상의 쓰레드들 간 공유되는 데이터로의 액세스를 직렬화 해야 한다. RT 애플리케이션 프로그래머들은 특정 시간 제약 속에서 프로그램을 실행해야 하는 상황에 놓인다. 이를 위해서, 구현 상세, 함축적 의미, 컴포넌트의 성능 애트리뷰트를 알아야 한다.

이제부터는 자바 언어가 제공하는 기본적인 동기화 프리머티브의 측면, 이러한 프리머티브가 RTSJ에서는 어떻게 변화되는지, 이러한 프리머티브를 사용할 때 RT 프로그래머가 알아야 하는 것에 대해 설명하겠다.

자바 동기화 개요

자바는 세 가지 핵심적인 동기화 프리머티브를 제공한다.

  • 동기화 된 메소드와 블록은 쓰레드가 시작 시 객체를 잠그고 종료 시 잠금을 해제하도록 한다.
  • Object.wait()는 객체 잠금을 해제하고 쓰레드는 대기한다.
  • Object.notify()는 객체에 대해 wait()을 수행하는 쓰레드의 블록을 해제한다. notifyAll()은 모든 대기자들의 차단을 해제한다.

wait()notify()를 수행하는 쓰레드는 객체를 잠가야 한다.

잠금 경쟁(Lock contention)은 또 다른 쓰레드에 의해서 이미 잠긴 객체를 잠그려고 할 때 발생한다. 이러한 일이 발생하면, 잠금을 획득하는 것에 실패한 쓰레드는 객체에 대한 잠금 경쟁자들의 논리적 큐에 놓이게 된다. 이와 비슷하게, 여러 쓰레드들은 같은 객체에 대해 Object.wait()을 수행할 수 있기 때문에, 객체 대기자들의 논리적 큐가 있다. JLS는 큐가 관리되는 방식을 지정하지 않지만, RTSJ는 이러한 작동을 요구하고 있다.

우선 순위 기반 동기화 큐

RTSJ는 쓰레드의 모든 큐들이 FIFO 및 쓰레드 기반이다. 우선 순위 기반 FIFO 역시 잠금 경쟁자들 및 잠금 대기자들의 큐에 적용된다. 논리적 관점에서 볼 때, 실행을 기다리는 쓰레드의 실행 큐와 비슷한 잠금 경쟁자들의 FIFO 우선 순위 기반 큐들이 있다. 또한 잠금 대기자들의 비슷한 큐가 있다.

잠금이 해제될 때, 시스템은 가장 높은 우선 순위 큐의 경쟁자들의 앞에서 쓰레드를 선택하여 객체 잠금을 시도한다. notify()가 수행될 때, 가장 높은 우선 순위 큐의 대기자들의 앞에 있는 쓰레드는 블록이 해제된다. 잠금 해제 또는 잠금 notify() 연산은 가장 높은 우선 순위 큐의 힙에 있는 쓰레드가 실행된다는 점에서 스케줄링 디스패치 연산과 비슷하다.

우선 순위 기반 동기화를 지원하기 위해, RT 리눅스를 수정해야 한다. WebSphere Real Time의 VM이 notify() 연산이 수행될 때 어떤 쓰레드의 블록이 해제되어야 하는지를 선택하는 책임을 리눅스에 위임한다.

우선 순위 도치 및 우선 순위 상속

우선 순위 도치(Priority inversion)는 높은 우선 순위 쓰레드가 낮은 우선 순위 쓰레드가 갖고 있는 잠금에 대해 블록 되는 조건이다. 중간 우선 순위 쓰레드는 잠금을 갖고 있고 낮은 우선 순위 쓰레드에 기반하여 실행되는 동안 낮은 우선 순위 쓰레드를 선점한다. 우선 순위 도치는 낮은 우선 순위 쓰레드와 높은 우선 순위 쓰레드의 진행을 지연시킨다. 우선 순위 도치에 기인한 지연 때문에 중대한 데드라인을 맞출 수 다. 이러한 상황은 그림 1의 첫 번째 타임 라인에 나타나 있다.

우선 순위 상속(Priority inheritance)은 우선 순위 도치를 피하는 기술이다. 우선 순위 상속은 RTSJ에서 의무화 하고 있다. 우선 순위 상속의 개념은 잠금 경쟁의 상황에서, 잠금 보유자의 우선 순위가 그 잠금을 획득하고자 하는 쓰레드의 우선 순위로 올라가는 것이다. 잠금 보유자의 우선 순위가 잠금이 해제될 때 기본 우선 순위로 "강등"된다. 이러한 상황에서 낮은 우선 순위 쓰레드는 잠금 경쟁이 발생할 때 높은 우선 순위로 실행되다가 잠금이 해제될 때 끝난다. 잠금 해제 시, 높은 우선 순위 쓰레드는 객체를 잠그고 실행을 계속한다. 중간 우선 순위 쓰레드는 높은 우선 순위 쓰레드를 지연시키지 못하도록 되어있다. 그림 1의 두 번째 타임 라인은 우선 순위 상속이 실행될 때 첫 번째 타임 라인의 잠금 작동이 어떻게 변경되는지를 보여주고 있다.


그림 1. 우선 순위 도치와 우선 순위 상속
우선 순위 도치와 우선 순위 상속

높은 우선 순위 쓰레드가 낮은 우선 순위 쓰레드의 잠금을 획득하려고 할 때, 낮은 우선 순위 쓰레드는 또 다른 쓰레드가 보유한 잠금으로 차단될 수 있다. 이 경우, 낮은 우선 순위 쓰레드와 다른 쓰레드가 올라간다. 우선 순위 상속은 쓰레드 그룹의 우선 순위 상승과 강등을 필요로 할 수 있다.

우선 순위 상속

우선 순위 상속은 POSIX 잠금 서비스를 통해 사용자 공간으로 보내지는 리눅스 커널 기능을 통해 제공된다. 사용자 공간에서의 솔루션은 바람직하지 못하다. 이유는,

  • 리눅스 커널은 선점될 수 있고 우선 순위 도치가 될 가능성이 있다. 우선 순위 상속 역시 특정 시스템 잠금에 필요하다.
  • 사용자 공간에서 솔루션을 시도하면 풀기 어려운 경쟁 조건을 일으킨다.
  • 우선 순위 상승에는 커널 호출이 필요하다.

POSIX 잠금 유형은 pthread_mutex이다. pthread_mutex를 만드는 POSIX API는 mutex가 우선 순위 상속 프로토콜을 구현하도록 한다. pthread_mutex를 잠그고 pthread_mutex의 잠금을 해제하는 POSIX 서비스가 있다. 이것은 우선 순위 상속 지원이 되는 포인트이다. 리눅스는 경쟁 없이 사용자 공간에서 모든 잠금을 수행한다. 잠금 경쟁이 발생하면, 우선 순위 상승과 동기화 큐 관리가 커널 공간에서 수행된다.

WebSphere VM은 POSIX 잠금 API를 사용하여 우선 순위 상속 지원을 위해 자바 동기화 프리머티브를 구현한다. 사용자 레벨 C 코드 역시 POSIX 서비스를 사용한다. 자바 잠금 연산 시, 고유한 pthread_mutex가 할당되고 머신 연산을 사용하여 자바 객체로 바인딩 된다. 자바 레벨의 잠금 해제 연산을 할 때, 잠금 경쟁이 없을 경우, pthread_mutex는 원자 연산을 사용하여 객체로부터 바인딩을 해제한다. 경쟁이 있을 때, POSIX 잠금과 잠금 해제 연산은 리눅스 커널 우선 순위 상속을 지원한다.

mutex 할당과 잠금 시간을 최소화 하기 위해, JVM은 글로벌 잠금 캐시와 쓰레드당 잠금을 관리한다. 이곳에서 각 캐시는 할당되지 않은 pthread_mutex를 포함하고 있다. 쓰레드 캐시의 mutex는 글로벌 잠금 캐시로부터 획득된다. 이것이 쓰레드 잠금 캐시에 놓이기 전에, mutex는 쓰레드에 의해 미리 잠긴다. 경쟁하지 않은 잠금 해제 연산은 잠긴 mutex를 쓰레드 잠금 캐시로 리턴한다. 비경쟁 잠금이 기준이라면, 사전에 잠긴 mutex를 재사용 함으로써 POSIX 레벨 잠금은 줄어들거나 양도될 수 있다.

JVM은 쓰레드 리스트와 글로벌 잠금 캐시 같은 중요한 JVM 리소스로의 액세스를 직렬화 하는데 사용되는 내부 잠금을 갖고 있다. 이러한 잠금들은 우선 순위 상속에 기반하고 있고 단기간 보유된다.




위로


RT 애플리케이션을 위한 동기화 고려 사항

이 섹션에서는 애플리케이션을 포팅하여 RT 쓰레드를 사용하거나 새로운 애플리케이션을 작성하여 RT 쓰레딩을 활용하는 개발자를 위해 일부 RT 동기화 기능을 제공한다.

일반 자바 쓰레드들과 RT 쓰레드간 잠금 경쟁

RT 쓰레드는 일반 자바 쓰레드가 보유한 잠금으로 차단될 수 있다. 이러한 일이 발생하면, 잠금을 보유하고 있는 한 우선 순위 상속이 발생하여 일반 자바 쓰레드의 우선 순위가 RT 쓰레드의 우선 순위로 상승한다. 일반 자바 쓰레드는 RT 쓰레드의 모든 스케줄링 특성을 상속받는다.

  • 일반 자바 쓰레드는 SCHED_FIFO 정책을 실행하기 때문에, 쓰레드는 타임 슬라이스를 수행하지 않는다.
  • 디스패치와 yielding은 상승한 우선 순위의 RT 실행 큐에서 발생하지 않는다.

이러한 작동은 일반 자바 쓰레드가 잠금을 해제할 때 SCHED_OTHER로 복귀한다. Listing 1에서 만들어진 쓰레드들 중 어떤 것이라도 RT 쓰레드에서 요구하는 잠금을 보유하고 있는 동안 실행되었다면, 그 프로그램은 종료되지 않고, RT 쓰레드를 사용한 문제 코드 예제에 설명했던 것과 같은 문제를 나타냈을 것이다. 이러한 상황이 가능하기 때문에, SPIN 루프와 yielding을 수행하는 것은 실시간 JVM에서 실행되는 쓰레드에는 권장하지 않는다.

NHRT와 RT 쓰레드 간 잠금 경쟁

NHRT는 RT 쓰레드(또는 일반 자바 쓰레드)가 보유한 잠금으로 차단될 수 있다. RT 쓰레드가 잠금을 보유하고 있는 동안, GC는 RT를 선점하고 NHRT를 간접적으로 선점할 수 있다. NHRT는 RT가 GC에 의해서 더 이상 선점되지 않을 때까지 기다린 다음 NHRT가 실행할 기회를 갖기 전에 잠금을 해제해야 한다. GC에 의한 NHRT 선점은 NHRT가 시간에 민감한 기능을 수행할 경우 심각한 문제가 된다.

WebSphere Real Time의 결정론적 가비지 컬렉터는 1 밀리초 이하로 중지 시간을 유지하면서, NHRT 선점을 더욱 결정적으로 만든다. 이와 같은 중지 기간을 참을 수 없다면, NHRT와 RT 쓰레드 간 공유되는 잠금을 피하여 이러한 문제를 해결할 수 있다. 잠금이 의무적인 것이라면, RT와 NHRT 지정 리소스와 잠금을 갖는 것도 고려해 볼만하다. 예를 들어, 쓰레드 풀링을 실행하는 애플리케이션은 NHRT와 RT 쓰레드를 위한 개별 풀과 풀 잠금을 고려해 볼 수 있다.

또한, javax.realtime 패키지는 다음을 제공한다.

  • WaitFreeReadQueue 클래스는 RT 쓰레드에서 NHRT로 객체를 전달한다.
  • WaitFreeWriteQueue 클래스는 NHRT에서 RT 쓰레드로 객체를 전달한다.

이러한 클래스들은 비 대기(wait-free) 연산을 수행하는 동안 GC에 의해 차단될 수 있고, 비 대기 연산을 수행할 때 NHRT에서 요구하는 잠금을 보유하지 않는다.

javax.realtime 패키지의 동기화

특정 javax.realtime 메소드는 잠금이 경쟁하지 않을 때 조차 동기화의 오버헤드가 발생하기 때문에 의도적으로 동기화 되지 않는다. 동기화가 필요하다면, 콜러(caller)는 필요한 javax.realtime 메소드를 동기화 된 메소드 또는 블록으로 래핑한다. 프로그래머는 java.realtime 패키지의 메소드가 사용될 때 이와 같은 동기화를 추가하는 것을 고려해야 한다.

핵심 JLS 패키지에서의 동기화

반대로, java.util.Vector 같은 핵심 JLS 서비스들은 이미 동기화 된다. 또한, 특정 JLS 서비스들은 내부 잠금을 수행하여 특정 공유 리소스들을 직렬화 한다. 이러한 동기화 때문에, 핵심 JLS 서비스를 사용할 때, GC에 의한 NHRT 선점의 문제를 피해야 한다. (NHRT와 RT 쓰레드간 잠금 경쟁).

비경쟁 잠금 성능

비 RT 애플리케이션의 벤치마킹과 계측을 통해 잠금이 경쟁하지 않는다는 것을 알 수 있다. 비경쟁 잠금은 RT 애플리케이션의 뛰어난 부분이고, 특히 기존 컴포넌트나 라이브러리들이 재사용 될 때 그렇다. 비경쟁으로 알려진 잠금에 대한 결정적인 비용을 갖는 것이 좋지만, 동기화 지시문들은 피하거나 제거하기 어렵다.

앞서 설명했던 것처럼, 비경쟁 잠금 연산에는 설정과 원자 머신 명령이 포함된다. 비잠금 연산에는 원자 머신 연산이 포함된다. 잠금 연산용 설정에는 미리 잠긴 mutex의 할당이 포함된다. 이 할당은 비 경쟁 잠금 연산에서 가장 많은 변수 비용을 차지한다. RealtimeSystem.setMaximumConcurrentLocks()는 이러한 변수 비용을 제어할 수 있다.

RealtimeSystem.setMaximumConcurrentLocks(int numLocks)는 WebSphere Real Time의 VM이 numLocks mutex를 글로벌 잠금 캐시로 사전 할당하도록 한다. 글로벌 잠금 캐시는 쓰레드 당 잠금 캐시를 쓰레드 당 잠금 캐시들을 제공한다. RealTimeSystem API를 제공함으로써, 시간에 민감한 코드 영역 내에서 잠금이 발생할 기회를 줄일 수 있다. RealTimeSystem.getMaximumConcurrentLocks()setMaximumConcurentLocks() 호출에 어떤 숫자가 사용되어야 하는지를 결정하는데 사용되지만, getMaximumConcurrentLocks()는 최고 수위가 아닌 호출 시점에 잠금을 제공한다. 미래의 RTSJ 버전은 고수위 마크를 제공하는 API를 선보일 예정이다. numLocks 값에 터무니 없이 큰 값을 제공하지 않도록 하라. setMaximimConcurrentLocks()에 대한 호출은 많은 잠금을 만드는데 불규칙하고 정해지지 않은 시간과 메모리를 소비한다. 이러한 API는 JVM 스팩이 되도록 정의되기 때문에, 다른 JVM은 호출을 무시하거나 다른 작동을 제공한다.

경쟁 잠금 성능

쓰레드는 한 개 이상의 잠금을 동시에 가질 수 있고, 이러한 잠금은 특정 순서대로 획득된다. 이러한 잠금 패턴은 잠금 계층(lock hierarchy)을 형성한다. 우선 순위 상속은 쓰레드 그룹의 상승과 강등을 의미할 수 있다. 이 그룹의 쓰레드 수는 시스템에서 가능한 최대한의 잠금 계층보다 커서는 안된다. 잠금 계층을 좁게 유지함으로써, 가능한 적은 객체들을 잠글 수 있고, 이는 우선 순위 조정이 필요한 최대 쓰레드의 수에 영향을 미친다.

동기화 연산 시간

Object.wait(long timeout, int nanos)는 대기 연산에 나노초의 세분성을 제공한다. HighResolutionTime.waitForObject() API는 Object.wait()과 비슷하고 나노초 세분성으로 지정될 수 있는 상대 시간과 절대 시간을 제공한다. WebSphere Real Time에서, 두 API들은 기본적인 POSIX 잠금 대기 서비스들로 구현된다. 이러한 서비스들은 최대한 마이크로초 세분성을 제공한다. 이식성이 필요할 경우 javax.realtime 패키지의 Clock 클래스의 getResolution() 메소드는 실행 플랫폼을 위한 해상도를 검색하는데 사용된다.




위로


요약

RTSJ는 javax.realtime 패키지의 새로운 RT 클래스와 API를 통해 쓰레딩 및 동기화 기능을 확장 및 강화한다. WebSphere Real Time에서, 이러한 기능들은 RT 버전의 리눅스 커널, POSIX 쓰레딩 수정, JVM 수정을 통해 구현된다. RTSJ 쓰레딩과 동기화를 이해하면 RT 애플리케이션을 작성 및 전개할 때의 문제를 피하는데 도움이 된다.



 

참고자료

교육


제품 및 기술 얻기


토론



 

필자소개

Patrick Gallop은 1984년 워털루대학교(University of Waterloo)를 졸업했다. 졸업 직후 IBM Toronto Lab에 입사했으며, 다양한 컴파일러와 컴파일러 툴 프로젝트에 참여했다. IBM Java 프로젝트에서 일했으며, 최근에는 IBM Real-time Java 프로젝트의 리더로 활동했다.


Mark Stoodley

Mark Stoodley는 2001년 토론토대학교(University of Toronto)에서 컴퓨터 엔지니어링 박사 학위를 받았고, 2002년에 IBM Toronto Lab에 합류하여 Java JIT 컴파일 기술 분야에서 일했다. 2005년 초반부터 IBM WebSphere Real Time용 JIT 기술에 대해 작업했고, 기존 JIT 컴파일러를 실시간 환경에 적용하는 것과 관련한 작업을 했다. 현재 자바 컴파일 컨트롤 팀의 리더이며, 실행 환경의 코드 컴파일의 효과를 증진하기 위해 노력하고 있다.

반응형
Java SE의 로드맵과 최신 6.0의 특징 (한국썬 임수경 차장)

Java SE 로드맵에 대해 살펴보고, 'mustang' 이라는 프로젝트 명으로 일컬어지는,
 
Java SE 6 의 변화된 내용과 향상된 기능을 이해한다.
반응형
Java 코딩 지침(2004/4/14)

심우곤(wgshim) 그린벨시스템즈 정보기술연구소

본 문서는 Geotechnical Software 에서 작성한 2004 년 1월 버전의 Geosoft 의 Java Programming Style Guidelines 문서를 근간으로 하여 Sun 의 코딩 표준 권고안 및 기타 표준안들을 참고하여 작성한 것입니다. 개선된 사항을 반영하기 위하여 예고없이 변경될 수 있으며 본 문건을 통해 발생한 문제에 대해 책임을 지지 않습니다.


목 차


1 들어가기

본 문서는 Java 개발자 커뮤니티 내에서 보편적인 Java 코딩 권고안들을 나열하고 있습니다. 이 권


고안들은 이미 널리 표준으로 받아들여지고 있는 몇몇 문건들(예를 들면, [1], [2], [3], [4], [5])에


기반을 두고 있으며, 전세계의 수 많은 소프트웨어 전문가들의 피드백을 받아들여 작성되었습니다.


기존 지침들이 갖는 주된 단점은, 지침들이 너무 일반적이다보니 보다 구체적이고 상세한 규칙들(예


를 들면, 명명 규칙:naming rule)이 확립되어야 할 필요가 있다는 것입니이다. 따라서, 본 지침은 여


타의 지침들에 비하여 프로젝트의 코드 검토 단계에 사용하기 용이하도록 각 항목마다 주석과 예제


를 두었습니다. 추가적으로, 일반적으로 프로그래밍 권고안들은 코딩 스타일 이슈와 언어 고유한 기


술적 이슈들을 혼합하여 기술함으로써 혼란을 초래하는 경향이 있기 때문에, 본 문서에서는 Java 언


어의 기술적인 권고안에 대해서 일절 언급하지 않고 오로지 프로그래밍 스타일에 대해서만 다루고


자 합니다. IDE 도구를 사용하는 개발 환경에서는 접근제한자, 키워드/문법 하일라이팅, 자동 포맷


팅 등을 제공하여 가독성을 향상시킬 수 있을 것입니다. 하지만 프로그래머라면 절대로 그러한 기능


종속되어서는 안됩니다. 소스 코드를 단순히 IDE 도구 내에서 개발되는 코드뿐 아니라 더 큰/다


양한 범위로 확대하여야 하며, 어떠한 IDE 도구를 사용하는지와 상관없이 가독성을 극대화할 수 있


도록 소스 코드를 작성하여야만 합니다.

1.1 권고안 레이아웃

권고안들은 주제별로 그룹지어져 있으며, 각각의 권고안들은 코드 검토 시 손쉽게 참조할 수 있도록

숫자를 부여하였습니다. 본 문서에서 사용할 권고안 레이아웃은 다음과 같습니다:


지침에 대한 간략한 설명
적용이 가능한 경우, 적용사례
동기(motivation), 배경 및 추가적인 정보


동기 섹션은 중요합니다. 코딩 표준과 지침들은 일종의 "성전(聖戰)" 과 같은 성격을 띄기 마련입니


다. 따라서 각 권고안의 배경을 언급하는 것은 중요합니다.

1.2 권고안의 중요도

지침 섹션에서 사용되는 반드시 한다(must), 한다(should), 할 수 있다(can) 와 같은 용어들은 특별

한 의미를 가집니다. 반드시 한다는 필히 준수해야 한다는 것을, 한다는 강한 권고를, 할 수 있다는

일반적인 지침을 의미합니다.

2 일반적인 지침들

1. 가독성을 증진시킬 수 있는 타당한 이유가 있다면 본 지침을 어길 수도 있다.
 
본 지침의 궁극적인 목표는 가독성을 향상시키는 것입니다. 가독성이 향상되면 이해가 용이해지고 유지보수가 수월해지며 일반적으로 코드의 품질도 좋아집니다. 일반적인 지침을 거론하는 본 지침서에서 모든 상황들을 감안한 지침을 제공해 드릴 수 없기 때문에, 프로그래머의 판단에 의하여 가변적으로 활용하실 수 있습니다.

3 명명 관례(Naming Conventions)

3.1 일반적인 명명 관례

2. 패키지를 표현하는 이름은 모두 소문자를 사용한다.
mypackage, com.company.application.ui
이 패키지 명명 관례는 Java 핵심 패키지들의 명명에 Sun 社 가 사용하고 있는 것입니다. 패키지 이름의 시작부는 반드시 도메인 이름으로 소문자를 사용해야만 합니다.

3. 타입을 표현하는 이름은 대소문자를 혼용할 수 있지만, 반드시 명사를 사용하고 시작 글자를 대문자로 지정한다.
Account, EventHandler
Java 개발자 커뮤니티에서 사용하는 일반적인 관습이며, Sun 社 가 Java 핵심 패키지들의 타입 명명에 사용하고 있는 것입니다.

4. 변수의 이름은 대소문자를 혼용할 수 있지만 반드시 소문자로 시작한다.
account, eventHandler
Java 개발자 커뮤니티에서 사용하는 일반적인 관습이며, Sun 社 가 Java 핵심 패키지들의 변수이름 명명에 사용하고 있는 것입니다. 변수와 타입을 손쉽게 구별할 수 있도록 하면, 이름의 충돌이 발생하는 것을 효과적으로 해결할 수 있게 됩니다. 예) Account account; // 선언문

5. 상수(final 변수)를 표현하는 이름은 반드시 모두 대문자로 지정하되 '_' 를 사용하여 단어들을 구분한다.
MAX_ITERATIONS, COLOR_RED
Java 개발자 커뮤니티에서 사용하는 일반적인 관습이며, Sun 社 가 Java 핵심 패키지에 사용하고 있는 것입니다. 일반적으로, 이러한 상수의 사용은 최소화해야만 합니다. 대다수의 경우 상수 변수를 메소드로 구현하는 것이 더 낫습니다:
int getMaxIterations()     // NOT: MAX_ITERATIONS = 25
{
  return 25;
}

이러한 양식이 보다 읽기에 편하며, 클래스의 값을 참조하는 일관된 인터페이스를 제공할 수 있다는 장점을 가집니다.
 

6. 메소드의 이름은 대소문자를 혼용할 수 있지만 반드시 동사를 사용하며 소문자로 시작한다.
getName(), computeTotalWidth()
Java 개발자 커뮤니티에서 사용하는 일반적인 관습이며, Sun 社 가 Java 핵심 패키지에 사용하고 있는 것입니다. 이 방식은 변수의 이름을 지을 때 사용하는 관례와 동일합니다. 하지만, Java 에서는 이미 특별한 양식(getter/setter, 동사로 시작하는 이름, JVM Spec, 등)에 의거하여 메소드는 변수를 구분합니다.

7. 축약형(Abbreviation) 과 두문자어형(頭文字語: Acronym) 을 이름에 사용할 경우에는 전부 대문자로 지정하지 않는다.
exportHtmlSource();    // exportHTMLSource(); 가 아님
openDvdPlayer();       // openDVDPlayer(); 가 아님
축약형 혹은 두문자어형 이름을 모두 대문자로 지정하게 되면 앞서 기술한 명명 지침들과 충돌이 발생하게 됩니다 (대표적으로 상수에 대한 명명 지침과 혼동될 수 있습니다). 그렇다고 이 유형의 이름을 dVD, hTML 등 과 같이 다양한 형태로 지정할 경우에는 가독성이 떨어집니다. 또 다른 문제점으로는 위의 예제에서 살펴볼 수 있는 바와 같이, 이 유형의 이름이 다른 이름과 결합되는 경우 가독성이 극도로 나빠진다는 것입니다. 후속하는 단어가 있을 경우는 더욱 각별히 주의해야 합니다.

8. private 접근 제한자를 갖는 클래스 변수에 '_' 접미사를 사용한다.
class Well
{
  private int  depth_;
  ...
}
변수의 이름이나 타입과는 별개로, 변수의 범위(scope)는 매우 중요한 특성입니다. 접미사 '_' 를 사용함으로써 클래스 범위의 변수(로컬 변수가 아닌)임을 쉽게 구별할 수 있게 됩니다. 클래스 변수는 메소드 내에서 선언되는 로컬 변수에 비해 중요도가 높기 때문에 프로그래머가 각별히 주의해야 합니다. '_' 를 사용하는 명명 규칙은 부가적으로 setter 메소드에서의 이름 충돌문제를 깔끔하게 해결해 줍니다: void setDepth (int depth)
{
  depth_ = depth;
}
제기될 수 있는 이슈거리는 '_' 를 변수의 접두사로 쓸 것인가 접미사로 쓸 것인가에 대한 것입니다. 두 가지 모두 일반적으로 널리 사용되는 방법이기는 합니다만, 이름을 읽기 편하다는 측면에서 후자의 것을 권고합니다. 변수의 이름에 볌위를 식별할 수 있도록 하자는 제안은 한 동안 많은 논쟁을 불러일으켰습니다. 그런데 현재에는 이러한 방법이 수용되어 전문 개발자 커뮤니티에서도 일반적인 관례로 점차 정착이 되고 있는 것 같습니다.
 

9. 일반적인 변수의 이름은 타입의 이름과 동일하게 지정한다.
void setTopic (Topic topic)      // void setTopic (Topic value) 이 아님
                                 // void setTopic (Topic aTopic) 이 아님
                                 // void setTopic (Topic x) 이 아님

void connect (Database database) // void connect (Database db) 가 아님
                                 // void connect (Database oracleDB) 가 아님
용어나 이름의 수를 줄이는 것이 코드의 복잡도를 줄여줍니다. 또한 변수의 이름만으로도 그 타입을 손쉽게 유추할 수 있게 해준다는 장점도 있습니다. 만약 어떠한 이유에선가 이러한 관례가 맞지 않는 것처럼 느끼신다면, 이는 분명 타입이름 자체를 잘못 지정한 것입니다. 일반적이지 않은 변수들은 각기 나름의 역할(role)을 가지고 있습니다. 이러한 변수들은 역할과 변수의 타입을 결함하여 이름을 짓곤 합니다. Point startingPoint, centerPoint;
Name  loginName;

 

10. 모든 이름은 영어로 작성한다.
fileName;    // filNavn 나 파일이름 이 아님
국제적인 개발에 있어서 영어가 선호되기 때문입니다.

11. 넓은 범위에 영향을 미치는 변수는 긴 이름을 부여하고, 좁은 범위의 변수는 짧은 이름을 부여한다 [1].
 
임시 저장공간이나 인덱스로 사용되는 Scratch variable (주: 의미있는 값을 갖지 않고 그때그때 상황에 따라 값들을 잠시 보관해 두기 위한 변수로, 대개 보유한 값이 얼마 후에 의미가 없어지거나 삭제됨)들은 매우 짧은 이름을 부여하십시요. 프로그래머가 그러한 변수들을 읽음과 동시에, 이 변수는 몇 라인 뒤에 그 값이 유효하지 않을 것임을 짐장할 수 있게 해야 합니다. 보편적인 scratch variable 로는 정수를 저장하는 i, j, k, m, n 가 있고 문자를 저장하는 c, d 가 있습니다.

12. 호출하려는 객체의 이름을 통해 의미를 짐작할 수 있다면, 메소드의 이름을 간략화할 수 있다.
line.getLength();    // line.getLineLength(); 가 아님
클래스 선언 시에는 후자의 것이 자연스럽지만, 사용할 때에는 위 예에서 볼 수 있듯이 중언부언하는 느낌을 줄 수 있습니다.


3.2 특수한 명명 관례

13. get/set 이라는 용어는 반드시 애트리뷰트에 직접 접근하는 메소드에 사용한다.
employee.getName();
matrix.getElement (2, 4);
employee.setName (name);
matrix.setElement (2, 4, value);
이 접근 메소드 명명 관례는 Java 핵심 패키지들의 명명에 Sun 社 가 사용하고 있는 것입니다. 실제 자바빈즈를 작성할 때에는 이 명명규칙을 준수해야 합니다.

14. is 접두사를 불리언 변수와 메소드에 사용한다.
isSet, isVisible, isFinished, isFound, isOpen
이 불리언 메소드와 변수에 대한 명명 관례는 Java 핵심 패키지들의 명명에 Sun 社 가 사용하고 있는 것입니다. 실제 자바빈즈를 작성할 때에는 이 명명규칙을 준수해야 합니다. is 접두사를 사용함으로써 일반적으로 statusflag 와 같은 좋지 않은 불리언 이름을 선택하는 문제를 해결할 수 있습니다. isStatusisFlag 는 간결하지만 프로그래머에게 의미를 풍부하게 전달할 수 없다는 점에서 바람직하지 못합니다. 일부 상황에서는 is 접두사가 아닌 보다 더 적합한 접두사를 사용할 수도 있습니다. has, can, should 접두사들을 그런 대안으로 활용하실 수 있을 것입니다: boolean hasLicense();
boolean canEvaluate();
boolean shouldAbort = false;

 

15. compute 라는 용어는 무엇인가를 계산(시간이 소요되는)하는 메소드에 사용할 수 있다.
valueSet.computeAverage();  matrix.computeInverse()
프로그래머로 하여금 보는 즉시 이 메소드는 잠재적으로 시간이 소요되는 작업을 수행하고 있다는 것을 주지심으로써, 이 메소드의 결과를 반복해서 사용하고자 할 경우에 프로그래머는 계산된 값을 캐시해 둘 것을 고려하게 만들 수 있습니다. 용어를 일관성있게 사용하는 것은 가독성을 높이는 지름길입니다.

16. find 라는 용어는 무엇인가를 찾는 메소드에 사용할 수 있다.
vertex.findNearestVertex();   matrix.findMinElement(); 
프로그래머로 하여금 보는 즉시 이 메소드는 최소한의 계산을 통해 요청하는 결과를 찾을 수 있는 메소드라는 것을 알려주십시오. 용어를 일관성있게 사용하는 것은 가독성을 높이는 지름길입니다.

17. initialize 라는 용어는 객체나 개념이 확립되어지는 곳에 사용할 수 있다.
printer.initializeFontSet();
미국식 initialize 가 영국식 initialise 보다 바람직합니다. init 라는 축약형 이름은 절대로 사용하지 마십시오.

18. JFC (Java Swing) 변수들은 각 컴포넌트의 타입을 접두사로 사용한다.
widthScale, nameTextField, leftScrollbar, mainPanel, fileToggle, minLabel, printerDialog
변수의 이름 자체가 자신이 어떤 타입의 변수인지 알려주기 때문에 객체가 어떤 자원인지 쉽게 알 수 있습니다.

19. 컬렉션의 이름은 반드시 복수형으로 사용한다.
vertex (one vertex),   vertices(a collection of vertices)
account (one account),    accounts(a collection of accounts)
 
여기서의 컬렉션은 java.util.Collection 변수와 단순 배열과 같은 Collection 의 자손들을 의미합니다.

20. n 접두사는 객체의 개수를 나타내는 변수에 사용한다.
nPoints, nLines
이 표기법은 개체의 개수를 나타내는 수식의 표기법에서 차용한 것입니다. Sun 社 는 Java 핵심 패키지에서 이런 용도로 num 접두사를 사용한다는 것에 주목해 보겠습니다. 이는 필경 number of 에 대한 축약어를 의미할 것입니다만, 그 보다는 number 를 연상시킴으로써 변수 이름을 이상하게 만들거나 혼동을 불러일으킬 수 있습니다. 만약 "number of" 를 더 선호하신다면, n 대신에 numberOf 접두사를 사용하실 수도 있을겁니다. num 접두사는 사용하시지 않기를 강력하게 권합니다.
 

21. No 접미사는 엔터티 번호를 나타내는 변수에 사용한다.
tableNo, employeeNo
이 표기법은 엔터티의 번호를 나타내는 수식의 표기법에서 차용한 것입니다. 또 다른 우아한 표기 방법으로는 i 을 접두사로 사용하는 것입니다: iTable, iEmployee. 이 방법은 효과적으로 변수에 iterator 로의 역할을 부여하게 됩니다.
 

22. Iterator 변수들은 i, j, k 등 과 같은 이름을 사용한다.
while (Iterator i = pointList.iterator(); i.hasNext(); ) {
  :
}

for (int i = 0; i < nTables; i++) {
  :
}
 
이 표기법은 iterator 를 나타내는 수식의 표기법에서 차용한 것입니다. j, k 등과 같은 이름의 변수들은 중첩된 반복문 내에서만 사용합니다.
 

23. 대응하는 단어가 있는 이름은 반드시 함께 사용한다 [1].
get/set, add/remove, create/destroy, start/stop, insert/delete, increment/decrement, old/new, begin/end, first/last, up/down, min/max, next/previous, old/new, open/close, show/hide
대칭을 이루도록 함으로써 복잡도를 줄일 수 있습니다.

24. 축약형 이름의 사용은 피한다.
computeAverage();     // compAvg(); 가 아님
고려해 볼 두 종류의 단어들이 있습니다. 먼저 프로그래밍 언어를 구사하는 데에 있어 빈번하게 사용되는 일반적인 단어들이 있습니다. 이들 단어는 절대로 축약형을 사용하지 말아야 합니다: command   대신 cmd
copy      
대신 cp
point    
대신 pt
compute  
대신 comp
initialize
대신 init
등등.
그 다음, 도메인에서 사용하는 특수한 두문자어나 축약어들은 축약형을 그대로 사용합니다. 이들 구문은 굳이 풀어 나열하지 않고 축약형을 사용하십시오. 다음과 같이 사용하시면 안됩니다: html 대신 HypertextMarkupLanguage
cpu
대신 CentralProcessingUnit
pe  
대신 PriceEarningRatio
등등.
 

25. 불리언 변수 이름은 절대로 부정적인(거짓인) 이름을 사용하지 않는다.
boolean isError;    // isNotError 가 아님
boolean isFound;    // isNotFound 가 아님
문제는 불리언 변수에 부정적인 이름을 사용할 경우, not 연산자를 사용할 경우 부정의 부정을 계산해야 하는 혼동을 초래한다는 것입니다. 예를 들어, !isNotError 에서 처럼, 신속하게 이것이 무엇을 의미하는지 인지할 수 없게 됩니다.

26. 관련있는 상수(final 변수)들은 공통 타입의 이름을 접두사로 사용하여 그룹핑한다.
final int COLOR_RED   = 1;
final int COLOR_GREEN = 2;
final int COLOR_BLUE  = 3;

final int MOOD_HAPPY  = 1;
final int MOOD_BLUE   = 2;
 
이 표기방법은 상수들이 어디에 속해있는지 상수가 무엇을 나타내고 있는지 식별하는데 도움이 됩니다.

27. 예외(Exception) 클래스들은 Exception 이라는 접미사를 사용한다.
class AccessException
{
  :
}
 
사실 예외 클래스들은 프로그램의 핵심 디자인에 해당되지 않기 때문에, 이와 같은 방식으로 이름을 지정함으로써 상대적으로 클래스들 중에서 눈에 띄게(구분을 용이하게) 해두는 것이 좋습니다. 이 명명 관례는 Java 기본 라이브러리들에 Sun 社 가 사용하고 있는 표준입니다.

28. 디폴트 인터페이스 구현은 Default 라는 접두사를 사용할 수 있다.
class DefaultTableCellRenderer
implements TableCellRenderer
{
  :
}
 
이 방법은 인터페이스 메소드들에 대한 디폴트 처리작업을 제공하는 구현물에, 간단하게 이름을 부여하는 방법으로 널리 사용되고 있는 방법입니다. Default 접두사를 클래스의 이름에 부여하는 이 명명 관례는, Java 기본 라이브러리들에 Sun 社 가 채택한 방식입니다.

29. 함수(객체/결과를 리턴하는 메소드)의 이름은 '처리 후 무엇을 리턴하는지'를 의미하고, 프로시저(void 메소드)의 이름은 '무엇을 처리하는지'를 의미한다.
 
메소드가 무엇을 하는지. 의도하지 않는 것들이 무엇인지를 명확하게 해주기 때문에, 가독성을 높여줍니다. 또한 이 방법은 예기치 못한 부작용(side effect)들로부터 코드를 명쾌하게 유지시켜 줄 수 있습니다.


4 파일

30. Java 소스 파일의 확장자는 .java 이다.
Point.java
Java 개발툴에 의해 강제적으로 준수해야 하는 규칙입니다.

31. 클래스는 각각의 파일에 선언하며, 클래스의 이름과 파일의 이름이 동일해야 한다. 이너(inner) 클래스나 private 으로 선언된 secondary 클래스들은 굳이 별도의 파일로 구분하지 않고도 primary 클래스와 같은 파일 내에서 선언할 수 있다.
 
Java 개발툴에 의해 강제적으로 준수해야 하는 규칙입니다.

32. 파일의 내용물은 반드시 80 컬럼을 벗어나지 않는다.
 
80 컬럼은 다양한 편집기, 프린터, 터미널 에뮬레이터, 디버거에서도 일반적인 길이로, 각 라인이 80 컬럼을 넘지 않도록 소스코드를 작성한다면 다양한 환경의 여러 개발자들과 파일을 공유해서 사용할 수 있습니다. 이는 프로그래머들 간에 파일을 전달하였을 때 의도하지 않은 들여쓰기나 개행문자가 포함되어 가독성이 떨어지는 것을 예방합니다.

33. 탭(TAB) 문자나 페이지 구분(page break) 문자와 같은 특수한 문자들은 절대로 사용하지 않는다.
 
이러한 문자들은 프로그래머가 여럿이거나 다수의 플랫폼에서 개발하는 환경에서, 편집기 별, 프린터 별, 터미널 에뮬레이터 별, 디버거 별로 지정된 설정에 따라 소스코드를 들쭉날쭉하게 만들어 가독성을 떨어뜨리게 됩니다.

34. 여러 라인으로 분리한 문장들은 반드시 명확하게 만든다 [1].
totalSum = a + b + c +
           d + e);
function (param1, param2,
          param3);
setText ("Long line split" +
         "into two parts.");
for (tableNo = 0; tableNo < nTables;  
     tableNo += tableStep)
한 문장이 80 컬럼을 벗어날 때 라인을 분리해서 작성하게 됩니다. 어떻게 라인을 분리해야 하는지에 대한 엄격한 규칙을 제시하기는 어렵습니다만, 위 예제들은 일반적인 힌트를 제공해줄 것입니다: 일반적으로:
  • 콤마(,) 뒤에서 분리한다.
  • 연산자 뒤에서 분리한다.
  • 이전 라인에서의 표현식 시작 부분에 맞추어 정렬한다.


5 문장

5.1 package 와 import

35. package 문은 반드시 파일의 첫번째 라인에 나타나야 하며, 모든 파일은 특정 패키지에 소속된다.
 
package 문의 위치는 Java 언어 코딩 표준에서 권고하고 있는 것입니다. 모든 파일들을 실제 패키지(디폴트 패키지보다는 이름이 있는 패키지)에 두는 것은 Java 객체 지향 프로그래밍 기법에 도움을 줍니다.

36. import 문은 반드시 package 문 뒤에 나와야 한다. import 문은 가장 기본이 되는 패키지들 부터 순차적으로 정렬하며, 관련있는 패키지들은 함께 묶어 두고 빈 라인을 삽입하여 일목요연하게 정리한다.
import java.io.*;
import java.net.*;

import java.rmi.*
import java.rmi.server.*;

import javax.swing.*;
import javax.swing.event.*;

import org.linux.apache.server.*;
 
import 문의 위치는 Java 언어 코딩 표준에서 권고하고 있는 것입니다. 많은 import 문들을 정렬해두면 편리하게 import 문들을 검토할 수 있고, 이를 통하여 현재 이 패키지가 어떤 용도로 설계되었는지를 쉽게 파악할 수 있습니다. 또한 import 문을 그룹핑하면 관련된 정보들을 공통의 유닛으로 관리할 수 있기 때문에 복잡도를 줄일 수 있습니다.

37. import 문을 사용할 때에는 와일드 카드 문자(*)를 사용하지 않는다.
import java.io.*;

대신:

import java.io.File;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.io.PrintWriter;
import java.io.FileNotFoundException;
 
사실 #36 의 예제와 같이 import 문에 와일드 카드 문자인 * 를 사용하는 것은, 프로그래머에게는 편리할지 모르지만 성능에 얼마간 영향을 미치게 됩니다. 더구나 import 문만 보고도 정확하게 현재 클래스/인터페이스가 어떤 작업을 할 것인지 예측할 수 있도록 하려면, 명시적으로 사용하는 클래스와 인터페이스들에 대해서만 import 하시는 것이 필요합니다.


5.2 클래스와 인터페이스

38. Class 와 Interface 의 선언은 다음과 같은 방식으로 조직화하여 사용한다:
  1. Class/Interface 문서(javadoc 주석)
  2. classinterface 선언문
  3. 클래스 변수(static으로 선언된) 들을 public, protected, package (접근제한자가 없는), private 순서대로 나열한다.
  4. 인스턴스 변수들을 public, protected, package (접근제한자가 없는), private 순서대로 나열한다.
  5. 생성자.
  6. 메소드 (메소드에는 특별한 순서가 없음).
 
각각의 클래스/인터페이스 구성요소들의 등장 위치를 지키게되면, 현재 코드 상에 어떤 요소들이 다음에 등장할 것인지 예측할 수 있게되어 복잡도를 낮추게 됩니다.


5.3 메소드

39. 메소드 지시자는 다음의 순서대로 사용한다:
<access> static abstract synchronized <unusual> final native
만약 <access> 지시자가 존재한다면 반드시 맨 처음에 나타나야 한다.
 
<access> 지시자는 public, protected, private 중 하나이고, <unusual> 부분은 volatiletransient 중 하나가 지정됩니다. 여기서 가장 중요한 점은 접근(access) 지시자가 맨 처음에 나타나야 한다는 것입니다. 사용할 수 있는 다양한 지시자들이 있겠지만, 이는 매우 중요한 사항이기 때문에 반드시 메소드 선언문에 반영되어야 합니다. 기타 지시자들의 순서는 덜 중요하지만 일관된 관례를 따르는 것이 바람직합니다. 여기서 제안한 지시자 나열 순서는 Teach Yourself Java in 21 Days 의 저자인 Charles L. Perkins 씨가 자신의 책에서 제시한 방식을 차용한 것입니다.


5.4 타입

40. 타입 컨버전(변환)은 반드시 명시적으로 한다. 묵시적인 타입 컨버전(변환)은 절대로 사용하지 않는다.
floatValue = (float) intValue;     // floatValue = intValue; 가 아님
위 예제의 경우, 프로그래머가 서로 다른 두 타입을 사용하였으며 그가 의도적으로 두 타입을 혼합해서 사용하고 있음을 알려줍니다. 만약 묵시적으로 floatValue = intValue 라고 하였다면 의도적으로 그러한 것인지, 타입 컨버전을 깜빡 한 것인지 혼동할 수 있습니다.

41. 배열 지시자([])는 변수의 이름 뒤가 아니라 타입의 이름 뒤에 나온다.
int[] daysOfMonth;      // int daysOfMonth[]; 가 아님
배열은 타입의 한 속성이지 변수의 속성이 아니기 때문입니다. 어떤 이유에서인지 Java 에서는 두 가지 모두 문법적으로 허용하고 있습니다만, 하나로 통일하는 것이 좋겠습니다.


5.5 변수

42. 변수는 선언된 지점에서 초기화하며, 가능한 사용범위를 최소화하여 선언한다.
 
이 규칙은 어느 시점에서든 변수가 유효한 값을 가진다는 것을 보장해줍니다. 종종 선언하는 시점에 변수에 유효한 값을 초기화하는 것이 불가능한 경우가 있습니다만, 그러한 경우에도 초기화하지 않은 상태로 내버려두는 것보다는 임의의 값이라도 사용하여 초기화 해두는 것이 좋습니다.

43. 변수는 절대로 하나 이상의 의미를 가져서는 안된다.
 
무의미한 이름을 부여한 변수(scratch 변수)를 가지고 이값 저값을 할당하여 사용하게 되면, 어느 순간 이 변수가 현재 어떤 의미의 값을 가지고 있는지 모호해지는 경우가 발생합니다. 일관되고 유일한 의미를 부여함으로써 가독성을 향상시키고 더불어 부작용까지 예방할 수 있습니다.

44. static 으로 선언된 클래스 변수들은 절대로 public 으로 선언하지 않는다.
 
public 변수들은 Java 의 정보은닉과 캡슐화 컨셉에 위배됩니다. 대신 변수를 private 으로 선언하시고 이 변수를 접근할 수 있는 메소드를 사용하게 하십시오. 한 가지 예외적인 상황은 클래스가 데이터 구조로만 사용될 때입니다. 이 경우 클래스는 어떠한 행위(메소드)도 갖지 않고 오로지 데이터를 보관하는, 즉 C++ 에서의 struct 와 동일한 형태로 사용됩니다. 이 경우에는 클래스의 인스턴스 변수들을 public 으로 선언할 수도 있습니다 [2].

45. 동일한 타입의 변수 중 관련있는 변수들은 하나의 구문에서 선언할 수 있다.
즉, 관련이 없는 변수들을 같은 라인에서 선언하지 않는다.
float  x, y, z;
float  revenueJanuary, revenueFebrury, revenueMarch;
위의 예제를 여러 라인에 나누어 선언하는 것은 바람직하지 않습니다. 변수들을 그룹핑함으로써 가독성을 높일 수 있기 때문입니다. 그러나 여기서 주의할 점은, 대다수의 경우에서는 이 규칙보다 각각의 변수는 선언 시 초기화 한다는 규칙이 우선적으로 적용되어야 한다는 것입니다.

46. 변수의 생존기간을 가능한 짧게 유지한다.
 
변수에 대한 조작을 작은 범위 내에 국한시킴으로써, 영향과 부작용을 통제하는 것이 용이해집니다.


5.6 반복문

47. 반드시 반복을 제어하는데 사용되는 문장들만 for() 구문 내에 포함시킨다.
sum = 0;                   // NOT:     for (i=0, sum=0; i<100; i++)
for (i=0; i<100; i++)      //            sum += value[i];
  sum += value[i];
관리의 용이성과 가독성을 향상시킬 수 있습니다. 무엇이 반복문을 제어하고 무엇이 반복문 내에서 사용되는지 명쾌하게 하십시오.

48. 반복문에 사용하는 변수는 반복문 직전에 초기화한다.
boolean isDone = false;  // NOT:   boolean  isDone = false;
while (!isDone) {        //         :
  :                      //        while (!isDone) {
}                        //          :
                                   }
 

49. do .... while 문의 사용을 피한다.
 
여기에는 두 가지 이유가 있습니다. 첫번째, 제어문에 사용되는 구문 요소(키워드)들이 분산되어 있어 난잡합니다. do .... while 문을 사용한 모든 문장은 while 문이나 for 문을 사용하여 동일하게 바꾸어 작성할 수 있습니다. 사용되는 구문 요소들을 최소화함으로써 복잡도를 줄일 수 있습니다. 두번째 이유는 가독성과 관련되어 있습니다. 반복의 조건을 체크하는 부분이 하단에 위치하는 것은, 상단에서 조건을 체크하는 반복문보다 읽기가 더욱 어렵습니다.
 

40. 반복문 내에서 breakcontinue 의 사용을 자제한다.
 
이 두 문장은 구조화된 반복문보다 훨씬 가독성이 높을 때에만 사용하십시오. 일반적으로 break 문은 case 문 내에서만 사용하시고, continue 문은 반복문이든 case 문이든 어떤 경우라도 사용을 피해주시기 바랍니다.
 


5.7 조건문

51. 복잡한 조건식은 반드시 피한다. 그 대신 임시 불리언 변수를 활용하라 [1].
if ((elementNo < 0) || (elementNo > maxElement)||
    elementNo == lastElement) {
  :
}
다음과 같이 변경한다: boolean isFinished      = (elementNo < 0) || (elementNo > maxElement);
boolean isRepeatedEntry = elementNo == lastElement;
if (isFinished || isRepeatedEntry) {
  :
}

 
조건식을 불리언 변수에 할당함으로써, 프로그램 그 자체로 의미를 명확하게 전달하고 있습니다. 조건문을 평가하는 구문은 보다 쉽게 해석할 수 있으며, 더불어 디버깅할 때에도 편리해집니다.

52. if 문에서, 일반적인 상황은 if 블록에 위치시키고 예외 상황은 else 블록에 위치시킨다 [1].
boolean isError = readFile (fileName);
if (!isError) {
  :
}
else {
  :
}
일반적인 처리 흐름과 예외상황 처리 흐름을 불명확하지 않게 하십시오. 이 지침은 가독성과 성능 모두에 영향을 미치는 중요한 사항입니다.

53. 조건을 평가하는 문장은 별도의 라인으로 분리한다.
if (isDone)              // NOT:  if (isDone) doCleanup();
  doCleanup();
이는 디버깅을 위한 목적으로 한 것입니다. 한 라인에 이것저것 기술하게 되면, 조건식 테스트가 참인지 거짓인지를 확인하기가 어려워집니다.

54. 절대로 조건식에 실행문을 사용하지 않는다.
file = openFile (fileName, "w");  // NOT:   if ((file = openFile (fileName, "w")) != null) {
if (file != null) {               //         :
  :                               //        }
}
조건식에 실행문을 사용하는 것은 간편하지만 읽기가 매우 어려워집니다. 이 지침은 Java 에 발을 처음으로 들여놓는 개발자들에게 특히 강조하고 싶은 지침입니다.


5.8 기타

55. 코드 상에서 매직 넘버(magic number: constants, array size, character positions, conversion factors와 같이 프로그램내에 등장하는 숫자/문자값)의 사용을 자제한다. 01 이외의 숫자는 차라리 상수로 정의하여 사용하도록 한다.
 
명확한 의미를 갖지 않는 숫자를 사용할 경우, 가독성 향상을 위하여 상수로 정의하여 사용하도록 합니다.

56. 실수값 상수는 항상 소수점과 최소한 소숫점 이하 한 자리 숫자를 사용하여 지정한다.
double total = 0.0;   // double total = 0; 가 아님
double speed = 3.0e8; // double speed = 3e8; 가 아님

double sum;
:
sum = (a + b) * 10.0;
특별한 경우에 정수와 실수가 동일한 값을 갖는다 하더라도 기본적으로 특성이 다르기 때문에 구별하는 것이 좋습니다. 또한 위 마지막 라인의 예제에서 살펴볼 수 있듯이, 값을 할당받은 변수(sum)는 다른 변수의 타입을 알 수 없다고 하더라도 명백하게 실수값을 계산해 낸다는 것을 보장할 수 있습니다 (* 10.0 을 통해서)
 

57. 실수값 상수는 항상 정수부에 숫자를 사용하여 지정한다.
double total = 0.5;   // double total = .5; 가 아님
Java 에서 사용하는 숫자와 표현식은 수학식 표기법에서 차용하였으므로, 프로그래머는 가능한 수학적 표기법을 준수하는 것이 바람직합니다. 실제로 .5 (간편한 표기) 보다 0.5 (수학적 표기) 가 더 읽기에 쉽습니다. 이 수식에서 정수 5 가 함께 사용되는 경우에는, .5 와 정수 5 를 혼동하여 잘못된 연산을 초래할 가능성이 높아지게 됩니다.


6 레이아웃과 주석

6.1 레이아웃

58. 기본 들여쓰기(indentation)는 4 자로 한다.
for (i = 0; i < nElements; i++)
  a[i] = 0;
1 자짜리 들여쓰기는 너무 작아서 논리적인 코드의 레이아웃에 사용합니다. 한편 4 자 이상 들여쓰기를 할 경우 중첩 들여쓰기한 코드를 읽기가 어려워지며, 한 문장을 여러 라인으로 쪼개야하는 상황이 자주 발생하게 됩니다. 2, 3, 4 글자 중에서 선택하시는 것이 바람직합니다만, 2 자와 4자가 가장 보편적입니다. 2 자로 하였을 경우에는 한 문장을 여러 라인으로 쪼개야할 상황을 최소화할 수 있습니다. Sun 社 에서는 4 자 들여쓰기를 권고하고 있습니다. 이 지침은 본래 2 자였던 것을 Sun 표준인 4 자로 변경한 것입니다.


59. 블록 레이아웃은 아래 예제 1 (권고안) 나 예제 2 를 따르되, 예제 3 의 형태는 결코 취해서는 안된다. 클래스, 인터페이스, 메소드 블록은 예제 2 레이아웃을 따른다.
while (!isDone) {
  doSomething();
  isDone = moreToDo();
}
 
while (!isDone)
{
  doSomething();
  isDone = moreToDo();
}
 
while (!isDone)
  {
    doSomething();
    isDone = moreToDo();
  }
 
예제 3은 추가적인 들여쓰기를 사용하고 있으나 예제 1 와 예제 2 를 사용한 것보다 나을 것이 없습니다.

60. classinterface 선언문은 다음과 같은 형태를 따른다:
class SomeClass extends AnotherClass
  implements SomeInterface, AnotherInterface
{
  ...
}
 
이 지침은 위에서 언급한 #59 의 일반적인 블록 규칙에 의거한 것입니다. Java 개발자 커뮤니티에서는 클래스를 선언하는 문장의 끝에서 중괄호를 여는 것이 일반적입니다. 실제로 이 괄호 스타일은 모든 종류의 블록에 대하여 사용할 수 있는 방식입니다. 개인적인 선호의 차이가 있겠지만, 이 방식은 C/C++ 에서 클래스와 메소드 블록은 다른 블록들과 구분할 수 있도록 하는 것을 차용한 것입니다.

61. 메소드 선언문은 다음과 같은 형태를 따른다:
public void someMethod()
  throws SomeException
{
  ...
}
 
class 문장에 대한 설명을 참고하십시오 (#59).

62. if-else 문장은 다음과 같은 형태를 따른다:
if (condition) {
  statements;
}

if (condition) {
  statements;
}
else {
  statements;
}

if (condition) {
  statements;
}
else if (condition) {
  statements;
}
else {
  statements;
}
 
이 지침은 위에서 언급한 #59 일반적인 블록 규칙에 의거한 것입니다. 하지만, else 절이 직전의 ifelse 절의 닫히는 중괄호와 동일한 라인에 있어야 한다고 한다면 논의를 해 볼 수도 있을 것입니다:

if (condition) {
  statements;
} else {
  statements;
}

이 방식은 Sun 社의 코딩 표준 권고안에서 제시한 것과 동일한 것입니다. 여기서 제시한 방법은 if-else 문장을 별도의 행에 분리되어 있기 때문에, else 절을 쉽게 식별할 수 있고 (직전 ifelse 절의 닫히는 중괄호 때문에 else 절이 잘 드러나지 않는 경우도 있습니다) 심지어 else 절을 이동한다거나 하는 경우에 손쉽게 if-else 문장을 조작할 수 있다는 점에서 조금 더 낫다고 할 수 있겠습니다.

63. A for 문장은 다음과 같은 형태를 따른다:
for (initialization; condition; update) {
  statements;
}
 
이 지침은 위에서 언급한 #59 일반적인 블록 규칙에 의거한 것입니다.

64. 비어있는 for 문장은 다음과 같은 형태를 따른다:
for (initialization; condition; update)
  ;
이 방식은 프로그래머가 의도적으로 for 문장을 비워두었음을 분명히할 수 있습니다.

65. while 문장은 다음과 같은 형태를 따른다:
while (condition) {
  statements;
}
 
이 지침은 위에서 언급한 #59 일반적인 블록 규칙에 의거한 것입니다.

66. do-while 문장은 다음과 같은 형태를 따른다:
do {
  statements;
} while (condition);
 
이 지침은 위에서 언급한 #59 일반적인 블록 규칙에 의거한 것입니다.

67. switch 문장은 다음과 같은 형태를 따른다:
switch (condition) {
  case ABC :
    statements;
    // Fallthrough 혹은 '계속'
  case DEF :
    statements;
    break;
  case XYZ :
    statements;
    break;
  default :
    statements;
    break;
}
 
이 방식은 Sun 社의 코딩 표준 권고안과는 들여쓰기와 공백문자 처리에 있어서 약간 다릅니다. 특히, 각각의 case 키워드들은 모두 switch 문에 소속되어 있음을 부각시키기 위하여 switch 키워드를 기점으로 들여쓰기를 수행하였습니다. 이로써 switch 문이 강조되었습니다. 한 가지 더 주목해서 보셔야 할 것은 casedefault 키워드들과 : 문자 사이에 추가 공백이 들어있다는 점입니다. break 문을 사용하지 않는 case 문에는 반드시 명시적으로 Fallthrough계속 이라는 주석을 달아주십시오. break 문을 빠뜨리는 것은 일반적인 실수이기 때문에, 이와 같은 주석을 명시함으로써 실수가 아니라 의도적으로 break 문을 생략했다는 것을 분명히 할 수 있습니다.

2004-04-14 08:43:56.0 (wgshim 222.108.45.193) D
68. try-catch 문장은 다음과 같은 형태를 따른다:
try {
  statements;
}
catch (Exception exception) {
  statements;
}

try {
  statements;
}
catch (Exception exception) {
  statements;
}
finally {
  statements;
}
 
이 지침은 위에서 언급한 #59 일반적인 블록 규칙에 의거한 것입니다. 이 형태 역시 #61 의 if-else 문이 그랬던 것처럼, Sun 社의 코딩 표준 권고안에서 제시한 것과는 다릅니다. #61 을 참고하시기 바랍니다.

69. 단일 문장의 if-else, for, while 문은 중괄호 없이 작성할 수도 있다.
if (condition)
  statement;

while (condition)
  statement;

for (initialization; condition; update)
  statement;

 
일반적인 코딩 권고안은 Sun 社의 Java 코딩 표준 권고안에도 포함되어 있는 것과 같이, 모든 경우에 항상 중괄호를 사용하여 블록화 하라는 것입니다. 하지만 블록은 여러 문장을 그룹핑하기 위한 용도로 사용되기 때문에, 단일 문장을 묶는 것은 오히려 번거로울 수 있습니다. #52 와 같이 조건을 평가하는 부분과 실행문을 분리해서 기록하며 들여쓰기를 제대로 한다면 크게 문제되지는 않습니다.


6.2 공백문자

70.
- . 를 제외한 모든 binary 연산자(conventional operator)는 피연산

     자들 사이에 스페이스로 분리한다.
     예외적으로 Unary operator(++, --) 와 피연산자 사이에는 공백

     을 두지 않는다.


- Java 예약어/키워드 뒤에 공백문자 하나를 추가한다.


- 콤마(,) 뒤에 공백문자 하나를 추가한다.


- 콜론(:) 의 앞뒤에는 공백문자를 추가한다.


- for 문 내의 세미콜론(;) 문자 뒤에 공백문자 하나를 추가한다.

a = (b + c) * d;            // NOT:   a=(b+c)*d
while (true) {              // NOT:   while(true) ...
doSomething (a, b, c, d);   // NOT:   doSomething (a,b,c,d);
case 100 :                  // NOT:   case 100:
for (i = 0; i < 10; i++) {  // NOT:   for (i=0;i<10;i++){
이 방법은 문장의 각 구성요소들을 부각시켜 가독성을 높여줍니다. 여기서 제안하는 Java 코드 상에서의 공백문자 사용에 관한 모든 예제를 다 나열하기는 어렵겠습니다만, 위에 제시한 예제로도 충분히 그 의도를 제시해주고 있습니다.

71. 메소드의 이름과 메소드의 여는 괄호 사이에 공백문자를 사용하지 않는다.
(참고: 본래 이 문서의 원본에는 Sun 社의 권고안과는 달리 '메소드의 이름과 여는 괄호 사이에도 공백문자를 넣어 모든 이름을 부각시키자' 라고 언급하고 있었으나 변경함)
doSomething(currentFile);    // doSomething (currentFile); 가 아님
#69 지침에 의해 Java 의 예약어/키워드 뒤에 공백문자를 추가하기 때문에, 메소드의 이름과 여는 괄호 사이에 공백을 없앰으로써 메소드 호출과 키워드를 구별하는데 도움을 줍니다.

72. 블록 내의 논리적인 유닛들은 빈 라인을 하나 삽입하여 구분한다.
 
블록 내 논리적 유닛들 사이에 공백을 삽입함으로써 가독성을 향상시키게 됩니다.

73. 메소드 선언들 간에는 3-5 개의 빈 라인을 삽입하여 구분한다.
 
메소드 내에서 사용하는 빈 라인보다 많은 라인을 삽입함으로써 클래스 내 각각의 메소드들을 부각시키게 됩니다.

74. 선언문에서의 변수는 타입과 구분하여 변수끼리 좌측정렬 한다.
AsciiFile  file;
int        nPoints;
float      x, y;
타입과 변수를 구분하여 정렬함으로써 변수를 부각시켜 가독성을 높이게 됩니다.

75. 문장은 가독성을 높이기 위하여 정렬한다
if      (a == lowValue)    compueSomething();
else if (a == mediumValue) computeSomethingElse();
else if (a == highValue)   computeSomethingElseYet();


value = (potential        * oilDensity)   / constant1 +
        (depth            * waterDensity) / constant2 +
        (zCoordinateValue * gasDensity)   / constant3;


minPosition     = computeDistance (min,     x, y, z);
averagePosition = computeDistance (average, x, y, z);


switch (value) {
  case PHASE_OIL   : phaseString = "Oil";   break;
  case PHASE_WATER : phaseString = "Water"; break;
  case PHASE_GAS   : phaseString = "Gas";   break;
}
일반적인 지침에 위배될 수도 있겠지만 위 예제에서는 가독성을 높이기 위하여 코드 사이에 공백문자를 넣었습니다. 공백문자를 넣을 때에는 코드의 정렬방식을 준수합니다. 번거로울 수 있겠지만 코드를 정렬함으로써 코드를 한 눈에 들어오게 만들 수 있습니다. 이렇게 공백문자를 넣어 정렬하는 코드정렬에 대해 일반적인 지침을 제시하기는 어렵겠습니다만, 위에 제시한 예제로도 충분히 그 의도를 제시해주고 있습니다.


6.3 주석

76. 난잡한 코드에는 주석을 다는 것보다는, 코드를 구조화하여 재작성하고 명료한 코드에 주석을 다는 것이 낫다 [1] .
 
일반적으로 적절한 이름 선정과 명쾌한 논리 구조를 바탕으로, 코드 자체만으로도 충분히 이해가 되도록 작성하는 것이 우선이고 그 후 주석을 다는 것이 바람직합니다. 주석을 달지 않는 프로그래머를 나쁜 습관을 가졌다고 하지만, 역설적으로 코딩을 정말 잘하는 프로그래머는 주석을 많이 달 필요가 없는 것입니다.

77. 모든 주석은 영어로 작성한다.
 
국제적인 제품을 개발하고자 할 때에는, 주석도 자국어를 사용하는 것 보다는 영어를 사용하는 것이 바람직합니다.

78. JavaDoc 주석이 아닌 모든 주석과 여러 줄을 사용하는 주석에 // 를 사용한다.
// 한 줄 이상의
// 주석이랍니다

// /*
   여러 라인의 주석을 해제합니다.
// */
Java 에서 멀티 레벨 주석을 지원하지 않기 때문에, 디버깅이나 기타 목적으로 사용한 /* */ 블럭의 주석을 해제하는데 // 를 사용하기도 합니다.

79. 주석은 설명하려는 코드와 같은 위치에서 들여쓰기하여 작성한다 [1].
while (true) {          // NOT:    while (true) {
  // Do something       //         // Do something
  something();          //             something();
}                       //         }
이 방법은 주석이 프로그램의 논리적인 구조를 깨뜨리지 않게 해줍니다.

80. collection 변수를 선언할 때에는, 컬렉션에 담겨질 요소들의 공통 타입에 대한 주석을 작성한다.
private Vector  pointList_;  // Point 객체에 대한 Vector
private Set     shapeSet_;   // Shape 객체에 대한 Set
 
이러한 추가적인 주석이 없다면 컬렉션이 어떤 요소들로 구성되어 있는지 파악하기가 어려워지기 때문에, 컬렉션의 요소들을 어떻게 다뤄야 하는지 혼동이 되는 경우가 발생합니다. 컬렉션 변수를 입력 인자로 받아들이는 메소드에서는, JavaDoc 주석을 사용하여 컬렉션 입력 인자의 요소들의 공통 타입을 기술하십시요.

81. 모든 public 클래스와 public 클래스의 public, protected 로 선언된 메소드들에 JavaDoc 방식의 주석을 작성한다.
 
이 방식은 간편하게 온라인 코드 API 문서를 항상 최신내용으로 유지시킬 수 있도록 합니다.


82. 각 파일에는 package 선언문 상단에 다음과 같은 헤더를 삽입한다:
/*
 * (@)# <<파일의 이름>>.java
 *
 * $Header$
 * $Revision$
 * $Date$
 *
 * ====================================================================
 *
 * <<회사 이름>>., Software License, Version 1.0
 *
 * Copyright (c) 2002-2004 <<회사 이름>>.,
 * <<회사 주소>>  * All rights reserved.
 *
 * DON'T COPY OR REDISTRIBUTE THIS SOURCE CODE WITHOUT PERMISSION.
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <<회사 이름>> OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * For more information on this product, please see
 * <<회사 웹 사이트 URL 주소>>
 *
 */
각 파일의 상단에 코드의 소유자(혹은 조직)와 라이센스 및 책임의 한계에 대한 정보를 기술합니다.


83. class, interface 및 메소드 선언문 위에는 다음과 같은 JavaDoc 주석을 삽입한다:
/**
 * 클래스, 인터페이스 메소드에 대한 설명을 기술합니다.
 *
 *   메소드의 경우는 다음 항목들을 기록하십시오:
 *      - 메소드의 작성의도
 *      - 메소드를 사용하기 위한 사전조건(pre condition) 과 사후조건(post condition)
 *      - 부작용
 *      - 종속성 (종속되는 라이브러리, 클래스, 메소드 등)
 *      - 사용시 주의해야 할 점
 *      - 누가 이 메소드를 호출하는가
 *      - 언제 이 메소드를 재정의해야 하는가
 *      - 메소드 재정의 시 어디서 부모 메소드를 호출해야 하는가(super 키워드를 사용하여)
 *      - 제어 흐름 및 상태 전이에 관련된 내용들
 *
 * @author (클래스와 인터페이스에서만 사용합니다)
 * @version (클래스와 인터페이스에서만 사용합니다)
 *
 * @param (메소드와 생성자에서만 사용합니다)
 * @return (메소드에서만 사용합니다)
 * @exception (Javadoc 1.2 의 @throws 와 같은 표현입니다)
 * @see
 * @since
 * @serial (또는 @serialField 나 @serialData)
 * @deprecated (deprecated 된 클래스, 인터페이스, 메소드에 대해 어떻게 대응해야 하는지 기록합니다)
 */

public class Example { ...
 
JavaDoc에 대한 자세한 최신 정보는 이곳을 참고하시기 바랍니다.

7 참고문헌

[1] Steve McConnel, "Code Complete," Microsoft Press. [2] Sun, "Java Code Conventions," http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html [3] Netscape, "Software Coding Standards for Java," http://developer.netscape.com/docs/technote/java/codestyle.html [4] NASA, "C / C++ / Java Coding Standards," http://v2ma09.gsfc.nasa.gov/coding_standards.html [5] AmbySoft, "Coding Standards for Java," http://www.ambysoft.com/javaCodingStandards.html

 


www.jlab.net
 

이 컨텐츠는 JPL에 의해서 보호 받습니다. 오탈 자나 건의는 이곳에 해주십시오
이 컨텐츠는 © 2004 Geotechnical Software Services. 에서 제공하는 Geosoft 의 Java Programming Style Guidelines 문서를 번역 및 수정한 것임을 밝힙니다.

반응형
가. 프로세스 생성 및 종료

일반적으로 프로그램을 실행시키면, 하나의 프로세스로서 동작하게 됩니다. 다시 말해서, 우리가 실행시키는 하나의 프로그램은 하나의 프로세스로서 나타나게 됩니다. 자바에서의 프로세스는 자바 런타임 환경과 밀접한 관계를 갖고 있습니다. 왜냐하면, 자바 런타임 환경은 프로세스가 실행될 수 있는 기반 환경을 제공해 주기 때문입니다. 프로세스는 다른 프로세스를 생성할 수 있는데, 이 때 생성된 프로세스를 자식 프로세스라하고 기존에 있던 프로세스를 부모 프로세스라 합니다. 이러한 부모/자식 프로세스 개념은 하나의 자바 프로그램에서 다른 프로그램을 실행시키고자 할 때, 주로 사용됩니다. 다시 말해서, 플랫폼 독립적인 자바 프로그램이 플랫폼과 밀접한 관련이 있는 작업을 해야 할 경우, 해당 작업을 수행할 프로그램을 다른 언어로 해당 플랫폼에 맞도록 작성하고, 이 프로그램을 자바 프로그램에서 실행시켜 주는 것입니다.

이를 위해, 플랫폼 종속적인 시스템 함수들을 호출할 수 있도록 해 주는 Runtime 클래스와 실행하고자 하는 응용프로그램을 위한 프로세스를 관리할 수 있도록 해 주는 Process 클래스를 사용할 수 있습니다. 자바에서 프로세스를 생성하기 위하여 다음과 같이 해 줍니다.

            - “Runtime runtime = Runtime.getRuntime();”: 런타임 객체를 생성합니다.

            - “Process p = runtime.exec(“프로그램경로명”);”: exec 메소드를 이용하여 프로세스를 생성합니다.

위와 같이 프로세스를 생성할 수 있고, 프로세스의 작업을 마치거나 또는 프로세스를 강제고 종료하기 위해서는 다음 중 한 가지 방법으로 할 수 있습니다.

            - “p.waitFor();”: 자식 프로세스가 종료될 때까지 기다립니다.

            - “p.destroy();”: 부모 프로세스에서 자식 프로세스를 강제로 종료시킵니다.

            - “System.exit(0);”: 부모 프로세스만 종료되고 자식 프로세스는 계속 실행됩니다.

Runtime 클래스가 제공해 주는 주요 메소드를 살펴보면 다음과 같습니다.

            - public static Runtime getRuntime(): 현재 실행되고 있는 자바 애플리케이션과 관련된 런타임 객체를 리턴해 줍니다.

            - public void exit(int status): 현재 자바 가상머신을 종료합니다. status 매개변수는 종료시의 상태값을 나타내며, 일반적으로 0 이외의 값은 비정상적으로 종료되었음을 의미합니다.

            - public Process exec(String command) throws IOException: 주어진 명령어를 독립된 프로세스로 실행시켜 줍니다. exec(command, null)와 같이 실행시킨 것과 같습니다.

            - public Process exec(String command, String envp[]) throws IOException: 주어진 명령어를 주어진 환경을 갖는 독립된 프로세스로 실행시켜 줍니다. 이 메소드는 명령어 문자열을 토큰으로 나누어 이 토큰들을 포함하고 있는 cmdarray라는 새로운 배열을 생성합니다. 그리고 나서 exec(cmdarray, envp)을 호출합니다.

            - public Process exec(String cmdarray[]) throws IOException: 주어진 문자열 배열에 있는 명령어와 매개변수를 이용하여 독립된 프로세스로 실행시켜 줍니다. exec(cmdarray, null)을 호출합니다.

            - public Process exec(String cmdarray[], String envp[]) throws IOException: 주어진 문자열 배열에 있는 명령어와 매개변수를 이용하여 주어진 환경을 갖는 독립된 프로세스로 실행시켜 줍니다. 문자열 배열 cmdarray에는 명령어와 명령행 인자들을 나타내고 있습니다.

            - public native long freeMemory(): 시스템에 남아있는 메모리의 양을 얻습니다. 이 값은 항상 totalMemory() 메소드에 의해 얻어지는 값보다 작습니다.

            - public native long totalMemory(): 자바 가상머신의 최대 메모리 크기를 얻습니다.

Process 클래스가 제공해 주는 주요 메소드를 살펴보면 다음과 같습니다.

            - public abstract OutputStream getOutputStream(): 자식 프로세스의 출력 스트림을 얻습니다.

            - public abstract InputStream getInputStream(): 자식 프로세스의 입력 스트림을 얻습니다.

            - public abstract InputStream getErrorStream(): 자식 프로세스의 에러 스트림을 얻습니다.

            - public abstract int waitFor() throws InterruptedException: 자식 프로세스가 종료될 때까지 기다립니다.

            - public abstract int exitValue(): 자식 프로세스가 종료할 때의 상태값을 얻습니다.

            - public abstract void destroy(): 자식 프로세스를 강제로 종료시킵니다.

다음에 나오는 자바 프로그램은 위의 Runtime 클래스 및 Process 클래스를 이용하여 새로운 프로세스를 생성하고 종료하는 과정을 보여주기 위해 윈도우의 계산기를 실행시키는 간단한 예제입니다.

import java.io.*;

public class ProcessTest {

   static public void main(String args[]) {

      try {

         Process p1 = Runtime.getRuntime().exec("calc.exe");

         Process p2 = Runtime.getRuntime().exec("freecell.exe");

         Process p3 = Runtime.getRuntime().exec("Notepad.exe");

         p1.waitFor();

         p2.destroy();

         System.out.println("Exit value of p1: "+p1.exitValue());

         System.out.println("Exit value of p2: "+p2.exitValue());

      } catch(IOException e) {

         System.out.println(e.getMessage());

      } catch(InterruptedException e) {

         System.out.println(e.getMessage());

      }

      System.exit(0);

   }

}

/*

 * Results:

 D:\AIIT\JAVA\06>java ProcessTest

 Exit value of p1: 0

 Exit value of p2: 1

 D:\AIIT\JAVA\06>

 */

<프로그램 1. ProcessTest.java>

나. 상호작용 명령어의 실행

그런데, 위와 같이 프로세스를 이용하여 방식으로 명령어를 실행하다 보면, 명령어에 따라 사용자에게 메시지를 출력하고 이에 대한 적절한 답을 사용자로부터 입력 받기를 원하는 명령어가 있습니다. 이러한 명령어를 상호작용(interactive) 명령어라고 합니다.

D:\AIIT\JAVA\06>ping 203.252.134.126

Pinging 203.252.134.126 with 32 bytes of data:

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

D:\AIIT\JAVA\06>

<그림 1. 상호작용 명령어의 실행>

그림에서와 같이 ping 명령어를 실행시키게 되면, ping 명령어는 그 실행 결과를 표준 출력을 이용하여 화면상에 출력해 줍니다. 이렇게 자바에서 실행시킨 프로세스가 출력하는 결과를 자바 프로그램은 알아야 하고, 또한 프로세스가 자바 프로그램으로부터 어떤 대답을 원할 경우가 있는데 이 때 사용자는 이에 대해 적절하게 답을 해 주어야 합니다. 이 때, ping 명령어는 메시지를 자신의 표준 출력에 장치에 출력하게 되는데, 이렇게 프로세스의 표준 출력을 자바 프로그램에서는 p.getInputStream 메소드를 이용하여 얻고, p.getOutputStream 메소드를 이용하여 프로세스의 표준 입력 장치에 쓰게 됩니다. 이 때, 한 가지 주의할 사항은 표준 출력 스트림에 대답을 쓴(write) 후, 꼭 flush 또는 close 메소드를 이용하여 표준 출력 스트림을 비워(flush) 주어야 합니다.

<그림 2. 자바 프로그램과 프로세스 간의 데이터의 전달>

다음에 나오는 자바 프로그램은 도스 상에서 상호작용 명령어를 사용하는 간단한 예제를 보여줍니다.

import java.io.*;

import java.lang.*;

public class InteractiveProcessTest {

   public static void main(String[] args) {

      try {

         Process p = Runtime.getRuntime().exec("ping 203.252.134.126");

         byte[] msg = new byte[128];

         int len;

         while((len=p.getInputStream().read(msg)) > 0) {

            System.out.print(new String(msg, 0, len));

         }

         String rs = "\n";

         byte[] rb  = new byte[] { (byte)'\n' } ; //rs.getBytes();

         OutputStream os = p.getOutputStream();

         os.write(rb);

         os.close();

      } catch (Exception e) {

         e.printStackTrace();

      }

   }

}

/*

 * Results:

 D:\AIIT\JAVA\06>java InteractiveProcessTest

 Pinging 203.252.134.126 with 32 bytes of data:

 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

 D:\AIIT\JAVA\06>

*/

<프로그램 2. InteractiveProcessTest.java>

+ Recent posts