posted by 구름너머 2007. 3. 23. 09:13

기존 WL6.1에서는

oracle.sql.CLOB

사용하였으나

WL81에서는

How helpful was this Support Pattern? Rating: Comments:


WebLogic Server
Support Pattern
More Support Patterns
CLOB/BLOB Data Type Handling Issues

Problem Description

WebLogic Server versions that support the JDBC 2.0 specification support the use of BLOB (Binary Large Object) and CLOB (Character Large Object) data types. These data types were made available with the release of Oracle version 8 and above. Sybase and MS SQL Server, databases do not natively support these data types, however certain Sybase and SQL Server drivers (for example Datadirect) provide both read and write support for BLOB/CLOB data types for Sybase and MS SQL Server. Users face many problems when they use incorrect programming techniques with the above data types. Also, changes in the database driver APIs when migrating from one WebLogic Server version to another leads to incompatible code. A number of cases related to these issues have been raised.


Problem Troubleshooting

This pattern provides some common methodologies that can be followed to troubleshoot CLOB/BLOB problems using Oracle database.


Quick Links:


Why Does the Problem Occur?


Recommended Programming Techniques

Many times due to incorrect programming techniques customers face problems while inserting or retrieving CLOB/BLOB objects from the database. The CLOB/BLOB data insertion and retrieval happens differently with Oracle and WebLogic database drivers.


WLS 8.1:

Standard Programming Technique to insert, update and retrieve rows with Clob/Blob using WebLogic and Oracle drivers are mentioned below.


How to Insert/Update BLOB Data types

Oracle Driver

To access the BEA supported methods, the BLOB object java.sql.Blob needs to be typecast to weblogic.jdbc.vendor.oracle.OracleThinBlob interface. Sample programming is shown below.


java.sql Objects Initialization


PreparedStatement pstmtInsert = null;
PreparedStatement pstmtSelect = null;
java.sql.Blob myRegularBlob = null;
java.io.OutputStream os = null;
Connection myConnect = null;

BLOB Field Initialization

When you first insert a row containing a BLOB data type, you must insert the row with an "empty" BLOB before the field can be updated with real data. You can insert an empty BLOB with the Oracle EMPTY_BLOB() function.


myConnect = getConnection();
String insertSQL = "insert into myBlobTable values (?, EMPTY_BLOB())";
pstmtInsert = myConnect.prepareStatement(insertSQL);
pstmtInsert.setString(1, String.valueOf(i));
pstmtInsert.executeUpdate();

Write to BLOB Field

In the following try catch block, you get the BLOB locator and access the Oracle BLOB extension for writing.


try
{
// get our BLOB locator..
String selectSQL="select pk, myBlobColumn from myBlobTable where pk=? for update";
pstmtSelect = myConnect.prepareStatement(selectSQL);
pstmtSelect.setString(1, String.valueOf(i));
rs = pstmtSelect.executeQuery();
while (rs.next())
{
System.out.println("PK for update is " + rs.getString("PK"));
myRegularBlob = rs.getBlob("myBlobColumn");
}
// Access the underlying Oracle extension functionality for
// writing. Cast to the OracleThinBlob interface to access
// the Oracle method.
os = ((OracleThinBlob)myRegularBlob).getBinaryOutputStream();
os.write(bytes);
os.flush();

} catch (SQLException sqe) { System.out.println("ERROR(general SQE): " +
sqe.getMessage());
}

WebLogic Driver

Since the WebLogic driver uses WebLogic wrapper classes, no typecasting is required when using this driver. If typecasting is used, ClassCastException will be thrown


The actual exception that occurs is on the line


“os = ((OracleThinBlob)myRegularBlob).getBinaryOutputStream();"

is


ava.lang.ClassCastException: weblogic.jdbc.rmi.SerialOracleBlob_weblogic_jdbc_rmi_internal_OracleTBlobStub_weblogic_jdbc_rmi_ internal_
OracleTBlobImpl_weblogic_jdbc_wrapper_Blob_weblogic_jdbc_base_BaseBlob_814_WLStub

To resolve this issue, the above code needs to be modified.


Replace


os = ((OracleThinBlob)myRegularBlob).getBinaryOutputStream();

with

os = myRegularBlob.setBinaryStream(1);

Top of Page


How to Read BLOB Data Types

The following code snippet depicts how to correctly read a BLOB data type for both Oracle and WebLogic drivers.


try{
myConnect = getConnection();
String selectSQL = "select myBlobColumn from myTable1 where pk = ?";
pstmtSelect = myConnect.prepareStatement(selectSQL);
pstmtSelect.setString(1, String.valueOf(i));
ResultSet rs = null;
byte[] inBytes = new byte[256];
rs = pstmtSelect.executeQuery();
while (rs.next())
{
myRegularBlob = rs.getBlob("myBlobColumn");
java.io.InputStream readis = myRegularBlob.getBinaryStream();
for (int k=0 ; k < 256 ; k++) {
inBytes[k] = (byte) readis.read();
System.out.println("output [" + k + "] = " + inBytes[k]);
}
} catch (SQLException sqe) { System.out.println("ERROR(general SQE): " + sqe.getMessage()); }

Top of Page


How to Insert/Update CLOB Data Types


Oracle Driver


Initializing a CLOB Field

The code is very similar to the BLOB example.


Replace


String insertSQL = "insert into myBlobTable values (?, EMPTY_BLOB())";

With

String insertSQL = "insert into myClobTable values (?, EMPTY_CLOB())";

In the try catch block, you get the CLOB locator and access the Oracle CLOB extension as follows using the same procedure as BLOB. getClob method is used instead of getBlob and getAsciiOutputStream instead of getBinaryOutputStream.


An example is shown below.


Replace


os = ((OracleThinBlob)myRegularBlob).getBinaryOutputStream();

With

os = ((OracleThinClob)myRegularClob).getAsciiOutputStream();

WebLogic Driver

The code is very similar to the BLOB example with some changes to avoid a ClassCastException, as shown below.


Replace


os = ((OracleThinClob)myRegularClob).getAsciiOutputStream();

With

os = myRegularClob.setAsciiStream(1);

Top of Page


How to Read Clob Data Types

The procedure is the same as the BLOB example, except that you need to use getClob instead of getBlob and getAsciiStream instead of getBinaryStream.


WLS 7.0 and WLS 6.1:

The implementation of the BLOB and CLOB APIs in WLS 8.1 also works in WLS 7.0 and WLS 6.1.


Top of Page


When and How to Use the API (weblogic.jdbc.common.OracleBlob) or the API (weblogic.jdbc.vendor.oracle.OracleThinBlob):

The earlier versions of WLS always went through the RMI interface, even if local. This was changed due to customer complaints and because it significantly improved performance to avoid RMI whenever possible. Now you get different behaviors depending on whether or not your code is in an RMI client or on the server, and by service pack, and by what driver you are using.


To access the BEA supported methods you need to typecast java.sql.Blob either to the weblogic.jdbc.vendor.oracle.OracleThinBlob as in WLS 8.1 example or the weblogic.jdbc.common.OracleBlob interface.


You need code something like this:


java.io.OutputStream getOS(java.sql.Blob lob) throws SQLException {

if (lob instanceof oracle.sql.BLOB) {
// Oracle thin/oci driver non-rmi
return ((oracle.sql.BLOB)lob).getBinaryOutputStream();
}

else if (lob instanceof weblogic.jdbc.vendor.oracle.OracleThinBlob) {
// Oracle thin/oci driver via rmi
return ((weblogic.jdbc.vendor.oracle.OracleThinBlob)lob).getBinaryOutputStream();
}

else if (lob instanceof weblogic.jdbc.common.OracleBlob) {
// WebLogic Type 2 Driver for Oracle
return ((weblogic.jdbc.common.OracleBlob) lob).getBinaryOutputStream();
}

else {
// New Weblogic type 4 driver for Oracle or any non-Oracle driver
return ((java.sql.Blob) lob).setBinaryStream(1);
}
}

Your code can be smaller if you don't use the full variety of drivers.


Note that as in WLS 8.1 in case of WebLogic Type 4 drivers, you don't need to use the Oracle extension getBinaryOutputStream(). You can use the standard setBinaryStream(1). In general, if you can avoid using an extension, do so.


Clarification:

WLS 6.1 SP5 was the service pack that returned an oracle.sql.BLOB, which was inconsistent with earlier versions of WLS 6.1. This was changed in WLS 6.1 SP6.


With the exception of WLS 5.1 (which is End-of-life as of 2/1/2004) and WLS 6.1 SP5 (which was an error), you will always get weblogic.jdbc.vendor.oracle.OracleThinBlob when going through WLS.


To get oracle.sql.BLOB, you need to use connections directly.


The reasons are different for various releases/service packs.


<>WLS 6.1,WLS 6.1 SP1-SP4, WLS 7.0, WLS 7.0 SP1-SP2 - going through RMI

WLS 6.1 SP6+, WLS 7.0 SP3+ - consistency with earlier service packs


WLS 8.1 - dynamic wrappers only work with interfaces, not classes, and there is no interface for oracle.sql.BLOB.


This discussion also applies to CLOB usage.


Top of Page


Migrating Applications Due To WebLogic Server Upgrade

When migrating applications from WebLogic Server 5.1 to WebLogic Server 6.1 or to WebLogic Server 7.0 and using Oracle 8.1.7 as the database server, the following problem appears while accessing CLOB/BLOB data types.


In WebLogic Server 5.1 the sample code shown below obtains a return object of type oracle.sql.BLOB.


try {
// get our BLOB locator…
String selectSQL = "select pk, myBlobColumn from myTable1 where pk=? for update";
pstmtSelect = myConnect.prepareStatement(selectSQL);
pstmtSelect.setString(1, String.valueOf(i));
rs = pstmtSelect.executeQuery();
while (rs.next())
{
myRegularBlob = rs.getBlob("myBlobColumn");
}
// WLS 5.1-type code where we type-casted the database returned Blob object
// to oracle.sql.BLOB
os = ((oracle.sql.BLOB)myRegularBlob).getBinaryOutputStream();
os.write(bytes);
os.flush();
myConnect.commit();

When the same code is migrated to WLS 6.1 or to WLS 7.0 you receive a ClassCastException. The actual exception on the line


“os = ((oracle.sql.BLOB)myRegularBlob).getBinaryOutputStream();”

is

java.lang.ClassCastException: weblogic.jdbc.rmi.SerialOracleBlob

Using Oracle objects resulted in problems and so wrappers were added back in WLS 6.1. This URL is a link to the documentation that explains the correct way to access the BLOB object, http://e-docs.bea.com/wls/docs70/oracle/advanced.html#1158571.


In summary, change the cast from oracle.sql.BLOB to weblogic.jdbc.common.OracleBlob and all should work.


Similar to BLOBs, with CLOBs you will get the same ClassCastException (java.lang.ClassCastException: weblogic.jdbc.rmi.SerialOracleClob) if the following typecast is used:


os = ((oracle.sql.CLOB)myRegularClob).getAsciiOutputStream();

To avoid the ClassCastException, the above line of code should be replaced with


os = ((weblogic.jdbc.common.OracleClob)myRegularClob).getAsciiOutputStream();

Reading CLOBs is exactly same way as BLOBs.


Top of Page


Database Driver

Another cause of the CLOB/BLOB problem could be a JDBC driver problem. In order to isolate whether the problem is a driver problem or WebLogic connection pool problem, you may try the following if you have a reproducible test case.


  1. Get connection from the driver directly.
    In your test case, get JDBC connections directly from the driver and bypass WebLogic datasource. Use the application code snippet to insert/update/retrieve/delete records from database containing CLOB/BLOB columns. Sample code to create a Byte array to use as CLOB/BLOB data to write to database is as follows:

//Creating Blob to write
byte[] bytes = new byte[256];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = 1;
}

  1. Use reflection to find out the actual class name and hierarchy of the BLOB/CLOB object returned from the database. Use sample code as follows:

System.out.println("The Blob classname is " + myRegularBlob.getClass());
Class[] classes = myRegularBlob.getClass().getInterfaces();
System.out.println("The Class has " + classes.length + " interfaces" );
for (int m = 0; m < classes.length; m++)
{
Class intrface = classes[m];
System.out.println("An interface is " + intrface.toString());
}
System.out.println("The superclass is " +
myRegularBlob.getClass().getSuperclass().toString());
System.out.println("The superclass of that is " +
myRegularBlob.getClass().getSuperclass().getSuperclass().toString());

You will get a typical response as follows:


The Blob classname is class weblogic.jdbc.rmi.SerialOracleBlob_weblogic_jdbc_rmi_internal_
OracleTBlobStub_weblogic_jdbc_rmi_internal_OracleTBlobImpl_weblogic_jdbc_wrapper_Blob_oracle_sql_BLOB_814_WLStub
The Class has 6 interfaces
An interface is interface java.io.Serializable
An interface is interface weblogic.utils.wrapper.Wrapper
An interface is interface weblogic.rmi.extensions.server.StubDelegateInfo
An interface is interface weblogic.rmi.internal.StubInfoIntf
An interface is interface weblogic.jdbc.rmi.internal.OracleTBlobImpl_weblogic_jdbc_wrapper_Blob_oracle_sql_BLOB_RemoteInterface
An interface is interface weblogic.jdbc.rmi.internal.OracleTBlob
The superclass is class weblogic.jdbc.rmi.SerialOracleBlob
The superclass of that is class weblogic.jdbc.wrapper.JDBCWrapperImpl

The next step is to investigate the classes involved and check their APIs to find out why the exception is occurring.


  1. Try different JDBC drivers. You may try a JDBC driver from a different vendor or an updated version of driver to see whether the problem still occurs. You may use metadata to verify that a correct driver is used. Sample code is like this:

DatabaseMetaData dmd = myConnect.getMetaData();
System.out.println("JDBC Driver Name is " + dmd.getDriverName());
System.out.println("JDBC Driver Version is " + dmd.getDriverVersion());

Top of Page


Known WebLogic Server Issues

You can periodically review the Release Notes for your version of WLS for more information on Known Issues or Resolved Issues in Service Packs and browse for JDBC-related issues. For your convenience, see the following:

In this section we will examine some known WebLogic Server problems that involve CLOB/BLOB data types.

  1. WLS (ALL) - Currently, Container Managed Persistence does not support the java.sql.Blob data type. The primary reason for this appears to be due to the lack of a standard facility for updating BLOBs. Alternatives include using:
    1. LONG_RAW data type instead of a BLOB data type
    2. Bean Managed Persistence (BMP) and writing proprietary JDBC code to update the BLOB.
  2. WLS (ALL) - If you use a prepared statement to update a CLOB and the new value is shorter than the previous value, the CLOB will retain the characters that were not specifically replaced during the update. For example, if the current value of a CLOB is abcdefghij and you update the CLOB using a prepared statement with zxyw, the value in the CLOB is updated to zxywefghij. To correct the values updated with a prepared statement, you should use the dbms_lob.trim procedure to remove the excess characters left after the update. See the Oracle documentation for more information about the dbms_lob.trim procedure. Here is a link that contains sample code on how to use trim:
    http://www.cs.umb.edu/cs634/ora9idocs/appdev.920/a96591/adl03prg.htm
  3. WLS 8.1 SP2 - Except for DB2 UDB 8.1, CLOB data types are limited to 32K characters because of DRDA listener limitations.
  4. WLS 8.1 SP2 - The WebLogic Type 4 JDBC drivers allow PreparedStatement.setXXX and ResultSet.getXXX methods on BLOB/CLOB data types, in addition to what is described in the JDBC specification. The supported conversions are generally the same as those for LONGVARBINARY/LONGVARCHAR, except where limited by database support.
  5. WLS (ALL) - BLOBs and CLOBs in Oracle behave differently than other data types in regards to transactional boundaries (statements issued before an SQL commit or rollback statement) in that a BLOB or CLOB will become inactive as soon as a transaction is committed. If AutoCommit is set to TRUE, the transaction will be automatically committed after each command issued on the connection, including SELECT statements. For this reason you will need to set AutoCommit to false if you need to have a BLOB or CLOB available across multiple SQL statements. You will then need to manually commit (or rollback) the transactions at the appropriate time.
    To set AutoCommit to false, use the following line of code:
    conn.setAutoCommit(false); // where conn is our connection object
  6. WLS 6.1 - When using a non-transaction-enabled DataSource, a "java.sql.SQLException: ORA-01002" error will occur when creating a CMP entity EJB which has a field mapped to Oracle CLOB/BLOB.
    The EJB container inserts a row which has CLOB/BLOB columns as follows:
    1. Insert into the table initializing the LOB using EMPTY_BLOB() or EMPTY_CLOB().
    2. Retrieve the LOB locator using select for update statement.
    3. Modify the LOB.
    Although the connection from a non-transaction-enabled DataSource is in auto-commit mode, Oracle LOB locators that are used to write cannot span the transactions. So the transaction is closed at step 1 and then ORA-1002 occurs at step 2.
    Workaround:
    You need to use a transactional DataSource (TxDataSource) when using Oracle CLOB/BLOB with CMP.
  7. WLS 6.x - You cannot use BLOBs and CLOBs when using the RMI driver in conjunction with the WebLogic jDriver for Oracle. BLOBs and CLOBs are not serializable and therefore are not supported with the JDBC RMI Driver used with WebLogic Server 6.x.


Top of Page


Known Oracle Thin Driver Issue

The 9.2.0.1 and 9.2.0.2 versions of the Oracle Thin driver do not allow you to work with a CLOB in tables that also contain a long. When you retrieve a CLOB from the table and call clob.length(), you will get a SQL protocol violation.


Workaround: In this scenario, you can read the LONG column before calling clob.length().


This issue is fixed in version 9.2.0.3 and 10G.


Need Further Help?

If you have followed the pattern, but still require additional help, you can:

If this does not resolve your issue and you have a valid Support Contract, you can open a Support Case by logging in at: http://support.bea.com/


Top of Page


External Resources

Search for known oracle bugs with CLOBs and BLOBs at:

https://metalink.oracle.com/metalink/plsql/ml2_gui.startup


Feedback

Click Support Pattern Feedback to rate this Support Pattern and comment on such things as:

  • Were you successful in solving your issue with this pattern?
  • Was there any additional information that could be included in this pattern that would help solve your issue.
  • What other Support patterns would you like to see developed?

DISCLAIMER NOTICE:

BEA Systems, Inc. provides the technical tips and patches on this Website for your use under the terms of BEA's maintenance and support agreement with you. While you may use this information and code in connection with software you have licensed from BEA, BEA makes no warranty of any kind, express or implied, regarding the technical tips and patches.


Any trademarks referenced in this document are the property of their respective owners. Consult your product manuals for complete trademark information.



'JAVA' 카테고리의 다른 글

가비지 컬렉션, Garbage Collection  (0) 2008.03.03
디자인 패턴  (0) 2007.10.09
자바 강좌 링크  (0) 2007.03.13
데이터베이스 프로그래밍에서 기본적으로 지켜야할 사항  (0) 2007.01.12
MarshalException  (0) 2006.10.19
posted by 구름너머 2007. 3. 13. 13:50

'JAVA' 카테고리의 다른 글

디자인 패턴  (0) 2007.10.09
WL81에서 BLOB 사용법  (0) 2007.03.23
데이터베이스 프로그래밍에서 기본적으로 지켜야할 사항  (0) 2007.01.12
MarshalException  (0) 2006.10.19
Code Convention : jsp page 작성 요령  (0) 2006.07.21
posted by 구름너머 2007. 1. 12. 13:16
데이터베이스 프로그래밍에서 기본적으로 지켜야할 사항 DB를 공부해 Boa요

2004/04/17 17:29

http://blog.naver.com/bluejames77/80001813232

서민구님께서 javastudy 고급강좌에 기고한 강좌입니다.

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

db 프로그래밍을 짜실 때 꼭 한가지를 기억하세요.. 저는 자바로 설명하겠지만 이건 꼭 언어를 따지는
건 아닙니다.

가령 이런 SQL문이 있다고 하죠.

select * from tab where id = 1;

그리고 시스템에서 id 를 입력받아서 매번 각 id에 대해 위의 쿼리를 실행한다고 하죠.

그러면 다음과 같이 쿼리를 생성해서는 안됩니다.

Connection con = null;
Statement stmt = null;

String query = "select * from tab where id = " + id;

stmt = con.createStatement(stmt);
ResultSet rs = stmt.executeQuery();

'JAVA' 카테고리의 다른 글

WL81에서 BLOB 사용법  (0) 2007.03.23
자바 강좌 링크  (0) 2007.03.13
MarshalException  (0) 2006.10.19
Code Convention : jsp page 작성 요령  (0) 2006.07.21
CLOB 데이터 넣기.  (0) 2006.05.24
posted by 구름너머 2006. 10. 19. 10:46

아.. 일주일째 해결 못함!...

[2006-10-19 10:43:15 - ERR - com.kt.icbs.fc.FCBCDG730E.saveRecord(), line 55] fc - FCBCDG730E: saveRecord()
java.rmi.MarshalException: failed to marshal saveRecord(Lcom.gauce.GauceDataSet;); nested exception is:
java.io.NotSerializableException: java.io.ByteArrayInputStream
java.io.NotSerializableException: java.io.ByteArrayInputStream
at java.io.ObjectOutputStream.outputObject(ObjectOutputStream.java:1143)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:361)
at java.io.ObjectOutputStream.outputArray(ObjectOutputStream.java:1093)
at java.io.ObjectOutputStream.checkSubstitutableSpecialClasses(ObjectOutputStream.java:451)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:356)
at java.io.ObjectOutputStream.outputClassFields(ObjectOutputStream.java:1822)
at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:475)
at java.io.ObjectOutputStream.outputObject(ObjectOutputStream.java:1209)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:361)
at java.io.ObjectOutputStream.outputArray(ObjectOutputStream.java:1093)
at java.io.ObjectOutputStream.checkSubstitutableSpecialClasses(ObjectOutputStream.java:451)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:356)
at java.io.ObjectOutputStream.outputClassFields(ObjectOutputStream.java:1822)
at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:475)
at java.io.ObjectOutputStream.outputObject(ObjectOutputStream.java:1209)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:361)
at weblogic.rmi.internal.CBVOutputStream.writeObjectInternal(CBVOutputStream.java:216)
at weblogic.rmi.internal.CBVOutputStream.writeObject(CBVOutputStream.java:209)
at weblogic.rmi.internal.CBVOutputStream.writeObject(CBVOutputStream.java:186)
at weblogic.rmi.internal.ObjectIO.writeObject(ObjectIO.java:36)
at weblogic.rmi.internal.BasicRemoteRef.marshalArgs(BasicRemoteRef.java:215)
at weblogic.rmi.cluster.ReplicaAwareRemoteRef.invoke(ReplicaAwareRemoteRef.java:257)
at weblogic.rmi.cluster.ReplicaAwareRemoteRef.invoke(ReplicaAwareRemoteRef.java:230)
at weblogic.rmi.internal.ProxyStub.invoke(ProxyStub.java:35)
at $Proxy2701.saveRecord(Unknown Source)
at com.kt.icbs.fc.FCBCDG730E.saveRecord(FCBCDG730E.java:53)
at jsp_servlet._bcdg._bcdg700.__bcdg730eCtl._jspService(__bcdg730eCtl.java:307)
at weblogic.servlet.jsp.JspBase.service(JspBase.java:27)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:262)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:321)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:198)
at weblogic.servlet.internal.WebAppServletContext.invokeServlet(WebAppServletContext.java:2678)
at weblogic.servlet.internal.ServletRequestImpl.execute(ServletRequestImpl.java:2412)
at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:140)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:121)
com.pjedf.df.common.JException: 시스템 오류가 발생했습니다. 운영자에게 문의하십시오.
at com.kt.icbs.fc.FCBCDG730E.saveRecord(FCBCDG730E.java:56)
at jsp_servlet._bcdg._bcdg700.__bcdg730eCtl._jspService(__bcdg730eCtl.java:307)
at weblogic.servlet.jsp.JspBase.service(JspBase.java:27)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:262)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:321)
at weblogic.servlet.internal.ServletStubImpl.invokeServlet(ServletStubImpl.java:198)
at weblogic.servlet.internal.WebAppServletContext.invokeServlet(WebAppServletContext.java:2678)
at weblogic.servlet.internal.ServletRequestImpl.execute(ServletRequestImpl.java:2412)
at weblogic.kernel.ExecuteThread.execute(ExecuteThread.java:140)
at weblogic.kernel.ExecuteThread.run(ExecuteThread.java:121)

posted by 구름너머 2006. 7. 21. 09:20
jsp page 작성 요령 | Java/Jsp 2006/06/13 14:27
http://blog.naver.com/perker/25261313

Articles Index

Code Conventions for the

JavaServer PagesTM Version 1.x Language


JavaServer PagesTM (JSPTM)가 점점 널리 웹기반 어플리케이션으로 자리잡아가면서, 개발과 유지보수에 종사하는 많은 JSP 프로그래머들과 웹 개발자들은, 예전에 자바프로그래머들이 겪었던 것과 같은 딜레마에 직면했다. "어떻게 JSP 코드를 짜면 읽기, 쓰기, 유지보수하기를 일관적으로 쉽게 할 수 있을까?"

이 글은, 웹 컴포넌트를 사용하는 전형적인 소프트웨어 프로젝트에서 준수해야 할JSP(1.1 과 1.2버전) 표준 작성요령을 제안한다. 이 문서의 모체는 자바 언어 코드 작성요령이다. (JSP 와 관련된) 코드 작성 방법 스펙에 소개된 다양하고 중요한 요소들을 확인할 수 있는 템플릿으로 삼았다. 특히, 파일명, 구성, 들여쓰기, 주석, 지시자, 선언문, 스크립틀릿, 표현식, 공백, 명명법(이름 짓는 법), 프로그래밍 습관 등을 소개한다. 처음으로 시도되는 JSP 코드 작성요령이기 때문에, 이 권고 문서에 대한 여러분들의 관심과 피드백을 기다린다. 연락처는 jsp-codeconv-comments@sun.com이다.

JSP 2.0 스펙에서는 1.2 버전을 모두 포용하지만, 스크립트가 없는 프로그래밍 스타일(선언문, 스크립틀릿, 표현식이 없는)을 지향하고, 수많은 새 특징들을 갖고 있어서, 이 작성요령을 더 진보시켜야 될 필요가 있다. 가능한 한, 이 문서는 새로운 JSP 2.0 특징에 도움을 주는 작성방식을 선택했다.

마지막으로, 이 글을 읽는 수준은 JSP, Java, 자바 코드 작성요령에 익숙해야 하고, 이미 프로젝트를 수행하는 팀에 적용이 되어 있어야 된다. 그렇지 않다면, 이 링크를 읽어 보기를 추천한다. Java 링크 JSP 링크.

 

왜 코드 작성요령이 필요한가?

Why Have Code Conventions?

코드 작성요령은 프로그래머와 웹 컨텐츠 개발자에게 여러 가지 이유로 중요하다:

  1. 소프트웨어 산출물의 가독성을 향상시킨다.
  2. 견습 관리와 노력을 경감한다.
  3. 표준화에 대한 조직적 참여도를 배가시킨다.

파일 이름과 위치

File Names and Locations

파일 이름짓기는 툴 벤더와 웹 컨테이너로 하여금 파일 타입을 인식하고, 각각에 맞게 해석할 수 있는 방법을 제공한다. 다음 테이블은 추천하는 방식의 파일 확장자와 위치 목록이다.

File Type

File

확장자

추천 위치
JSP .jsp<context root>/<subsystem path>/
JSP fragment .jsp<context root>/<subsystem path>/
.jspf<context root>/WEB-INF/jspf/<subsystem path>/
cascading style sheet .css<context root>/css/
javascript .js<context root>/js/
HTML page .html<context root>/<subsystem path>/
web resource .gif, .jpg, etc. <context root>/images/
tag library descriptor .tld<context root>/WEB-INF/tld/

위 테이블을 볼 때 몇 가지 유념할 것이 있다. 우선, <context root> 는 웹 어플리케이션(.war 파일 내의 루트 디렉토리) 컨텍스트의 루트이다. 둘째로, <subsystem path> 는 동적, 정적 웹페이지 컨텐츠의 논리적인 그룹으로 나누도록 한다. 작은 웹 어플리케이션일 경우, 이게 필요하지 않을 수도 있다. (한 곳에 모두 놓아도 된다.)

세째로, 다른 JSP 파일에 포함되는 JSP 를 언급할 때 JSP fragment (JSP 프래그먼트 조각)라는 용어를 사용한다. 주의할 것은 JSP 2.0 에서는 "JSP fragment" 용어가 겹치기 때문에 "JSP 세그먼트(JSP segment)" 용어가 대신 사용된다. JSP fragment 는 확장자로 .jsp 또는 .jspf 를 사용하고, /WEB-INF/jspf나 다른 정적인 컨텐츠와 함께 각각 위치하면 된다. 불완전한 JSP fragment는 언제나 .jspf 확장자를 사용하고, /WEB-INF/jspf에 위치해야 된다. 네째로, JSP 스펙에서는 JSP fragment 확장자로 .jspf.jsp 양쪽 모두를 권장하지만, 추천하는 것은 .jspf 인데, .jsfJavaServer Faces™ 스펙에서 사용될 것이기 때문이다.

끝으로, 태그 라이브러리 디스크립터 파일이나 다른 비공개적인 컨텐츠는 WEB-INF/ 또는 이 하위 디렉토리에 놓아두는 것은 보편적으로 좋은 습관이다. 웹 컨테이너는 WEB-INF/ 아래 있는 어떤 파일도 송출하지 않기 때문에, 이 방법을 사용하면, 클라이언트가 접근하거나 볼 수 없게 된다.

배치 설명자(web.xml)의 welcome-file 엘리먼트에 선언되어 있는 웰컴 파일의 이름은 동적인 컨텐츠를 생성하려면, index.jsp 가 되어야 되고, 정적인 웰컴 페이지라면 index.html 로 해야 한다.

JSP 파일을 다국어로 작성할 때, 로케일에 따라 디렉토리를 나누어 그룹 짓기를 추천한다. 예를 들어서 index.jsp 의 US English 버전은 /en_US/index.jsp 에, 같은 파일의 일본어판은 /ja_JP/index.jsp 에 있게 한다. 일반적으로 자바 코드의 국제화에 관한 추가적인 정보는 Java Tutorial 에 있고, 웹 어플리케이션의 국제화에 대한 정보는 Designing Enterprise Applications with the J2EETM Platform 책에 나와있다.

파일 구조

File Organization

구조가 잘 갖춰진 소스 코드 파일은 읽기 쉽고, 파일 사이에 정보를 더 빨리 놓아둘 수 있다. 이 섹션에서, JSP와 태그 라이브러리 디스크립터 파일 모두를 위한 구조를 소개하려한다.

JSP 파일 / JSP Fragment 파일

JSP 파일은 다음의 섹션들이 나열한 순서대로 구성된다:

  1. 도입 주석
  2. JSP 페이지 지시자(directive)
  3. 선택적 태그 라이브러리 지시자
  4. 선택적 JSP 선언문
  5. HTML 과 JSP 코드

도입 주석

Opening Comments

JSP 파일 또는 프래그먼트 파일은 서버측 주석으로 시작한다:

<%--
- 작성자:
- 일자:
- 저작권 표시:
- @(#)
- 설명:
--%>

이 주석은 서버측에서만 볼 수 있다. JSP 변환 과정에서 제거되기 때문이다. 여기에는 웹 개발자를 위한 JSP에 대한 작성자, 일자, 개정판의 저작권, 증명과 설명이 포함된다. "@(#) " 문자 조합은 증명의 시작을 지시하는 것으로 어떤 프로그램에서 인식된다. 이런 표식을 사용하는 프로그램들은 많이 사용되지 않지만, 있어도 해가 될 것은 없다. 추가로, 이 조합은 때때로 "$Id$" 를 덧붙여서, 어떤 종류의 버전 관리 프로그램에서는 자동으로 증명 정보를 JSP에 기록하도록 한다. 설명 부분은 JSP의 목적에 관한 축약된 정보를 제공한다. 한 문단 이상 넘어가지 않도록 한다.

어떤 상황에서는, 도입 주석이 JSP 변환과정을 지나서 출처가 확실하다는 뜻과 법적인 목적으로 클라이언트의 브라우저에 보여질 필요가 있다. 이럴 경우 주석 부분을 두개로 나눠서 우선 클라이언트측 스타일의 주석을 단다:

<!--
- Author(s):
- Date:
- Copyright Notice:
--%>

그리고 나서 짧아진 나머지 서버측 스타일의 주석을 단다:

<%--
- @(#)
- Description:
--%>

JSP 페이지 지시자

JSP Page Directive(s)

JSP 페이지 지시자는 JSP 변환 시점에 관련된 속성을 정의한다. JSP 스펙은 같은 페이지에 JSP 페이지 지시자의 수량을 제한하지 않는다. 그래서 다음 두 개의 코드 예제는 똑같다.(다른 점이 있는데, 첫 예제는 출력시에 맨 위에 두 개의 빈 줄이 포함된다):

코드 예제 1:

<%@ page session="false" %>
<%@ page import="java.util.*" %>
<%@ page errorPage="/common/errorPage.jsp" %>

페이지 지시자 같이 지시자의 길이가 일반적인 JSP 폭(80 문자)을 넘어갈 경우 여러 줄로 나눠진다:

코드 예제 2:

<%@ page session="false"
import="java.util.*"
errorPage="/common/errorPage.jsp"
%>

일반적으로, 코드 예제 2는 코드 예제 1보다 즐겨 사용된다. 예외가 있는데, 매우 긴 import 속성을 쓰게 되는, 여러 개의 자바 패키지가 JSP페이지에서 필요할 경우이다:

<%@ page session="false"
import="java.util.*,java.text.*,
com.mycorp.myapp.taglib.*,
com.mycorp.myapp.sql.*, ..."
...
%>

이런 경우, 페이지 지시자를 다음과 같이 나누는 것이 즐겨사용된다:

<%-- import를 제외한 모든 속성 --%>
<%@ page
...
%>
<%-- import 속성들은 여기부터 시작 --%>
<%@ page import="java.util.*" %>
<%@ page import="java.text.*" %>
...

주의할 점은, 일반적으로 import문은 자바의 지역 코드 작성요령을 준수한다. 예를 들어, 같은 패키지에서 세 개의 클래스까지만 사용되면, import 는 패키지를 선언하기 보다는, 클래스를 일일이 선언한다. 세 개를 넘어가면, 클래스를 일일이 지정할 것인지 아니면, ".*" 표시를 사용할 것인지는 웹 개발자 마음이다. 전자의 경우, 외부 클래스가 무엇이 사용되는지를 쉽게 알수 있다. 특히 버그가 많은 클래스를 위치시키려고 하거나, JSP 가 자바 코드와 어떻게 상호작용하는지를 이해하려할 경우에 유용하다. 예를 들어서, 아래처럼 임포트한 자바 패키지들에 관해 알지 못하면, 웹 개발자는 Customer 클래스를 찾기 위해서 이 모든 패키지들을 검색해봐야 된다:

<%@ page import="com.mycorp.bank.savings.*" %>
<%@ page import="com.thirdpartycorp.cashmanagement.*" %>
<%@ page import="com.mycorp.bank.foreignexchange.*" %>
...

후자의 경우, 작성된 JSP 가 깔끔하지만, 사용한 클래스가 무엇인지 아는 것은 어려워진다. 일반적으로, JSP 에 임포트 지시자가 너무 많으면, 자바 코드가 너무 많이 들어가 있기 쉽다. 보다 나은 선택은 JSP 태그를 더 사용하는 것이다.(후에 언급하겠다).

선택적 태그 라이브러리 지시자

Optional Tag Library Directive(s)

태그 라이브러리 지시자는 JSP 에서 사용하는 커스텀 태그 라이브러리를 선언한다. 짧은 지시자는 하나의 줄에 선언할 수 있다. 여러 개의 태그 라이브러리 지시자들은 JSP 의 한 곳에 같이 모아둔다:

<%@ taglib uri="URI1" prefix="tagPrefix1" %>
<%@ taglib uri="URI2" prefix="tagPrefix2" %>
...

페이지 지시자처럼, 태그 라이브러리 지시자의 길이가 일반적인 JSP 폭(80 문자)을 넘어갈 경우 여러 줄로 나눠진다:

<%@ taglib
uri="URI2"
prefix="tagPrefix2"
%>

오직 페이지에서 사용하는 태그 라이브러리만 임포트되어야 한다.

JSP 1.2 부터, 웹 어플리케이션의 JSP에 JSTL;JSP 표준 태그 라이브러리사용하기를 적극 권장한다. 페이지에서 JSP 스크립틀릿 사용할 필요를 줄여주기 때문이다. 일반적으로, JSTL을 사용하는 페이지는 소스읽기와 유지보수가 보다 편하다.

선택적 JSP 선언문

Optional JSP Declaration(s)

JSP 선언문은 JSP에서 소속된 메소드와 변수를 선언한다. 이들 메소드와 변수는 자바 프로그램에서의 선언문과 다를바 없다. 그래서 적절한 코드 작성요령을 따르기만 하면 된다. 선언문은 <%! ... %> JSP 선언문 블록 하나에 모두 몰아서 한 곳에 집중해 놓는 것이 좋다. 예를 들면:

다른 선언문 블록들바람직한 선언문 블록

<%! private int hitCount; %>

<%! private Date today; %>

...

<%! public int getHitCount() {
return hitCount;
}
%>

<%!
private int hitCount;
private Date today;

public int getHitCount() {
return hitCount;
}

%>

HTML 과 JSP 코드

HTML and JSP Code

이 섹션은 JSP 코드와 HTML 이 섞여있는 페이지에 대해 설명한다. JSP 표현식, 스크립틀릿, 자바빈즈 지시자 등이 해당된다.

태그 라이브러리 설명서

Tag Library Descriptor

태그 라이브러리 설명서(TLD)는 적당한 XML선언과 올바른 DTD 문으로 시작한다. 예를 들어서, JSP 1.2 TLD 는 다음과 같이 시작해야 된다:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib
PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
"http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">

바로 뒤이어 서버측 주석과 같은 스타일의 주석을 기입한다. 작성자, 일자, 저작권, 증명 정보, 라이브러리에 대한 짧은 설명이다:

<!--
- 작성자:
- 일자:
- 저작권 표시:
- @(#)
- 설명:
-->

여기에 관한 규칙과 가이드라인은 JSP 파일/프래그먼트 파일에서 정의된 것과 동일하다.

태그 라이브러리 파일의 나머지는 다음에 나오는 순서대로 이루어져 있다:

  • 태그 라이브러리 검증기 하나의 선택적 선언문
  • 이벤트 리스너들의 선택적 선언문
  • 하나 이상의 가능한 태그들의 선언

TLD 에서 엘리먼트 마지막에 선택적 서브 엘리먼트를 항상 덧붙이기를 추천한다. 이 선택적 서브 엘리먼트는 태그 디자이너가 TLD의 습성과 추가적인 정보를 문서화할 장소를 제공한다. 이것은 웹 컴포넌트 개발자들에게는 보이지 않게 된다.

TLD

Element

JSP 1.2

Recommended

Sub-element

JSP 1.1

Recommended

Sub-element

attribute

(JSP 1.2)

description

init-param

(JSP 1.2)

description
tagdisplay-name, description, examplename, info
tagliburi, display-name, descriptionuri, info

validator

(JSP 1.2)

description

variable

(JSP 1.2)

description

들여쓰기

Indentation

들여쓰기는 반드시 공백문자로 채워야한다. 탭은 사용하는 에디터마다 표시하는 공백의 수가 다르기 때문에 JSP 내에서 들여쓰기로 사용하기에는 적합하지 않다. 특별히 통합개발환경(IDE) 툴에서 제한되지 않았다면, 기본 들여쓰기 단위는 4개의 공백문자로 한다.

다음은 그 사용예이다:

<myTagLib:forEach var="client" items="${clients}">
<myTagLib:mail value="${client}" />
</myTagLib:forEach>

연속 들여쓰기는 이전 줄의 적당한 지점에서 시작한다. 연속 들여쓰기의 공백은 기본 들여쓰기 공백의 배수이다. (4개의 공백문자의 배수):

<%@ page attribute1="value1"
attribute2="value2"
...
attributeN="valueN"
%>

스크립트 엘리먼트의 들여쓰기

Indentation of Scripting Elements

JSP 스크립트 엘리먼트(선언문, 스크립틀릿, 표현식과 같은)가 한 줄에 맞지 않는 경우, 스크립트 언어의 들여쓰기 작성요령이 엘리먼트 내에 적용된다. 몸체는 엘리먼트의 여는 기호 <%=와 같은 줄에서 시작한다. 다음줄의 기준도 여는 기호 <%=가 된다. 몸체의 끝은 다른 줄에 있는 엘리먼트의 닫는 기호(%>)로 마무리된다. 예제이다:

<%= (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
= Calendar.SUNDAY) ?
"Sleep in" :
"Go to work"
%>

첫줄과 마지막 줄을 제외한 몸체의 가운데 줄들은 단위 들여쓰기 공백(앞 예제에서 로 보인다)이 앞에 있기 때문에 나머지 JSP 보다 확연히 드러나게 된다.

JSP, HTML, JAVA의 복합 들여쓰기

Compound Indentation with JSP, HTML and Java

자바의 스크립트 코드와 템플릿 텍스트(HTML)가 섞여있는 JSP 엘리먼트를 위해서 복합 들여쓰기가 필요한 이유는 JSP 소스를 이해하기 위해 드는 노력을 덜어주기 위해서이다. 기존의 들여쓰기 요령은 JSP 소스파일을 더 알아보기 어렵게 할 수 있기 때문이다. 보편적인 규칙에 따라서, 다른 소스 사이에 끼어 있을 경우마다 보통 들여쓰기에 특별한 단위를 할당한다. 클라이언트에 보여지는 들여쓰기에 영향을 준다. 추가적인 들여쓰기는 (브라우저에 의해) 보통 무시되고, 브라우저에 나타나는 결과는 차이가 없다. 예를 들어, <TABLE> 태그 앞에 공백이 몇 개 더 추가된다고 해서, 브라우저에서 보여지는 테이블의 위치가 달라지는 것은 아니다. 그래서 이러한 작성요령을 사용하는 것이 더 좋아 보인다:

<table>
<% if { tableHeaderRequired ) { %>
<tr>
<th>Last Name</th>
<th>First Name</th>
</tr>
<% } %>
<c:forEach var="customer" items="${customers}">
<tr>
<td><c:out value="${customer.lastName}"/></td>
<td><c:out value="${customer.firstName}"/></td>
</tr>
</c:forEach>
</table>


위 방식보다는 아래 방식이 낫다:

<table>
<% if { tableHeaderRequired ) { %>
<tr>
<th>Last Name</th>
<th>First Name</th>
</tr>
<% } %>
<c:forEach var="customer" items="${customers}">
<tr>
<td><c:out value="${customer.lastName}"/></td>
<td><c:out value="${customer.firstName}"/></td>
</tr>
</c:forEach>
</table>

주석

Comments

주석은 추가 정보 전달이나 코드를 둘러쌀 목적으로 사용한다. 여기서는 JSP에서 사용하는 두가지 형태의 주석을 볼 것이다: JSP 측과 클라이언트 측 주석이다.

JSP 주석

JSP Comments

JSP 주석(또는 서버 측 주석)은 서버에서만 볼 수 있다(즉, 클라이언트로 전파되지 않는다). 순수 JSP 주석을 스크립팅 언어와 섞여있는 JSP 주석보다 즐겨 사용한다. 전자가 기반 스크립트 언어에 덜 종속적이기 때문이고, JSP 2.0 으로 진화하기 더 쉽기 때문이다.

다음 도표는 이것을 설명한다:

Line

JSP 스크립틀릿과

스크립트 언어 주석

순수 JSP

주석

한 줄

<% /** ... */ %>

<% /* ... */ %>

<% // ... %>

<%-- ... --%>
여러 줄

<%

/*

*

...

*

*/

%>

<%--

-

...

-

-- %>

<%

//

//

...

//

%>

클라이언트 측 주석

Client Side Comments

클라이언트 측 주석(<!-- ... -->)은 응답에 대해서 부가적인 정보를 클라이언트로 보내기 위해 응답에 주석을 다는 것이다. 서버 어플리케이션의 행동이나 내부적인 구조에 대한 것과 응답 내용을 생성하는 코드가 포함되어서는 안된다.

클라이언트/사용자의 입장에서 서버에서 보내온 응답을 해석하기 위해서 이런 종류의 주석을 직접 볼 필요가 없기 때문에, 클라이언트 측 주석은 보편적으로 사용되지 않는다. 앞서 얘기한대로 증명이나, 저작권 정보 등의 법적인 목적의 증명에 사용될 경우는 예외가 되겠다. 다른 예외적인 경우는 HTML 저작도구들이 HTML 문서 구조의 틀을 잡아주기 위해 작은 양의 HTML 주석들을 사용하는 경우가 되겠다. 예를 들면 다음과 같다:

<!-- 툴바 섹션 -->
...
<!-- 왼쪽 네비게이션 메뉴 -->
...
<!-- main body -->
...
<!-- footer -->
...

여러 줄의 주석

Multiline Comment Block

JSP 또는 클라이언트 측 모두, 여러 줄의 주석은 대시 문자 "-" 로 꾸며진다. XML 스펙에서, 더블-대시 문자열 "--"은 XML 주석 문 내에 허용되지 않는다. 그러므로, 이 스펙에 호환성과 일관성을 갖기 위해서, 더블-대시 문자열은 여러줄의 주석문 내에 사용되면 안된다. 다음 도표는 클라이언트 측 여러 줄 주석 사용법을 보여준다:

권장 XML과 맞지 않는 법

<!--

- line 1

- line 2

...

-->

<!--

-- line 1

-- line 2

...

-->

JSP 선언문

JSP Declarations

자바 코드 작성요령에 따라서, 같은 타입의 변수 선언문은 다른 줄에서 이루어져야 된다:

비 추천추천
<%! private int x, y; %>

<%! private int x; %>

<%! private int y; %>

자바빈즈는 <jsp:useBean> 액션 태그를 사용하는 대신, JSP 선언문을 통해서 선언되고 인스턴스화되면 안된다.

일반적으로, 변수를 위한 JSP 선언문은 그 자체가 스크립트 언어의 사용을 부추기기 때문에 사용하지 않는 것이 좋다. JSP 는 프리젠테이션 목적으로 설계되었는데, 여기에 비지니스 로직과 자바코드를 섞어놓기 때문이다. 그리고, 변수의 스코프 관리에 따른 부담도 생기게 된다.

JSP 스크립틀릿

JSP Scriptlets

가급적, 태그라이브러리로 동일한 기능을 구현할 수 있으면, JSP 스클립틀릿은 피하기 바란다. 이렇게 하면 페이지 가독성도 높이고, 유지보수도 쉬워진다. 비지니스 로직을 프리진테이션 로직에서 분리하는데 일조하며, JSP 2.0 스타일의 페이지로 쉽게 옮겨갈 수 있다(JSP 2.0 도 스크립틀릿을 지원하지만 덜 강조한다). 다음 예제는 customers 표시 방법이 데이터 형에 따라서 다른 스크립틀릿을 사용하는 것을 보여준다:

customers 는 Customers의 배열

<table>

<% for ( int i=0; i<customers.length; i++ ) { %>

<tr>

<td><%= customers[i].getLastName() %></td>

<td><%= customers[i].getFirstName() %></td>

</tr>

<% } %>

</table>

customers 는 Enumeration

<table>

<% for ( Enumeration e = customers.elements();

e.hasMoreElements(); ) {

Customer customer = (Customer)e.nextElement();

%>

<tr>

<td><%= customer.getLastName() %></td>

<td><%= customer.getFirstName() %></td>

</tr>

<% } %>

</table>

그러나, 공통 태그라이브러리를 사용하면, customers 의 데이터 형이 달라도 보다 높은 유연성이 생긴다. 예를 들어서, JSTL 에서는customers 를 나타내기 위해서 배열과 Enumeration 양쪽 모두 지원하는 다음과 같은 코드로 표시할 수 있다:

<table>

<c:forEach var="customer" items="${customers}">

<tr>

<td><c:out value="${customer.lastName}"/></td>

<td><c:out value="${customer.firstName}"/></td>

</tr>

</c:forEach>

</table>

비지니스 로직과 프리젠테이션 층과의 결합도을 줄이기 위한 모델-뷰-컨트롤러(MVC) 디자인 패턴을 적용하려고 한다면, JSP 스크립틀릿은 비지니스 로직을 구현하기 위해서 사용되어서는 안된다. 오히려, JSP 스크립틀릿은 클라이언트의 요청을 처리한 뒤에 나오는 데이터("가치 객체 value objects"로 불리는)를 적절한 클라이언트의 준비된 형식에 전송하기 위해 필요하다면 사용된다. 심지어 그럴 경우에도, 프론트 컨트롤러 서블릿이나 커스텀 태그로 하는 게 더 낫다. 예를 들면, 다음의 코드는 데이터베이스에서 직접 customers 의 이름을 가져와 클라이언트에 보여준다:

<% // 스크립틀릿을 이용하는 것은 추천하지 않음!

Connection conn = null;

try {

// Get connection

InitialContext ctx = new InitialContext();

DataSource ds = (DataSource)ctx.lookup("customerDS");

conn = ds.getConnection();

// Get customer names

Statement stmt = conn.createStatement();

ResultSet rs = stmt.executeQuery("SELECT name FROM customer");

// Display names

while ( rs.next() ) {

out.println( rs.getString("name") + "<br>");

}

} catch (SQLException e) {

out.println("Could not retrieve customer names:" + e);

} finally {

if ( conn != null ) conn.close();

}

%>

다음의 JSP 코드는 데이터베이스 처리부분을 내부적으로 데이터베이스 코드의 의존성을 숨기고 캡슐화한 커스텀태그myTags:dataSource에 위임하고 있기 때문에 더 보기 좋다:

<myTags:dataSource name="customerDS" table="customer" columns="name" var="result" /> <c:forEach var="row" items="${result.rows}"> <c:out value="${row.name}" /> <br /> </c:forEach>

result 는 커스텀 태그 myTags:dataSource가 customer 데이터베이스에서 customer의 이름들 뽑아낸 결과를 담으려고 만들어 낸 스크립팅 변수이다. JSP 코드는 클라이언트의 요구에 따라 동적으로 다른 종류의 출력(HTML, XML, WML)을 생성하기 위해 확장할 수 있다. 그것도 백엔드 코드(dataSource 태그를 위한)를 변경하지 않고도 가능하다. 더 나은 선택은 이것을 프론트 컨트롤러 서블릿에 위임하는 것이다. 이 서블릿은 데이터를 불러와서 그 결과를 request 스코프의 속성을 통해서 JSP에 제공한다. 예제는 자바 BluePrint의 엔터프라이즈 섹션을 참고하기 바란다.

요약하면 :

  • 이상적으로, 스크립트 언어로부터 독립하기 위해서 JSP 스크립틀릿은 JSP 내에서 사라져야 되고, JSP 내의 비지니스 로직 구현은 피해야 된다.
  • 그것이 불가하면, 서버 측에서 정보를 전달하는데 값 객체(자바빈즈)를 사용하고, JSP 스크립틀릿은 클라이언트 출력에 이 값 객체를 전달하는 데 사용한다.
  • 가능하면 커스텀 태그(태그 핸들러)를 사용해서 서버측에서 정보를 처리하도록 한다.

JSP 표현식

JSP Expressions

JSP 표현식은 JSP 스크립틀릿처럼 가능하면 삼가는 것이 좋다. 동일한 작업을 수행하는 다음 3가지 예제가 잘 설명해준다:

Example 1 (자바 코드 사용):

<%= myBean.getName() %>

Example 2 (JSP 태그 사용):

<jsp:getProperty name="myBean" property="name" />

Example 3 (JSTL 태그 사용):

<c:out value="${myBean.name}" />


Example 1 은 myBean 이라는 스크립트 변수가 선언된 것이라고 가정한다. 다른 두 예제에서 myBean은 어떤 스코프의 속성으로 PageContext.findAttribute()을 통해서 접근할 수 있다고 가정한다. 두 번째 예제는 또한 <jsp:useBean> 에 의해서 페이지에 소개된 myBean이라고 가정한다.

셋 중에서 JSTL 태그 예제가 낫다. JSP 표현식 만큼 짧고, 가독성도 좋고, 유지보수하기도 보다 편하다. 그리고, 자바 스크립틀릿에 의존하지 않는다(스크립틀릿은 자바 언어와 API 함수들에 익숙해야 다룰 수 있다.) 더우기, JSP 2.0 스타일 프로그램으로 쉽게 이동할 수 있는 장점이 있다. 어떤 선택이 채택되든, 모든 개발자들의 동의를 얻어서, 같은 프로젝트에 있는 JSP 결과물들은 전체적으로 일관된 방식을 사용해야 된다. 주의할 점은, JSTL 예제는 로컬 자바 스크립트 변수로부터가 아닌, page 컨텍스트로부터 myBean 의 값을 가져온다는 것이 실질적으로 조금 다른 점이다.

마지막으로, JSP 표현식은 기반 스크립팅 언어의 문법에 의존하는, 같은 기능의 JSP 스크립틀릿보다 낫다. 예를 들어,

<%= x %>

로 하는 게 아래 방법보다 좋다.

<% out.print( x ); %>

공백

White Space

코드의 이해와 유지보수에 드는 노력을 줄이기 위해서 JSP 코드를 보기 좋게 들여쓰기하는데 공백을 사용한다. 특히, 빈 줄과 공백은 필요한 곳에서 JSP 의 여러 곳에 삽입되어야 한다.

빈 줄

Blank Lines

빈 줄은 출력에 원하지 않는 영향만 주지 않는다면 JSP의 가독성을 높이기 위해서 가끔 사용된다. 아래 예제에서, HTML <PRE> 블록 내에 두 개의 JSP 표현식 사이에 있는 빈 줄은 클라이언트의 브라우저에 HTML 출력 결과에서 한줄 더 생기게 만든다. 그러나 빈 줄이 <PRE> 블록 사이에 있지 않다면, 브라우저의 출력에 나타나지 않는다.

JSP 구문

클라이언트의

HTML 출력

<pre>

<%= customer.getFirstName() %>

<%= customer.getLastName() %>

</pre>

Joe

Block

<pre>

<%= customer.getFirstName() %>

<%= customer.getLastName() %>

</pre>

Joe

Block

<%= customer.getFirstName() %>

<%= customer.getLastName() %>

Joe

Block

빈 문자열

Blank Spaces

공백 문자(; 역자주:공백은 길이가 1, 빈 문자열은 ""처럼 길이가 0)는 JSP 태그와 몸체 사이에 쓰여져야 된다.

예를 들어 다음

<%=customer.getName()%>

코드는 아래 것보다 좋다.

<%=customer.getName()%>

JSP 주석 태그와 주석을 분리하는 공백 문자들이 있어야 된다:

<%--

-여러 줄의 주석이 나뉘어지면, 각각의 줄은

-한 줄씩 차지한다. --%>

<%--짧은 주석--%>

이름짓는 요령

Naming Conventions

이름짓는 요령을 적용하면, 프로젝트 내의 웹 컴포넌트 엘리먼트들을 쉽게 확인, 분류, 통합할 수 있다. 이 섹션에서, JSP에 적합한 요령을 살펴보자.

JSP 이름

JSP Names

JSP (파일) 이름은 언제나 소문자로 시작해야 한다. 여러 개의 단어로 구성될 수 있고, 이러한 경우 단어들은 모두 붙여쓰고, 이어지는 단어의 첫글자는 대문자로 쓴다. JSP 이름은 간단한 명사나 짧은 문장이 될 수 있다. 동사뿐인 JSP 명은 개발자에게 충분한 의미를 전달하지 못하기 때문에, 피하도록 한다. 예를 들면:

perform.jsp

파일은 다음 파일보다 명확하지 않다.

performLogin.jsp

JSP 이름에 동사가 들어가는 경우, 은연 중에 뒷쪽에서 처리하고 있다는 표시로 현재형을 쓴다:

showAccountDetails.jsp

로 쓰는 게 아래 파일명 보다 좋다

showingAccountDetails.jsp

태그 이름

Tag Names

태그 핸들러와 관련된 클래스에 대한 이름짓는 요령은 다음과 같다:

설명

클래스명

XXX tag extra info

(javax.servlet.jsp.tagext.TagExtraInfo를 상속함)

XXXTEI

XXX tag library validator

(javax.servlet.jsp.tagext.TagLibraryValidator를 상속함)

XXXTLV

XXX tag handler interface (javax.servlet.jsp.tagext.Tag/IterationTag/BodyTag를 상속함)

XXXTag

XXX tag handler implementation

XXXTag

덧붙여, 태그 이름은 자바에 관련된 코드작성법에서 정한 클래스와 인터페이스 이름 짓는 요령을 따라서 정해야 된다.

태그 관련된 클래스와 다른 클래스와 더 분명하게 구분하기 위해서, 패키지 접미사(tags 또는 taglib)를 클래스의 패키지명에 적용한다.

예를 들면:

com.mycorp.myapp.tags.XXXTag

태그 prefix 이름

Tag Prefix Names

태그 prefix 는 짧지만 의미가 담긴 명사를 사용해야 하고, 첫문자는 소문자로 해야한다. 태그 prefix 는 알파벳 문자만 포함해야 된다. 몇 가지 예를 들면 다음과 같다:

OK?
mytaglibno
myTagLibyes
MyTagLibno
MyTagLib1no
My_Tag_Libno
My$Tag$Libno

XML 문법의 JSP 페이지

JSP Pages in XML Syntax

JSP 는 두가지 다른 문법을 제공한다: JSP 페이지를 작성하는 '표준 문법'과 JSP를 XML문서로 작성하는 'XML 문법'이 있다. 표준 문법을 사용하는 JSP 페이지들이 'JSP 페이지'로 많이 쓰인다. XML 문법으로 작성된 JSP는 'JSP 문서'라고 일컫는다. 이 기사는 우선적으로 JSP 페이지를 소개하지만, 많은 부분의 개념들이 JSP 문서에도 역시 적용될 수 있다. XML이 좀 더 유행하면 JSP 문서 사용도 많아질 것이라고 기대된다. JSP 2.0 스펙에서는 더 많은 XML 문법 친화적인 것들이 소개될 것이다.

주의할 것은 XML 문법을 이용해서 JSP를 만드는 것과 JSP로 XML 결과(view)를 만들어 내는 것과 혼동해서는 안된다. 페이지 제작도구는 JSP를 만들기 위해 표준 또는 XML 문법을 사용한다. 그리고 나서 컨테이너는 JSP를 XML 결과로 변환하고, 이것은 태그 라이브러리 밸리데이터(유효성 검증기)에서 확인한다.

JSP 문서 구조

JSP Document Structure

JSP 문서는 다음과 같은 기본 구조를 갖는다:

<? xml version="1.0" ?>

<!--

- Author(s):

- Date:

- Copyright Notice:

- @(#) - Description:

-->

<jsp:root xmlns:jsp=http://java.sun.com/JSP/Page

xmlns:prefix1="URI-for-taglib1"

xmlns:prefix2="URI-for-taglib2"

version="1.2">

JSP Document ...

</jsp:root>

첫 줄은, 이 페이지가 XML 문서라고 정의하는 선택적 XML 프롤로그이다. 다음으로 오는 것은 문서에 대한 주석이다. <jsp:root> 엘리먼트는 이것이 JSP 문서임을 정의하고, 반드시 이 엘리먼트가 루트 엘리먼트로 나타나야 된다. jsp 이름공간은 반드시 가져와야 되고, 모든 태그 라이브러리는 이 루트 엘리먼트를 사용해서 가져와야 된다. 버전 속성이 필요하고, 어떤 버전을 사용할 지를 정해준다. JSP 문서의 실제 내용은 <jsp:root> 엘리먼트의 서브엘리먼트에 나타난다. 표준 XML 들여쓰기 룰에 따라서 문서 전체적으로 일관되게 적용하는 들여쓰기 단위는 4개의 공백문자이다.

JSP 문서는 형식이 잘 갖춰진 XML 문서이기 때문에, <% %> 와 같은 엘리먼트는 <jsp:scriptlet /> 같이 XML로 바꿔서 표기해야 된다. 자세한 것은 JSP 스펙을 보기 바란다.

XML 주석

XML Comments

JSP 스펙은 XML 스타일의 주석을 어떻게 처리할 지에 대해 불분명하기 때문에, 주석이 클라이언트로 안전하게 전달되기 위해서는, <jsp:text>로 다음과 같이 감싸주어야 된다:

...

<jsp:text><![CDATA[

<!--

- Multiline comment

- to be sent to client.

-->

]]></jsp:text>

...

JSP 문서 내의 자바 코드 Java Code in JSP Documents

선언문, 스크립틀릿, 표현식에서 자바 코드를 쓰려고 할 때, 코드가 문서의 구조를 망가뜨리지 않는다면, CDATA엘리먼트를 사용한다.

...

<jsp:scriptlet>

for( int level = 0; level < 3; level++ ) {

</jsp:scriptlet>

<tr>

<td>

<jsp:expression><![CDATA[

"<h" + level + ">Text</h" + level + ">"

]]></jsp:expression>

</td>

</tr>

<jsp:scriptlet>

}

</jsp:scriptlet>

...

표준 문법에서와 다르게, XML 들여쓰기 법칙은 엘리먼트의 내용과 상관없이 준수되어야 한다.

프로그래밍 습관

Programming Practices

일반적으로, 다음과 같은 이유로 JSP 페이지에 자바 코드(선언문, 스크립틀릿, 표현식) 쓰기를 피한다:

  • JSP 페이지의 자바 코드 문법 에러는 페이지가 배치되기 전까지 발견되지 않는다. 반면에, 태그 라이브러리와 서블릿에 있는 문법 에러는 배치 이전에 발견된다.
  • JSP 페이지 내의 자바 코드는 디버깅하기 힘들다.
  • JSP 페이지 내의 자바 코드는 유지보수하기 힘들다. 특히 자바 전문가가 아니라면 더욱 더 그렇다.
  • 보통 복잡한 비즈니스 로직과 프리젠테이션 로직을 섞어놓지 않는 것이 납득이 가는 습관이다. JSP는 우선적으로 프리젠테이션 로직을 목적으로 한다.
  • 자바 코드가 포함된 코드와 HTML과 다른 스크립트 명령어가 섞여있다면 더 읽기 어려워진다.
  • JSP 2.0 은 더 단순한 표현 언어를 덕분에 스크립틀릿의 중요도를 낮추었다. JSP 에서 JSP 2.0 스타일로 쉽게 옮기기 위해서는 페이지에서 자바 코드를 사용하지 않는 것이 좋다.

자바 BluePrints의 엔터프라이즈 섹션에 있는 더 자세한 정보와 가이드라인을 참고하기 바란다.

자바빈즈 초기화

JavaBeans Initialization

JSP는 JavaBeanTM의 프로퍼티로 증명되는 모든 PropertyDescriptor를 초기화하기 위해 편리한 엘리먼트를 제공한다. 예를 들면:

<jsp:setProperty name="bankClient" property="*"/>

그러나, 주의해서 사용해야 한다. 첫째로, 빈이 프로퍼티를 갖고 있다, 즉 amount 프로퍼티가 있다고 하고, 현재 ServletRequest 객체에 파라미터(amount)가 없거나 파라미터 값이 ""라고 한다면, 아무것도 하지 않는다: JSP는 빈의 특정 프로퍼티에 null를 할당하지도 않는다. bankClient 빈에 amount의 기존 값이 무엇이든지 간에 영향을 받지 않는다. 둘째로, PropertyEditors를 선언하지 않은 비-요소적인 프로퍼티는 ServletRequest 객체의 스트링 값에서 암묵적으로 초기화 되지 않기 때문에, 명시적인 변환이 필요하다. 세째로, 만일 어플리케이션이 주의 깊게 설계되지 않았다면, 악의를 가진 사용자는 추가적으로 요청 파라미터를 덧붙여서 빈의 사용하지 말아야할 프로퍼티 값을 설정할 수 있다.

깔끔한 코드를 위해서 jsp:setProperty 태그에 property="*"를 사용한다면, jsp:setProperty 태그 이전에 ServletRequest 객체를 통해서 받아와야 할, 빈을 초기화하기 위한 파라미터들에 대해 주석을 달기를 권한다. 다음 예제에서는 bankClient 빈을 초기화하기 위해서 firstNamelastName 파라미터가 필요하다는 것을 주석에서 읽을 수 있다:

<%--

- ServletRequest에서 firstName와 lastName를 요구

--%>

<jsp:setProperty name="bankClient" property="*" />

JSP 내장 객체

JSP Implicit Objects

API 호출을 통해서 객체를 참조하기 보다는 JSP 내장 객체를 직접 사용하는 것이 좋다. 그래서 ServletContext 인스턴스에 의해 제공되는 초기 파라미터에 접근하기 위해 다음을 쓰는 대신에

getServletConfig().getServletContext().getInitParameter("param")

기존 내장 객체를 사용하는 게 낫다:

application.getInitParameter("param")

초기 파라미터 값이 출력이 되어야 하는 경우에는, JSTL로 접근해서 출력하는 것이 더 좋다:

<c:out value="${initParam['param']}" />

인용부호

Quoting

일정한 인용부호의 사용을 채택한다. 인용부호는 작은따옴표 ' 대신 큰따옴표 " 를 사용한다.

불규칙한 인용부호좋은 인용부호 사용
<%@ page import='javabeans.*'%>
<%@ page import="java.util.*" %>
<%@ page import="javabeans.*" %>
<%@ page import="java.util.*" %>

예외적인 경우는 작은따옴표가 필요할 때이다. 예를 들어서 스크립트 언어에서 큰따옴표를 사용하는 경우를 들 수 있다:

<jsp:include page='<%= getFoodMenuBar("Monday") %>' />

커스텀 태그 사용하기

Using Custom Tags

만일 커스텀태그가 몸체가 없다면, 컨텐트는 (생략해서 "JSP" 기본값으로 하지 말고) empty라고 명확하게 선언되어야 한다. 태그 라이브러리 설명서에서 다음과 같이 정해준다:

<tag>

<name>hello</name>

<tag-class>com.mycorp.util.taglib.HelloTagSupport</tag-class>

<body-content>empty</body-content>

...

</tag>

이렇게 하면 JSP 컨테이너는 태그 몸체가 무조건 비어야 되며 파싱할 어떤 JSP 문법도 포함하지 않아야 된다고 인식한다. 효과는 빈 몸체를 파싱하기 위해서 불필요하게 자원의 할당이 이뤄지는 것을 제거한다.

빈 태그는, 가독성향상을 위해 열고 닫는 XML 엘리먼트 쌍으로 표시하기보다는, 짧은 XML 엘리먼트로 표시한다. 그래서

<myTag:hello />

라고 표기하는 것이 아래보다 낫다.

<myTag:hello></myTag:hello>

TagExtraInfo와 TagLibraryValidator 사용

Use of TagExtraInfo and TagLibraryValidator

때때로, 태그 라이브러리를 사용하는데 TLD 하나로 유효성을 검증할 수 없는 경우가 있다. 그 때는 TLD에 등록된 TagExtraInfo 클래스나 TagLibraryValidator 클래스를 사용해서, 변환할 때 태그 라이브러리에 있는 에러를 잡아낼 수 있다.

자바스크립트 사용

Use of JavaScriptTM

스크립트가 제대로 돌아가기 위해서는 자바스크립트는 브라우저 타입의 특성과 관계없어야 된다.

동작을 한다면, JSP에서 자바스크립트 코드를 독립적인 파일로 분리하고, 다음과 같이 JSP에서 자바스크립트를 불러쓰는 방식은 좋은 생각이다:

<script language=javascript src="/js/main.js">

이렇게 하면 자바스크립트의 재사용성도 향상되고, 여러 JSP 사이에 일관된 습성의 자바스크립트를 유지할 수 있고, JSP페이지의 복잡성을 줄여준다.

캐스케이딩 스타일 시트

CSS Cascading Style Sheets

CSS를 사용해서 헤딩, 테이블 등의 공통적인 특성을 중앙에서 제어하도록 한다. 이 방법은 사용자들에게 프리젠테이션의 일관성을 향상시키고, 유지보수 노력과 JSP 페이지의 코드 사이즈를 줄여준다. 그래서, 다음과 같이 HTML 태그에서 스타일 정보를 끼어쓰지 말고:

<H1><FONT color="blue">Chapter 1</FONT></H1>

...

<H1><FONT color="blue">Chapter 2</FONT></H1>

...

하나의 스타일 시트 파일myJspStyle.css에 다음과 같은 스타일 정보를 정의한다:

H1 { color: blue }

그리고 JSP 페이지에 스타일 시트를 적용한다:

<link rel="stylesheet" href="css/myJspStyle.css" type="text/css">

...

<H1>Chapter 1</H1>

...

<H1>Chapter 2</H1>

...

컴포짓 뷰 패턴 사용

Use of Composite View Patterns

여러 JSP 페이지에서 반복되는 고정되고 복잡한 구조가 요구될 때, 이것을 다루는 방법으로 컴포짓 뷰 패턴(Composite View pattern;자바 Blueprints 의 패턴 섹션 참고)을 사용해서, 조각조각으로 나눠서 처리하는 법이 있다. 예를 들어, 프리젠테이션 구조에서 JSP 는 때때로 다음과 같은 로직 레이아웃을 갖고 있다:

header
menu barmain body
footnote
footer

이런 방식으로, 컴포짓 JSP 는 각각 분리된 JSP로 구현되는 다른 모듈로 나뉠 수 있다. 구성성분이 되는 JSP는 서블릿으로 변환할 때나 요청시에 include JSP 태그를 사용해서 컴포짓 JSP 의 적당한 위치에 놓이게 된다. 보편적으로, 독립적으로 요청되지 않는 페이지를 불러쓰는, 정적인 include 지시자를 사용할 때는 .jspf 확장자를 사용하고, 웹 어플리케이션 (war)에서 /WEB-INF/jspf/ 디렉토리에 놓아둘 것을 기억하기 바란다. 예를 들면 다음과 같다:

<%@ include file="/WEB-INF/jspf/header.jspf" %>

...

<%@ include file="/WEB-INF/jspf/menuBar.jspf" %>

...

<jsp:include page="<%= currentBody %>" />

...

<%@ include file="/WEB-INF/jspf/footnote.jspf" %>

...

<%@ include file="/WEB-INF/jspf/footer.jspf" %>

...

다른 추천사항

Other Recommendations

이 문서에서, JSP 코드와 웹 컴포넌트를 작성하는데 보다 유지보수하기 쉽고, 일관된 추천 코드 작성법 한 세트를 제시했다. 이 주제에 보다 더 관심있다면, 더 많은 최고의 작성법이 존재한다. 예를 들어, JSP 1.2 스펙에서는 다음을 추천한다:

  • 새로운 내장 객체 선언하기
  • 벤더 고유의 안내서 읽기
  • 커스텀 태그 라이브러리 사용하기

덧붙여서, 자바 BluePrints 는 더 큰 스케일에서 최고의 작성법을 제공한다. (패턴 섹션에서 보여지는) 모델-뷰-컨트롤러 패턴과 같은 것을 예로 들 수 있다.

우리는 이 문서에서 제시한 작성요령에 대한 여러분의 의견에 관심이 있습니다. JSP 코드 작성요령에 대해서 여러분의 다른 의견이 있다면 나누면 좋겠습니다. 피드백은 여기로 보내기 바랍니다.

jsp-codeconv-comments@sun.com.

아래에, 위에서 설명한 코드 작성요령을 적용해 놓은 완전한 웹 어플리케이션의 소스를 소개한다.

이 어플리케이션의 WAR 파일을 여기 에서 다운로드 받을 수 있다.

코드 예제

Code Examples

여기에 있는 웹 어플리케이션 예제는 이 문서에 있는 내용이 어떻게 적용되는지 보여주기 위한 것이다. .war 파일에는 다음의 소스코드 파일과 디렉토리 구조로 되어 있다:

/index.jsp

/WEB-INF/classes/codeconv/GetResultsTag.class

/WEB-INF/jspf/footer.jspf /WEB-INF/lib/jstl.jar

/WEB-INF/lib/standard.jar /WEB-INF/tld/lotterylib.tld

/WEB-INF/web.xml

index.jsp 페이지에서는 오늘까지 포함된 이번 달의 모의 복권 추첨결과를 보여주는 커스텀 태그 라이브러리( lotterylib.tld )를 사용한다. 그리고, HTML 에 보여질 결과를 형식화하고, 반복하기 위해서 JSTL을 사용한다.

예제는 JSP 1.2 과 JSTL 1.0 을 사용한다.

/index.jsp 파일 소스

<%--
- Author: Mark Roth
- Date: January 17, 2003
-
- Copyright 2003 Sun Microsystems, Inc. All Rights Reserved.
-
- This software is the proprietary information of
- Sun Microsystems, Inc.
- Use is subject to license terms.
-
- @(#)
- Description: Renders fake lottery results for the current month.
--%>

<%@ taglib prefix="c"
uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="fmt"
uri="http://java.sun.com/jstl/fmt" %>
<%@ taglib prefix="lottery"
uri="http://codeconv/lotterylib" %>

<html>
<head>
<title>Fake Lottery Results for this Month
</title>
</head>
<body>
<jsp:useBean id="now" class="java.util.Date" />
<h1>
Fake Lottery Results for
<fmt:formatDate value="${now}"
pattern="MMMM yyyy" />
</h1>
<hr />
<table border="1" cellpadding="5">
<%-- Store current month and year --%>
<c:set var="currentMonth">
<fmt:formatDate value="${now}" pattern="M" />
</c:set>
<c:set var="currentYear">
<fmt:formatDate value="${now}"
pattern="yyyy" />
</c:set>

<%-- Determine last day to report results for --%>
<c:set var="endDay">
<fmt:formatDate value="${now}" pattern="d" />
</c:set>

<%-- Display one row for each day --%>
<c:forEach var="day" begin="1" end="${endDay}">
<%-- Construct date --%>
<c:set var="dateString">
<c:out value="${currentMonth}/${day}/${
currentYear}" />
</c:set>

<fmt:parseDate var="date"
value="${dateString}"
pattern="M/d/yyyy" />
<jsp:useBean id="date" class="java.util.Date"
/>

<tr>
<td>
<c:out value="${dateString}" />
</td>
<%-- Retrieve and display fake results
for this day --%>
<lottery:getResults var="numbers"
date="<%= date %>" />
<c:forEach var="number" items="${numbers}">
<td>
<c:out value="${number}" />
</td>
</c:forEach>
</tr>
</c:forEach>
</table>
<%@ include file="/WEB-INF/jspf/footer.jspf" %>
</body>
</html>

/WEB-INF/classes/codeconv/GetResultsTag.java 소스
/*
* codeconv.GetResultsTag
*
* Copyright 2003 Sun Microsystems, Inc. All Rights Reserved.
*
* This software is the proprietary information of
* Sun Microsystems, Inc.
* Use is subject to license terms.
*
* Version: @(#)
*
* Date: January 13, 2003
*
*/

package codeconv;

import java.util.Date;
import java.util.Random;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.TagSupport;

/**
* Retrieves fake lottery results for the given date and
* returns the result in the given attribute
*
* @version @(#)
* @author Mark Roth
*/
public class GetResultsTag extends TagSupport {

/** The maximum number that can be drawn */
private static final int MAX_NUMBER = 58;

/** The number of numbers that are drawn */
private static final int NUMBER_COUNT = 6;

/** The variable to store results to */
private String var;

/** The date on which the numbers were drawn */
private Date date;

/** The PageContext, used to return the result */
private PageContext pageContext;

/**
* Remember the PageContext so it can be used later
* to return the result.
*
* @param pageContext The page context of the calling page
*/
public void setPageContext( PageContext pageContext ) {
this.pageContext = pageContext;
}

/**
* Returns fake lottery results for the given date.
*
* @return EVAL_PAGE so the rest of the page is evaluated.
*/
public int doEndTag() throws JspException {
// Generate the (fake) results.
Random random = new Random( this.date.getTime() );
int[] result = new int[NUMBER_COUNT];
for( int i = 0; i < NUMBER_COUNT; i++ ) {
result[i] = random.nextInt( MAX_NUMBER ) + 1;
}

// Place the result in the scoped attribute named by 'var'.
pageContext.setAttribute( this.var, result );

return EVAL_PAGE;
}

/**
* Setter for date
*
* @param date The fake date on which the lottery numbers
* were drawn
*/
public void setDate( Date date ) {
this.date = date;
}

/**
* Getter for date
*
* @return The fake date on which the lottery numbers
* were drawn
*/
public Date getDate() {
return this.date;
}

/**
* Setter for var
*
* @param var The name of the variable to store the result in
*/
public void setVar( String var ) {
this.var = var;
}

/**
* Getter for var
*
* @return The name of the variable to restore the result in
*/
public String getVar() {
return this.var;
}
}

/WEB-INF/jspf/footer.jspf 소스
<%--
- Author: Mark Roth
- Date: January 17, 2003
-
- Copyright 2003 Sun Microsystems, Inc. All Rights Reserved.
-
- This software is the proprietary information of
- Sun Microsystems, Inc.
- Use is subject to license terms.
-
- @(#)
- Description: Sample Footer that can be included on pages.
- Assumes JSTL fmt taglib is imported.
--%>
<!-- common footer -->
<hr />
<jsp:useBean id="footerNow" class="java.util.Date" />
<i>
Last Updated:
<fmt:formatDate type="both" value="${footerNow}"
dateStyle="full" timeStyle="full" />
</i>

/WEB-INF/tld/lotterylib.tld 파일 소스
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib
PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
"http://java.sun.com/j2ee/dtd/web-jsptaglibrary_1_2.dtd">

<!--
- Author: Mark Roth
- Date: January 17, 2003
-
- Copyright 2003 Sun Microsystems, Inc. All Rights Reserved.
-
- This software is the proprietary information of
- Sun Microsystems, Inc.
- Use is subject to license terms.
-
- @(#)
- Description: Tag Library for fake lottery results.
-->

<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>lottery</short-name>
<uri>http://codeconv/lotterylib</uri>
<display-name>FakeLotteryTaglib</display-name>
<description>
Tag Library for fake lottery results.
</description>

<tag>
<name>getResults</name>
<tag-class>codeconv.GetResultsTag</tag-class>
<body-content>empty</body-content>
<display-name>getResults</display-name>
<description>
Retrieve an array of fake lottery results for the given
date and store them in the given scoped attribute.
</description>
<variable>
<name-from-attribute>var
</name-from-attribute>
<declare>true</declare>
<description>
The fake lottery results for the given date.
</description>
</variable>
<attribute>
<name>var</name>
<required>true</required>
<description>
The name of the variable to store the results in.
</description>
</attribute>
<attribute>
<name>date</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
<type>java.util.Date</type>
<description>
The date on which the fake lottery numbers
were drawn.
</description>
</attribute>
<example><![CDATA[
<lottery:getResults var="numbers"
date="${date}" />
]]></example>
</tag>
</taglib>

/WEB-INF/web.xml 소스
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<!--
- Author: Mark Roth
- Date: January 17, 2003
-
- Copyright 2003 Sun Microsystems, Inc. All Rights Reserved.
-
- This software is the proprietary information of
- Sun Microsystems, Inc.
- Use is subject to license terms.
-
- @(#)
- Description: Web Deployment Descriptor for JSP 1.2 fake
- lottery example
-->

<web-app>
<display-name>FakeLotteryExample</display-name>
<description>
JSP 1.2 example application that illustrates correct JSP
coding style.
</description>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<taglib>
<taglib-uri>http://codeconv/lotterylib</taglib-uri>
<taglib-location>/WEB-INF/tld/lotterylib.tld
</taglib-location>
</taglib>
</web-app>

감사의 말 Acknowledgments

이 문서의 이전 드래프트에 소중한 조언을 제공해 주신 Gregory Murray (Java BluePrints Team), Damian Fauth (Sun Java Center), Kate Morris (Sun Java Center) 님들께 감사드립니다.

References

  1. Code Conventions for the Java Programming Language.
  2. JSP best practices.
  3. Hans's Top Ten JSP Tips.
  4. JavaServer Pages Specification version 1.2.
  5. Alur D., Crupi J., Malks D., Core J2EE Patterns: Best Practices and Design Strategies, Sun Microsystems Press, Prentice Hall Inc., 2001.
  6. Naming Conventions for Enterprise Applications.
  7. JavaServer Pages Standard Tag Library.
  8. Gamma E., Helm R., Johnson R., Vlissides J., Design Patterns: Elements of Reusable Software, Addison-Wesley, 1995.
  9. JavaServerTM Faces Technology.
  10. Java BluePrints.
posted by 구름너머 2006. 5. 24. 18:24

CLOB 칼럼에 데이터 넣기 위해 몇 일 헤메다

아래 문서보고 성공했습니다.

새로운 길이 되길...

환경: WL6.1-Oracle9i

예:

java.sql.Clob myClob = null;

strQuery = new StringBuffer();
strQuery.append(" SELECT USED_SQL FROM tb_Log ")
.append(" WHERE USED_SYS='IVOC' ")
.append("AND LOG_ID ="+seqVal+" ")
.append(" FOR UPDATE ");
pstmt = conn.prepareStatement(strQuery.toString());
rs = pstmt.executeQuery();

if (rs.next()) {
myClob = rs.getClob("USED_SQL");
java.io.Writer wr = ((weblogic.jdbc.common.OracleClob) myClob).getCharacterOutputStream();
char[] b = useSQL.toCharArray();// converts 's' to a character array
wr.write(b);
wr.flush();

wr.close();
}
conn.commit();

문서위치 : http://e-docs.bea.com/wls/docs81/oracle/advanced.html

Recommended Data Type Mapping

The following table shows the recommended mapping between Oracle data types and JDBC types. There are additional possibilities for representing Oracle data types in Java. If the getObject() method is called when result sets are being processed, it returns the default Java data type for the Oracle column being queried.

Note: In Oracle 9i, Oracle introduced the Timestamp datatype. The WebLogic jDriver for Oracle does not support this datatype.

Table 5-1 Oracle Types Mapped to WebLogic Server

Oracle

Java

Varchar

String

Number

Tinyint

Number

Smallint

Number

Integer

Number

Long

Number

Float

Number

Numeric

Number

Double

Long

Longvarchar

RowID

String

Date

Timestamp

Raw

(var)Binary

Long raw

Longvarbinary

Char

(var)Char

Boolean*

Number OR Varchar

MLS label

String

Blob

Blob

Clob

Clob


* Note that when PreparedStatement.setBoolean() is called, it converts a VARCHAR type to 1 or 0 (string), and it converts a NUMBER type to 1 or 0 (number).


WebLogic Server and Oracle's NUMBER Column

Oracle provides a column type called NUMBER, which can be optionally specified with a precision and a scale, in the forms NUMBER(P) and NUMBER(P,S). Even in the simple unqualified NUMBER form, this column can hold all number types from small integer values to very large floating point numbers, with high precision.

WebLogic Server reliably converts the values in a column to the Java type requested when a WebLogic Server application asks for a value from such a column. Of course, if a value of 123.456 is asked for with getInt(), the value will be rounded.

The method getObject(), however, poses a little more complexity. WebLogic Server guarantees to return a Java object which will represent any value in a NUMBER column with no loss in precision. This means that a value of 1 can be returned in an Integer, but a value like 123434567890.123456789 can only be returned in a BigDecimal.

There is no metadata from Oracle to report the maximum precision of the values in the column, so WebLogic Server must decide what sort of object to return based on each value. This means that one ResultSet may return multiple Java types from getObject() for a given NUMBER column. A table full of integer values may all be returned as Integer from getObject(), whereas a table of floating point measurements may be returned primarily as Double, with some Integer if any value happens to be something like "123.00". Oracle does not provide any information to distinguish between a NUMBER value of "1" and a NUMBER of "1.0000000000".

There is some more reliable behavior with qualified NUMBER columns, that is, those defined with a specific precision. Oracle's metadata provides these parameters to the driver so WebLogic Server will always return a Java object appropriate for the given precision and scale, regardless of the values in the table.

Table 5-2 Conversion Types for Oracle's Number Column Definitions

Column Definition

Returned by getObject()

NUMBER(P <= 9)

Integer

NUMBER(P <= 18)

Long

NUMBER(P = 19)

BigDecimal

NUMBER(P <=16, S 0)

Double

NUMBER(P = 17, S 0)

BigDecimal


Using Oracle Long Raw Data Types

There are two properties available for use with WebLogic Server in support of Oracle's chunking of BLOBs, CLOBs, Long, and Long raw data types. Although BLOB and CLOB data types are only supported with Oracle Version 8 and JDBC 2.0, these properties also apply to Oracle's Long raw data type, which is available in Oracle Version 7.


Waiting on Oracle Resources

Note: The waitOnResources() method is not supported for use with the Oracle 8 API.

The WebLogic Server driver supports Oracle's oopt() C functionality, which allows a client to wait until resources become available. The Oracle C function sets options in cases in which requested resources are not available, such as whether to wait for locks.

A developer can specify whether a client will wait for DBMS resources, or will receive an immediate exception. The following code is an excerpt from a sample code file (examples/jdbc/oracle/waiton.java):

java.util.Properties props = new java.util.Properties();
props.put("user", "scott");
props.put("password", "tiger");
props.put("server", "myserver");

Driver myDriver = (Driver)
Class.forName("weblogic.jdbc.oci.Driver").newInstance();

// You must cast the Connection as a weblogic.jdbc.oci.Connection
// to take advantage of this extension
Connection conn =(weblogic.jdbc.oci.Connection)
myDriver.connect("jdbc:weblogic:oracle", props);

// After constructing the Connection object, immediately call
// the waitOnResources method

conn.waitOnResources(true);

Use of this method can cause several error return codes to be generated while the software waits for internal resources that are locked for short durations.

To take advantage of this feature, you must do the following:

  1. Cast your Connection object as a weblogic.jdbc.oci.Connection.
  2. Call the waitOnResources() method.

This functionality is described in section 4-97 of The OCI Functions for C.


Autocommit

The default transaction mode for JDBC WebLogic Server assumes autocommit to be true. You can improve the performance of your programs by setting autocommit to false, after creating a Connection object, with the following statement:

Connection.setAutoCommit(false);


Transaction Isolation Levels

WebLogic Server supports the following transaction isolation levels:

  • SET TRANSACTION ISOLATION LEVEL READ COMMITTED
  • SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

The Oracle DBMS supports only these two isolation levels. Unlike other JDBC drivers, the WebLogic jDriver for Oracle throws an exception if you try to use an isolation level that is unsupported. Some drivers silently ignore attempts to set an unsupported isolation level.

The READ_UNCOMMITTED transaction isolation level is not supported.


Codeset Support

JDBC and the WebLogic Server driver handle character strings in Java as Unicode strings. Because the Oracle DBMS uses a different codeset, the driver must convert character strings from Unicode to the codeset used by Oracle. The WebLogic Server examines the value stored in the Oracle environment variable NLS_LANG and select a codeset for the JDK to use for the conversion, using the mapping shown in Table5-3. If the NLS_LANG variable is not set, or if it is set to a codeset not recognized by the JDK, the driver cannot determine the correct codeset. (For information about the correct syntax for setting NLS_LANG, see your Oracle documentation.)

If you are converting codesets, you should pass the following property to the WebLogic Server with the Driver.connect() method when you establish the connection in your code:

props.put("weblogic.oci.min_bind_size", 660);

This property defines the minimum size of buffers to be bound. The default is 2000 bytes, which is also the maximum value. If you are converting codesets, you should use this property to reduce the bind size to a maximum of 660, one-third of the maximum 2000 bytes, since Oracle codeset conversion triples the buffer to allow for expansion.

WebLogic Server provides the weblogic.codeset property to set the codeset from within your Java code. For example, to use the cp863 codeset, create a Properties object and set the weblogic.codeset property before calling Driver.connect(), as shown in the following example:

java.util.Properties props = new java.util.Properties();
props.put("weblogic.codeset", "cp863");
props.put("user", "scott");
props.put("password", "tiger");

String connectUrl = "jdbc:weblogic:oracle";

Driver myDriver = (Driver)
Class.forName("weblogic.jdbc.oci.Driver").newInstance();

Connection conn =
myDriver.connect(connectUrl, props);

Codeset support can vary with different JVMs. Check the documentation for the JDK you are using to determine whether a particular codeset is supported.

Note: You must also set the NLS_LANG environment variable in your Oracle client to the same or a corresponding codeset.

Table 5-3 NLS_LANG Settings Mapped to JDK Codesets

NLS_LANG

JDK codeset

al24utffss

UTF8

al32utf8

UTF8

ar8iso8859p6

ISO8859_6

cdn8pc863

Cp863

cl8iso8859p5

ISO8859_5

cl8maccyrillic

MacCyrillic

cl8mswin1251

Cp1251

ee8iso8859p2

ISO8859_2

ee8macce

MacCentralEurope

ee8maccroatian

MacCroatian

ee8mswin1250

Cp1250

ee8pc852

Cp852

el8iso8859p7

ISO8859_7

el8macgreek

MacGreek

el8mswin1253

Cp1253

el8pc737

Cp737

is8macicelandic

MacIceland

is8pc861

Cp861

iw8iso8859p8

ISO8859_8

ja16euc

EUC_JP

ja16sjis

SJIS

ko16ksc5601

EUC_KR

lt8pc772

Cp772

lt8pc774

Cp774

n8pc865

Cp865

ne8iso8859p10

ISO8859_10

nee8iso8859p4

ISO8859_4

ru8pc855

Cp855

ru8pc866

Cp866

se8iso8859p3

ISO8859_3

th8macthai

MacThai

tr8macturkish

MacTurkish

tr8pc857

Cp857

us7ascii

ASCII

us8pc437

Cp437

utf8

UTF8

we8ebcdic37

Cp1046

we8ebcdic500

Cp500

we8iso8859p1

ISO8859_1

we8iso8859p15

ISO8859_15_FDIS

we8iso8859p9

ISO8859_9

we8macroman8

MacRoman

we8pc850

Cp850

we8pc860

Cp860

zht16big5

Big5


Support for Oracle Array Fetches

WebLogic Server supports Oracle array fetches. When called for the first time, ResultSet.next() retrieves an array of rows (rather than a single row) and stores it in memory. Each time that next() is called subsequently, it reads a row from the rows in memory until they are exhausted, and only then will next() go back to the database.

You set a property (java.util.Property) to control the size of the array fetch. The property is weblogic.oci.cacheRows; it is set by default to 100. Here's an example of setting this property to 300, which means that calls to next() only hit the database once for each 300 rows retrieved by the client.

Properties props = new Properties();
props.put("user", "scott");
props.put("password", "tiger");
props.put("server", "DEMO");
props.put("weblogic.oci.cacheRows", "300");

Driver myDriver = (Driver)
Class.forName("weblogic.jdbc.oci.Driver").newInstance();

Connection conn = myDriver.connect("jdbc:weblogic:oracle", props);

You can improve client performance and lower the load on the database server by taking advantage of this JDBC extension. Caching rows in the client, however, requires client resources. You should tune your application for the best balance between performance and client resources, depending on your network configuration and your application.

If any columns in a SELECT are of type LONG, BLOB, or CLOB, WebLogic Server temporarily resets the cache size to 1 for the ResultSet associated with that select statement.


Using Stored Procedures

This section describes variations in the implementation of stored procedures that are specific to Oracle.

  • Binding a Parameter to an Oracle Cursor
  • Notes on Using CallableStatement

Binding a Parameter to an Oracle Cursor

WebLogic has created an extension to JDBC (weblogic.jdbc.oci.CallableStatement) that allows you to bind a parameter for a stored procedure to an Oracle cursor. You can create a JDBC ResultSet object with the results of the stored procedure. This allows you to return multiple ResultSets in an organized way. The ResultSets are determined at run time in the stored procedure.

Here is an example. First define the stored procedures as follows:

create or replace package
curs_types as
type EmpCurType is REF CURSOR RETURN emp%ROWTYPE;
end curs_types;
/

create or replace procedure
single_cursor(curs1 IN OUT curs_types.EmpCurType,
ctype in number) AS BEGIN
if ctype = 1 then
OPEN curs1 FOR SELECT * FROM emp;
elsif ctype = 2 then
OPEN curs1 FOR SELECT * FROM emp where sal 2000;
elsif ctype = 3 then
OPEN curs1 FOR SELECT * FROM emp where deptno = 20;
end if;
END single_cursor;
/
create or replace procedure
multi_cursor(curs1 IN OUT curs_types.EmpCurType,
curs2 IN OUT curs_types.EmpCurType,
curs3 IN OUT curs_types.EmpCurType) AS
BEGIN
OPEN curs1 FOR SELECT * FROM emp;
OPEN curs2 FOR SELECT * FROM emp where sal 2000;
OPEN curs3 FOR SELECT * FROM emp where deptno = 20;
END multi_cursor;
/

In your Java code, you'll construct CallableStatements with the stored procedures and register the output parameter as data type java.sql.Types.OTHER. When you retrieve the data into a ResultSet, use the output parameter index as an argument for the getResultSet() method.

java.sql.CallableStatement cstmt = conn.prepareCall(
"BEGIN OPEN ? " +
"FOR select * from emp; END;");
cstmt.registerOutParameter(1, java.sql.Types.OTHER);

cstmt.execute();
ResultSet rs = cstmt.getResultSet(1);
printResultSet(rs);
rs.close();
cstmt.close();

java.sql.CallableStatement cstmt2 = conn.prepareCall(
"BEGIN single_cursor(?, ?); END;");
cstmt2.registerOutParameter(1, java.sql.Types.OTHER);

cstmt2.setInt(2, 1);
cstmt2.execute();
rs = cstmt2.getResultSet(1);
printResultSet(rs);

cstmt2.setInt(2, 2);
cstmt2.execute();
rs = cstmt2.getResultSet(1);}
printResultSet(rs);

cstmt2.setInt(2, 3);
cstmt2.execute();
rs = cstmt2.getResultSet(1);
printResultSet(rs);
cstmt2.close();

java.sql.CallableStatement cstmt3 = conn.prepareCall(
"BEGIN multi_cursor(?, ?, ?); END;");
cstmt3.registerOutParameter(1, java.sql.Types.OTHER);
cstmt3.registerOutParameter(2, java.sql.Types.OTHER);
cstmt3.registerOutParameter(3, java.sql.Types.OTHER);

cstmt3.execute();

ResultSet rs1 = cstmt3.getResultSet(1);
ResultSet rs2 = cstmt3.getResultSet(2);
ResultSet rs3 = cstmt3.getResultSet(3);

For the full code for this example, including the printResultSet() method, see the examples in the samples/examples/jdbc/oracle/ directory.

Note that the default size of an Oracle stored procedure string is 256K.

Notes on Using CallableStatement

The default length of a string bound to an OUTPUT parameter of a CallableStatement is 128 characters. If the value you assign to the bound parameter exceeds that length, you'll get the following error:

ORA-6502: value or numeric error

You can adjust the length of the value of the bound parameter by passing an explicit length with the scale argument to the CallableStatement.registerOutputParameter() method. Here is a code example that binds a VARCHAR that will never be larger than 256 characters:

CallableStatement cstmt =
conn.prepareCall("BEGIN testproc(?); END;");

cstmt.registerOutputParameter(1, Types.VARCHAR, 256);
cstmt.execute();
System.out.println(cstmt.getString());
cstmt.close();


DatabaseMetaData Methods

This section describes some variations in the implementation of DatabaseMetaData methods that are specific to Oracle:

  • As a general rule, the String catalog argument is ignored in all DatabaseMetaData methods.
  • In the DatabaseMetaData.getProcedureColumns() method:
    • The String catalog argument is ignored.
    • The String schemaPattern argument accepts only exact matches (no pattern matching).
    • The String procedureNamePattern argument accepts only exact matches (no pattern matching).
    • The String columnNamePattern argument is ignored.


Support for JDBC Extended SQL

The JavaSoft JDBC specification includes SQL Extensions, also called SQL Escape Syntax. All WebLogic jDrivers support Extended SQL. Extended SQL provides access to common SQL extensions in a way that is portable between DBMSs.

For example, the function to extract the day name from a date is not defined by the SQL standards. For Oracle, the SQL is:

select to_char(date_column, 'DAY') from table_with_dates

The equivalent function for Sybase and Microsoft SQL Server is:

select datename(dw, date_column) from table_with_dates

Using Extended SQL, you can retrieve the day name for both DBMSs as follows:

select {fn dayname(date_column)} from table_with_dates

Here's an example that demonstrates several features of Extended SQL:

String query =
"-- This SQL includes comments and " +
"JDBC extended SQL syntax.\n" +
"select into date_table values( \n" +
" {fn now()}, -- current time \n" +
" {d '1997-05-24'}, -- a date \n" +
" {t '10:30:29' }, -- a time \n" +
" {ts '1997-05-24 10:30:29.123'}, -- a timestamp\n" +
" '{string data with { or } will not be altered}'\n" +
"-- Also note that you can safely include" +
" { and } in comments or\n" +
"-- string data.";
Statement stmt = conn.createStatement();
stmt.executeUpdate(query);

Extended SQL is delimited with curly braces ("{}") to differentiate it from common SQL. Comments are preceded by two hyphens, and are ended by a new line ("\n"). The entire Extended SQL sequence, including comments, SQL, and Extended SQL, is placed within double quotes and passed to the execute() method of a Statement object. Here is Extended SQL used as part of a CallableStatement:

CallableStatement cstmt =
conn.prepareCall("{ ? = call func_squareInt(?)}");

This example shows that you can nest extended SQL expressions:

select {fn dayname({fn now()})}

You can retrieve lists of supported Extended SQL functions from a DatabaseMetaData object. This example shows how to list all the functions a JDBC driver supports:

DatabaseMetaData md = conn.getMetaData();
System.out.println("Numeric functions: " +
md.getNumericFunctions());
System.out.println("\nString functions: " +
md.getStringFunctions());
System.out.println("\nTime/date functions: " +
md.getTimeDateFunctions());
System.out.println("\nSystem functions: " +
md.getSystemFunctions());
conn.close();


Overview of JDBC 2.0 for Oracle

The following JDBC 2.0 features are implemented in WebLogic jDriver for Oracle:

  • BLOBs (Binary Large Objects)?WebLogic Server can now handle this Oracle data type.
  • CLOBs (Character Large Objects)?WebLogic Server can now handle this Oracle data type.
  • Character Streams for both ASCII and Unicode characters?A better way to handle characters streams, as streams of characters instead of as byte arrays.
  • Batch Updates?You can now send multiple statements to the database as a single unit.

These features have been added to the existing JDBC functionality previously available in the WebLogic Server. All of your existing code for previous drivers will work with the new WebLogic jDriver for Oracle.

Note: WebLogic Server also supports Oracle extension methods for prepared statements, callable statements, arrays, STRUCTs, and REFs. However, to use these extensions, you must use the Oracle Thin Driver to connect to your database.


Configuration Required to Support JDBC 2.0

WebLogic Server Version runs on an SDK that provides the Java 2 environment required by JDBC 2.0. For a complete list of supported configurations, see the WebLogic Server Supported Configurations page.


BLOBs and CLOBs

The BLOB (Binary Large Object) and CLOB (Character Large Object) data types were made available with the release of Oracle version 8. The JDBC 2.0 specification and WebLogic Server also support these data types. This section contains information about using these data types.

Transaction Boundaries

BLOBs and CLOBs in Oracle behave differently than other data types in regards to transactional boundaries (statements issued before an SQL commit or rollback statement). in that a BLOB or CLOB will be come inactive as soon as a transaction is committed. If AutoCommit is set to TRUE, the transaction will be automatically committed after each command issued on the connection, including SELECT statements. For this reason you will need to set AutoCommit to false if you need to have a BLOB or CLOB available across multiple SQL statements. You will then need to manually commit (or rollback) the transactions at the appropriate time. To set AutoCommit to false, enter the following command:

conn.setAutoCommit(false); // where conn is your connection object

BLOBs

The BLOB data type, available with Oracle version 8, allows you to store and retrieve large binary objects in an Oracle table. Although BLOBs are defined as part of the JDBC 2.0 specification, the specification does not provide methods to update BLOB columns in a table. The BEA WebLogic implementation of BLOBs, however, does provide this functionality by means of an extension to JDBC 2.0.

Connection Properties

weblogic.oci.selectBlobChunkSize

This property sets the size of an internal buffer used for sending bytes or characters to an I/O stream. When the Chunk size is reached, the driver will perform an implicit flush() operation, which will cause the data to be sent to the DBMS.

Explicitly setting this value can be useful in controlling memory usage on the client.

If the value of this property is not explicitly set, a default value of 65534 will be used.

Set this property by passing it to the Connection object as a property. For example, this code fragment sets weblogic.oci.selectBlobChunkSize to 1200:

Properties props = new Properties();
props.put("user", "scott");
props.put("password", "tiger");
props.put("server", "DEMO");

props.put ("weblogic.oci.selectBlobChunkSize","1200");

Driver myDriver = (Driver)
Class.forName("weblogic.jdbc.oci.Driver").newInstance();

Connection conn =
driver.connect("jdbc:weblogic:oracle:myServer", props);

weblogic.oci.insertBlobChunkSize

This property specifies the buffer size (in bytes) of input streams used internally by the driver.

Set this property to a positive integer to insert BLOBs into an Oracle DBMS with the BLOB chunking feature. By default, this property is set to zero (0), which means that BLOB chunking is turned off.

Import Statements

To use the BLOB functionality described in this section, import the following classes in your client code:

import java.sql.*;
import java.util.*;
import java.io.*;
import weblogic.jdbc.common.*;

Initializing a BLOB Field

When you first insert a row containing a BLOB data type, you must insert the row with an "empty" BLOB before the field can be updated with real data. You can insert an empty BLOB with the Oracle EMPTY_BLOB() function.

To initialize a BLOB field:

  1. Create a table with one or more columns defined as a BLOB data type.
  2. Insert a new row with an empty BLOB column, using the Oracle EMPTY_BLOB() function:
  3. stmt.execute("INSERT into myTable values (1,EMPTY_BLOB()");
  4. Obtain a "handle" to the BLOB column:
  5. java.sql.Blob myBlob = null;
    Statement stmt2 = conn.createStatement();
    stmt2.execute("SELECT myBlobColumn from myTable
    where pk = 1 for update");
    ResultSet rs = stmt2.getResultSet();
    rs.next() {
    myBlob = rs.getBlob("myBlobColumn");
    // do something with the BLOB
    }
  6. You can now write data to the BLOB. Continue with the next section, Writing Binary Data to a BLOB.

Writing Binary Data to a BLOB

To write binary data to a BLOB column:

  1. Obtain a handle to the BLOB field as described above, in Initializing a BLOB Field, step 3.
  2. Create an InputStream object containing the binary data.
  3. java.io.InputStream is = // create your input stream
  4. Create an output stream to which you write your BLOB data. Note that you must cast your BLOB object to weblogic.jdbc.common.OracleBlob.
  5. java.io.OutputStream os =
    ((weblogic.jdbc.common.OracleBlob) myBlob).getBinaryOutputStream();
  6. Write the input stream containing your binary data to the output stream. The write operation is finalized when you call the flush() method on the OutputStream object.
  7. byte[] inBytes = new byte[65534]; // see note below
    int numBytes = is.read(inBytes);
    while (numBytes > 0) {
    os.write(inBytes, 0, numBytes);
    numBytes = is.read(inBytes);
    }
    os.flush();

    Note: The value [65534] in the above code presumes that you have not set the weblogic.oci.select.BlobChunkSize property whose default is 65534. If you have set this property, setting the byte[] value to match the value set in theweblogic.oci.select.BlobChunkSize property will provide the most efficient handling of the data. For more information about this property, see Connection Properties.

  8. Clean up:
  9. os.close();
    pstmt.close();
    conn.close();

Writing a BLOB Object

Writing a BLOB object to a table is performed with Prepared Statements. For example, to write the myBlob object to the table myOtherTable:

PreparedStatement pstmt = conn.preparedStatement(
"UPDATE myOtherTable SET myOtherBlobColumn = ? WHERE id = 12");
pstmt.setBlob(1, myBlob);

Reading BLOB Data

When you retrieve a BLOB column with the getBlob() method and then use a ResultSet from a SQL SELECT statement, only a pointer to the BLOB data is returned; the binary data is not actually transferred to the client until the getBinaryStream() method is called and the data is read into the stream object.

To read BLOB data from an Oracle table:

  1. Execute a SELECT statement:
  2. stmt2.execute("SELECT myBlobColumn from myTable");
  3. Use the results from the SELECT statement.
  4. int STREAM_SIZE = 10;
    byte[] r = new byte[STREAM_SIZE];
    ResultSet rs = stmt2.getResultSet();
    java.sql.Blob myBlob = null;
    while (rs.next) {
    myBlob = rs.getBlob("myBlobColumn");
    java.io.InputStream readis = myBlob.getBinaryStream();
    for (int i=0 ; i < STREAM_SIZE ; i++) {
    r[i] = (byte) readis.read();
    System.out.println("output [" + i + "] = " + r[i]);
    }
  5. Clean up:
  6. rs.close();
    stmt2.close();

Note: You can also use a CallableStatement to generate a ResultSet. This ResultSet can then be used as shown above. See your JDK documentation under java.sql.CallableStatment for details.

Other Methods

The following methods of the java.sql.Blob interface are also implemented in the WebLogic Server JDBC 2.0 driver. For details, see your JDK documentation:

  • getBinaryStream()
  • getBytes()
  • length()

The position() method is not implemented.

CLOBs

The CLOB data type, available with Oracle version 8, enables storage of large character strings in an Oracle table. Since the JDBC 2.0 specification does not include functionality to directly update CLOB columns, BEA has implemented the methods getAsciiOutputStream() (for ASCII data) and getCharacterOutputStream() (for Unicode data) to insert or update a CLOB.

Codeset Support

Depending on which version of the Oracle Server and client you are using you may need to set one of the following properties by passing them to the Connection object when you establish your connection the DBMS in your Java client code.

weblogic.codeset

This property allows you to set a codeset from within your Java code. You must also set the NLS_LANG environment variable for the Oracle client.

weblogic.oci.ncodeset

This property sets the National codeset used by the Oracle server. You must also set the NLS_NCHAR environment variable for the Oracle client.

weblogic.oci.codeset_width

This property tells the WebLogic Server which type you are using. Note the following restrictions on codeset use:

Possible Values:

0 for variable-width codesets

1 for fixed-width codesets (1 is the default value)

2 or 3 for the width, in bytes, of the codeset

weblogic.oci.ncodeset_width

If you are using one of Oracle's National codesets, specify the width of that codeset with this property. Note the following restrictions on codeset use:

Possible Values:

0 for variable-width codesets

1 for fixed-width codesets (1 is the default value)

2 or 3 for the width, in bytes, of the codeset

Initializing a CLOB Field

When you first insert a row containing a CLOB data type, you must insert the row with an "empty" CLOB before the field can be updated with real data. You can insert an empty CLOB with the Oracle EMPTY_CLOB() function.

To initialize a CLOB column:

  1. Create a table with one or more columns defined as a CLOB data type.
  2. Insert a new row with an empty CLOB column, using the Oracle EMPTY_CLOB() function:
  3. stmt.execute("INSERT into myTable VALUES (1,EMPTY_CLOB()");
  4. Obtain an object for the CLOB column:
  5. java.sql.Clob myClob = null;
    Statement stmt2 = conn.createStatement();
    stmt2.execute("SELECT myClobColumn from myTable
    where pk = 1 for update");
    ResultSet rs = stmt2.getResultSet();
    while (rs.next) {
    myClob = rs.getClob("myClobColumn");
    }
  6. You can now write character data to the CLOB. If your data is in the ASCII format, Continue with the next section, Writing ASCII Data to a CLOB. If your character data is in Unicode format, see Writing Unicode Data to a CLOB

Writing ASCII Data to a CLOB

To write ASCII character data to a CLOB column:

  1. Obtain a "handle" to the CLOB as described above, in Initializing a CLOB Field, step 3.
  2. Create an object containing the character data:
  3. String s = // some ASCII data
  4. Create an ASCII output stream to which you write your CLOB characters. Note that you must cast your CLOB object to weblogic.jdbc.common.OracleClob.
  5. java.io.OutputStream os =
    ((weblogic.jdbc.common.OracleClob) myclob).getAsciiOutputStream();
  6. Write the input stream containing your ASCII data to the output stream. The write operation is finalized when you call the flush() method on the OutputStream object.
  7. byte[] b = s.getBytes("ASCII");
    os.write(b);
    os.flush();
  8. Clean up:
  9. os.close();
    pstmt.close();
    conn.close();

Writing Unicode Data to a CLOB

To write Unicode character data to a CLOB column:

  1. Obtain a "handle" to the CLOB as described earlier, in step 3 of "Initializing a CLOB Field."
  2. Create an object containing the character data:
  3. String s = // some Unicode character data
  4. Create a character output stream to which you write your CLOB characters. Note that you must cast your CLOB object to weblogic.jdbc.common.OracleClob.
  5. java.io.Writer wr =
    ((weblogic.jdbc.common.OracleClob) myclob).getCharacterOutputStream();
  6. Write the input stream containing your ASCII data to the output stream. The write operation is finalized when you call the flush() method on the OutputStream object.
  7. char[] b = s.toCharArray(); // converts 's' to a character array
    wr.write(b);
    wr.flush();
  8. Clean up:
  9. wr.close();
    pstmt.close();
    conn.close();

Writing CLOB Objects

Writing a CLOB object to a table is performed with Prepared Statements. For example, to write the myClob object to the table myOtherTable:

PreparedStatement pstmt = conn.preparedStatement(
"UPDATE myOtherTable SET myOtherClobColumn = ? WHERE id = 12");
pstmt.setClob(1, myClob);

Updating a CLOB Value Using a Prepared Statement

If you use a prepared statement to update a CLOB and the new value is shorter than the previous value, the CLOB will retain the characters that were not specifically replaced during the update. For example, if the current value of a CLOB is abcdefghij and you update the CLOB using a prepared statement with zxyw, the value in the CLOB is updated to zxywefghij. To correct values updated with a prepared statement, you should use the dbms_lob.trim procedure to remove the excess characters left after the update. See the Oracle documentation for more information about the dbms_lob.trim procedure.

Reading CLOB Data

When a CLOB column is retrieved using a result set from a SQL SELECT statement, only a pointer to the CLOB data is returned; the actual data is not transferred to the client with the result set until the getAsciiStream() method is called and the characters are read in to the stream.

To read CLOB data from an Oracle table:

  1. Execute a SELECT statement:
  2. java.sql.Clob myClob = null;
    Statement stmt2 = conn.createStatement();
    stmt2.execute("SELECT myClobColumn from myTable");
  3. Use the results from the SELECT statement:
  4. ResultSet rs = stmt2.getResultSet();
    while (rs.next) {
    myClob = rs.getClob("myClobColumn");
    java.io.InputStream readClobis =
    myReadClob.getAsciiStream();
    char[] c = new char[26];
    for (int i=0 ; i < 26 ; i++) {
    c[i] = (char) readClobis.read();
    System.out.println("output [" + i + "] = " + c[i]);
    }
    }
  5. Clean up:
  6. rs.close();
    stmt2.close();

Note: You can also use a CallableStatement to generate a ResultSet. This ResultSet can then be used as shown above. See your JDK documentation under java.sql.CallableStatment for details.

Other Methods

The following methods of the java.sql.Clob interface are also implemented in the WebLogic Server (a JDBC 2.0 driver):

  • getSubString()
  • length()

For details about these methods, see the JDK documentation.

Note: The position() method is not implemented.


Character and ASCII Streams

Some new methods in the JDBC 2.0 specification allow character and ASCII streams to be manipulated as characters rather than as bytes, as in earlier versions. The following methods for handling character and ASCII streams are implemented in WebLogic Server.

Unicode Character Streams

getCharacterStream()

The java.sql.ResultSet interface uses this method for reading Unicode streams as the Java type java.io.Reader. This method replaces the deprecated getUnicodeStream() method.

setCharacterStream()

The java.sql.PreparedStatement interface uses this method for writing a java.io.Reader object. This method replaces the deprecated setUnicodeStream() method.

ASCII Character Streams

getAsciiStream()

The java.sql.ResultSet interface uses this method for reading ASCII streams as the Java type java.io.InputStream.

setAsciiStream()

The java.sql.PreparedStatement interface uses this method for writing a java.io.InputStream object.

For details about using these methods, see your JDK documentation.


Batch Updates

Batch updates are a feature of JDBC 2.0 that allows you to send multiple SQL update statements to the DBMS as a single unit. Depending on the application, this can provide improved performance over sending multiple update statements individually. The Batch update feature is available in the Statement interface and requires the use of SQL statements that do not return a result set.

The following SQL statements can be used with Batch updates:

  • INSERT INTO
  • UPDATE
  • DELETE
  • CREATE TABLE
  • DROP TABLE
  • ALTER TABLE

Using Batch Updates

This is the basic procedure for using Batch updates:

  1. Get a connection from a connection pool that uses the WebLogic jDriver for Oracle as described in Using the Connection Pool in an Application.
  2. Create a statement object using the createStatement() method. For example:
  3. Statement stmt = conn.createStatement();
  4. Use the addBatch() method to add SQL statements to the batch. These statements are not sent to the DBMS until the executeBatch() method is called. For example:
  5. stmt.addBatch("INSERT INTO batchTest VALUES ('JOE', 20,35)");
    stmt.addBatch("INSERT INTO batchTest VALUES ('Bob', 30,44)");
    stmt.addBatch("INSERT INTO batchTest VALUES ('Ed', 34,22)");
  6. Use the executeBatch() method to send the batch to the DBMS for processing. For example:
  7. stmt.executeBatch();

    If any of the statements fail an exception is thrown, and none of the statements is executed.

To use batch updates with for PreparedStatements and CallableStatements, create the statement normally, set the input parameters, and then call the addBatch() method with no arguments. Repeat for each additional set of input parameter values (each update), and then call executeBatch(). For example:

  1. Get a connection from a connection pool that uses the WebLogic jDriver for Oracle as described in Using the Connection Pool in an Application.
  2. Create a PreparedStatement object using the prepareStatement() method. For example:
  3. PreparedStatement pstmt = conn.prepareStatement("INSERT INTO batchTest VALUES (?,?,?)");
  4. Set the input parameters and then call addBatch() to add an update to the batch. These statements are not sent to the DBMS until the executeBatch() method is called. For example:
  5. pstmt.setString(1,"Joe");
    pstmt.setInt(2,20);
    pstmt.setInt(3,35);
    pstmt.addBatch();
  6. Repeat for each additional set of input parameter values:
  7. pstmt.setString(1,"Bob");
    pstmt.setInt(2,30);
    pstmt.setInt(3,44);
    pstmt.addBatch();
    pstmt.setString(1,"Ed");
    pstmt.setInt(2,34);
    pstmt.setInt(3,22);
    pstmt.addBatch();
  8. Use the executeBatch() method to send the batch to the DBMS for processing. For example:
  9. pstmt.executeBatch();

    If any of the prepared statements fail, an exception is thrown and none of the statements is executed.

Batch updates also work with CallableStatement objects for stored procedures. Each stored procedure must return an update count. The stored procedure cannot take any OUT or INOUT parameters.

Clearing the Batch

You may clear a batch of statements that was created with the addBatch() method, by using the clearBatch() method. For example:

stmt.clearBatch();

Update Counts

According to the JDBC 2.0 specification, the executeBatch() method should return an array of Integers containing the number of rows updated for each Statement. The Oracle DBMS, however, does not supply this information to the driver. Instead, the Oracle DBMS returns -2 for all updates.


New Date Methods

The following methods have a signature which takes a java.util.Calendar object as a parameter. java.util.Calendar allows you to specify time zone and location information that is used to translate dates. Consult your JDK API guide for details about using the java.util.Calendar class.

java.sql.ResultSet.getDate(int columnIndex, Calendar cal)

(returns a java.sql.Date object)

java.sql.PreparedStatement.setDate

'JAVA' 카테고리의 다른 글

MarshalException  (0) 2006.10.19
Code Convention : jsp page 작성 요령  (0) 2006.07.21
자바웹프로그래머의기본  (1) 2006.04.01
자바 웹 프로젝트 개발 환경 갖추기  (0) 2006.04.01
파일비교 툴  (0) 2006.03.31
posted by 구름너머 2006. 4. 1. 13:09
자바웹프로그래머의기본

마소 2005년 1월호 기고
1 제목
2 발문
3 필자 소개
4 본문
4.1 서론, 어떻게 공부할 것인가
4.2 본론
4.2.1 web.xml
4.2.2 예외 처리
4.2.3 로깅
4.2.4 예외 추적
4.2.5 한글 문제
4.2.6 URL 인코드
4.2.7 클래스패스의 리소스 사용법
4.2.8 서블릿/액션 멤버 변수 공유 문제
4.3 결론, 생각하기
4.4 참조

1 제목 #

자바 웹 프로그래머의 기본

2 발문 #

프로그래밍 초보자가 능히 한 사람 몫을 할 정도로, 혼자 코딩하도록 내버려둬도 다른 사람들이 불안에 떨지 않을 만큼 성장하는 가장 빠른 방법은 무엇일까? 디자인 패턴을 공부하고 최신 기술을 익히고 실전 프로그래밍을 많이 해보는 것? 그것도 물론 중요하다. 그러나, 이보다 훨씬 더 중요한 것은 기초를 다지는 것이다. 슬램덩크에서 강백호는 농구부 입단 후 2주일 간 드리블 연습만 했고 이것이 그가 빠른 시간 안에 한 사람 몫을 해내는데 밑거름이 되었다. 잠시 더블 클러치 연습은 멈추고 드리블을 해보자. 복잡한 이론, 어려운 신기술은 잠시 접어두고 프로그래머로서의 기본을 재점검해보자.

3 필자 소개 #

박영록 refactorer@naver.com code for human, not for programmer. 인간다운 프로그래머, 게으른 프로그래머를 지향한다. 현재 NHN에서 프레임웍 개발과 서버 관리를 담당하고 있다.

4 본문 #

4.1 서론, 어떻게 공부할 것인가 #

4년 전, 학교에서 어느 벤처 경영인의 강연을 들은 적이 있다. 미국에서 벤처를 시작해서 어느 정도의 성공을 거둔 기업가였다. 그는 강연 내내 기본을 강조했다. 미국과 한국의 기업 문화의 차이를 비교하면서 미국의 벤처들은 대체로 경영인으로서의 기본적으로 지켜야할 것들을 잘 지키는 반면 한국의 벤처는 기본적인 것들을 제대로 지키지 못하고 그로 인해 실패하는 경우가 많다고 했다. 벤처 붐이 일 때 수많은 학생 벤처가 경영에 대한 무지로 가진 기술을 펼쳐보지도 못하고 망한 현상에 대해 벤처는 경영이며 경영을 하려면 경영에 대해 배워야하는 것은 기본인데 그 기본이 지켜지지 않았기 때문이라고 했다. 당시 부도덕한 벤처 기업가들의 행태가 사회적으로 논란이 되고 있었는데 이에 대해서는 사회인으로서의 기본적인 소양이 갖추어져 있지 않기 때문이라고 했다. 그는 모든 것을 기본이란 말 하나로 설명했다. 기본이 물론 성공의 충분조건은 아니다. 그러나, 기본을 지키지 않고는 성공할 수 없다. 어떤 분야든 이것은 예외가 없을 것이다.

그렇다면 프로그래머, 그 중에서도 자바 웹 프로그래머의 기본은 무엇일까? 당연히 자바 언어에 대해서 잘 아는 것이다. 웹 프로그래밍이라는 것도 결국 사용하는 API가 다른 것 뿐, 좋은 자바 웹 프로그래머가 되려면 먼저 좋은 자바 프로그래머가 되어야한다. 너무도 당연한 말 같지만 현실은 그렇지 않다. 여러 자바 커뮤니티에 가보면 자바에 대한 정말 기본적인 질문들이 수도 없이 올라오며, 현업 프로그래머 중에도 기초가 부족한 사람이 너무나도 많다. 자바 프로그래머라면 자바에 관한 기본서 하나 정도는 마스터하고 시작하도록 하자. 자바 기본서들은 대체로 내용이 충실하므로 아무 거나 사도 나쁜 선택은 아닐 것이다. 그래도 추천이 필요하다면 Thinking in Java를 추천한다. 프로그래밍에 처음 입문하는 거라면 예제들을 직접 따라해보는 것도 좋을 것이다.

자바에 익숙해졌다면 다음 단계는 웹 기술이다. 웹 프로그래밍의 기본은 웹과 관련된 스펙(Specification)에 대한 지식, 구체적으로 Servlet/JSP 스펙, HTTP 스펙(RFC 2068), HTML ?W3C 스펙 등이다. 이 스펙들에 대해 상세히 다 알 필요는 없지만 웹 프로그래밍에서 사용하는 API들이 어떤 스펙에 기반하고 있는지, 자세히 알고 싶으면 무엇을 찾아야하는지는 알아야한다. 공대생이 공학수학의 내용을 전부 알고 있을 필요는 없지만 미분방정식을 풀고 싶으면 어느 페이지를 찾아봐야하는지는 알고 있어야하는 것처럼 어떤 요구사항이 발생했을 때 그 요구사항을 구현하려면 어떤 스펙을 찾아봐야하는지 정도는 알고 있어야한다. 그리고 의외로 많은 웹 프로그래머들이 HTML, CSS에 익숙지 않은데 이 때문에 웹사이트의 브라우저 호환성이 떨어질 뿐만 아니라 지저분한 코드를 양산하게 된다. HTML 코드 역시 유지보수 대상이 되는 코드이며 자바 코드 못지 않게 깔끔하게 유지할 수 있어야함을 기억하자. 이를 위해서는 HTML과 CSS에 대해 상세히 알아둘 필요가 있다. XML은 이제 프로그래머의 기본이니 언급할 필요도 없을 것이다. XML 파일을 이용하는 것이 편하게 느껴질 정도가 되면 코드의 유연성을 높일 좋은 방법들을 많이 생각해낼 수 있을 것이다.

스펙을 실제로 활용하는 것은 API(Application Programming Interface)를 통해서이다. Servlet/JSP API는 스펙과는 달리 실제로 API를 통해서 무엇을 할 수 있는지를 상세하게 알고 있어야한다. 이것은 비단 Servlet/JSP API 뿐 아니라 Java 기본 API, 각종 라이브러리의 API도 마찬가지다. 필자가 이제껏 자바에 관해 받아본 질문 중 대부분은 API 문서만 잘 들여다보면 해결되는 것이었다. API 문서를 자주 찾아보는 습관을 들이자. 리눅서들은 매뉴얼을 읽지 않고 질문하는 사람에게 RTFM(Read The Fucking Manual)이라는 대답을 해준다. 자바 역시 RTFM이 필요하다. ?J2EE 기본서를 하나 사서 보는 것도 좋을 것이다. ?J2EE 기본서에는 웹 관련 스펙 중 중요한 부분들, Servlet/JSP 스펙 및 API들이 잘 정리되어 있다. Java Server Programming, ?J2EE Edition 정도면 훌륭한 참고서가 될 것이다.

이제부터 이런 기본적인 지식 중에 중요하지만 간과하기 쉬운 것들, 간단하지만 알면 도움이 되는 정보들, 자주 부딪히게 되는 고민들 등 몇 가지 작은 문제들을 짚어볼 것이다. 모두 기본 학습 과정을 잘 거쳤다면 자연스럽게 알 수 있는 내용들이다. 이런 하나하나의 지식들을 통해 자신에게 부족한 점을 되짚어볼 수 있는 계기를 마련할 수 있기를 바란다.

4.2 본론 #

4.2.1 web.xml #
배치 서술자(deployment descriptor)라고 부르는 web.xml은 웹 프로젝트를 구성하는데 있어 필수적이면서 웹 애플리케이션의 동작을 여러 가지로 조정하는 역할을 한다. 스트러츠를 사용하는 경우도 스트러츠를 사용하기 위한 설정은 web.xml에 하게 되는데 그 설정들이 무슨 의미를 가지고 있는지 정도는 상식으로 알아두는 것이 좋을 것이다. 다음의 실제 스트러츠 설정 예제를 보자. <servlet>
<servlet-name>action</servlet-name>
<servlet-class>
org.apache.struts.action.ActionServlet
</servlet-class>
<init-param>
<param-name>config</param-name>
<param-value>
/WEB-INF/struts-config.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>PHP, ASP 등의 다른 서버 사이드 스크립트나 JSP 페이지는 페이지를 호출하는 경로에 실제 스크립트 파일이 존재해야하지만 서블릿은 이와 달리 web.xml의 설정을 이용해서 URL을 특정 서블릿으로 매핑시킬 수 있다. 위의 설정은 호출된 URL을 스트러츠의 Action으로 매핑시키기 위한 설정이다. servlet 설정에서 action이라는 이름의 서블릿을 org.apache.struts.action.?ActionServlet 클래스로 등록하고 아래의 servlet-mapping 설정에서 *.do라는 URL로 호출된 페이지들을 action이라는 이름의 서블릿으로 매핑시킨다. url-pattern 값을 *.nhn으로 바꾼다면 *.nhn으로 호출된 요청들이 ?ActionServlet으로 매핑될 것이다. 스트러츠는 이 ?ActionServlet에서 요청을 각 Action으로 분기시켜준다. init-param은 서블릿을 초기화할 때 사용할 파라미터값이며 getInitParameter 메쏘드를 통해서 읽어올 수 있다. load-on-startup은 서블릿 엔진이 스타트될 때 로드될 우선 순위를 지정하는 값이다.

인덱스 페이지를 지정하는 것도 web.xml에서 할 수 있다. 많은 웹사이트들이 구체적인 경로 지정 없이 도메인명까지만 써줘도 페이지를 표시한다. 이를테면 http://www.hangame.com으로 호출할 경우 다음과 같이 설정해두면 www.hangame.com의 /index.jsp를 호출하게 만들 수 있다. <welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>태그명에서 짐작할 수 있듯이 인덱스 페이지는 여러 개를 둬서 순서대로 검색하게 할 수 있다. 예를 들어 index.html과 index.jsp가 순서대로 지정된다면 서블릿 엔진은 index.html이 있으면 index.html을 보여주고 없으면 index.jsp를 호출한다. 이것도 없으면 404 에러가 나거나 디렉토리 목록이 보이게 된다. 이 인덱스 페이지는 모든 경로에 대해서 동작한다. 위와 같은 설정의 경우 http://www.hangame.com/login/을 호출한다면 http://www.hangame.com/login/index.jsp를 찾게 되는 것이다. 이 설정은 사실 아파치 등의 웹서버에서도 해줄 수 있으나 보통 웹 서버에서는 인덱스 페이지가 실제 파일로 존재해야 보여줄 수 있는데 서블릿 엔진에서는 실제 파일로 존재하지 않고 서블릿 매핑으로 지정만 되어 있어도 보여줄 수 있다는 장점이 있다.

접근 권한도 설정할 수 있다. 권한 체계가 간단한 웹 애플리케이션이라면 web.xml만으로도 충분한 권한 설정을 해줄 수 있다. <security-constraint>
<web-resource-collection>
<web-resource-name>retail</web-resource-name>
<url-pattern>/acme/retail/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>CONTRACTOR</role-name>
<role-name>HOMEOWNER</role-name>
</auth-constraint>
</security-constraint>위의 예는 서블릿 스펙 문서에 있는 예다. 이것의 의미는 GET이나 POST로 /retail/*와 같은 요청은 CONTRACTOR와 HOMEOWNER라는 role을 가진 사용자에게만 허락하겠다는 뜻이다. 이외의 사용자는 권한이 없다는 401 에러 페이지를 보게 된다. 이런 접근 제한 뿐 아니라 로그인 처리도 login-config 설정을 이용하면 가능하다. 실제 톰캣의 admin과 manager 애플리케이션은 이 설정을 이용해서 인증과 권한 처리를 한다. 자세한 스펙은 서블릿 스펙 문서에 정의되어 있으나 실제 활용하기엔 다소 부족한 감이 있고 톰캣의 실제 활용 예를 보는 것이 도움이 될 것이다. 이외에도 서블릿 필터 설정, 세션 설정, 리소스 설정 등 여러 가지 유용한 설정을 해줄 수 있고 공통적인 에외 처리를 위한 에러 페이지 설정도 가능하다. 에러 페이지 설정 부분은 이후 예외 처리에서 자세히 다룰 것이다.

4.2.2 예외 처리 #
자바의 강점 중 하나가 편리한 예외 처리 방식이다. C 언어 등 예외 처리 문법이 없는 언어를 먼저 접한 프로그래머에게는 생소한 개념일 수 있겠지만 알면 알수록 편리한 것이 자바의 예외 처리이다. 하지만 의외로 많은 자바 프로그래머들이 예외 처리를 어려워하고 예외 처리를 제대로 하지 않아 여러 가지 문제를 발생시킨다. 기본이라고 할 수도 있는 부분이긴 하나 사실 이것은 자바의 예외 처리 문법만 배운다고 되는 문제는 아니며 예외 처리에 대한 많은 고민이 필요하다. 특히 웹 애플리케이션의 예외 처리는 프로그래머를 위한 부분과 웹사이트 방문객을 위한 부분 두 가지를 모두 고려해야한다.

먼저 프로그래머의 입장을 살펴보자. 예외가 발생하면 어디까지는 그냥 던지고 어디서 캐치하는 것이 좋을까? 자바의 예외는 자바 코드의 모든 영역에서 발생할 수 있다. 이 모든 영역에 다 try-catch를 걸고 예외를 잡을 수는 없는 일이다. 대부분의 예외는 일단 그냥 던지는 것이 좋다. 자바의 예외가 좋은 것은 꼭 예외가 발생한 그 지점에서 처리를 하지 않아도 된다는 것 때문이다. 예외를 던짐으로서 예외를 처리하기에 적절한 위치에서 처리하게 만들 수 있다. 어떻게 처리해야할지 잘 모르겠다면 그냥 그대로 던지도록 하는 것이 좋다. 예외를 잡아서 처리해야하는 곳은 일반적으로 사용자에게 화면을 보여주기 직전이며 이것은 웹 애플리케이션이 MVC(Model-View-Controller) 패턴으로 작성되어 있다면 컨트롤러에서 이 역할을 하게 된다. 컨트롤러에서 예외를 보고 판단을 해서 사용자에게 보여줄 화면을 결정하는 것이다. 쇼핑몰에서 마일리지 적립액으로 상품을 구매하는 과정을 예로 들어보자. 만약 고객이 자신의 마일리지보다 더 많은 금액의 상품을 구매하려한다면 구매를 수행하는 모델 객체에서 예외가 발생할 것이다. 그러면 이 모델 클래스에서 예외를 바로 잡지 말고 던져서 구매 프로세스의 컨트롤러 객체에서 이를 잡아서 예외 페이지로 포워드를 시켜서 예외 메시지를 보여주는 식으로 코딩하면 된다.

웹사이트 방문객을 위해 중요한 것은 자바 예외가 발생했을 때 이해할 수 없는 시스템 에러 메시지나 스택트레이스 등의 황당한 화면이 아닌 친절한 에러 메시지를 표시해주는 것이다. 이를 위해서는 컨트롤러에서도 처리하지 못하고 던져진, 정말 예상 밖의 예외를 모두 끌어모아서 처리하는 부분이 필요하다. Servlet/JSP에서는 이런 부분의 처리를 위한 기능을 여러 가지로 제공하고 있고 스트러츠 등의 프레임웍에서도 다양한 방법을 제공하고 있다. JSP의 에러 페이지 설정이 그 한 예다. 그러나, JSP의 에러 페이지 설정 방식은 모든 JSP 페이지에 설정해야 작동한다는 단점이 있다. 만약 에러 페이지 지정을 빠뜨린 페이지에서 예외가 발생한다면 서블릿 엔진의 에러 메시지가 그대로 웹사이트 방문객에게 전달되고 만다. 이런 부분을 쉽게 처리하기 위한 방법이 있다. 이것은 위에서 설명했던 web.xml의 에러 페이지 설정을 이용하는 것이다. 우선 다음의 예를 보자. <error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/common/error.jsp</location>
</error-page>

<error-page>
<error-code>404</error-code>
<location>/common/error.jsp</location>
</error-page>이렇게 설정해두면 웹 애플리케이션 전반에서 발생하는 예외 중 java.lang.Exception을 상속한 예외는 모두 잡혀서 /common/error.jsp 페이지에서 처리하게 된다. 예외가 발생하면 request 객체에 예외 상황에 대한 정보가 attribute로 저장된 후 /common/error.jsp로 포워딩되어 이곳에서 request에 담긴 정보들을 바탕으로 에외 처리를 해줄 수 있다. 이 곳에서는 일반적인 에러 메시지를 사용자에게 보여주면 된다. 자바 예외 뿐 아니라 HTTP 에러 코드도 잡아낼 수 있다. 이를테면 없는 페이지를 호출해서 404 에러가 나는 경우 이를 잡아서 페이지가 없다는 에러 메시지를 좀더 친절한 메시지로 보여줄 수 있다. 덧붙여, 이 에러 처리 페이지는 가급적 순수한 서블릿으로 만드는 것이 좋다. 스트러츠의 Action으로 에러 페이지를 구성해본 적이 있었는데 설정 상의 문제로 스트러츠의 ?ActionServlet 로딩이 실패할 경우 예외를 제대로 표시하지 못한다. JSP로 만드는 것도 나쁘진 않으나 복잡한 로직이 들어갈수록 서블릿이 더 코딩하기 더 편할 수 있다. 만약 이 에러페이지 자체에서 또다시 예외가 발생하면 찾기 힘든 경우가 많기 때문에 주의를 많이 기울여야한다.

4.2.3 로깅 #
에러 페이지에서 해야할 또 하나 중요한 일은 예외 상황에 대한 로그를 남기는 것이다. 에러 페이지까지 왔다는 것은 이미 개발자의 예상을 벗어난 동작을 하고 있다는 것이므로 이 사실은 개발자에게 빨리 전달되어야한다. 때문에 로그를 제대로 남겨서 조회하기 편한 시스템을 구축해야한다. 로깅 API는 여러 가지가 있고 JDK 자체에도 포함되어 있지만 log4j가 가장 널리 사용되고 성능, 기능, 안정성 등 여러 가지 면에서 다른 것들보다 낫다. 여러 가지 로깅 API를 바꿔가면서 사용할 수 있게 해주는 자카르타의 commons-logging 프로젝트도 쓸만하다. 로거 객체는 일반적으로 클래스 당 하나를 클래스의 전체 이름으로 생성해서 사용한다. 다음은 commons-logging을 사용하는 예다. package com.hangame.avatar;

import ...

public class Avatar {
private static Log log = LogFactory.getLog(Avatar.class);

public void changeBackgroud() {
log.debug("avatar changing..");
}
}이러면 로그 객체는 Avatar 클래스의 전체 이름, com.hangame.avatar.Avatar로 생긴다. 만약 여기에 log4j를 붙여서 사용한다면 다음과 같은 log4j 설정을 사용할 수 있다. <?xml version="1.0" encoding="UTF-8" ?>

<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="normal" class="org.apache.log4j.ConsoleAppender">
<param name="Threshold" value="DEBUG"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5p] %m%n"/>
</layout>
</appender>
<appender name="memory" class="com.nhn.logging.MemoryAppender" >
<param name="Threshold" value="ERROR"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5p](%F:%L) %m%n"/>
</layout>
</appender>

<logger name="com.hangame" additivity="false">
<level value="DEBUG"/>
<appender-ref ref="normal"/>
<appender-ref ref="memory"/>
</logger>
<logger name="org.apache" additivity="false">
<level value="INFO"/>
<appender-ref ref="normal"/>
</logger>

<root>
<level value="WARN"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>위의 설정은 com.hangame와 org.apache라는 이름의 로거를 두 개 생성하고 있다. 로거의 특성은 이름으로 상속된다. com.hangame.avatar.Avatar라는 이름의 로거는 com.hangame의 속성을 모두 상속 받게 된다. 그러면 com.hangame이 normal과 memory라는 두 개의 appender를 갖고 있기 때문에 com.hangame.avatar.Avatar 로거가 찍은 로그는 표준 출력으로도 나가고 메모리에도 남게 된다. log4j의 이런 특성을 이용하면 다양한 방식으로 로그를 남길 수 있고 로그를 선택적으로 켜고 끄는 것이 가능하다. 이런 기능들을 잘 활용하면 로그를 조회하기 쉽게 구성할 수 있다. 위에서 예를 든 것처럼 메모리에 최근 로그를 남겨두고 이를 조회할 수 있는 페이지를 만든다거나 데이터베이스에 로그를 쌓을 수도 있다. 그리고 주기적으로 이런 로그 조회 페이지를 모니터링하면서 로그 리포트를 개발자에게 메일 등으로 자동 발송해주는 시스템도 구상해 볼 수 있을 것이다.

4.2.4 예외 추적 #
예외 처리 시스템을 구축하고 예외 로그를 남겼으면 다음은 이 정보를 바탕으로 문제점을 찾아들어가는 것이다. 예외 추적의 출발점은 당연히 예외 스택 정보이다. 대부분의 문제는 예외 스택 정보만 가지고도 찾아낼 수 있다. 하지만 의외로 많은 프로그래머들이 예외가 발생했을 때 스택 정보를 보지 않고 자신의 경험에 의지해서 문제점을 예측하려 하곤 한다. 이런 실제 상황에 기반하지 않은 예측은 운 좋게 문제를 바로 짚어내는 경우도 있겠지만 대개의 경우 시간만 낭비하게 된다. 예외가 발생하면 반드시 스택 정보에 찍힌 소스의 라인부터 살펴보는 습관을 기르는 것이 좋다. 스택 정보는 가끔 수백 라인에 이를 정도로 길어지는 경우도 간혹 있다. 이 모든 정보를 다 찾아볼 필요는 없다. 스택 정보는 메쏘드가 호출된 역순으로 찍히므로 위에 있는 정보가 예외가 발생한 위치와 가까운 정보다. 그렇다고 늘 제일 위의 정보를 봐야하는 것은 아니다. 웹 애플리케이션의 경우 스택 정보는 자신이 작성한 클래스 뿐 아니라 서블릿 엔진을 포함한 여러 가지 클래스의 정보들이 같이 담겨 있다. 이런 정보들은 보통 볼 필요가 없고 스택 정보에서 자신이 작성한 클래스 중 제일 위에 있는 것, 이것이 예외가 발생한 지점이며 이곳을 찾아보면 대부분의 문제점은 정확하게 추적 가능하다.

또 한 가지 자바 초보자를 괴롭히는 문제는 ?NullPointerException이다. 사실 이것은 초보자에게는 아주 까다로운 문제지만 조금만 알면 가장 찾기 쉬운 문제 중 하나가 ?NullPointerException이다. ?NullPointerException은 객체의 멤버 변수나 메쏘드를 이용하려고 할 때 그 객체가 null인 경우에 발생한다. 따라서 ?NullPointerException이 발생하면 위의 방법대로 예외가 발생한 라인을 찾아들어간 다음 그 라인에서 멤버 지정 연산자(.) 앞에 있는 객체를 보면 된다. 이 사실만 알고 있어도 ?NullPointerException이 발생했을 때 어떤 객체가 null인지를 쉽게 찾아낼 수 있을 것이다.

간혹 ?NullPointerException이 싫어서 다음과 같은 코드를 작성하는 경우가 있다. if ("Y".equals(param)) doSomthing();
else doOther();이런 코드는 조심해서 써야한다. param의 null 체크가 귀찮아서 이런 식의 코드를 쓰곤 하는데 만약 param의 값이 Y인 경우는 doSomething()을 실행하고 N이나 null이면 doOther()를 실행해야하는 경우라면 이 코드는 문제가 없다. 그러나, 만약 param은 null이면 안되는 상황이라면 어떻게 될까? 다른 부분의 버그로 param에 null이 들어와도 프로그래머는 이것을 알아차리지 못하고 넘어가게 된다. 즉, 버그를 은폐하는 코드가 된다. 당장의 문제를 발생하지 않더라도 이런 코드는 나중에 찾기 힘든 문제를 유발할 수 있다. 이런 경우는 그냥 ?NullPointerException이 발생하도록 내버려 두면 param에 null 값이 들어왔을 때 다른 부분에 버그가 있기 때문이라는 사실을 감지할 수 있다. 상황에 따라 위와 같은 코드를 써도 되는지를 신중히 검토한 후 사용해야한다. 예외 발생이 두려워서 버그를 은폐할 수 있는 코드를 만들지 말자.

4.2.5 한글 문제 #
웹 프로그래머들을 괴롭게 하는 문제를 꼽을 때 빠지지 않는 것이 한글 문제다. 한글 문제가 지금처럼 골치아프게 된 데는 역사적으로 복잡한 원인들이 얽혀 있는데 이런 문제는 접어두고 자바 웹 프로그래머로서 한글 문제를 해결하기 위해 알아야하는 것들을 살펴보자.

자바는 문자열과 바이트 스트림을 다르게 취급한다. 자바의 스트링은 유니코드의 문자셋을 사용하며 문자열을 파일에 쓰거나 네트워크로 전송하는 등 실제 입출력이 일어날 때는 문자열을 바이트 스트림으로 변환하게 된다. 이 때 바이트 스트림으로 변환하는 규칙이 인코딩이다. 따라서 바이트 스트림으로 전달된 것을 문자열로 바꾸거나 문자열을 바이트 스트림으로 전달할 때는 반드시 인코딩을 지정해야한다. 이런 인코딩 중 한글을 표현할 수 있는 인코딩은 자바에서 사용하는 이름을 기준으로 하면 EUC-KR, ?MS949, UTF-8, UTF-16 정도가 있다. EUC-KR은 ?KSC5601-1987에 기반한 인코딩으로 한글의 모든 문자를 다 표현할 수 없다. ?MS949는 EUC-KR을 확장해서 모든 한글을 표현할 수 있지만 비표준이고 코드 자체에 기술적인 결함이 많다. UTF-8과 UTF-16은 유니코드의 인코딩들이며 모든 한글을 표현할 수 있고 표준이며 한글 이외의 다른 캐릭터셋과 함께 표현이 가능하다. 보통 많이 쓰이는 EUC-KR은 RFC 표준 인코딩이긴 하나 한글의 확장 문자들을 제대로 표시하지 못한다. 그래서 자바 웹 프로그래밍에서는 ?MS949를 많이 쓰게 된다. 자바에서 스트링 객체를 생성할 때는 이 중에 하나로 인코딩을 줘서 생성해야 한글을 표현할 수 있게 인코딩된다.

웹 서버로 전달되는 요청은 클라이언트의 웹브라우저가 문자열을 바이트 스트림으로 인코딩하는데 이 때 사용하는 인코딩은 일반적으로 한글 윈도우의 기본 인코딩인 ?MS949다. 그런데, 서블릿 엔진에서 요청을 처리하는데 사용하는 기본 인코딩이 ISO-8859-1이기 때문에 아무 것도 지정하지 않으면 ?MS949로 인코딩된 바이트들을 ISO-8859-1 인코딩의 스트링 객체로 만들기 때문에 한글이 깨져보이게 된다. 따라서 기본 인코딩을 ?MS949로 지정해주면 인코딩이 보존된 상태로 한글이 깨지지 않게 된다. ?HttpServletRequest.setCharacterEncoding() 메쏘드에서 이것을 지정해줄 수 있다. 그러나, 이것에도 약간 문제가 있다. 서블릿 스펙상 이 메쏘드는 POST 요청에만 적용된다. 즉, POST 요청의 파라미터는 setCharacterEncdoing에서 지정한 인코딩으로 스트링 객체가 생성되기 때문에 한글을 보존할 수 있으나 GET 요청은 setCharacterEncoding의 적용을 받지 않기 때문에 GET으로 받은 파라미터는 인코딩 변환을 다시 해주어야한다. 다만, 이것은 서블릿 엔진에 따라 다르다. 톰캣의 경우도 4.1 버전과 5.0 버전이 다르게 동작하니 주의가 필요하다.

웹 서버에서 다시 클라이언트로 응답을 할 때는 반대의 과정이다. 자바의 스트링 객체가 바이트 스트림으로 변환되며 이 때 역시 인코딩을 지정해야한다. 이 인코딩은 JSP 페이지에서 페이지 지시자의 pageEncoding 속성을 통해 지정을 해줄 수 있고 서블릿 2.4 스펙에서는 ?HttpServletResponse.setCharacterEncoding을 사용할 수 있다. HTTP 요청을 읽는 과정과 역순이라고 생각하면 된다. 그리고, 웹 서버에서 요청을 읽을 때 ?MS949를 지정해 주듯이 클라이언트의 웹브라우저도 웹 서버에서 생성한 응답을 정확하게 읽으려면 어떤 인코딩을 사용해야하는지 알아야한다. 이것을 지정해주는 것이 HTML의 Content-Type이다. 다음과 같이 지정할 수 있다. <meta http-equiv="Content-Type" content="text/html;charset=euc-kr" />여기서 지정하는 charset은 원칙적으로는 당연히 웹 서버에서 응답 객체를 생성할 때 지정한 인코딩값과 같아야 제대로 한글로 읽을 수 있다. 그러나, 여기 지정하는 charset이 RFC 표준 문자셋이 아닐 경우 브라우저에 따라 인식을 못할 수도 있다. 그래서 ?MS949로 인코딩했다면 ?MS949를 지정해야 정상이지만 ?MS949가 RFC 표준이 아니기 때문에 문제가 생길 수 있다. 그렇다고 응답의 인코딩을 EUC-KR로 지정하게 되면 확장 한글을 표시할 수 없기 때문에 문제가 된다. 그래서 페이지 인코딩은 ?MS949로 하지만 Content-Type에는 euc-kr을 지정해주게 되는 것이다. 물론 이렇게 되면 경우에 따라 확장 한글이 깨질 수 있지만 다행스럽게도 대부분의 브라우저에서 이렇게 지정하면 잘 동작한다.

사실 이 부분은 응답 스트림에 적용되는 인코딩과 HTML Content-Type에 지정하는 인코딩이 같기만 하면 되기 때문에 굳이 ?MS949를 사용할 필요는 없고 UTF-8 등의 인코딩을 사용해도 무방하다. 따라서 응답 스트림의 인코딩도 UTF-8로 하고 Content-Type도 UTF-8로 지정하는 것이 가장 확실한 방법일 수 있다. 또한, HTML의 Content-Type에 UTF-8이 지정되어 있으면 이 페이지에서 폼을 전송할 경우에도 UTF-8로 인코딩되어 요청을 파싱하는 쪽에서도 UTF-8을 사용할 수 있다. 유니코드의 인코딩들인 UTF-8, UTF-16은 한 인코딩으로 다국어를 처리할 수 있기 때문에 다국어 지원이 필요한 웹 애플리케이션은 실제로 UTF-8로 작성된 것이 많다. 다국어 지원이 필요 없다고해도 UTF-8을 사용하는 것이 오히려 한글 문제를 더 쉽게 해결하는 방법이 될 수 있다.

웹 뿐 아니라 데이터베이스나 파일에 입출력을 할 때도 마찬가지의 원리가 적용된다. 사용하는 인코딩이 다르면 변환 과정을 거쳐야한다. 이것은 리눅스나 유닉스에서 문제가 될 수 있다. 리눅스는 ?MS949를 지원하지 않고 EUC-KR만 지원하기 때문이다. 따라서 윈도우에서 개발하고 리눅스에서 돌리는 경우 문제가 되는 경우가 간혹 있다. ?MS949가 또 하나 문제가 되는 영역은 XML 파서다. 현재 가장 널리 사용되는 XML 파서는 Xerces인데 이 파서는 RFC 표준 문자셋 외에는 지원하지 않기 때문에 ?MS949 인코딩은 파싱 에러가 난다. 그런 반면 JDK 1.4에 포함된 파서인 Crimson은 네임스페이스 파싱에 버그가 있다. ?MS949를 XML 인코딩으로 쓸 경우 XML 파서 선택이 문제가 될 수 있는 것이다. 다행스럽게도 JDK 5.0에 포함된 파서는 Xerces를 썬에서 패치한 것인데 이것은 아무 문제가 없다. 하지만 여전히 많은 오픈소스 라이브러리들이 Xerces를 사용하고 있기 때문에 문제가 되는 경우는 계속 나타날 수 있을 것이다. 이것 때문에라도 UTF-8을 사용할 필요가 있다.

자바에서의 한글 문제는 문자열과 바이트스트림의 변환에 인코딩이 주어져야한다는 사실만 생각하면 다 쉽게 해결가능하다. 역시 기본이 잘 갖춰져 있으면 한글 문제도 쉽게 해결할 수 있는 것이다.

4.2.6 URL 인코드 #
URL 인코딩이 필요한 것은 URL에 사용가능한 문자가 제한되어 있기 때문이다. URL 스펙(RFC 1738)에 정의된 바로는 URL에 사용할 수 있는 문자는 알파벳, 숫자와 몇 가지의 특수문자 뿐이다. 따라서 다양한 문자들을 URL로 전달하려면 URL에서 허용하는 문자로 변환시켜서 전달해야한다. 이것은 GET 요청의 파라미터로 값을 전달하려할 때 문제가 된다. 예를 들어 http://website.com/process.jsp에 로그인 안된 상태에서 접근하면 자동으로 로그인 페이지인 http://website.com/login.jsp로 리다이렉트된 후 로그인을 하면 원래 요청했던 페이지로 다시 리다이렉트되도록 해야한다고 하자. 그러면 /process.jsp에서는 로그인 페이지로 리다이렉트시키면서 파라미터로 현재 요청한 URL, 즉 /process.jsp를 넘겨주고 login.jsp에서는 로그인 처리가 끝난 후 이 URL로 다시 리다이렉트를 시키면 된다. 여기서 /process.jsp에서는 http://website.com/login.jsp?redirect=http://website.com/process.jsp와 같은 형식으로 리다이렉트를 해주면 될 것이다. 여기서 문제는 redirect 파라미터의 값이 URL이기 때문에 URL 안에 URL이 들어간 형태가 되어 제대로 파싱이 되지 않는다. 그래서 파라미터로 넘겨야하는 URL 부분을 ?URLEncoder로 인코딩을 해서 http://website.com/login.jsp?redirect=http%3A%2F%2Fwebsite.com%2Fprocess.jsp와 같은 형태로 넘겨야한다. 이 값을 받는 부분에서는 다시 디코딩을 해줄 필요가 없다. URL은 자동으로 웹 서버에서 파싱할 때 디코딩을 해주기 때문이다. URL을 통해서 GET 요청의 파라미터로 보내야하는 값은 반드시 URL 인코딩을 거쳐야한다는 사실만 기억하도록 하자. 참고로 자바스크립트에서도 escape, unescape 함수를 통해서 URL 인코딩, 디코딩과 유사한 작업을 수행할 수 있다.

4.2.7 클래스패스의 리소스 사용법 #
웹 애플리케이션은 보통 애플리케이션의 설정을 담고 있는 파일이 필요하다. web.xml, struts-config.xml 등의 설정 파일들은 보통 웹 애플리케이션의 /WEB-INF/에 위치하게 되는데 그 외에 애플리케이션에서 사용하는 파일들은 어디에 놓고 사용하는 것이 편리할까? 가장 관리하기 쉽고 부가적인 작업이 적은 방법은 클래스패스에 두는 것이다. /WEB-INF/classes에 두면 자바의 클래스로더를 이용해서 이런 파일들에 접근할 수 있다. log4j 등 많은 라이브러리들이 자신의 설정 파일을 클래스패스에서 가장 먼저 찾게 된다. 다음의 예제를 보자 public File getFile(String name) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
return new File(loader.getResource(name).getFile());
}

public void doSomeProcess() {
File file = getFile("config.xml");
}위의 코드는 클래스패스에서 config.xml을 읽는다. 웹 애플리케이션의 기본 클래스패스는 /WEB-INF/classes이므로 기본적으로 여기서 찾게 된다. 이것으로 jar 파일 안의 내용도 읽을 수 있다. 이 경우는 ?ClassLoader.getResourceAsStream을 통해서 스트림으로 파일 내용을 읽을 수 있다. 대부분의 IDE나 maven 등의 빌드 툴에서는 소스 경로에 있는 파일들 중 자바 소스가 아닌 파일들을 자동으로 클래스패스로 복사해주므로 이용하기도 편리하다. 자카르타의 commons-discovery 프로젝트는 이런 기능들을 모아서 편리하게 이용할 수 있게 제공하고 있다.

4.2.8 서블릿/액션 멤버 변수 공유 문제 #
JSP가 보급되기 시작하던 초기에 많이 발생하던 문제로 웹사이트의 이용자가 접속했을 때 자신의 정보가 아닌 다른 사람의 정보가 나타나면서 엉키는 경우가 있었다. 이것의 원인은 서블릿에 대한 이해가 부족해서 발생한 것이었다. 다음의 예제를 보자. public class BadServlet extends HttpServlet {

Map userInfo;

protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {

String username = req.getParameter("name");
Map userInfo = UserManager.getUserInfo(username);
req.setAttribute("userInfo", username);
}
}얼핏 별 문제가 없어보이지만 이 코드는 심각한 문제가 있다. 서블릿은 보통 서블릿 엔진에서 하나만 생성되고 한 번 생성된 서블릿 객체가 계속 재활용된다. 때문에 A와 B라는 두 사용자가 동시에 이 서블릿을 호출하게 되면 A의 호출을 수행하는 중에 B의 호출이 userInfo의 값을 바꿔버릴 수 있다. 그러면 A는 B의 정보를 보거나 그 반대의 경우가 생길 수 있는 것이다. 혼자서 테스트할 때는 한 번에 한 쓰레드만 service 메쏘드를 호출하기 때문에 이런 문제가 잘 드러나지 않기 때문에 별 문제 없는 줄 알고 있다가 서비스를 오픈하고 나면 문제가 되는 경우가 있으므로 조심해야한다. JSP에서 <%! %>를 통해서 선언하는 내용도 마찬가지 문제가 발생하므로 주의하자. 이런 내용 역시 자바 클래스와 멤버 변수의 기본 개념을 이해하고 서블릿 스펙만 한 번 읽어본다면 금방 알 수 있는 내용이다.

4.3 결론, 생각하기 #

이 내용들을 읽으면서 모르는 내용이 하나도 없었다면 자바 웹 프로그래머로서 어느 정도 기본은 되어 있다고 할 수 있다. 이런 내용들은 그 하나하나에 대한 지식을 쌓는 것도 중요하지만 더 중요한 것은 이런 내용을 알아야한다는 사실을 아는 것이다. 무엇을 알아야하는가를 가르쳐주는 것은 스펙이다. 스펙 문서들은 대부분 영어이고 그다지 친절하게 되어 있진 않지만 해당 분야에 대해 가장 정확한 정보를 담고 있다. 자세한 내용을 다 알진 못하더라도 스펙에 어떤 내용이 있는가 정도는 알아야 그 내용 중 자신에게 필요한 내용을 찾아서 공부할 수가 있는 것이다. 이런 정보를 어디서 찾을 수 있는가를 알고 있는 것도 중요하다. 기본적으로 www.ietf.org, jcp.org, java.sun.com, www.w3.org 정도의 사이트에는 익숙해지는 게 좋을 것이다.

많은 프로그래머들이 실제로 자기 손으로 프로그래밍해보는 게 실력이 느는 제일 좋은 방법이라고 말하지만 필자는 여기에 동의하지 않는다. 물론, 실제 경험을 쌓는 것이 필수적인 과정이긴 하다. 그러나, 기본 지식을 등한시한 상태에서 코딩만 해보는 것으로는 실력이 잘 늘지 않는다. 코딩 기술은 늘 수 있겠지만 정말 실제 서비스를 해야하는 프로그래밍에서 중대한 실수를 저지르게 되거나 남들이 쉽게 쉽게 하고 있는 일들을 어렵게 빙 둘러가면서 하게될 수 있다. 그래서 기본기를 갖추는 것이 중요한 것이다.

거듭해서 기본의 중요성을 강조했는데 한 가지 덧붙이고 싶은 말은 이런 기본 지식 뿐 아니라 기본을 활용하는 능력을 키우는 것도 잊지 말아야한다는 것이다. 앞서 언급한 예외 처리 같은 내용은 기본이긴 하나 자바 문법만 잘 안다고 알 수 있는 내용들은 아니며 기본을 바탕으로 좋은 판단을 내릴 수 있는 능력이 있어야한다. 결국 좋은 프로그래머가 되려면 먼저 좋은 사고 능력을 가지고 있어야하는 것이다. 글짓기를 잘하는 방법으로 흔히 다독(多讀), 다작(多作), 다상량(多商量)을 이야기한다. 많이 읽고 많이 쓰고 많이 생각하라는 것이다. 프로그래밍도 이와 비슷하다. 각종 스펙들과 좋은 코드들을 많이 읽어보고 직접 코딩도 많이 해보면 분명 실력이 늘지만 이것으로는 충분치 않다. 프로그래밍을 하면서 끊임없이 생각해야한다. 지금 작성한 코드는 좋은 코드인가, 이렇게 코딩하면 불편한데 개선할 방법은 없을까, 이 API들로 무엇을 할 수 있을까, 좀더 개발 속도를 향상시키려면 어떻게 해야할까 등등 생각을 많이 해야 진짜 발전을 이룰 수 있다. 만일 손가락이 아플 정도로 하루 종일 키보드를 두드리고 있다면 좋은 프로그래머라고 할 수 없다. 생각하는데 좀더 많은 시간을 써야한다. 모니터를 구부정하게 들여다보면서 키보드를 두드리는 것은 것보다는 의자에 편안히 기대서 생각하는 시간을 늘리자. 복잡한 문제가 있으면 바깥 공기를 쐬면서 산책을 하면서 생각을 하는 것도 좋다. 굳이 건강을 생각하지 않더라도 걷는 것은 두뇌를 활성화시키기 때문에 해결책을 더 빨리 찾을 수 있게 해 준다. 남들이 보기에는 게을러보일 수 있지만 놀고 있는 게 아니라는 것은 결과로 충분히 보여줄 수 있다. 물론 이런 생각을 잘 이어나가기 위해서는 생각의 재료가 되는 기본에 충실해야함은 물론이다. 어둠침침한 구석에 앉아 키보드만 두드리는 geek가 아닌 보다 인간다운 프로그래머가 되자.

4.4 참조 #


'JAVA' 카테고리의 다른 글

Code Convention : jsp page 작성 요령  (0) 2006.07.21
CLOB 데이터 넣기.  (0) 2006.05.24
자바 웹 프로젝트 개발 환경 갖추기  (0) 2006.04.01
파일비교 툴  (0) 2006.03.31
접속된 사용자 정보 지우기에 대해서..  (0) 2006.03.29
posted by 구름너머 2006. 4. 1. 12:10
자바 웹 프로젝트 개발 환경 갖추기
2006/04/01 오전 10:44 | JAVA

출처: 자바 프로그램 성공담



연재순서
1회.
자바 웹 프로그래머의 기본
2회. 웹 프로젝트 개발 환경 갖추기
3회. 패턴, 프레임워크, XP
지난 글에서는 자바 웹 프로그래머로서 한 사람 몫을 해내기 위한 기초에 대해 다루었습니다. 다음 단계는 한 사람 몫을 넘어서 팀 전체에 영향을 미치는 일을 할 수 있는 능력을 기르는 것이겠죠. 팀에서 어떠한 역할을 맡아도 잘해낼 수 있으려면 프로그래밍도 잘해야 하지만 개발 환경을 구성하는 방법도 잘 알고 있어야 합니다.

이 글에서는 개발 환경이 잘 갖춰진 팀의 프로그래머는 이미 구축된 환경에 대해 좀 더 잘 이해할 수 있도록, 그렇지 못한 경우는 스스로 개발 환경을 구축해나갈 수 있도록 여러 가지 개발에 필요한 것들을 살펴보겠습니다.

레이싱 경기장에는 피트(pit)라고 부르는 장소가 있습니다. 경주차의 수리나 조정, 타이어 교환, 연료 보급 등을 하는 곳이죠. 이런 일을 담당하는 사람을 미캐닉(mechanic)이라고 부릅니다. 레이싱 경기가 얼핏 보면 차의 성능과 드라이버의 기량만이 승부를 결정짓는 것 같지만, 장거리 레이스가 될수록 미캐닉의 역할이 승부를 가르는 경우도 적지 않습니다. 미캐닉이 잘한다고 레이싱에서 승리하는 것은 아니지만 미캐닉이 못하면 십중팔구 패배하게 됩니다.

웹 프로젝트도 이런 면에서 레이싱과 비슷한 점이 있습니다. 웹 프로젝트의 성공에 있어 가장 중요한 것은 요구사항의 구현이고, 기술적인 성공을 좌우하는 것은 프로그래밍이지만 프로그래밍 외의 기술적인 잡무들은 웹 프로젝트의 실패를 결정지을 수 있는 조건입니다. 소스 관리를 비롯해서 웹의 특성상 자주 발생하는 서버 설치, 프로젝트의 배치(deploy), 서버 리스타트 등이 그런 일들이죠. 이런 일들은 대체로 단순 반복 작업이면서도 실수할 가능성이 있고 또 실수를 하면 그 파급 효과가 클 수 있습니다.

그래서 이런 개발 주변 환경을 제대로 갖춰놓지 않으면 이것이 프로젝트를 실패로 이끌기도 합니다. 고로, 레이싱에서 승리하기 위해서는 좋은 미캐닉과 좋은 장비들을 확보해서 피트에서의 작업을 효율적으로 해내야 하는 것처럼 웹 프로젝트를 성공으로 이끌기 위해서는 개발 환경을 잘 갖춰서 프로그래머들의 잡무 부담을 줄이는 것이 중요합니다.

경우에 따라서 미캐닉처럼 이런 잡무를 전담하는 사람을 따로 두는 경우도 있습니다. 혹은 일부러 전담자를 두지 않더라도 한두 사람만이 수행 방법을 숙지하게 되어 결국 이 사람들에게 잡무 요청이 몰려 실질적인 전담자가 되버리는 경향이 있습니다. XP(eXtreme Programming)의 지속적인 통합(Continuous Integration)도 이런 문제를 이야기합니다. 통합 작업을 자동화해서 누구나 쉽게 할 수 있게 만들지 않으면 한두 사람만이 통합 작업을 할 수 있게 되고 통합을 자주 수행할 수도 없게 됩니다.

그러면 각자 개발한 모듈들을 통합할 때 많은 문제가 발생할 수 있고, 그런 경우 통합이 잘못된 것인지 아니면 각자 개발한 소스가 잘못된 것인지 찾기 힘들게 되죠. 이런 상황을 ‘Integration Hell’이라고 부르는데, 이런 상황을 피하기 위해서는 지속적인 통합을 통해 문제를 조기에 발견하여 문제가 쌓이지 않게 해야 합니다. 이 글에서는 지속적인 통합을 확장해 프로그래머들을 잡무에서 해방시켜 좀 더 창조적인 업무에 전념할 수 있도록 개발 환경을 구축하는 방법들을 이야기할 것입니다.

‘무엇을 해야 하는가’에 대한 고민
좋은 소프트웨어를 만드는 첫걸음은 어떻게 만드는가를 아는 것이 아니라 무엇을 만들어야 하는가를 아는 것입니다. 아무리 고급 코딩 기술과 어려운 알고리즘을 자유자재로 구사한다고 해도 사용자가 원하지 않는 소프트웨어를 만들어 냈다면 실패한 것입니다. 개발 환경 구축도 각종 도구들의 사용법을 익히는 것도 중요하지만 그 전에 자신이 속한 조직에서 어떤 일들이 있고 이런 일들을 잘하려면 무엇을 갖추어야 하는가를 고민하는 것이 첫 번째입니다.

이런 요구사항은 팀의 상황에 따라 많이 달라질 수도 있고 범위를 어떻게 잡느냐에 따라 간단한 빌드 작업으로 한정될 수도 있고 종합적인 프로젝트 관리를 포함하는 광범위한 내용이 될 수도 있습니다. 그럼 웹 프로젝트에는 어떤 요구사항이 있고 이런 요구사항을 충족하기 위해서 어떤 개발 환경이 필요한지를 살펴보겠습니다.

의사소통 관리
비단 웹 프로젝트 뿐 아니라 대부분의 프로젝트들에서 가장 중요한 성공 요인은 프로젝트의 성공을 향한 팀원들의 의지와 팀원간의 효율적인 의사소통(communication)입니다. 얼핏 둘 다 비기술적인 이슈라서 프로그래머의 영역 밖이라고 생각할 수도 있겠지만 의사소통은 이미 개발 방법론의 영역에 들어와 있습니다. 의사소통은 흔히 구성원들의 적극성의 문제, 즉 사람의 문제로 치부되는 경향이 있고 이것이 어느 정도는 사실입니다.

하지만 최근 유행하는 방법론들에서는 환경적 요인이 의사소통에 미치는 영향이 크다고 보고 의사소통을 효율적으로 하기 위한 장치들을 많이 제시하고 있습니다. 일반적인 프로그래밍 작업에서의 의견 교환, 의사 결정을 위한 회의, 문서화, 업무 요청 관리, 고객의 요구사항 관리 등이 모두 의사소통이며 개발 환경을 통해 이런 부분들을 향상시킬 수 있습니다.

XP에서는 보통 의사소통 비용은 물리적인 거리의 제곱에 비례해서 늘어난다고 합니다. 그래서 의사소통을 효율적으로 하기 위한 방법들로 물리적인 요소를 많이 제시합니다. 프로그래밍을 두 사람이 붙어 앉아서 같이 하는 짝 프로그래밍(Pair Programming), 회의가 소모적으로 흐르는 것을 방지하기 위한 기립 회의(Stand up Meeting), 자연스러운 정보 확산과 의견 교환을 위한 정보 방열기(Information Radiator) 등이 그런 것들이죠. 이런 프랙티스(practice)들을 잘 실천하려면 짝 프로그래밍을 할 수 있는 자리 배치, 팀원들이 쉽게 모여서 이야기할 수 있는 공간, 화이트보드 등 물리적 환경을 갖추어 놓아야 합니다.

소프트웨어적으로 지원할 수 있는 부분도 많습니다. 업무 요청 관리는 보통 사내 그룹웨어 등으로 소화하기 마련인데 이 부분이 잘 되어 있지 않으면 일의 진행에 병목현상이 발생하고 때로는 팀원간 마찰의 원인이 되기도 합니다. 단순히 메일을 이용하는 것보다는 업무 요청을 보내는 것, 진행 상황, 결과 등을 종합적으로 관리할 수 있는 소프트웨어가 있는 것이 좋습니다. 이슈 트래커(issue tracker)도 어느 정도 이런 부분을 소화할 수 있으나 팀의 상황에 맞게 개발해두는 것도 좋을 것입니다.

문서화 역시 시스템으로 지원해야 할 부분입니다. 문서화 시스템에서 가장 중요한 것은 문서를 빠르게 작성하고 작성한 문서를 즉시 공유할 수 있는 것입니다. 그리고 버전 관리도 되어야 하죠. 이런 요구사항을 가장 잘 만족하는 것은 위키(wiki)입니다. 대규모 CMS(Content Management System)도 많지만 오히려 단순한 위키가 더 높은 유연성을 발휘하는 경우가 많습니다.

소프트웨어 형상 관리
소프트웨어 개발 환경에서 기술적으로 가장 중요한 것은 소스 버전 관리입니다. 개발팀에서 작성하는 모든 소스는 버전 관리가 되어야 합니다. 프로그램 소스는 물론이고 스크립트, SQL, 각종 문서, 설정 파일들까지 포함합니다. 소스 버전 관리의 목적은 작업 기록의 보존을 통해 문제가 발생했을 때 원인을 추적하거나 이전 상태로 되돌리기 위한 것이죠.

보통 소스 버전 관리를 위해서 CVS(Concurrent Versions System)를 많이 사용합니다. CVS를 좀 더 개선한 Subversion도 있고 여러 가지 상용 툴도 있지만 여전히 CVS가 오픈소스 공동체에서 가장 많이 쓰이며 IDE와도 잘 통합되어 있습니다. CVS에 대한 내용 설명은 『실용주의 프로그래머를 위한 버전 관리 using CVS』라는 책을 추천하는 것으로 대신하겠습니다.

소스 관리는 사실 SCM(Software Configuration Management)의 일부분이기도 합니다. SCM은 소스에 대한 버전 관리 뿐 아니라 소프트웨어의 기능성 수준에 대한 변동 기록, 버그 수정 내역, 요구사항의 변화 등을 종합적으로 관리하는 것을 말합니다. 소프트웨어의 새 버전이 발표되면 보통 릴리스 노트(Release Notes)가 같이 배포되는데 이런 것이 SCM의 대표적인 산출물이죠.

상용 SCM 툴도 많지만 보통은 소스 관리에 CVS를 쓰고 이외에는 앞에서 언급한 이슈 트래커를 쓰는 것으로 SCM의 대부분의 영역이 커버됩니다. 소스 외의 형상 관리는 사실 업무 요청 관리와 기능적으로 아주 비슷하기 때문이죠. 오픈소스와 친한 사람이라면 버그질라(bugzilla)를 접해본 적이 있을 것입니다. 버그를 보고하고 버그가 수정되는 과정을 기록으로 남기고 조회할 수 있게 해주는 시스템이죠.

이런 것을 버그 트래커(bug tracker)라고 하는데 이를 확장한 것이 위에서 언급한 이슈 트래커입니다. 오픈소스 소프트웨어 중에도 Mantis나 TUTOS 등이 있고 위키와 이슈트래커를 합친 Trac이 있습니다. 이런 툴 하나 정도는 갖춰 놓아야할 것입니다.

빌드 자동화
지속적인 통합에서는 빌드 자동화를 가장 중요하게 다룹니다. 빌드는 소스코드를 컴파일하고 여러 가지 변환 작업을 거쳐 프로그램이 동작할 수 있게 구성하는 작업을 말합니다. 일반적인 자바 애플리케이션은 간단한 컴파일만으로 빌드가 완료되지만 웹 프로젝트는 좀 더 할 일이 많습니다. 컴파일한 클래스들이 웹 프로젝트 구조에 맞게 위치해야 하고 각종 라이브러리, TLD 파일들도 적절히 위치시켜 웹 컨테이너(서블릿 엔진)에 배치시켰을 때 정상적으로 동작하도록 해야 합니다.

프로젝트 구조가 복잡하면 그만큼 빌드 자동화도 더 복잡해집니다. 그래서 빌드 자동화 작업을 하기 전에 우선 프로젝트 구조를 잘 구성해 놓아야 합니다. <표 1>은 빌드 자동화 툴인 Ant에서 제시하는 프로젝트 구조에 대한 권고안입니다.

<표 1> Ant에서 제시하는 프로젝트 구조에 대한 권고안

여기서 눈여겨봐야 할 부분은 src와 web입니다. 자바 소스는 src에, 웹 자원은 web 아래에 컨텍스트 구조대로 배치시킵니다. 여러 IDE(Integrated Development Environment)들에서 웹 프로젝트를 구성할 때도 src와 web의 이름은 달라지기도 하지만 기본적으로 src와 web을 나누는 기준은 같습니다. 경우에 따라서 자바 소스는 src 아래에서 다시 한번 분류를 하기도 합니다. src/java, src/test로 소스와 테스트를 나누기도 하고 src/resources 등의 폴더를 만들어서 xml이나 properties를 보관하기도 합니다. 이 구조를 기준으로 보면 웹 프로젝트 빌드 작업은 다음과 같은 단계로 진행됩니다.

[1] 웹 컨테이너에서 웹 애플리케이션의 루트 역할을 하는 CONTEXT_ROOT 디렉토리를 만든다. 앞의 구조에서 web 디렉토리를 그대로 사용하기도 하고 dist/web, dist/<컨텍스트명>과 같이 만들기도 한다.
[2] 자바 클래스를 컴파일하고 CONTEXT_ROOT/WEB-INF/classes로 복사한다.
[3] 클래스패스 리소스를 CONTEXT_ROOT/WEB-INF/classes로 복사한다.
[4] 필요한 라이브러리를 CONTEXT_ROOT/WEB-INF/lib로 복사한다.
[5] JSP, HTML, CSS, JS 등의 웹 자원들을 CONTEXT_ROOT로 복사한다.
[6] tld 파일을 CONTEXT_ROOT/WEB-INF/tld나 CONTEXT_ROOT/WEB-INF/lib로 복사한다.

여기서 CONTEXT_ROOT를 web으로 그대로 사용할 수도 있는데, 그렇다면 4~6번 과정은 필요 없거나 한번만 해도 되는 작업이 됩니다. 대신 경우에 따라 프로젝트에서 빌드한 결과물을 지우고 다시 빌드하고 싶을 때 일이 복잡해질 수 있죠. 보통 IDE에서는 web 디렉토리를 그대로 CONTEXT_ROOT로 사용합니다.

만약 이런 일련의 빌드 과정을 수동으로 한다면 그 비효율은 말할 나위가 없겠죠? 그래서 IDE에서는 간단한 설정만 해두면 자동으로 빌드해줍니다. 하지만 IDE 없이 빌드해야 할 때도 있습니다. 개발할 때는 당연히 IDE로 개발을 하고 빌드도 할 수 있겠지만 IDE에서 지원하는 것 이외의 부가적인 작업을 해야 한다거나 개발자의 PC가 아닌 서버에서 직접 빌드하고 배치해야 할 때도 있습니다.

이럴 때의 빌드를 자동화하기 위해 여러 가지 빌드 자동화 도구를 사용할 수 있습니다. 유닉스/리눅스 환경에서는 오래 전부터 이런 목적을 위해 사용해 온 make라는 도구가 있습니다. Makefile이라는 파일에 빌드를 위한 스크립트들을 make 문법에 맞게 나열해두고 make 명령을 실행하면 빌드가 실행되죠.

하지만 make에서 지원하는 기능이 너무 빈약해서 빌드가 복잡할 경우 Makefile에 모든 빌드 과정을 기술해야 하기 때문에 Makefile이 복잡해져서 유지보수가 힘들다는 문제가 있습니다. 그래서 자바 진영에서는 make를 넘어서는 툴을 만들고자 했고 그래서 Ant가 등장했습니다. 그러나 Ant 역시 한계가 있었기 때문에 Ant를 한 단계 더 발전시킨 Maven이 등장했죠. 현재 대부분의 오픈소스 자바 프로젝트는 Ant나 Maven 둘 중의 하나를 빌드 도구로 사용하고 있습니다. 여기서는 구체적인 도구의 사용법을 설명하기보다 어떠한 도구들이 있는지를 간단히 살펴보고 각 도구의 장단점을 비교해 보도록 하겠습니다.

Ant
Ant는 구조적으로는 make를 그대로 이어 받았습니다. build.xml에 빌드 설정을 해두고 ant 명령을 실행하면 빌드 작업이 수행됩니다. Ant는 자주 하는 작업들을 미리 자바 클래스로 코딩해서 태스크로 만들어 두었고 클래스패스 설정이나 파일, 디렉토리 선택 등을 쉽게 할 수 있는 문법을 갖추어 놓았기 때문에 make보다 훨씬 간단하게 빌드 설정을 할 수 있죠. <리스트 1>의 build.xml의 예를 봅시다.

<리스트 1> build.xml의 예

<?xmlversion="1.0"encoding="UTF-8"?>
<projectdefault="build">
<description></description>

<targetname="build"depends="compile, resource"description="build project">
</target>

<targetname="compile">
<javacsrcdir="src/java"destdir="web/WEB-INF/classes">
<classpath>
<filesetdir="lib">
<includename="*.jar"/>
</fileset>
</classpath>
</javac>
</target>

<targetname="resource">
<copytodir="web/WEB-INF/classes">
<filesetdir="src/conf">
<includename="*.xml"/>
<includename="*.properties"/>
</fileset>
</copy>
</target>
</project>

이것은 자바 컴파일을 하고 리소스를 복사하는 빌드 파일입니다. target 태그가 작업의 단위를 정의하는 역할을 하며 다음과 같이 target을 지정하여 실행할 수 있습니다.

ant <target 이름>

앞의 build.xml에서는 build, compile, resource라는 세 개의 target을 정의하고 있고 build라는 target은 compile, resource에 의존하고 있기 때문에 build target을 실행하면 자동으로 compile과 resource가 먼저 실행됩니다. 그리고 project 태그에서 default로 build를 정의하고 있기 때문에 target을 지정하지 않고 ant를 실행하면 자동으로 build가 실행되죠. compile target에서는 javac 태스크를 이용해서 컴파일을 하고 resource 태스크에서는 copy 태스크를 이용해서 복사를 합니다. 이것이 Ant의 기본적인 구조이고 다른 부가적인 기능들도 있지만 대체로 이런 식으로 build.xml을 구성하게 됩니다.

사실 Ant 개발자들은 Ant가 make보다 훨씬 선언적으로 빌드를 정의할 수 있다는 점을 장점으로 내세우지만 이건 사실 javac, copy와 같은 태스크를 미리 자바 클래스로 코딩해 놓았기 때문에 그런 것일 뿐 실제적으로는 Make의 메커니즘과 큰 차이가 없습니다. 그런 반면 build.xml을 선언적으로 작성할 수 있게 하기 위해 build.xml의 문법에 스크립트적인 요소를 최소한으로 줄였고 또 XML 자체가 프로그래밍이 필요한 부분을 기술하기에는 적합하지 않기 때문에 Ant의 파워는 오히려 make보다 낮아졌습니다.

그리고 실제로 자바 클래스 컴파일에 파일 몇 개 복사하는 정도라면 아주 간단하게 빌드 파일을 작성할 수 있지만 복잡해지기 시작하면 build.xml은 점점 이해하기 힘든 코드가 되어갑니다. 그래서 Ant에서는 build.xml에 주석을 충분히 달아놓을 것을 권고하고 있습니다.

하지만 주석을 많이 달아야한다는 것은 주석을 달지 않으면 안될 만큼 지저분한 코드를 만들게 된다는 뜻이기도 합니다. 사실 빌드 스크립트야 한번 만들어두면 계속 쓰니까 이해하건 말건 무슨 상관이냐고 할 수도 있겠지만 프로젝트 규모가 커지고 연관 프로젝트가 많아질수록 빌드 요구사항도 계속 변합니다.

그래서 자주 수정할 수 있도록 빌드 스크립트를 명료하게 유지할 필요가 있죠. 결국 Ant는 make보다는 조금 사정이 나아졌지만 본질적으로는 make와 동일한 문제를 갖고 있습니다.

주석과 나쁜 냄새
학교에서는 꽤 오랫동안 소스코드에는 주석을 많이 달아야 다른 사람이 유지보수하기 쉽다고 가르쳐 왔습니다. 하지만 이것은 사실이 아닙니다. 오히려 많은 주석은 리팩토링에서 말하는 나쁜 냄새 중 하나입니다.

마틴 파울러가 쓴 『리팩토링』에서는 나쁜 코드를 식별하는 방법으로 냄새라는 표현을 사용합니다. 코드에 좋지 않은 부분이 있으면 ‘냄새 나는 코드’라는 거죠. 마틴 파울러는 주석은 그 자체가 나쁜 냄새를 풍기는 것은 아니지만 보통 코드의 나쁜 냄새를 감추기 위한 탈취제로 사용되기 때문에 주석을 써야 할 필요성을 느낀다면 주석을 쓰는 대신 코드를 리팩토링해서 코드에서 냄새가 나지 않도록 하라고 말합니다. 좋은 코드는 주석을 많이 달아놓은 코드가 아니라 코드 자체만으로도 쉽게 이해할 수 있는 코드라는 뜻이죠.

썬에서 제시하는 자바 코드 컨벤션(Java Code Convention)에서도 구현에 대해 설명하는 주석은 되도록 쓰지 말라고 권고하고 있습니다. 빌드 스크립트라고 예외가 되어선 안됩니다. 개발팀에서 개발하고 유지하는 모든 소스코드는 냄새가 나지 않는 ‘깨끗한 코드’가 되어야 합니다.

Maven
Maven은 이런 점들을 해결하고 나아가 종합적인 프로젝트 관리까지 소화하기 위해 만들어졌습니다. 그래서 기본 구조 자체도 make, Ant와는 상당히 다릅니다. 우선 Ant의 문제점이라고 지적되던 부분, 빌드를 위한 프로세스와 데이터가 섞여서 build.xml에 기술된다는 것을 해결하기 위해 데이터와 프로세스를 완전히 분리했습니다. Maven에서는 make의 Makefile, Ant의 build.xml에 대응되는 것으로 project.xml을 작성하는데 이 project.xml에는 프로세스가 전혀 들어가지 않고 오로지 프로젝트에 대한 설명만이 들어갑니다. <리스트 2>를 봅시다.

<리스트 2> project.xml의 예

<?xmlversion="1.0"encoding="UTF-8"?>
<project>
<pomVersion>3</pomVersion>
<artifactId>fireside</artifactId>
<name>fireside</name>
<groupId>fireside</groupId>
<currentVersion>1.0</currentVersion>
<inceptionYear>2005</inceptionYear>
<logo>/images/logo.gif</logo>
<mailingLists>
<mailingList>
<name>${pom.name} Dev List</name>
</mailingList>
<mailingList>
<name>${pom.name}User List</name>
</mailingList>
</mailingLists>
<dependencies>
<dependency>
<groupId>jsp-api</groupId>
<artifactId>jsp-api</artifactId>
<jar>jsp-api.jar</jar>
<type>jar</type>
</dependency>
<dependency>
<groupId>servlet-api</groupId>
<artifactId>servlet-api</artifactId>
<jar>servlet-api.jar</jar>
<type>jar</type>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/java</sourceDirectory>
<unitTestSourceDirectory>src/test</unitTestSourceDirectory>
<unitTest>
<includes>
<include>**/*Test.java</include>
</includes>
</unitTest>
<resources>
<resource>
<directory>src/conf</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>

이것이 project.xml의 예입니다. 보다시피 어떻게 빌드를 할 것인가에 대한 내용은 전혀 없습니다. 프로젝트의 구조에 대한 설명들만 있죠. 어떻게 빌드할 것인가는 Maven의 플러그인에 있습니다. 플러그인은 Jelly 스크립트라는 XML 기반의 스크립트 언어와 자바 클래스를 결합해서 만들게 되며 goal이라는 것을 정의하는데 이것은 Ant의 target과 비슷하며 다음과 같이 실행할 수 있습니다.

maven <goal 이름>

Maven 플러그인의 Jelly 스크립트는 Ant의 build.xml 보다 훨씬 더 프로그래밍 요소를 많이 포함하고 있기 때문에 더 강력합니다. 게다가 데이터와 프로세스가 완전히 분리되어 있어 사용하는 입장에서는 데이터만 잘 정의해도 됩니다. 따라서 Ant보다 더 선언적이고 작성하기도 쉽습니다.

그리고 Ant는 라이브러리 의존성, 프로젝트간 의존, 상속 관계 등에 대한 지원이 전혀 없었는데 Maven에서는 Repository라는 개념을 도입하여 이런 문제를 해결하고 있습니다. 프로젝트가 어떤 라이브러리를 사용하는지, 어떤 프로젝트의 특성을 상속받는지 등을 project.xml에 설정만 해주면 필요한 jar 파일을 자동으로 Maven Repository에서 다운받아서 빌드를 해주는 것이죠. 데비안 GNU/리눅스를 사용해 봤다면 dselect를 써봤을 것입니다.

dselect에서는 어떤 소프트웨어를 설치하려고 선택하면 그 소프트웨어가 필요로 하는 라이브러리나 다른 소프트웨어를 자동으로 다운로드해서 설치해줍니다. Maven이 제공하는 의존성 관리도 이와 같은 개념입니다.

또 Maven은 프로젝트에 대한 각종 문서가 집약된 사이트를 생성하도록 해줍니다. 오픈소스 프로젝트의 홈페이지를 돌아다니다 보면 ‘built by maven’이라는 딱지가 붙은 사이트가 많습니다. 이런 사이트에는 공통적으로 Project Info와 Project Reports가 있고 이 안에 소스 저장소 연결, Javadoc, 테스트 리포트, checkstyle 리포트 등의 문서들이 있습니다.

이것은 Maven의 site 플러그인이 자동으로 생성한 사이트입니다. 여러 가지 유용한 정보들을 자동으로 생성해주기 때문에 프로젝트에 대한 정보를 팀내에서 쉽게 공유할 수 있죠. 스케쥴러를 이용해서 매일 특정 시간에 빌드하고 사이트를 생성하도록 하면 매일 매일 개발 진척 상황을 살펴볼 수 있습니다.

고민할 필요 없이 Maven을 쓰면 되겠구나 싶지만 아직은 아닙니다. Maven은 지원하는 기능이 많다보니 maven을 제대로 배우는 데는 적지 않은 시간이 걸립니다. 게다가 플러그인들은 여러 가지 구조적인 부분에서 많은 ‘가정’을 포함한 상태로 개발되어 있습니다. 이를테면 Maven으로 빌드하는 jar 파일은 항상 버전 넘버가 뒤에 붙어야 한다는 식이죠.

이런 암묵적인 룰들이 많기 때문에 Maven 플러그인을 확장하고 싶을 때 자신의 생각했던 것과 다르게 동작하는 경우가 많습니다. 게다가 Maven에서 사용하는 스크립트 언어인 Jelly 스크립트는 Ant보다는 낫지만 여전히 복잡한 요구사항을 수용하기에는 ‘진짜 프로그래밍 언어’에 비해 부족한 점이 많습니다. 결국 Jelly 스크립트로 작성한 코드는 Ant의 build.xml 못지 않게 어지러운 코드가 되곤 하죠. 게다가 Maven은 속도가 Ant에 비해 아주 느린데 이 점이 의외로 개발하면서 자주 빌드해야 할 때는 치명적인 문제가 될 수 있습니다.

선택의 문제
이런 점들 때문에 Maven을 무작정 권고하기는 어렵습니다. 그래서 또 다른 대안을 찾는 사람들도 있습니다. Groovy+Ant라는 것도 있습니다. Groovy라는 자바 기반의 스크립트 언어에서 Ant의 태스크들을 쉽게 사용할 수 있게 구성해 놓은 것이죠. 나름대로 괜찮은 대안이지만 아직 Maven의 다양한 기능들을 소화하고 있진 못합니다. 결국 선택은 개발자의 몫입니다. Ant는 한계가 있지만 쉽고 가벼운 툴이고, Maven은 기능이 다양하지만 어렵고 약간의 문제가 있습니다.

일단은 Ant로 구성해 놓고 Maven을 개선한 새로운 툴이 나오기를 기다리는 것도 나쁘지 않을 것입니다. 그리고 사실 빌드 도구 만드는 것이 어려운 일은 아니므로 스스로 자신의 팀에 맞는 빌드 도구를 만들어보는 것도 좋습니다. 목적은 위에서 나열한 웹 프로젝트의 빌드 과정을 효과적으로 해내는 것이지 어떤 도구를 사용하느냐가 중요한 것은 아닙니다.

관리의 자동화
자동화의 대상은 프로젝트의 빌드만이 아닙니다. 그 외에 개발에서 발생하는 수많은 잡무들도 모두 자동화 대상입니다. 다음이 그런 작업들입니다.

[1] 서버 설치 및 서버 환경 구성
[2] 빌드한 프로젝트를 웹 컨테이너에 배치하기
[3] 웹 컨테이너를 스탑/스타트/리스타트 시키기
[4] 주기적으로 프로젝트를 테스트하고 결과를 리포팅하기
[5] 상황에 따라 서버의 셋팅 변경하기

Ant와 Maven은 이런 일에도 상당 부분 도움을 줍니다. 이미 이런 작업들이 Ant 태스크나 Maven 플러그인으로 많이 만들어져 있기도 하고 또한 빌드 자동화와 일관된 방식으로 문제를 해결할 수 있다는 것도 장점입니다. 하지만 사실 이런 부분들은 빌드 툴보다는 bash 같은 셸 스크립트가 더 간편한 경우가 많습니다. 서버에 접속해야 하는 일들도 많고 Ant나 Maven의 태스크보다 리눅스/유닉스의 툴들이 더 유용한 경우가 많습니다. 리눅스/유닉스 세계에서는 이미 오래 전부터 이런 일들을 쉘 스크립트로 해왔습니다.

하지만 여기에도 문제가 있는 것이 셸 스크립트 역시 Ant나 Maven의 Jelly 스크립트처럼 완전한 프로그래밍 언어가 아니기 때문에 파워도 부족하고 문법도 좀 덜 친숙합니다. 그래서 이런 경우는 펄(Perl)이나 파이썬 등의 스크립트 언어를 사용하기도 합니다. 스크립트 언어는 셸 명령을 셸 스크립트에 비해 큰 불편 없이 실행시킬 수 있고 프로그래밍 언어로서의 파워도 갖고 있다는 장점이 있는 반면 배워야 할 언어가 하나 더 늘고 이런 류의 작업을 위해 이미 만들어진 것들이 많지 않다는 문제가 있습니다. 결국 또다시 선택의 문제로 귀결됩니다.

참고로 필자의 선택은 간단한 경우는 셸 스크립트, 좀 복잡해지면 파이썬입니다. 중요한 것은 잡무를 모두 자동화해서 프로그래머가 개발한 것을 쉽게 테스트하고 또 실제 서비스에 쉽게 반영할 수 있게 하는 것입니다. 필요하다면 어떤 도구라도 사용할 수 있다는 유연한 사고방식이 필요합니다.

게으를 수 있는 권리
“인간은 누구나 게으를 권리가 있다.” 폴 라파르크의 『게으를 수 있는 권리』에 나오는 말입니다. 이 책에서는 일의 노예가 되어가는 현대인들에게 삶의 목적에 대해 다시 한 번 생각해볼 것을 요구하며 또한 무작정 부지런하기만 한 것이 효율적이지도 않다고 말합니다.

프로그래머에게도 이 말은 적용됩니다. 주어진 일은 무조건 열심히 하고 보자는 생각에 낡은 방식대로 묵묵히 지겨운 일을 해내는 것은 개인에도 조직에도 바람직하지 않습니다. 이건 조직의 입장에서도 생산성 향상을 할 수 있는 기회를 놓치기 하기 때문에 또다른 의미의 태업입니다. 프로그래머는 귀찮은 일에 대해 게을러져야 더 나은 방법을 찾아낼 수 있습니다. 지퍼의 발명도 구두끈을 매기 귀찮아하는 사람이 해낸 것이고 많은 발명들이 게으름의 소산입니다.

프로그래머에게 이런 게으름은 이미 권리를 넘어서 의무입니다.

물론 게으름만으로는 충분하지 않습니다. 마냥 게을러지기만 해서는 귀찮은 일을 하지 않게 될 뿐이고 더 나은 방식으로 하게 하진 못합니다. 결국 할 일을 하면서 좀 더 게을러지기 위해서는 귀찮음을 해결하는 과정에서의 부지런함은 필요합니다. 구두끈을 매기 귀찮아 하는 사람이 지퍼를 발명하는 수고를 해야 했듯이 말이죠. 그리고 기존의 방식을 개선해야 하기 때문에 어느 정도의 창의성도 있어야 합니다.

‘우리는 이제까지 잘해왔다’는 논리에 빠지는 것 또한 경계 대상입니다. 한번 성공을 거둔 경험이 있는 조직은 자신들의 방식이 성공을 가져왔기 때문에 쉽게 매너리즘에 빠집니다. 하지만 IT 세계는 빠르게 발전하고 있고 예전에는 효율적이었던 방식이 더 효율적인 기술들이 나옴에 따라 오히려 발목을 붙잡고 있는 경우도 많다는 사실을 기억해야 합니다.

그럼 이런 자동화로 절약한 시간들은 어떻게 써야 할까요? 물론 더 게을러지기 위해서 한 일들이지만 그렇다고 정말로 게을러져서는 곤란합니다. 프로그래머의 게을러질 권리는 단순한 반복 작업에만 해당되는 것입니다. 좋은 개발 환경으로 인해 절약한 시간들은 좀 더 창조적인 일을 하는데 재투자해야 합니다. 프로그래밍 자체에 집중하는 것도 좋고 좀 더 높은 수준의 자동화에 도전해보는 것도 좋습니다. 개발 환경 구축 과정에서 경험하게 된 것들을 공유해보는 것도 여러 사람에게 유익한 결과가 될 것입니다.

학창 시절 배웠던 러다이트 운동(Luddite Movement)을 기억할 것입니다. 산업혁명 이후 기계가 인간이 할 일을 대체하기 시작하면서 많은 사람들이 일자리를 잃고 임금도 급락했습니다. 그래서 노동자들이 들고 일어나서 기계를 파괴하고 다니는 비밀 조직을 결성한 것이 바로 러다이트 운동입니다.

프로그래머에게도 이런 일이 닥치지 말란 법이 없습니다. 기계가 할 수 있는 일은 기계에 맡기고 좀 더 창조적인 일을 할 줄 알아야 기계에게 일자리를 빼앗기지 않겠죠. Ant라는 이름에도 이런 생각이 담겨 있습니다. 모 광고 카피처럼 단순 반복 작업은 부지런한 개미에게 맡기고 당신은 프로그래밍을 즐기라는 뜻인 거죠.

사실 자신의 잡무를 스스로 자동화할 수 있는 사람은 프로그래머 밖에 없습니다. 이것이 프로그래머라는 직업의 매력이기도 하죠. 하루 일과에서 가장 많은 시간을 일하면서 보내는데 이 일이 즐거워야 하지 않겠습니까. 즐거운 프로그래밍을 하는 첫걸음은 귀찮고 일을 제거하는 것입니다. 귀찮은 것들일랑 모두 컴퓨터에 맡기고 우리는 프로그래밍을 즐겨 봅시다.@