잊지 않겠습니다.

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


View는 다양한 방법으로 표현이 가능합니다. 지금까지 우리는 Html로만 표현되는 View를 알아봤습니다. 그럼 다른 Type의 View는 어떤 것들이 있을까요?

먼저, 가장 자주 쓰이는 Excel과 pdf가 있을수 있습니다. 그리고, json 형태로 표현되어서 다른 이기종간의 데이터 교환으로 사용될 수 있는 json format이 있을 수 있습니다.
마지막으로 View에서 넘길수 있는 특이한 형태로서, 파일을 upload를 알아보도록 하겠습니다. 

1. Excel

excel 파일을 생성하기 위해서는 apache poi jar가 필요합니다. apache poi는 microsoft office 파일을 다루기 위한 java open source library입니다. 
poi에서 다룰 수 있는 파일 종류는 다음과 같습니다. 

# Excel 
# Word
# PowerPoint
# Visio
# Outlook data file
# MS Publisher

2001년부터 시작된 오래된 java project 중 하나입니다. 참고 사이트는 다음과 같습니다. http://poi.apache.org/
poi를 사용하기 위해서 pom.xml에 다음 항목을 추가합니다. 

    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi</artifactId>
      <version>3.9</version>
    </dependency>
    <dependency>
      <groupId>net.sourceforge.jexcelapi</groupId>
      <artifactId>jxl</artifactId>
      <version>2.6.12</version>
    </dependency>

Excel 파일을 다루기 위해서는 org.springframework.web.servlet.view.document.AbstractJExcelView 를 상속받는 View를 구성해야지 됩니다. 이 View는 poi를 기반으로 구성이 되어 있으며, 다음 1개의 method만 재 정의하면 excel 파일을 작성할 수 있습니다.  

Spring에서는 Excel에 대해서 2개의 Abstract class를 제공하고 있습니다. AbstractJExcelView와 AbstractExcelView가 바로 그것입니다. 이 둘의 차이는 API의 사용 유무에 따라 다릅니다. JExcelView는 jexcelapi를 기반으로 구성되어 있으며, AbstractExcelView는 poi를 기반으로 구성되어 있습니다. 어느것이나 사용해도 좋지만, 좀 더 사용하기 편한 jexcelapi를 이용해보도록 하겠습니다.


    /**
     * Subclasses must implement this method to create an Excel Workbook
     * document, given the model.
     * @param model the model Map
     * @param workbook the Excel workbook to complete
     * @param request in case we need locale etc. Shouldn't look at attributes.
     * @param response in case we need to set cookies. Shouldn't write to it.
     * @throws Exception in case of failure
     */
    protected abstract void buildExcelDocument(Map<String, Object> model, WritableWorkbook workbook,
            HttpServletRequest request, HttpServletResponse response) throws Exception;

다음은 excel 파일을 만드는 View 코드입니다. 
public class BookExcelView extends AbstractJExcelView {
    @Override
    protected void buildExcelDocument(Map<String, Object> model, WritableWorkbook workbook, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        String fileName = createFileName();
        setFileNameToResponse(request, response, fileName);

        WritableSheet sheet = workbook.createSheet("책목록", 0);

        sheet.addCell(new Label(0, 0, "제목"));
        sheet.addCell(new Label(1, 0, "저자"));
        sheet.addCell(new Label(2, 0, "설명"));
        sheet.addCell(new Label(3, 0, "출판일"));

        @SuppressWarnings("unchecked")
        List<Book> books = (List<Book>) model.get("books");

        int row = 1;
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        for (Book book : books) {
            sheet.addCell(new Label(0, row, book.getTitle()));
            sheet.addCell(new Label(1, row, book.getAuthor()));
            sheet.addCell(new Label(2, row, book.getComment()));
            sheet.addCell(new Label(3, row, dateFormat.format(book.getPublishDate())));
            row++;
        }
    }

    private void setFileNameToResponse(HttpServletRequest request, HttpServletResponse response, String fileName) {
        String userAgent = request.getHeader("User-Agent");
        if (userAgent.indexOf("MSIE 5.5") >= 0) {
            response.setContentType("doesn/matter");
            response.setHeader("Content-Disposition", "filename=\"" + fileName + "\"");
        } else {
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        }
    }

    private String createFileName() {
        SimpleDateFormat fileFormat = new SimpleDateFormat("yyyyMMdd");
        return new StringBuilder("책리스트").append("-").append(fileFormat.format(new Date())).append(".xls").toString();
    }
}

그리고 작성된 View는 생성의 비용을 줄이기 위해서 applicationContext에 다음과 같이 등록할 수 있습니다. 

<bean id="bookExcelView" class="com.xyzlast.mvc.bookstore.view.BookExcelView"/>

사용하는 Controller 코드는 다음과 같습니다. 
    @RequestMapping(value = "exceldownload", method=RequestMethod.GET)
    public ModelAndView downloadExcel() {
        List<Book> books = bookService.getAll();
        ModelAndView mv = new ModelAndView();
        mv.addObject("books", books);
        mv.setView(bookExcelView);
        return mv;
    }

Controller를 통해 다운 받은 Excel 파일은 다음과 같습니다. 



2. pdf - iText 2.x ~ 4.x

pdf 파일 포멧은 문서 포멧으로 가장 각광받는 포멧입니다. OS platform에 중립적인 문서 포멧으로 각광을 받고 있습니다. pdf 파일을 만들기 위해서는 iText가 필요합니다. 기본적으로 Spring은 iText 2.x대를 기반으로 구성이 되어있습니다. iText의 내용은 너무나 방대하기 때문에, http://itextpdf.com/book/ 를 참고해주시길 바랍니다. 
기본적으로 org.springframework.web.servlet.view.document.AbstractPdfView 객체를 상속받아서 pdf의 내용을 구현하는 것을 기본으로 하고 있습니다. 먼저, iText를 사용하기 위해서 다음 항목을 pom.xml에 추가합니다. 

<dependency>
    <groupId>com.lowagie</groupId>
    <artifactId>itext</artifactId>
    <version>2.1.7</version>
</dependency>


그리고, Pdf를 만들기 위한 View를 추가합니다. 
public class BookPdfView2 extends AbstractPdfView {

    @Override
    protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        String fileName = createFileName();
        setFileNameToResponse(request, response, fileName);

        Chapter chapter = new Chapter(new Paragraph("this is english"), 1);
        chapter.add(new Paragraph("이건 메세지입니다."));
        document.add(chapter);
    }

    private void setFileNameToResponse(HttpServletRequest request, HttpServletResponse response, String fileName) {
        String userAgent = request.getHeader("User-Agent");
        if (userAgent.indexOf("MSIE 5.5") >= 0) {
            response.setContentType("doesn/matter");
            response.setHeader("Content-Disposition", "filename=\"" + fileName + "\"");
        } else {
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        }
    }

    private String createFileName() {
        SimpleDateFormat fileFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        return new StringBuilder("설문조사").append("-").append(fileFormat.format(new Date())).append(".pdf").toString();
    }
}

buildPdfDocument를 override 시켜서 문서를 만드는것을 확인해주세요. Controller에서 사용하는 방법은 Excel과 동일하기 때문에 생략하도록 하겠습니다. 꼭 한번 해주시길 바랍니다. 이 코드에는 심각한 버그가 하나 있습니다. ^^


3. pdf - iText 5.x


위에 소개드린것처럼 기본적으로 Spring은 2.x대의 iText를 기반으로 구성되어 있습니다. 그렇지만, iText 5.x대를 사용하도록 View 인터페이스를 구현하면 iText 5.x대의 버젼을 사용할 수 있습니다. iText 5.x를 이용한 새로운 View를 만들어보도록 하겠습니다. 
먼저, iText 5.x를 추가하기 위해서 pom.xml에 다음 설정을 추가합니다. 

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.4.0</version>
</dependency>


iText가 버젼업이 되면서 가장 문제가 되는 것은 package 명이 변경되었다는 점입니다. 사용되는 객체들의 이름은 변경된 것이 없지만, 전체 package 명이 바뀌면서 기존의 AbstractPdfView를 사용할 수가 없게 되어버렸습니다. 새로운 View를 만들어서 iText5를 지원할 수 있도록 해봅시다. 
View를 새로 만들어줄 때는 Spring에서 제공하는 AbstractView를 상속받아서 처리하는 것이 일반적입니다. IText5를 지원하기 위해서, AbstractIText5PdfView 객체를 생성했습니다. 
override된 다음 method들을 살펴볼 필요가 있습니다. 

    protected boolean generatesDownloadContent();
    protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
    protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException;

# generatesDownloadContents
Download가 이루어질 View인지, 아니면 Html과 같이 Rendering될 View인지를 결정하는 method입니다. true/false로 설정하면됩니다.

# renderMergedOutputModel
실질적으로 View가 생성되는 코드입니다. 여기에서는 iText를 이용한 pdf document를 생성하는 코드가 위치하게 됩니다.

# writeToResponse
HttpResponseServlet에 output을 보내는 코드입니다. 

만들어진 AbstractIText5PdfView의 전체 코드는 다음과 같습니다.

public
abstract class AbstractIText5PdfView extends AbstractView { public AbstractIText5PdfView() { setContentType("application/pdf"); } @Override protected boolean generatesDownloadContent() { return true; } @Override protected final void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ByteArrayOutputStream baos = createTemporaryOutputStream(); Document document = newDocument(); PdfWriter writer = newWriter(document, baos); prepareWriter(model, writer, request); buildPdfMetadata(model, document, request); document.open(); buildPdfDocument(model, document, writer, request, response); document.close(); writeToResponse(response, baos); } @Override protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException { response.setContentType(getContentType()); ServletOutputStream out = response.getOutputStream(); baos.writeTo(out); out.flush(); } protected Document newDocument() { return new Document(PageSize.A4); } protected PdfWriter newWriter(Document document, OutputStream os) throws DocumentException { return PdfWriter.getInstance(document, os); } protected void prepareWriter(Map<String, Object> model, PdfWriter writer, HttpServletRequest request) throws DocumentException { writer.setViewerPreferences(getViewerPreferences()); } protected int getViewerPreferences() { return PdfWriter.ALLOW_PRINTING | PdfWriter.PageLayoutSinglePage; } //Document가 open되기 전, pdf의 meta 정보를 넣을 때 사용 protected abstract void buildPdfMetadata(Map<String, Object> model, Document document, HttpServletRequest request) throws Exception; //pdf의 내용을 추가하는데 이용 protected abstract void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response) throws Exception; }


여기에서 중요한 method는 buildPdfMetaData와 buildPdfDocument method입니다. 

# buildPdfMetadata
Pdf 문서의 정보를 지정할 수 있는 method입니다. 이때는 pdf document가 아직 open이 되지 않은 상태이기 때문에, 문서에 내용을 추가하거나 만드는 것이 아닌 문서 자체의 정보를 기록하게 됩니다. 이 method에서 할 수 있는 중요한 일은 암호화와 권한을 설정할 수 있습니다. 이 부분에 대해서는 watermark를 지원하는 pdf 또는 pdf security 부분을 참조해주시면 될 것 같습니다. 

# buildPdfDocument
Pdf 문서를 작성하는 method입니다. pdf document를 작성하는 실질적인 method입니다. IText 2.x를 지원하는 View와 동일한 코드를 작성해주면 됩니다.

아래는 만들어진 AbstractIText5PdfView를 상속받아서 구현된 BookPdfView입니다. 

public class BookPdfView extends AbstractIText5PdfView {
    @Override
    protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        String fileName = createFileName();
        setFileNameToResponse(request, response, fileName);
        Chapter chapter = new Chapter(new Paragraph("this is english"), 1);
        chapter.add(new Paragraph("이건 메세지입니다."));
        document.add(chapter);
    }

    private void setFileNameToResponse(HttpServletRequest request, HttpServletResponse response, String fileName) {
        String userAgent = request.getHeader("User-Agent");
        if (userAgent.indexOf("MSIE 5.5") >= 0) {
            response.setContentType("doesn/matter");
            response.setHeader("Content-Disposition", "filename=\"" + fileName + "\"");
        } else {
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        }
    }

    private String createFileName() {
        SimpleDateFormat fileFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
        return new StringBuilder("pdf").append("-").append(fileFormat.format(new Date())).append(".pdf").toString();
    }

    @Override
    protected void buildPdfMetadata(Map<String, Object> model, Document document, HttpServletRequest request) throws Exception {
        //Meta data 처리는 하지 않습니다.
    }
}

만들어서 사용해보면 이상한점을 발견할 수 있습니다. 영어 메세지는 표시가 되지만, 한글 메세지는 표시가 되지 않습니다. 그건 기본적으로 pdf가 영문 font만을 사용하도록 문서가 구성이 되어있기 때문입니다. pdf에서 영문 이외의 한글/한문/일어 를 보기 위해서는 itext-asian이라는 library를 추가로 사용해야지 됩니다. 

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-asian</artifactId>
    <version>5.2.0</version>
</dependency>

library를 추가를 하고, BookPdfView의 buildPdfDocument를 다음과 같이 수정하도록 합니다. 
    @Override
    protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        String fileName = createFileName();
        setFileNameToResponse(request, response, fileName);
        BaseFont cfont = BaseFont.createFont("HYGoThic-Medium", "UniKS-UCS2-H", BaseFont.NOT_EMBEDDED);
        Font objFont = new Font(cfont, 12);

        Chapter chapter = new Chapter(new Paragraph("this is english"), 1);
        chapter.add(new Paragraph("이건 메세지입니다.", objFont));
        document.add(chapter);
    }

한글 font를 지정하고, 그 Font를 사용하는 것을 볼 수 있습니다. 실질적으로 itext-asian은 java code가 하나도 없는 jar 파일입니다. 내부는 Font에 대한 meta data들만 추가되어 있는 상태에서 그 Font를 사용하도록 설정을 조금 변경시킨 것 밖에는 없습니다. 
다음은 만들어진 pdf 파일입니다. 




4. Json

json은 web api의 데이터 Protocol로 주로 사용됩니다. HTML과 atom은 xml을 base로 한 문서용 마크업언어입니다. 그런데, 이 형태는 데이터를 기술하기에는 표기가 너무나 중복이 됩니다. 그래서, 좀 더 단순한 데이터의 포멧이 제안되었고, 그 중에서 가장 각광받고, 자주 사용되고 있는 것이 json format입니다. 
지금 Book 객체에 대한 json 은 다음과 같이 표현됩니다. 

{
  • id335,
  • title"Changed Title > Title : 8",
  • authornull,
  • comment"Changed Comment : Comment : 8",
  • publishDate1362641616000,
  • createDate1362641616000,
  • updateDate1362641616000,
  • status"MISSING",
  • imageUrlnull,
  • rentUser:  {
    • id329,
    • loginId"User Id 8",
    • name"Name 8",
    • password"Password 8",
    • checkSum3069349407,
    • lastLoginTime1362641616000,
    • joinTime1362641616000,
    • pointnull,
    • levelnull,
    • histories: [ ]
    }
},

xml보다 간단한 표시 방법에, 다양한 데이터를 표현할 수 있어서 자주 사용되는 데이터 포멧입니다. 기술적으로는 다음과 같은 장점을 갖습니다. 

1) 데이터의 사이즈가 xml보다 작기 때문에 network 부하가 작습니다.
2) javascript에서 처리가 매우 간단합니다. 
3) 사용하기 쉽고, 직관적입니다. 

java에서 json data format으로 객체를 변경시키는 library는 jackson, gson 등 다양한 library 들이 존재합니다. spring에서는 jackson을 json default converter로 이용합니다. jackson에 대한 jar 설정을 pom.xml에 추가합니다. 

    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.1.4</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.1.4</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.1.4</version>
    </dependency>

또한, messageConvert에 jackson을 사용하도록 spring의 request adapter를 수정해야지 됩니다. 

그리고, controller에 book list를 jackson으로 얻어내는 method를 추가하도록 하겠습니다. 기본적으로 categories, histories는 json으로 얻어낼 필요가 없다고 생각되어서 이 두개의 property를 제외했습니다. 또한 user역시 histories를 얻을 필요가 없어서 property를 제거하도록 하겠습니다. jackson으로 변환되지 않기를 원하면 @JsonIgnore annotaion을 붙여 사용하면 됩니다. 변경된 Book 입니다.

@Entity
@Table(name="books")
public class Book {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private long id;
    @Column(name="title", length=255)
    private String title;
    @Column(name="author", length=255)
    private String author;
    @Column(name="comment", length=255)
    private String comment;
    @Column(name="publishDate")
    private Date publishDate;
    @Column(name="createDate")
    private Date createDate;
    @Column(name="updateDate")
    private Date updateDate;
    @Column(name="status")
    @Enumerated(EnumType.ORDINAL)
    private BookStatus status;
    @Column(name="imageUrl")
    private String imageUrl;
    @ManyToOne
    @JoinColumn(name="rentUserId", nullable=true)
    private User rentUser;
    @JsonIgnore
    @ManyToMany
    @JoinTable(name="books_categories",
               joinColumns = {@JoinColumn(name="bookId")},
               inverseJoinColumns = {@JoinColumn(name="categoryId")}
            )
    private Set<Category> categories = new HashSet<>();
    @JsonIgnore
    @OneToMany(mappedBy="book")
    private Set<History> histories = new HashSet<>();;

그리고, Controller에 다음 method를 추가합니다. 

    @RequestMapping(value="list/json", method=RequestMethod.GET)
    @ResponseBody
    public List<Book> listup() {
        return bookService.getAll();
    }

마지막으로 테스트코드를 통해서 json output이 정상적으로 되는지 확인해보도록  합니다.

    @Test
    public void listupJson() throws Exception {
        MvcResult result = mock.perform(get("/book/list/json")).andExpect(status().isOk()).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }

그리고 위와 같이 Entity를 바로 넘기게 되는 순환 참조 오류를 해결하기 위해서 다른 DTO를 생성해서 그 DTO를 넘겨주기도 합니다. 간단히 Map 객체를 만들어서 넘기는 Controller 코드는 다음과 같습니다.

    @RequestMapping(value = "book/json")
    @ResponseBody
    public Object convertToJson() {
        List<Book> books = bookService.listup();
        List<Map<String, Object>> maps = new ArrayList<>();

        for (Book book : books) {
            Map<String, Object> item = new HashMap<>();
            item.put("title", book.getTitle());
            item.put("comment", book.getComment());
            maps.add(item);
        }
        return maps;
    }

Controller에서 @ResponseBody를 처음 사용해봤습니다. @ResponseBody는 따로 View를 갖지 않고, html body 자체에 output을 적는 방법입니다. web api를 사용하는 경우에는 대부분 이러한 방법으로 output을 처리합니다. json에 대해서는 자주 사용되기 때문에 꼭 사용법을 익혀두시길 바랍니다.

5. File upload

File upload는 View가 아닙니다. 이번 장에서 설명하는 영역으로 약간 부적절한 면이 있긴 하지만 이 부분은 Controller 영역입니다. Spring에서 제공하는 HttpRequestServlet이 보내는 Multipart file upload 데이터를 Handling하는 방법을 소개하도록 하겠습니다. Multipart file upload를 처리하기 위해서는 다음 jar들이 필요합니다. 

    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.2.2</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.4</version>
    </dependency>

먼저, file을 upload하기 위해서는 반드시 다음과 같은 설정이 applicationContext.xml에 위치해야지 됩니다. 

  <bean id="multipartResolver"
    class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="2000000" />
  </bean>
  <bean id="uploadDirResource" class="org.springframework.core.io.FileSystemResource">
    <constructor-arg>
      <value>C:/upload/</value>
    </constructor-arg>
  </bean>

위 설정은 매우 민감한 설정입니다. bean의 id도 무조건 multipartResolver라는 이름으로 지정이 되어야지 되며, uploadDirResource 역시 bean의 id가 고정되어야지 됩니다. 이 id를 바꿔주는 것은 불가능합니다. 각 bean들은 file upload의 max size와 저장될 위치를 결정하게 됩니다. 반드시 bean id를 위 설정과 동일하게 설정해야지 된다는 것을 잊지 말아주세요. 

upload 할 데이터에 대한 dto를 만들어줍니다. 이 dto 객체는 Request에서 오는 http 데이터를 Controller로 전달하는 역활을 담당하게 됩니다. 

public class UploadItem {
    private String name;

    private CommonsMultipartFile fileData;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public CommonsMultipartFile getFileData() {
        return fileData;
    }
    public void setFileData(CommonsMultipartFile fileData) {
        this.fileData = fileData;
    }
}

CommonsMultipartFile 객체를 property로 갖는 것을 확인해보실 수 있습니다. 이제 이 객체와 mapping되는 Html 을 구성하도록 하겠습니다. 
기본적으로 Spring @MVC에서 form을 통한 데이터 전달을 할때는 form안에 위치한 input tag의 이름과 property의 이름간에 1:1로 mapping이 되게 됩니다. UploadItem DTO는 아래와 같은 Html과 mapping이 될 수 있습니다. file upload를 하기 위해서 enctype이 multipart/form-data로 되어있는 것을 확인하시길 바랍니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><META http-equiv="Content-Type" content="text/html;charset=UTF-8"><title>Upload Example</title></head><body>
    <form name="uploadItem" method="post" enctype="multipart/form-data">
        <fieldset>
            <legend>Upload Fields</legend>
            <p>
                <label for="name-id">Name</label>
                <input type="text" id="name-id" name="name" />
            </p>
            <p>
                <label for="fileData-id">파일 업로드</label>
                <input type="file" id="fileData-id" name="fileData" type="file" />
            </p>
            <p>
                <input type="submit" />
            </p>
        </fieldset>
    </form></body></html>



이제 이 form data를 받아올 Controller code를 알아보도록 하겠습니다. 지금까지 데이터를 form이나 url을 통해서 언제나 int, String 형태로만 받아오던 데이터를 이제는 DTO를 통해서 받아보도록 하겠습니다. 

    @RequestMapping(value="index", method=RequestMethod.POST)
    public String uploadCompleted(UploadItem uploadItem, BindingResult result) {
        if (result.hasErrors()) {
            for (ObjectError error : result.getAllErrors()) {
                System.err.println("Error: " + error.getCode() + " - " + error.getDefaultMessage());
            }
            return "upload/uploadForm";
        }

        if (!uploadItem.getFileData().isEmpty()) {
            String filename = uploadItem.getFileData().getOriginalFilename();
            String imgExt = filename.substring(filename.lastIndexOf(".") + 1, filename.length());

            // upload 가능한 파일 타입 지정
            if (imgExt.equalsIgnoreCase("JPG") || imgExt.equalsIgnoreCase("JPEG") || imgExt.equalsIgnoreCase("GIF")) {
                byte[] bytes = uploadItem.getFileData().getBytes();
                try {
                    File outFileName = new File(fsResource.getPath() + "_" + filename);
                    FileOutputStream fileoutputStream = new FileOutputStream(outFileName);
                    fileoutputStream.write(bytes);
                    fileoutputStream.close();
                } catch (IOException ie) {
                    System.err.println("File writing error! ");
                }
                System.err.println("File upload success! ");
            } else {
                System.err.println("File type error! ");
            }
        }

받아온 데이터를 이용해서 file을 저장할 수 잇는 것을 알 수 있습니다. 그런데, 지금까지보던 Controller의 input값과는 조금 다른 값이 보입니다. BindingResult가 그 결과인데요. BindingResult는 Request에서 보내지는 응답을 DTO로 변환하게 될 때, 변환과정에서 에러가 발생하는지를 확인하는 객체입니다. 그리고 Request가 DTO로 변환하는 과정을 Binding이라고 합니다. 


Summay

지금까지 5개의 Controller Action에 대해서 알아봤습니다. 

# excel
# pdf (iText 2.x)
# pdf (iText 5.x) - AbstractView를 이용한 신규 View의 생성
# json
# file upload

이 부분에 대해서 직접 돌아가는 web page를 한번 만들어보시길 바랍니다. 그리고 그 동작이 어떻게 일어나는지 확인하는 것이 이쪽까지의 과제입니다. 
다음은 오늘 잠시 나왔던 Binding에 대해서 좀 더 알아보도록 하겠습니다.


Posted by Y2K
,

23. View의 표현법 - HTML

Java 2013. 9. 13. 08:33

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


크게 View는 Html을 표시시키는 View와 Application을 이용할 때 사용되는 View. 두개로 나눌수 있습니다. 이번 장에서는 Html을 표시하는 View를 알아보도록 하겠습니다.

1. JSP & JSTL

jsp와 jstl을 사용하기 위해서는 특별한 View 설정은 필요없지만, 기본적으로 Spring에서 요구되는 View는 WEB-INF 폴더 안에 view file들을 위치시키고, client에서 직접 접근할 수 없도록 하는 것이 일반적입니다. 이때는 기본적으로 InternalResourceViewResolver를 사용해서 View 파일의 위치를 설정하도록 합니다.

  <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    <property name="prefix" value="/WEB-INF/views/" />
    <property name="suffix" value=".jsp" />
    <property name="contentType" value="text/html; charset=UTF-8" />
    <property name="order" value="1" />
  </bean>

code base로는 다음과 같이 설정하면 됩니다. 
    @Bean
    public UrlBasedViewResolver viewResolver() {
        UrlBasedViewResolver jspViewResolver = new UrlBasedViewResolver();
        jspViewResolver.setOrder(4);
        jspViewResolver.setPrefix("/WEB-INF/view/");
        jspViewResolver.setSuffix(".jsp");
        jspViewResolver.setContentType("text/html; charset=UTF-8");
        jspViewResolver.setViewClass(JstlView.class);
        return jspViewResolver;
    }

jsp만으로는 제어문을 구성할 수 없기 때문에 jstl을 이용해서 jsp를 확장해야지 됩니다. jstl을 사용하기 위해서는 pom.xml에 다음 항목을 추가해주세요.

        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

JSTL은 다양한 tag를 지원하고 있습니다. 이는 기존 jsp에서 <%= %>에서 빈번하게 만들어지는 code들을 단순화하고 읽기 편한 간단한 언어로 만들기 위해서 제공되고 있습니다.  jsp는 다음과 같은 tag를 선언함으로서 사용할 수 있습니다. 

<%@taglib prefix="c"uri="http://java.sun.com/jsp/jstl/core"%>

1) c:out  - 객체를 출력한다.
<!--기본 문법--><c:out value="${name}"/><!--  default 속성 : 값이 없을때 기본출력값을 정의-->
<c:out value="${age}" default="Null or empty"/> 
<!-- escapeXml 속성 : 기본적으로 XML 문자를 escape하는데 Escape 하지 않으려면  false로 설정-->
<c:out value="${name}" escapeXml="false"/>

2) c:set  - 객체를 저장(셋팅)한다.
<!--기본문법-->
<c:set var="name" value="홍길동" /><!--Scope 속성 : page | request | session | application 중 1개의 값 page가 기본값-->
<c:set scope="request" target="book" property="isbn" value="300"/><!--target,property속성:  홍길동의 값을 UserModel 객체의 UserName 프로퍼티값을 설정 -->
<c:set value="홍길동" target="UserModel" property="UserName"/>

3) c:remove - 객체를 삭제한다.
<!--기본문법-->  
<c:remove  var="name" scope="request" />  

4) c:if - 조건문
<!--기본문법-->  
<c:if test="${조건}">  
    조건 만족시 이 영역을 수행  
</c:if>  

jstl에서 if문은 조금 문제가 있습니다. 그건 else가 존재하지 않는 문제인데요. 우리가 주로 사용하는 if~else 문을 갖추기 위해서는 아래의 choose문을 사용해야지 됩니다.

5) c:choose, c:when, c:otherwise - switch 문이라 생각하면된다.
<!--기본문법-->   
<c:choose>  
    <c:when test="${value == 1}">  
        value가 1이면 이 영역을 수행  
    </c:when>  
    <c:when test="${value == 2}">  
        value가 2이면 이 영역을 수행  
    </c:when>  
    <c:otherwise>  
        value가 1,2가 아니면 이 영역을 수행(기본값)  
    </c:otherwise>  
</c:choose>  

6) c:foreach - 반복문
<!--기본문법(0~9까지 출력)-->   
<c:forEach begin="0" end="9" var="i">  
    <c:out value="${i}"/>  
</c:forEach>  
<!-- step 속성 : 정의된 수만큼 증가를 시킨다.(1,3,5,7,9출력)-->  
<c:forEach var="test" begin="1" end="10" step="2" >  
     <b>${test }</b>   
</c:forEach>  

7) c:forTokens - 구분자로 반복문
<!--기본문법(변수를 ','로 구분하여 출력한다. )-->   
<c:forTokens var="alphabet" items="a,b,c,d,e,f,g,h,i,j,k" delims="," varStatus="idx" >  
     <b>${alphabet }</b>  
</c:forTokens>  


8) c:url, c:param  - URL을 처리
<!--기본문법-->   
<c:url value="index.jsp"/>  
<!-- value의 속성값이 /로 시작하면 컨텍스트를 포함한다-->  
<!--Context가 Root이라면 /Root/index.jsp로 출력-->  
<c:url value="/index.jsp"/>  
<!--context 속성 : 다른 컨텍스트로 출력하고자할때 사용-->  
<!-- /newRoot/index.jsp로 출력-->  
<c:url value="/index.jsp" context="/newRoot"/>  

9) c:import - JSP파일을 인클루드한다.
<!-- 기본 문법 -->  
<!-- partialView.jsp의 내용을 출력 -->  
<c:import url="partialView.jsp"/>  
<!-- charEncoding 속성:   인코딩에 문제가 있을 시 사용 -->  
<c:import url="UserForm.jsp" charEncoding="UTF-8"/>  

10) c:redirect - 리다이렉션
<!-- 기본 문법 -->  
<c:redirect url="http://kkams.net"/>  


11) c:catch - 예외 발생시 처리
<!-- 기본 문법 -->  
<c:catch var="err">    
    <%=10 / 0%> <!--0으로 나누면 에러 발생-->  
</c:catch>  
<!--에러 출력-->  
<c:out value="${err }" />  


다양해보이지만, 상당히 빈약한 문법과 코드를 가지고 있는 것이 JSTL입니다. 자주 사용되기도 하고, 일단 표준이기 때문에 몰라서는 안되는 View 표현 방법입니다. 앞서 Controller에서 보내지는 Model의 값을 JSTL을 이용해서 표현하게 되는 것이 일반적입니다. 

지금까지 본 jstl을 이용한 book list의 표현과 book add의 표현방법입니다. 

    @RequestMapping("book/list")
    public String listup(Model model) {
        List<Book> books = bookService.listup();
        model.addAttribute("books", books);
        return "book/list";
    }

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<link href="/res/css/bootstrap.min.css" rel="stylesheet" />
<link href="/res/css/sample.css" rel="stylesheet" />
<script src="/res/js/bootstrap.min.js"></script>
</head>
<h2>jstl test code 입니다.</h2><div class="example-form">
    <h3>c:forEach example code</h3>
    <spring:message code="code.message"></spring:message>
    <table class="table table-striped table-bordered table-hover">
        <thead>
            <tr>
                <th>Title</th>
                <th>status</th>
                <th>Description</th>
            </tr>
        </thead>

        <tbody>
            <c:forEach var="book" items="${books}">
                <tr>
                    <td>${book.title}</td>
                    <c:set var="status" value="일반" />
                    <c:choose>
                        <c:when test="${book.status eq 'NORMAL'}">
                            <c:set var="status" value="일반" />
                        </c:when>
                        <c:when test="${book.status eq 'RENTNOW'}">
                            <c:set var="status" value="대여중" />
                        </c:when>
                        <c:otherwise>
                            <c:set var="status" value="분실중" />
                        </c:otherwise>
                    </c:choose>
                    <td>${status }</td>
                    <td>${book.comment}</td>
                </tr>
            </c:forEach>
        </tbody>
    </table>
</div>
</body>
</html>


2. JSTL fmt를 이용한 데이터의 가공

java의 원 데이터를 html로 표시할 때, 우리는 자주 데이터의 모양을 바꿔야지 되는 일들이 발생됩니다. 특히 통화량과 날짜, 소숫점의 표시에서 가장 일반적이라고 할 수 있습니다. 자리수 단락에서 ","를 추가한다던지, 소숫점 이하의 자리수를 어떻게 표시를 하는지에 대한 문제는 Controller와 같은 기술적인 issue와는 별개로 사용자들에게는 '모든 것'이 될 수 있는 문제입니다. 이런 일은 수치데이터를 직접 바꾸는 것이 아니라 수치의 표시를 어떻게 해줄 것인지에 대한 내용입니다. View에서 이런 일들을 해주는 것이 가장 좋습니다. 

이 부분에 대해서는 issue가 있습니다. View에서 특정 데이터를 빨간색으로 보여야지 되는 action에 대한 처리를 무엇으로 보느냐에 대한 논쟁입니다. 이것은 보는 방법을 바꾸는 일이기 때문에 View에서 봐야지 된다는 의견과 보는 방법이 아닌 이것 역시 Business Logic으로 보는 견해도 있습니다. BL이기 때문에 Server Code에서 구동이 되고 테스트가 가능한 방법으로 만들어야지 된다는 것이지요. 가장 좋은 위치는 Controller 영역으로 이야기하고 있습니다. 보여지는 Model을 변경하는 것이기 때문에 가장 좋은 Layer이니까요. 개인적인 의견으로는 View에 대한 영역은 모두 View에 넘기는 것이 좋지 않나. 라는 생각입니다. 저는 개인적으로는 전자의 의견에 좀더 한표를 주고 있는 편입니다. 이 부분에 대해서는 개발자들의 취향과 그 프로젝트의 PM에 따라서 많이 바뀌게 될 내용이라고 생각됩니다. 

지금 소개하는 JSTL fmt의 경우에는 View Layer, 즉 JSP에서 보이는 방법을 바꿔주는 방법입니다. 

선언 방법은 다음과 같습니다. 

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

선언된 데이터는 다음과 같이 사용이 가능합니다. 

<div class="example-form">
    <fieldset>
        <legend>FMT examples - Date</legend>
        <ul>
            <li><fmt:formatDate value="${date}" type="DATE" pattern="yyyy/MM/dd" /></li>
            <li><fmt:formatDate value="${date}" type="DATE" pattern="yyyy년 M월 dd일" /></li>
        </ul>
    </fieldset>
    <br />
    <fieldset>
        <legend>FMT example - Number</legend>
        <ul>
            <li>orginal : ${number}</li>
            <li><fmt:formatNumber value="${number}" groupingUsed="true" currencySymbol=","/></li>
            <li><fmt:formatNumber value="${number}" minFractionDigits="5"/></li>
            <li><fmt:formatNumber value="${number}" type="CURRENCY"/></li>
            <li><fmt:formatNumber value="234.3" pattern="△#,##0.00; ▼#,##0.00" /></li>
            <li><fmt:formatNumber value="-1234.56" pattern="△#,##0.00; ▼#,##0.00" /></li>
            <li><fmt:formatNumber value="0.99" type="percent"/></li>
        </ul>
    </fieldset>
</div>


위 HTML은 다음과 같이 표시가 됩니다. 



3. tiles

Tiles는 기본적으로 JSTL을 이용하지만, JSTL에서 중복되는 html code를 최소화하기 위해서 만들어진 Template Engine입니다. Tile는 기본적인 Html View 가 Composite View Pattern으로 동작할 때, 중복되는 코드들을 공통 요소로 뽑아서 이용가능합니다. 다음은 Tile에 대한 기본 개념의 정의입니다. 




일반적인 Html Page의 기본 구조입니다. 이 구조에서 페이지가 바뀌게 된다면 Body부분의 Content는 계속해서 바뀌게 되지만, Menu, Header, Footer들은 크게 바뀌지 않는 것이 일반적입니다. 
따라서, 각 부분을 재사용할 수 있다면 코드의 양은 매우 줄어들고, 유지보수에 용의함을 알 수 있습니다. 


tiles를 추가하기 위해서는 pom.xml에 다음 항목을 추가해주세요. 

        <dependency>
            <groupId>org.apache.tiles</groupId>
            <artifactId>tiles-jsp</artifactId>
            <version>3.0.1</version>
        </dependency>

tiles는 tag를 제공하고 있으며, tag를 이용해서 tiles page를 작성할 수 있습니다.  먼저 layout page를 알아보도록 하겠습니다. 

<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title><tiles:insertAttribute name="title"/></title>
<link href="/res/css/bootstrap.min.css" rel="stylesheet" />
<link href="/res/css/sample.css" rel="stylesheet" />
<script src="/res/js/bootstrap.min.js"></script>
</head>
<body>
    <tiles:insertAttribute name="content"/>
</body>
</html>

기본적인 content의 attribute를 지정하고, 그 attribute에 원하는 값을 넣어주는 것이 가능합니다. attribute는 String 문자열 또는 jsp 파일이 될 수 있습니다.  이러한 attribute와 view name을 설정하기 위해서 tiles는 설정 xml을 가지게 되는데, 이 xml은 다음과 같이 구성될 수 있습니다. 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE tiles-definitions PUBLIC
       "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN"
       "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>
    <definition name="main-layout" template="/WEB-INF/tiles/layouts/main-layout.jsp">
        <put-attribute name="title" value=""/>
        <put-attribute name="content" value=""/>
    </definition>
    <definition name="book/list" extends="main-layout">
        <put-attribute name="title" value="BOOK LIST"/>
        <put-attribute name="content" value="/WEB-INF/tiles/book/list.jsp"/>
    </definition>   
    <definition name="book/add" extends="main-layout">
        <put-attribute name="title" value="Add BOOK"/>
        <put-attribute name="content" value="/WEB-INF/tiles/book/add.jsp"/>
    </definition> 
</tiles-definitions>

여기서 각각의 definition name은 ModelAndView의 view name으로 이용됩니다. extends로 선언된 View를 Template로 이용하고, 각각의 View의 조각(tile)을 붙이는 형식으로 사용할 수 있습니다. 
tiles를 사용하기 위해서는 TilesViewResolver를 applicationContext에 선언해줘야지 됩니다. 일반적으로 controller-servlet.xml에서 구성합니다.  code base configuration은 다음과 같이 설정해줍니다. 

    @Bean
    public TilesConfigurer tilesConfigurer() {
        TilesConfigurer tilesConfigurer = new TilesConfigurer();
        tilesConfigurer.setDefinitions(new String[] { "/WEB-INF/tiles-configs.xml" });
        return tilesConfigurer;
    }

    @Bean
    public TilesViewResolver tilesViewResolver() {
        TilesViewResolver tilesViewResolver = new TilesViewResolver();
        tilesViewResolver.setOrder(1);
        return tilesViewResolver;
    }

2개의 @Bean을 설정해주는 것을 알 수 있습니다. 

보시면 <tiles:insertAttribute> tag로 구성된 부분에 tiles.xml 파일에서 지정한 tile이 들어가서 전체 웹페이지를 구성하게 되는 것을 알 수 있습니다. 매우 자주 쓰이고 있는 View Teamplate입니다. 전체 코드의 양을 줄일 수 있고, 전체 Framework 등의 문제를 쉽게 해결할 수 있는 방법이기도 합니다. 

tiles를 사용하시면 이제 book/list.jsp는 다음과 같이 변경이 될 수 있습니다.

<h2>jstl test code 입니다.</h2><div class="example-form">
    <h3>c:forEach example code</h3>
    <spring:message code="code.message"></spring:message>
    <table class="table table-striped table-bordered table-hover">
        <thead>
            <tr>
                <th>Title</th>
                <th>status</th>
                <th>Description</th>
            </tr>
        </thead>

        <tbody>
            <c:forEach var="book" items="${books}">
                <tr>
                    <td>${book.title}</td>
                    <c:set var="status" value="일반" />
                    <c:choose>
                        <c:when test="${book.status eq 'NORMAL'}">
                            <c:set var="status" value="일반" />
                        </c:when>
                        <c:when test="${book.status eq 'RENTNOW'}">
                            <c:set var="status" value="대여중" />
                        </c:when>
                        <c:otherwise>
                            <c:set var="status" value="분실중" />
                        </c:otherwise>
                    </c:choose>
                    <td>${status }</td>
                    <td>${book.comment}</td>
                </tr>
            </c:forEach>
        </tbody>
    </table></div>

layout에서 body 부분만을 제외하고 나머지 부분들을 모두 처리하고 있기 때문에 중복 코드를 없앨수 있는 것을 볼 수 있습니다. 조금 더 응용할 경우, 보다더 복잡한 UI를 좀 더 깔끔한 코드로 만들어주는 것이 가능합니다. Tiles의 경우에는 현장에서 주로 사용하고 있는 View Template 입니다. 꼭 사용법을 익혀주시길 바랍니다. 

4. velocity

Velocity는 JSTL의 문법의 복잡함을 해결하기 위해서 구성된 View Template 입니다. 기본적으로 제어문의 간편함과 간결한 html을 가능하게 하는 장점을 가지고 있습니다. 
기본적인 velocity로 구성된 html 파일을 한번 알아보도록 하겠습니다. (* velocity로 구성된 html파일은 대체적으로 .vm 확장자를 갖습니다.) velocity를 주로 이용하는 경우에는 eclipse에 velocity plugin을 설치하는 것이 좋습니다. Install New Software를 선택해서 http://veloeclipse.googlecode.com/svn/trunk/update/ 주소에서 veloctiy plugin을 설치해주면 .vm 파일의 syntax highlight가 가능합니다. 

기본적인 velocity 문법은 #으로 시작되는 제어문과 $로 시작되는 변수로 함축할 수 있습니다.  만들어진 book list에 대한 velocity code는 다음과 같습니다. 

<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>Velocity View Example</title><link href="/res/css/bootstrap.min.css" rel="stylesheet" /><link href="/res/css/sample.css" rel="stylesheet" /><script src="/res/js/bootstrap.min.js"></script></head><body>
    <h2>freemarker list view</h2>
    <div class="example-form">
        <h3>Book list</h3>
        <spring:message code="code.message"></spring:message>
        <table class="table table-striped table-bordered table-hover">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>status</th>
                    <th>Description</th>
                </tr>
            </thead>
            <tbody>
                #foreach($book in $books)
                <tr>
                    <td>$book.title</td>
                    #if($book.status == "NORMAL")
                        <td>일반</td>
                    #elseif($book.status == "RENTNOW")
                        <td>대여중</td>
                    #else
                        <td>분실</td>
                    #end
                    <td>$book.comment</td>
                </tr>                
                #end
            </tbody>
        </table>
    </div></body></html>

주의해서 보실 것이 #foreach와 #if문입니다.  jstl보다는 문법이 매우 쉬워지는 것을 알 수 있습니다. 좀더 보기도 편한것도 사실이고요. 

ViewResolver를 등록하기 위해서 Spring은 2개의 Bean을 정의하도록 되어 있습니다. 위에 Tiles에서와 완전히 동일합니다. 먼저 ViewConfigurer를 등록하고, ViewResolver를 등록하는 형식입니다. ViewConfigurer의 경우에는 View에 대한 Configuration을 주로 등록을 하고, ViewResolver의 경우에는 Resolver의 세부 설정을 넣는 형식입니다. Velocity의 code base configuration은 다음과 같이 구성됩니다. 

    @Bean
    public VelocityConfigurer velocityConfigurer() throws VelocityException, IOException {
        VelocityConfigurer configurer = new VelocityConfigurer();
        configurer.setResourceLoaderPath("/WEB-INF/vm");
        Properties properties = new Properties();
        properties.put("input.encoding", "UTF-8");
        properties.put("output.encoding", "UTF-8");
        configurer.setVelocityProperties(properties);

        return configurer;
    }

    @Bean
    public VelocityViewResolver velocityViewResolver() {
        VelocityViewResolver viewResolver = new VelocityViewResolver();
        viewResolver.setContentType("text/html; charset=UTF-8");
        viewResolver.setSuffix(".vm");
        viewResolver.setOrder(3);
        return viewResolver;
    }

Configurer에서 encoding을 설정하는 것을 잊지 말아주세요. 한글 데이터가 있는한, 그리고 영문만을 사용하지 않는한 모든 ViewResolver는 어떻게든 UTF-8 설정이 필요합니다. 


5. Freemarker

Freemarker는 기본적으로 velocity와 동일한 컨셉의 View Template Engine입니다. Velocity보다 빠르고, 가독성의 향상을 목적으로 하고 있습니다. 다음은 Freemarker로 만든 Html code입니다. 

<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title><tiles:insertAttribute name="title"/></title><link href="/res/css/bootstrap.min.css" rel="stylesheet" /><link href="/res/css/sample.css" rel="stylesheet" /><script src="/res/js/bootstrap.min.js"></script></head><body>
    <h2>freemarker list view</h2>
    <div class="example-form">
        <h3>Book list</h3>
        <spring:message code="code.message"></spring:message>
        <table class="table table-striped table-bordered table-hover">
            <thead>
                <tr>
                    <th>Title</th>
                    <th>status</th>
                    <th>Description</th>
                </tr>
            </thead>

            <tbody>
                <#list books as book>
                    <tr>
                    <td>${book.title}</td>                    
                    <#if book.status.name() == "NORMAL">
                        <td>일반</td>
                    <#elseif book.status.name() == "RENTNOW">
                        <td>대여중</td>
                    <#else>
                        <td>분실</td>
                    </#if>
                    <td>${book.comment}</td>
                    </tr>
                </#list>
            </tbody>
        </table>
    </div></body></html>

#을 이용한 간편한 foreach문이 보이시나요? #if 등 문법상으로는 개인적으로는 좀더 편해보이는 것이 freemarker이긴합니다. freemarker를 사용하기 위해서는 velocity와 동일하게 ViewConfiguration과 ViewResolver를 구성해야지 됩니다. 다음은 code base configuration입니다. 

    @Bean
    public FreeMarkerConfigurer freemarkerConfigurer() {
        FreeMarkerConfigurer freemarkerConfigurer = new FreeMarkerConfigurer();
        freemarkerConfigurer.setTemplateLoaderPath("/WEB-INF/fmt");
        Properties properties = new Properties();
        properties.put("default_encoding", "UTF-8");
        freemarkerConfigurer.setFreemarkerSettings(properties);
        return freemarkerConfigurer;
    }

    @Bean
    public FreeMarkerViewResolver freemarkerViewResolver() {
        FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();
        viewResolver.setContentType("text/html; charset=UTF-8");
        viewResolver.setSuffix(".ftl");
        viewResolver.setOrder(2);

        return viewResolver;
    }


Summary

지금까지 다양한 Html View Engine을 살펴봤습니다. View Engine은 매우 다양한 형태로 제공되고 있습니다. 지금 소개한 Veloctiy, Freemarker, tiles의 경우에는 Spring에서 공식적으로 제공하고 있는 View Template Engine입니다. 타 View Template Engine의 경우, Library에 Spring의 ViewResolver를 구현하고 있는 것이 일반적입니다. 다양한 View Engine은 Project에서 개발자의 맘 또는 전체 개발팀의 의향으로 정해지는 것이 일반적입니다. 그런데, 여기에서 조금 문제가 되는 것이 여러개의 View Engine을 섞어서 쓰지는 거의 못한다는 겁니다. 물론 섞어서 사용하는 경우도 왕왕 있습니다. 예를 들어, tiles에 freemarker를 섞어서 사용한다던지요. 이 부분에 대해서는 각자 개인의 학습이 좀 더 필요한 것 같습니다. 저도 몇 부분은 좀 더 해봐야지 될 것 같고요. 

가장 대중적인 것은 jstl을 사용하는 것입니다. 모든 View Template의 기본이 되고 있고, 많은 Reference를 가지고 있습니다. Tiles + JSTL의 경우가 호환성이나 code의 편의성이 가장 좋은 것 같습니다. 

숙제입니다. BookList를 jsp, tiles, velocity, freemarker를 이용해서 표시해주세요. 4개의 html page와 4개의 Template engine의 설정이 필요합니다. 반드시 한글 데이터가 나오도록 설정되어야지 됩니다. 또한 각 ViewEngine이 한 web server에서 동작하도록 구성을 해주시길 바랍니다.


Posted by Y2K
,

22. Spring View 처리 구조

Java 2013. 9. 12. 13:41

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


View는 사용자에게 최종적으로 보이는 영역입니다. 이 영역은 우리가 일반적으로 보는 Html이 될 수도 있고, Mobile App에게는 JSON format의 API 결과가 될 수도 있습니다. 
기본적으로 Spring Servlet/MVC를 기반으로 동작하게 됩니다. View 계층 또는 Presentation 계층이라고 합니다. 

Controller가 return한 결과에 대한 표현을 담당하는 영역으로 Controller가 넘겨주는 ModelAndView가 View에서 핵심 객체라고 할 수 있습니다. 

기본적인 동작은 DispatcherServlet이 ModelAndView에서 넘어온 값 ViewResolver를 통해서 해석한 결과를 사용자에게 보내줍니다. ViewResolver는 기본적으로 jsp를 기반으로 하고 있고, jsp 이외에 모든 web context가 될 수 있습니다. 

View의 기본 구조

기본적으로 Spring의 org.springframework.web.servlet.View interface를 구현한 객체를 View로 표현이 가능합니다. 일반적으로 View는 Spring 내부의 View를 확장하거나, pdf, rss, excel과 같은 다른 형태의 View를 표현하기 위해서 class를 확장해서 사용합니다. 또는 외부 View engine을 이용한 View의 표현 역시 가능합니다. 여기서 외부 View engine을 이용하는 것은 velocity, freemarker, tiles와 같은 외부의 View engine과의 연결 interface 및 설정을 구현함으로서 가능합니다. 

View의 기본 동작

Controller가 작업을 마친 후, View정보를 ModelAndView 객체에 담아서 보내주는 DispatcherServlet에 보내주는 것이 기본 동작입니다. 이에 대한 방법은 Spring @MVC는 두가지를 제공하고 있습니다. 첫번째는 View interface를 구현한 객체를 보내주는 방법이고, 다른 하나는 View의 이름만을 보내는 방법입니다. 첫번째 방법의 경우에는 View interface의 구현 방법에 따라 다양한 View를 표시하게 됩니다. DispatcherServlet과 Controller간에는 어떠한 동작도 존재하지 않습니다. 그렇지만 View의 이름만 보내주는 방식은 좀 다르게 동작하게 됩니다. 

View의 이름으로 표현되는 논리적인 View를 실질적인 View interface를 구현한 객체로 변경하는 작업이 필요하게되는데요. 이를 담당하는 객체를 ViewResolver라고 합니다. 
Spring에서 제공하는 주요 ViewResolver는 다음과 같습니다. 

ViewResolver 구현 classDescription
InternalResourceViewResolverView Name에서 jsp나 tiles 연동을 위한 View 객체를 반환
BeanNameViewResolverBean name을 기준으로 View 객체를 반환
ResourceBundlleViewResolverView 이름과 View 객체간의 mapping 정보를 저장하기 위해서 Resource 파일을 이용
XmlViewResolverView 이름과 View 객체간의 mapping 정보를 저장하기 위해서 xml 파일을 이용


기본적으로 만들어지는 ViewResolver의 interface는 다음과 같습니다. 
public interface ViewResolver {
    View resolveViewName(String viewName, Locale locale) throws Exception;
}

다국어 지원을 위한 locale 정보와 View 이름을 이용한 View 객체를 얻어내는 간단한 interface만을 가지고 있습니다. 이를 이용해서 새로운 ViewResolver를 만드는 것 역시 가능합니다. 

구성되는 View는 다음과 같은 interface를 가지고 있습니다. 

public interface View {
    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
    String PATH_VARIABLES = View.class.getName() + ".pathVariables";
    String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
    String getContentType();
    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}

들어오게되는 model을 어떻게 render하는지에 대한 interface 선언을 볼 수 있습니다. 

자, 전에 봤던 Spring MVC에서 Request가 처리되는 과정을 다시 한번 봐보도록 하겠습니다. 

지금까지는 Step2까지 알아본 상황입니다. 이제 Step 2에서  Step 3 사이의 ModelAndView가 Return이 되고 ViewResolver가 ViewName을 통해서 실질적인 View를 Return 시켜주는 영역을 알아봐야지 됩니다. 그리고, 그 설정은 Spring에서 제공하는 ViewResolver interface에서 담당하고 있습니다.  위 그림에서 Step 3이 생략되는 경우 역시 존재할 수 있습니다. 앞에서 이야기한것 처럼, 직접 View가 return 될 때에는 Step 3의 ViewResolver가 동작을 하지 않고, 바로 Step 4로 이동하게 됩니다. 



Summary

이번장에서는 Controller가 return 시켜주는 Model을 render 시켜주는 View interface에 대해서 알아봤습니다.  Spring MVC에서 Request가 처리되는 과정을 좀 더 살펴보시길 바랍니다. 


Posted by Y2K
,