PDF 파일 만들기 - fop 0.93

프로그래밍/Library 2007. 11. 28. 11:02 Posted by galad

http://blog.naver.com/dmsl01/80033371267


http://blog.naver.com/dmsl01/80033368280


http://blog.theple.com/parkhs76/103.html




1. 폰트파일(nGulim.ttf)를 폰트 매트릭스 파일(nGulim.xml)로 바꾼다

java -cp D:\work\FopToPdf\WebContent\WEB-INF\lib\fop.jar;D:\work\FopToPdf\WebContent\WEB-INF\lib\avalon-framework.jar;D:\work\FopToPdf\WebContent\WEB-INF\lib\commons-logging.jar;D:\work\FopToPdf\WebContent\WEB-INF\lib\commons-io.jar org.apache.fop.fonts.apps.TTFReader D:\Temp\fop\nGulim.ttf D:\Temp\fop\nGulim.xml


2. 폰트파일과 폰트매트릭스 파일을 컨피그에 등록.

<font metrics-url="D:\Temp\fop\nGulim.xml" embed-url="D:\Temp\fop\nGulim.ttf" kerning="yes">
    <font-triplet name="NewGulim" style="normal" weight="normal"/>
    <font-triplet name="NewGulim" style="normal" weight="bold"/>
</font>

3. 컨피그를 사용하겠다고 알림

// 유저가 설정한 컨피그 파일을 사용한다. 밑에서 유저 에이전트를 생성하기 전에 컨피그 파일을 설정해야 폰트 등이 적용된다
fopFactory.setUserConfig(new File("d:/Temp/fop/mycfg.xml")); 


FOUserAgent foUserAgent = fopFactory.newFOUserAgent();

.

.

.


4. fo 파일을 EUC-KR로 사용, 사용할 폰트를 설정

<?xml version="1.0" encoding="EUC-KR"?>
<fo:root font-family="NewGulim" font-size="12pt" font-style="normal" font-weight="normal" text-align="center" xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name='simpleA4' page-height='29.7cm' page-width='21cm' margin-top='2cm' margin-bottom='2cm' margin-left='2cm' margin-right='2cm'>
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference='simpleA4'>
    <fo:flow flow-name='xsl-region-body'>
      <fo:block>안녕</fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

가깝고도 먼 이웃 만들기「트랙백」
블로그는 네트워크 상에서 자신의 정체성을 나타낼 수 있는 좋은 표현 수단입니다. 블로그간 커뮤니케이션 수단에는 꼬리말, 트랙백, 핑백 등이 있는데, 이 중 트랙백은 기존의 게시판 활용에서 볼 수 없었던 새로운 의견 표현 방법과 다양한 활용 가능성을 제공해 관심을 끌고 있습니다.
박병권 (다음커뮤니케이션 카페팀 엔지니어) 참여
우리나라에서는 작년부터 1인 미디어 성격의 블로그 서비스들이 유행처럼 생겨났지만 무언가 빠진 듯한 느낌이 들었습니다. 사실 달력, 꼬리말, 카테고리 이러한 것들만으로 사용자들에게 기존의 게시판과의 차별성을 이해시키기는 힘듭니다. 개인의 정체성을 네트워크를 통해 표출하는 것에 정답이 있는 건 아니지만 기존의 블로그들은 더 ‘블로그답게’ 변해야 할 필요가 있었습니다. 그래서인지 요즘에는 트랙백, 관련 글 등의 이름으로 트랙백이 지원되는 블로그 서비스들이 많이 등장했습니다.

‘블로그다운’ 의사소통 방식
블로그란 사람입니다. 다만 존재하는 공간이 다를 뿐입니다. 우리는 실제 세계에 살고 있지만 블로그는 네트워크 상에서 살고 있습니다. 제가 블로그를 쓸 때면 가끔 애니메이션 ‘공각기동대’에 나오는 “네트워크로 다이브(dive)한다”라는 말이 떠오릅니다. 네트워크로 다이브해서 나의 정체성을 표현하고 네트워크에 다이브해서 들어온 다른 블로거들을 만나는게 블로깅(blogging)이라고 생각합니다.

구체적으로 말하자면 자신의 정체성을 표현한다는 것은 대개 글이 되겠지만 사진, 음악, 동영상 등 추상적인 멀티미디어로도 가능합니다. 기존의 웹 게시판을 통해 자신의 목소리를 표현해 온 네티즌들에게는 자신의 정체성을 표현하는 것은 자연스럽게 받아들일 수 있는 개념입니다.

사실 자신의 정체성을 표현하는 것보다 더 중요한 것이 링크입니다. 여기서 링크라고 말한 것은 단순히 연결된 상태만을 나타내는 것이 아니라 연결된 상태 위에서 이루어지는 상호 작용까지 포함한 말입니다. 사회에서 사람이 혼자 살 수 없듯이 네트워크상에서 블로그는 혼자 존재할 수 없고 다른 블로그들과 링크를 만들게 됩니다.

사람들은 말, 글 또는 몸짓 등을 매개물(medium)로 자신을 표현하고 다른 사람들과의 링크를 만들어 가지만 블로그는 네트워크라는 아직은 미숙한 사회에서 링크를 만들어가야 합니다. 이러한 블로그간의 링크 역할을 해줄 수 있는 매개물은 꼬리말(comment), 트랙백(trackback), 핑백(pingback) 등이 있습니다. 꼬리말의 경우 워낙 익숙한 표현 방법이라 따로 설명이 필요 없을 것이고 핑백은 다른 블로거의 포스팅(posting, 블로그에 쓰여진 하나의 글을 의미함)을 링크로써 인용한 경우 원래의 포스팅에 대해 링크가 걸렸다는 것을 알려주는 기능입니다.

이 글에서 설명할 내용인 트랙백은 서로 다른 웹 사이트간의 알림(notification)을 가능하게 해주는 기능으로써 핑백과 비슷하게 원래의 포스팅에 대해 링크를 알려주긴 하지만 역으로 자신의 의견을 원래의 포스팅에 보낼 수 있다는 점이 특징입니다.

트랙백의 아이디어는 매우 간단하지만 아직도 몇몇 유명 블로그에 ‘트랙백 테스트’, ‘트랙백이 뭐지?’ 등의 트랙백이 들어오는 것을 보면 처음 접하는 사람들이 이해하기는 쉽지 않은 것 같습니다. 필자 또한 처음 접할 당시 무엇에 쓰는 것인지 무슨 의미를 가지는 것인지 알아내기 위해 꽤나 고민했습니다.

트랙백의 원리
트랙백의 핵심은 매우 간단합니다. 누군가의 글에 대해 자신의 블로그에 의견을 피력하면서 그 글에 대해 “당신이 쓴 글에 대해 제가 의견을 썼습니다. 당신이 관심을 가질 것 같아서 일부 내용과 함께 알려드립니다”라고 알려주는 것이 바로 트랙백입니다. 트랙백을 사용할 때와 그렇지 않을 때 어떻게 커뮤니케이션이 이루어지는지 예를 들어 보겠습니다.

◆ 트랙백을 사용하지 않은 경우
디지털 카메라(이하 디카) 커뮤니티 사이트에서 활동하고 있는 A와 B가 있습니다. 어느 날 A는 B가 써놓은 글을 보게 되고 B가 자신이 가지고 있는 디카와 동일 기종을 가지고 있고 그 디카에 대한 활용팁 등을 적어 놓은 것을 보게 됩니다. 이 글을 본 A는 B의 글이 상당히 유용하지만 자신이 알고 있는 활용팁이 몇 가지 빠진 것을 보고 답글 또는 꼬리말 형식으로 B의 글을 보충해줍니다.

◆ 트랙백을 사용한 경우
디카에 대해 관심이 있는 A와 B가 각자의 블로그를 쓰고 있습니다. 어느 날 A는 B의 블로그에서 자신이 가지고 있는 디카와 동일 기종에 대한 활용팁을 적은 글을 보게 됩니다. A는 B의 글에 대해 꼬리말을 달아 자신만이 알고 있는 부분을 보충할 수도 있지만 그 글의 트랙백 URL을 찾아서 자신의 블로그에 나머지 활용팁을 적은 후 B가 자신의 글에 대해 관심을 가질 거라고 생각하고 B의 글에 대해 트랙백 핑을 보냅니다.

이 두 가지 예제는 웹 사이트에 올라온 글에 대해 트랙백을 사용한 경우와 사용하지 않은 경우 그 글에 대해 취할 수 있는 한 가지 행동 예를 보여 줍니다. 트랙백을 사용하지 않은 경우에 비해 트랙백을 사용한 경우가 가지는 가장 큰 차이점은 하나의 웹 사이트 공간(블로그)을 초월한 A와 B 사이의 링크가 만들어진다는 점과 A는 자신의 공간에서 자신의 의견을 독립적으로 표현할 수 있다는 것입니다.

프로토콜 명세
트랙백의 프로토콜은 매우 간단합니다. HTTP의 POST 메쏘드를 통해서 몇 가지 파라미터(매개변수)를 보내고 XML로 된 응답으로 성공, 실패 여부만 판단해주면 됩니다.

트랙백 핑 보내기
트랙백은 기본적으로 HTTP를 이용해서 보내게 됩니다. Trackback 1.0에서는 GET 메쏘드를 이용해서 보내게 되어있지만 현재 쓰이는 1.1에서는 POST 메쏘드를 이용하게 되어 있습니다. Content-Type request header와 url이란 이름의 파라미터는 필수 요소입니다. Content-Type은 항상 appli cation/x-www-form-urlencoded이어야 하고 파라미터의 charset을 다음과 같이 추가로 지정해줄 수도 있습니다.

[표 1] POST request body로 보낼 파라미터  
파라미터명 설명
titile 트랙백으로 전송되는 글 제목
excerpt 트랙백으로 전송되는 글의 일부분. 전송되는 글이 무엇인지 알 수 있을 정도의 양이면 된다. 보통 글자수로 잘라서 보내는데 movabletype에서는 255자가 넘는 경우 252자까지 자르고 "..."을 붙여서 보낸다. 트랙백을 위한 excerpt를 따로 작성하여 보내도 된다.
url 트랙백으로 전송되는 글의 permalink
blog_name 트랙백으로 전송되는 글이 쓰여진 블로그 이름

◆ Content-Type
application/x-www-form-urlencoded 또는 Content-Type: application/x-www-form-urlencoded; charset=EUC-KR 또는 Content-Type: application/x-www-form-urlencoded; charset=UTF-8
POST request body로 보낼 파라미터들은 [표 1]과 같습니다. 이 트랙백 요청을 보내는 것을 ‘트랙백 핑을 보낸다’라고 합니다. 예를 들어 어떤 글이 http://www.anyblog.com/ tb.cgi/7과 같은 트랙백 URL을 가지고 있다고 한다면 다음과 같은 HTTP 요청으로 그 글에 트랙백 핑을 보낼 수 있습니다. 여기서 트랙백 URL이란 트랙백 핑을 받는 프로그램이 실행되는 URL을 말합니다.

POST /tb.cgi/7 http/1.1
Host: www.anyblog.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 96

title=hello&excerpt=trackback%20test&url=http://www.mtgear.net/archieve/xx.html&blog_name=mtgear

알다시피 HTTP의 POST 메쏘드는 request body를 통해서 파라미터를 전달하고 HTTP request에서 request body가 있을 경우 Content-Length가 request body를 지정해줘야 합니다. 여기서 보내는 사람의 블로그는 mtgear(http:// mtgear .net)이고 제목이 hello라는 글을 쓰면서 글 내용의 발췌인 ‘trackback test’를 http://www.anyblog.com/tb.cgi/7이란 트랙백 URL을 가진 글에 트랙백 핑을 보내는 것입니다. 트랙백 핑을 보낸 후 성공한 경우에는 다음과 같은 응답을 받게 됩니다.

lt;?xml version=”1.0” encoding=”iso-8859-1”?><response><error>0</error> </response>

실패한 경우에는 다음과 같은 응답을 받게 되고 메시지의 텍스트 요소(text element)를 통해서 오류의 원인을 알 수 있습니다.

<?xml version=”1.0” encoding=”iso-8859-1”?><response><error>1</error> <message>The error message</message></response>

트랙백 핑을 보내는 프로그램 작성
그럼 실제로 트랙백 핑을 보내는 프로그램을 만들어 봅시다. 언어는 자바를 사용하겠습니다. 먼저 트랙백 자체를 나타내는 클래스를 만듭니다. 이 클래스에서는 트랙백을 통해서 보내지는 4가지 파라미터의 내용을 담고 있습니다.

 트랙백 자체를 나타내는 클래스
package bk.trackback;

public class Trackback {

  private String title;
  private String excerpt;
  private String url;
  private String blogname;

  public String getBlogname() {
    return blogname;
  }

  public void setBlogname(String blogname) {
    this.blogname = blogname;
  }

  public String getExcerpt() {
    return excerpt;
  }

  public void setExcerpt(String excerpt) {
    this.excerpt = excerpt;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getUrl() {
    return url;
  }

  public void setUrl(String url) {
    this.url = url;
  }
}

그리고 실제로 트랙백 핑을 보내는 클래스는 다음과 같습니다.

 트랙백 핑을 보내는 클래스
package bk.trackback;

import java.io.IOException;
import java.net.MalformedURLException;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;

public class TrackbackPing {

  private String trackbackURL;

  public TrackbackPing(String trackbackURL) throws MalformedURLException {
    this.trackbackURL = trackbackURL;
  }

  public boolean send(Trackback trackback, String charset) throws
    IOException {
    HttpClient client = new HttpClient();
    PostMethod post = new PostMethod(trackbackURL);
    if (charset == null) {
      post.setRequestHeader(“Content-Type”, “application/x-www-form-
        urlencoded;”);
    } else {
      post.setRequestHeader(“Content-Type”, “application/x-www-form-
        urlencoded; charset=” + charset);
    }
    post.addParameter(“url”, trackback.getUrl());
    post.addParameter(“title”, trackback.getTitle());
    post.addParameter(“blog_name”, trackback.getBlogname());
    post.addParameter(“excerpt”, trackback.getExcerpt());
     int statuscode = client.executeMethod(post);

   return statuscode == 200 && (post.getResponseBodyAsString().indexOf
     (“<error>0</error>”) > 0);
  }
}

여기서 HTTP 부분은 직관적으로 이해하기 쉽게 하기 위해 jakarta commons의 HttpClient를 이용했습니다. send() 부분에서 application/x-www-form-urlencoded으로 Content-Type을 설정하고 request body 부분의 character set을 설정해주는 부분과 트랙백으로 전송될 4가지 파라미터를 설정하는 부분이 있습니다.

트랙백 핑에 대한 응답으로 HTTP response code인 200 OK를 받는지와 response body 부분에 <error>가 0인지를 검사해서 트랙백 핑의 성공 여부를 알 수 있습니다. 그러면 앞의 코드가 실제로 작동하는지 테스트해 봅시다. 테스트 코드는 junit을 이용해 작성했습니다.

 코드가 작동하는지 테스트
package bk.trackback;

import java.io.IOException;

import junit.framework.TestCase;

public class TrackbackPingTest extends TestCase {

  public void testSend() throws IOException {
    TrackbackPing ping = new TrackbackPing(“http://www.anytrackbackurl.com/            
      tb/tb.cgi/xx”);
    Trackback trackback = new Trackback();
    trackback.setBlogname(“테스트”);
    trackback.setExcerpt(“안녕하세요”);
    trackback.setTitle(“그럼”);
    trackback.setUrl(“http://www.bk.com”);

    assertTrue(ping.send(trackback, “EUC-KR”));
  }
}

앞의 테스트에서 TrackbackPing의 constructor에 주는 값은 트랙백 URL이어야 합니다. 이 테스트가 성공한다면 실제로 트랙백이 보내진 것입니다.

트랙백 핑 받기
트랙백 핑을 받는 입장에서는 트랙백을 받아 자유롭게 저장해서 나중에 보여주기만 하면 되고 별다른 제약 사항은 없습니다. 다만 받는 입장에서는 글을 사람이 올리는게 아니라 다른 웹 사이트에서 올려진다는 것이 다를 뿐입니다.

트랙백 핑 목록 보기
어떤 글에 보내진 트랙백들을 보려면 HTTP GET 메쏘드로 트랙백 URL과 함께 __mode=rss 파라미터를 같이 주면 됩니다. 트랙백 핑이 POST 요청으로 보내지므로 GET 요청만으로도 트랙백 목록을 보여줘도 됩니다. 하지만 트랙백이 1.0에서 1.1로 올라오면서 GET에서 POST로 변한 것이기 때문에 아직은 GET 요청으로 트랙백 핑을 보내는 경우가 있다는 가정 하에서 앞의 파라미터를 따로 사용하는 것입니다. 하지만 추후에 발표될 트랙백 표준에서는 GET 요청만으로 트랙백 목록을 가져오도록 명세가 변한다고 합니다.

URL 자동 찾기, 오토 디스커버리
트랙백을 보내는 입장에서 트랙백 URL을 일일이 찾아 복사하기와 붙여넣기 후 트랙백 핑을 보낸다는게 꽤나 귀찮은 일입니다. 블로그 글을 읽으면서 그 글에 대한 트랙백 URL을 자동으로 찾아주는 기능이 있는데 그게 바로 오토 디스커버리(auto discovery)입니다.

최근 포털 사이트에서 서비스하는 블로그들을 보면 트랙백 또는 관련 글 등의 이름으로 트랙백이 지원되는데 다른 사람의 블로그에서 글을 읽다가 ‘관련 글 쓰기’ 등의 버튼을 누르고 글을 쓰면 그 글이 자동으로 자신의 블로그에 등록되고 읽고 있던 글에 트랙백 핑이 전송됩니다. 읽고 있던 글의 트랙백 URL을 몰라도 이렇게 트랙백 핑을 전송할 수 있도록 해주는 기능이 바로 오토 디스커버리입니다.

이는 트랙백 URL을 찾도록 HTML에 임베드된 RDF와 그 RDF에서 트랙백 URL을 찾아내는 부분으로 되어 있습니다. 트랙백 핑을 보내는 입장, 즉 트랙백 클라이언트는 블로그 글과 함께 제공되는 RDF에서 해당 글의 트랙백 URL을 추출해 냅니다. 이렇게 찾아낸 트랙백 URL은 전용 클라이언트 또는 웹 프로그램을 통해서 트랙백을 보내는데 이용됩니다. RDF 내에서 트랙백에 관련된 태그는 [표 2]와 같은 두 가지가 있습니다.

[표 2] RDF에서 트랙백에 관련된 태그  
trackback:ping 트랙백을 받을 수 있는 URL을 나타내며 오직 하나만 있다.
trackback:about 현재 글에 전송된 트랙백 핑을 보낸 곳의 트랙백 URL이 나타나며 여러개가 있을 수 있다.
예를 들어 A와 B가 C에 트랙백 핑을 전송했다면 C의 trackback:about에 A와 B의 트랙백 URL이 나타난다.

다음은 실제 블로그 글에 포함된 RDF의 일부 샘플이고 여기에서 trackback:ping으로 된 부분 http://mtgear.net/mt/ mt-tb.cgi/200이 이 글에 대한 트랙백 핑을 받을 트랙백 URL입니다.

 실제 블로그 글에 포함된 RDF
<!--
<rdf:RDF xmlns:rdf=”http://www.w3.org/1999/02/22-rdf-syntax-ns#”
  xmlns:trackback=”http://madskills.com/public/xml/rss/module/trackback/”
  xmlns:dc=”http://purl.org/dc/elements/1.1/”><rdf:Description
  rdf:about=”http://mtgear.net/archives/000207.php”
  trackback:ping=”http://mtgear.net/mt/mt-tb.cgi/200”
  dc:title=”천장에서 찍은 내모습(?)”
  dc:identifier=”http://mtgear.net/archives/000207.php”
  dc:subject=”끄적끄적”
  dc:description=” 진짜루?...”
  dc:creator=”wanderer”
  dc:date=”2004-04-05T22:10:08+09:00” /></rdf:RDF>
-->

트랙백의 활용
트랙백은 원격 꼬리말과 컨텐츠 수집 등과 같은 방법으로 많이 활용되지만 그 이외에 여러가지 방법으로 활용될 수 있습니다.

원격 꼬리말
원격 꼬리말(Remote commenting)은 어떤 글에 대한 자신의 의견을 그 글이 있는 곳에 답글이나 꼬리말 등으로 남기는 대신 자신의 사이트에 관련된 글을 남기고 그 글을 트랙백 핑으로 보내는 것입니다. 이렇게 보내진 트랙백 핑은 외부 사이트에 쓰여진 글이 마치 자신의 사이트에 꼬리말처럼 남겨지는 효과를 줍니다. 또한 이렇게 트랙백으로 자신의 URL을 보냄으로써 블로그간의 링크를 형성할 수도 있습니다.

컨텐츠 수집
http://www.blogkorea.org가 트랙백의 컨텐츠 수집(content aggregation)을 적절히 활용한 예라 할 수 있는데 트랙백을 받는 주체가 특정 글이 아닌 일종의 추상적인 카테고리가 될 수 있다는 것을 응용한 기술입니다. 예를 들어 내가 관심있는 주제에 대한 카테고리를 만들고 그 카테고리에 대한 트랙백 URL을 공개한다면 그 주제에 대해 공통 관심을 가진 사람들이 자신의 블로그에 글을 남기면서 그 카테고리에 트랙백 핑을 보내줄 것입니다. 이렇게 보내진 트랙백 핑들은 같은 관심사에 대한 글일 것이므로 자동으로 관련된 글들, 즉 컨텐츠를 수집할 수 있습니다.

적극적으로 정보를 전달하는 트랙백
자신의 의견을 표현하고 남이 읽어주기를 바라는 기존의 피동적인 웹 이용 행태와 비교해보면 나의 의견을 읽도록 적극적으로 강요(?)할 수 있는 트랙백은 자기 주장이 뚜렷하고 솔직해지는 요즈음의 사회 흐름에 잘 부합할 수 있는 기능입니다. 그리고 앞의 두 가지 트랙백 이용 사례가 있지만 정보의 흐름을 적극적으로 컨트롤할 수 있다는 측면은 더 나은 응용 사례를 만들어낼 수 있습니다. 더군다나 이런 생각이 표준으로 어느 곳에서나 활용될 수 있다는 것이 얼마나 멋집니까! @

* 이 기사는 ZDNet Korea의 자매지인 마이크로소프트웨어에 게재된 내용입니다.

출처 : Tong - '겨울나기'님의 RSS통

본 문서에서는 HTTP 헤더를 사용하여 Internet Explorer에서 웹 페이지의 캐싱을 제어하는 방법에 대해 설명합니다.

Microsoft Internet Information Server(IIS)를 사용하면 특정 ASP(Active Server Pages) 페이지의 맨 앞에 다음 스크립트를 사용하여 휘발성이 매우 높거나 매우 중요한 페이지를 쉽게 표시할 수 있습니다.
<% Response.CacheControl = "no-cache" %>
<% Response.AddHeader "Pragma", "no-cache" %>
<% Response.Expires = -1 %>
				

위로 가기

추가 정보

만료 및 Expires 헤더

모든 웹 서버에서는 모든 웹 페이지의 만료에 대한 구성을 사용하는 것이 좋습니다. 웹 서버가 요청 클라이언트에 반환되는 모든 리소스에 대해 HTTP Expires 응답 헤더를 통해 만료 정보를 제공하지 않으면 문제가 생길 수 있습니다. 대부분의 브라우저와 중간 프록시는 현재 이 만료 정보를 적용하고 이 정보를 사용하여 네트워크 상의 통신 효율을 향상시킵니다.

서버의 특정 파일을 클라이언트에서 업데이트해야 하는 가장 적절한 시간을 지정하려면 항상 Expires 헤더를 사용해야 합니다. 페이지가 정기적으로 업데이트되는 경우에는 다음 업데이트 기간이 가장 효율적인 응답입니다. 예를 들어, 인터넷에서 매일 오전 5시에 업데이트되는 일간 뉴스 페이지의 경우 이 뉴스 페이지의 웹 서버는 값이 다음 날 오전 5시인 Expires 헤더를 반환해야 합니다. 이 값이 반환되면 브라우저는 페이지가 실제로 변경될 때까지 웹 서버에 다시 연결할 필요가 없습니다.

변경될 것 같지 않은 페이지에는 대략 1년의 만료 날짜가 표시됩니다.

웹 서버에는 바로 변경될 정보가 들어 있는 휘발성 페이지가 하나 이상 있는 경우가 많습니다. 서버에서는 이러한 페이지의 Expires 헤더 값을 "-1"로 표시합니다. 사용자가 이후에 요청하면 Internet Explorer는 대개 조건부 If-Modified-Since 요청을 통해 해당 페이지를 업데이트하기 위해 웹 서버에 연결합니다. 그러나 해당 페이지는 디스크 캐시("임시 인터넷 파일")에 남아 있으며, 뒤로 단추와 앞으로 단추로 탐색 기록에 액세스하는 경우나 브라우저가 오프라인 모드에 있는 경우 원격 웹 서버에 연결하지 않고 적절한 상황에서 사용됩니다.

위로 가기

Cache-Control 헤더

특정 페이지는 휘발성이 매우 높거나 매우 중요해서 디스크 캐싱이 필요하지 않습니다. 이를 위해 Internet Explorer는 HTTP 1.1 Cache-Control 헤더를 지원합니다. 이 헤더는 HTTP 1.1 서버에서 캐시 값을 지정하지 않은 경우 특정 웹 리소스의 모든 캐싱을 방지합니다.

브라우저가 웹 서버에 다시 연결할 수 있을 때까지 캐시에 없는 페이지는 액세스할 수 없기 때문에 반드시 필요한 경우에만 서버에서 Cache-Control 헤더를 사용해야 합니다. 대부분의 경우에는 "Expires: -1"을 사용하는 것이 좋습니다.

위로 가기

Pragma: No-Cache 헤더

이전 HTTP 1.0 서버에서는 Cache-Control 헤더를 사용할 수 없다는 문제가 있습니다. HTTP 1.0 서버와의 호환성을 위해 Internet Explorer는 HTTP Pragma: no-cache 헤더의 특수한 사용을 지원합니다. 클라이언트가 보안 연결(https://)을 통해 서버와 통신하고 서버가 응답과 함께 Pragma: no-cache 헤더를 반환하면 Internet Explorer는 응답을 캐시하지 않습니다.

그러나 Pragma: no-cache 헤더는 이를 위해 만들어진 것은 아닙니다. HTTP 1.0과 1.1 사양에 따라 이 헤더는 응답이 아니라 요청 컨텍스트에서만 정의되며, 실제로는 중요한 특정 요청이 대상 웹 서버에 도달하지 못하게 할 수 있는 프록시 서버용으로 만들어졌습니다. 이후의 응용 프로그램에서는 Cache-Control 헤더가 캐싱을 제어하는 적절한 수단이 될 것입니다.

위로 가기

HTTP-EQUIV META 태그

HTML 페이지에서는 HTML 문서에서 특정 HTTP 헤더를 지정하는 META 태그의 특수한 HTTP-EQUIV 형식이 허용됩니다. 다음은 Pragma: no-cache와 Expires: -1을 모두 사용하는 간단한 HTML 페이지의 예입니다.
<HTML><HEAD>
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
<META HTTP-EQUIV="Expires" CONTENT="-1">
</HEAD><BODY>
</BODY>
</HTML>
				
Pragma: no-cache는 보안 연결을 통해 사용되는 경우에만 캐싱을 방지합니다. Pragma: no-cache META 태그는 비보안 페이지에서 사용되는 경우 Expires: -1과 동일하게 처리됩니다. 페이지는 캐시되지만 즉시 만료되는 것으로 표시됩니다.

Cache-Control META HTTP-EQUIV 태그는 무시되고 Internet Explorer 버전 4 또는 5에서 효과를 나타내지 않습니다. Cache-Control을 사용하려면 위의 Cache-Control 섹션에 설명된 HTTP 헤더를 사용하여 이 헤더를 지정해야 합니다.

표준 HTTP 헤더가 META 태그보다 훨씬 많이 사용됩니다. META 태그는 일반적으로 HTML HEAD 섹션의 맨 위에 나타나야 합니다. 또한 Pragma HTTP-EQUIV META 태그에는 알려진 문제점이 적어도 하나 있습니다. 자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
222064 (http://support.microsoft.com/kb/222064/) "Pragma: No-cache" 태그를 사용해도 페이지가 캐시될 수 있다

위로 가기

서버의 캐싱 옵션

ASP 페이지 이외의 페이지에서 Cache-Control 헤더를 사용해야 하는 경우에는 서버 구성의 옵션을 사용하여 이 헤더를 자동으로 추가해야 할 수 있습니다. HTTP 헤더를 특정 디렉터리의 서버 응답에 추가하는 프로세스는 해당 서버 설명서를 참조하십시오. 예를 들어, IIS 4에서는 다음과 같이 하십시오.
인터넷 서비스 관리자를 호출합니다.
컴퓨터와 서비스 트리를 사용하여 기본 웹 서버(또는 문제의 웹 서버)를 열고 Cache-Control 헤더가 필요한 콘텐츠가 들어 있는 디렉터리를 찾습니다.
해당 디렉터리의 등록 정보 대화 상자를 표시합니다.
HTTP 헤더 탭을 선택합니다.
사용자 지정 HTTP 헤더 그룹에서 추가 단추를 누른 다음 헤더 이름으로 "Cache-Control"을 추가하고 헤더 값으로 "no-cache"를 추가합니다.
전체 웹 서버에서 이 헤더를 사용하는 것은 좋지 않습니다. 클라이언트에서 절대로 캐시되지 않아야 하는 콘텐츠에만 이 헤더를 사용하십시오.

위로 가기

문제 검사 목록

본 문서에 나와 있는 방법을 적용했는데도 Internet Explorer와 캐싱에 문제가 있는 경우에는 Microsoft에 기술 지원을 요청하기 전에 다음과 같은 유용한 검사 목록을 단계별로 검토하십시오.
Cache-Control 헤더를 ASP "Response.CacheControl" 속성과 함께 사용하거나 반환된 HTTP 헤더를 통해 사용하고 있습니까? 이것은 Internet Explorer에서 캐싱을 완전히 방지하는 유일한 방법입니다.
Internet Explorer 4.01 서비스 팩 2 이상을 사용하고 있습니까? 이전 버전의 브라우저에서 캐싱을 완전히 방지하는 방법은 없습니다.
웹 서버에 HTTP 1.1이 설정되어 있고 Internet Explorer에 HTTP 1.1 응답을 반환 중인지 다시 확인했습니까? Cache-Control 헤더는 HTTP 1.0 응답에 적합하지 않습니다.
서버쪽에서 CGI/ISAPI/서블릿을 사용하는 경우, 특히 HTTP 헤더의 CRLF 종결과 관련하여 HTTP 1.1 사양을 정확하게 따르고 있습니까? 일반적으로 성능 면에서 Internet Explorer는 HTTP 1.1 사양을 위반하는 응답은 허용하지 않습니다. 이에 따라 대개 헤더가 무시되거나 예기치 않은 서버 오류 보고서가 만들어집니다.
HTTP 헤더의 철자가 정확합니까?

위로 가기

참조

자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
189409 (http://support.microsoft.com/kb/189409/) INFO: IIS 4.0에서 웹 페이지의 캐싱 제어
165150 (http://support.microsoft.com/kb/165150/) IIS와 IE에서 Pragma: No-cache를 사용하는 방법
HTTP/1.1에 대한 자세한 내용을 보려면 다음 웹 사이트를 방문하여 RFC 2616을 참조하십시오.
http://www.w3.org/Protocols/rfc2616/rfc2616.html (http://www.w3.org/Protocols/rfc2616/rfc2616.html)

Microsoft Internet Explorer cache issues

 

Internet Explorer implements caching for GET requests. Authors who are not familiar with HTTP caching expect GET requests not to be cached, or for the cache to be avoided as with the refresh button. In some situations, failing to circumvent caching is a bug. One solution to this is to use the POST request method, which is never cached; however, it is intended for non-idempotent operations.

Setting the "Expires" header to reference a date in the past will avoid caching of the response. Here is an example in PHP.

header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); 
// disable IE caching header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); 
header( "Cache-Control: no-cache, must-revalidate" ); header( "Pragma: no-cache" );
 

Caching may also be disabled, as in this Java Servlet example.

response.setHeader( "Pragma", "no-cache" ); 
response.addHeader( "Cache-Control", "must-revalidate" ); 
response.addHeader( "Cache-Control", "no-cache" ); 
response.addHeader( "Cache-Control", "no-store" ); 
response.setDateHeader("Expires", 0);
 

Caching disabled in this C# Handler example which forces all proxy caches to revalidate, prevents browser from caching and expires the response:

context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); 
context.Response.Cache.SetCacheability(HttpCacheability.NoCache); 
context.Response.Cache.SetNoStore(); 
context.Response.Cache.SetNoServerCaching(); 
context.Response.Cache.SetExpires(DateTime.Now); 
 

Alternatively, it is possible to force the XMLHttpRequest object to retrieve the content anyway, as shown in this example.

req.open( "GET", "xmlprovider.php" ); 
req.setRequestHeader( "If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT" ); 
//This line doesn't work. 
req.send( null ); 

Another method is to add a random string on the end of the url in the query:

req.open( "GET", "xmlprovider.php?sid=" + Math.random());
 

This will ensure that the browser always gets a fresh copy.

It is important to note that these techniques should be used only if the caching is inappropriate. If such methods are used indiscriminately, poor performance with the application may result. Another better way around that may be sending an expired date/time header (or other relevant headers) to the client to gain benefits from caching while letting the client know that new data may be available, as generally it is better to obtain the performance benefits from caching to reduce processing time and bandwidth consumption.


대부분 간단히 요청주소에 현재 시간을 덧붙여서 전송합니다.
'action.php?t=1144760075' 이런식으로요.. 주소가 달라지니 캐시도 사용되지 않겠죠.

req.open("GET", "xmlprovider.php?hash=" + Math.random());
위와 같이 캐쉬를 하지않게 호출할때 마다 요청에 유일한 쿼리스트링을 포함함돠

서버 프로그램 개발시에 운영체제의 메모리,스레드,파일 디스크립터, 프로세스 최대 지원 개수를 점검하는 간단한 방법을 소개한다.

일반적으로

항목 원도우 Linux
메모리 4G Byte 2.0.x : 1G Byte
2.2.x : 2G Byte
2.4.x : 64G Byte
최대 스레드 제한없음 기본설정 : 1024
변경가능
파일 디스크립트 제한없음 기본설정 : 1024
변경가능

1. checkpf.jar 다운로드

2. 메모리 점검

  • Usage

    [www@ihelpers dist]$ java -cp ./checkpf.jar checkpf.MemConsumer 128
    Allocated : 12
    Max : 63
    Total : 14
    Left : 2
    Allocated : 24
    Max : 63
    Total : 25
    Left : 1
    Allocated : 36
    Max : 63
    Total : 43
    Left : 7
    Allocated : 48
    Max : 63
    Total : 63
    Left : 15
    Allocated : 60
    Max : 63
    Total : 63
    Left : 3
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
            at checkpf.MemConsumer.main(MemConsumer.java:34)
    [www@ihelpers dist]$ java -cp ./checkpf.jar checkpf.MemConsumer 64

    128MB 짜리 배열을 생성하면 에러가 발생한다. 아래와 같이 최대 메모리를 128MB로 잡아주면 에러 없이 생행이 될수 있습니다. 자바 Virtual Machine 의 메모리를 너무 크게 잡게 되면 디스크 스와핑과 Garagage Collection 이 많이 발생하여 서버의 성능이 현저하게 떨어지게 됩니다. 자바 VM 최대 메모리는 물리적 메모리 보다 작게 하고 프로그램에서는 VM 최대 메모리의 약 70% 이하로 사용하는 것이 좋습니다.

    [www@ihelpers dist]$ java -Xmx128m -cp ./checkpf.jar checkpf.MemConsumer 128
    Allocated : 12
    Max : 127
    Total : 14
    Left : 2
    Allocated : 24
    Max : 127
    Total : 25
    Left : 1
    Allocated : 36
    Max : 127
    Total : 43
    Left : 7
    Allocated : 48
    Max : 127
    Total : 75
    Left : 27
    Allocated : 60
    Max : 127
    Total : 75
    Left : 15

  • java options

[www@ihelpers dist]$ java -X
    -Xmixed           mixed mode execution (default)
    -Xint             interpreted mode execution only
    -Xbootclasspath:<directories and zip/jar files separated by :>
                      set search path for bootstrap classes and resources
    -Xbootclasspath/a:<directories and zip/jar files separated by :>
                      append to end of bootstrap class path
    -Xbootclasspath/p:<directories and zip/jar files separated by :>
                      prepend in front of bootstrap class path
    -Xnoclassgc       disable class garbage collection
    -Xincgc           enable incremental garbage collection
    -Xloggc:<file>    log GC status to a file with time stamps
    -Xbatch           disable background compilation
    -Xms<size>        set initial Java heap size
    -Xmx<size>        set maximum Java heap size
    -Xss<size>        set java thread stack size
    -Xprof            output cpu profiling data
    -Xfuture          enable strictest checks, anticipating future default
    -Xrs              reduce use of OS signals by Java/VM (see documentation)
    -Xcheck:jni       perform additional checks for JNI functions
    -Xshare:off       do not attempt to use shared class data
    -Xshare:auto      use shared class data if possible (default)
    -Xshare:on        require using shared class data, otherwise fail.

3. 파일 디스크립터

  • Usage

    [www@ihelpers dist]$ java -cp ./checkpf.jar checkpf.FDConsumer 2000
    ...

    File Descriptor : 1012
    File Descriptor : 1013
    File Descriptor : 1014
    File Descriptor : 1015
    java.io.IOException: Too many open files


    이에 대한 설정 변경 내용은 참고를 참조해 주십시요.

3. 참고

http://blog.naver.com/galad/140033728088

Fancy URL 사용을 위한 준비 (Tomcat + Spring + urlRewrite Filter)
PRIVATE/- Developer's Diary | 2006/10/26 01:03

 

펌.: http://steelheart.tistory.com/32

 

더 좋은 방법이 있는지는 모르겠다.
일단 삽질한 거니 기록으로 남겨둔다.

아무튼... 먼저 Fancy URL 이란?

/board.html?action=list&page=2 이런 일반적인 URL 형식 대신
/board/list/2 이런 형식의 확장자가 없고 쿼리 스트링이 없는 간단한 URL을 말한다.
(fancy url의 정확한 뜻은 모르지만 나는 이렇게 이해하고 있다)

태터툴즈의 /tt/tags/태터툴즈 (태터툴즈란 태그가 있는 글목록을 출력하는 url) 이런걸 생각하면 된다.



가장 간단한 방법은 Apache의 mod_rewrite 모듈을 사용하면 된다.
하지만 이 경우는 아파치가 없거나 있어도 mod_rewrite 모듈 사용이 불가능한 걸 가정했다.

확장자가 없는 매핑이라도 특별한 패턴이 있으면 간단하게 할 수 있다. 예를 들면

/board/list
/board/view
/board/write

이런 매핑을 처리하려면 다음처럼 하면 된다.

   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>/board/*</url-pattern>
   </servlet-mapping>

하지만

/list
/view
/write

이런걸 매핑하려면?

   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>/list</url-pattern>
   </servlet-mapping>
   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>/view</url-pattern>
   </servlet-mapping>
   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>/write</url-pattern>
   </servlet-mapping>

특별한 패턴이 없는 url이기 때문에 전부다 매핑해 줘야 한다.
이건 세개뿐이니까 그렇지 매번 늘어날 때마다 매핑해 줄수도 없다.
특히 어떤 url로 요청이 들어올지 미리 알 수 없는 경우는 난감하다.
예를 들면 /(username) 했을 경우 해당 사용자의 정보를 보여준다든지 하는 것


아무튼 글의 요점은 확장자가 없는 모든 요청을 dispatcher가 받도록 하고 싶다는 거다.
내가 알기로는... 현재의 servlet-mapping은... 아래와 같은 식이나

   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>/*</url-pattern> <!-- 모든 요청을 처리하되 -->
      <url-pattern-exclude>*.*</url-pattern-exclude> <!-- 확장자 '.' 이 들어가면 안돼 -->
  </servlet-mapping>

또는 아래와 같은 형식으로는...

   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>^/([^.]*)$</url-pattern> <!-- . 이 포함되지 않은 패턴 -->
  </servlet-mapping>

이렇게는 지원이 안되기 때문에;;; 조금 편법을 써야 했다.


일단 매핑은 아래처럼...

   <servlet-mapping>
       <servlet-name>lucy</servlet-name>
       <url-pattern>/app/*</url-pattern>
  </servlet-mapping>


이렇게 해당 /app/로 시작하는 모든 요청에 대해 처리하도록 한다.


근데 이것도 맘에 안 드는게 확장자 없이 매핑하는건 되지만
url을 항상 /app/로 요청해야 한다.
그다지 의미있는 url은 아닌데 항상 따라다니는 것도 보기 안좋고...


appfuse에서 발견한... urlrewrite filter를 사용한다.
(appfuse에서는 이런 목적으로 쓰진 않았지만...)


https://urlrewrite.dev.java.net/ 에서 urlrewrite filter 3.0 을 받아 lib 폴더에 넣는다.

/WEB-INF/urlrewrite.xml 을 작성


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN" "http://tuckey.org/res/dtds/urlrewrite3.0.dtd">

<!-- https://urlrewrite.dev.java.net/manual/3.0 -->

<urlrewrite>

   <rule>
       <note>
           확장자가 없는 요청은 /app/* 요청이다.
           ex) /user/register -> /app/user/register
       </note>
       <from>^/([^.]*)$</from>
       <to type="forward">/app/$1</to>
   </rule>

</urlrewrite>



web.xml 에 url rewrite filter 등록


   <filter>
       <filter-name>urlRewriteFilter</filter-name>
       <filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
       <init-param>
           <param-name>logLevel</param-name>
           <param-value>commons</param-value>
       </init-param>
   </filter>


   <filter-mapping>
       <filter-name>urlRewriteFilter</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>


Spring Context XML 파일에서...

   <bean id="userMappings" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
       <property name="mappings">
           <map>
               <entry key="/user/register" value-ref="userRegisterController"/>
               <entry key="/user/login" value-ref="userLoginController"/>
           </map>
       </property>


앞에 /app를 붙여 주지 않아도 된다... 물론 브라우저에서 요청시에도 /app를 안 붙여도 되고...

http://blog.naver.com/galad/140033727453

출처 카페 > Team Four / 카탈
원본 http://cafe.naver.com/teamfour/109
 
정말 자바 웹 프로그래머가 알고있어야 할 기본입니다.
굉장히 방대한 분량이 간략히 정리되어있습니다.
 
=================================================================
 
마소 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 참조 #

'프로그래밍 > 읽을거리' 카테고리의 다른 글

[정리] 소셜 플랫폼  (0) 2010.10.05
[펌] SOAP란 무엇인가??  (0) 2009.02.18
[펌] OLAP & DataWarehouse  (0) 2008.11.26
[펌] AOP 개념 (한빛미디어 - 김대곤)  (0) 2007.11.28