빠에야는 개발중

스프링 퀵 스타트 : chap 4-2 본문

공부/스프링

스프링 퀵 스타트 : chap 4-2

빠에야좋아 2018. 2. 1. 00:02

비즈니스 레이어 통합

그동안 진행하면서 컨트롤러가 DAO 객체를 직접 가져다 쓰는 코드를 짰었는데, 이런 코드는 좋지 않기 때문에 개선하고자 한다. 우선 그 이유부터 알아보자.

왜 안되는가?

상황 1 : 만약 기존의 DAO가 다른 클래스로 교체된다면? DAO를 참조하고 있는 모든 컨트롤러의 코드를 수정해야만 한다. 


상황 2 : AOP를 사용하고 싶다. 그런데 AOP의 동작 시점인 비즈니스 메소드가 호출되지 않기 때문에 어드바이스도 호출되지 않는다. 실제로 지금까지의 코드에서는 AOP 설정을 해놨던 메소드가 전혀 호출되지 않고 있었다.


상황 1의 경우는 OCP를 위반하고 있다. 변경이 일어날 때마다 모든 부분을 수정하기 때문이다. 이는 추상화를 통하여 해결할 수 있다. 

상황 2는 비즈니스 레이어를 강요하고 있는 것처럼 보인다. 따라서 우리는 만들어놓고 쓰지 않았던 비즈니스 클래스들을 사용하고자 한다. 마침 추상화도 잘 되어있다.

적용

컨트롤러에서 DAO 객체를 직접 호출하는 부분을 모두 비즈니스 클래스 객체로 바꿔주자. 간단한 과정이다. 

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/updateBoard.do")
public String updateBoard(@ModelAttribute("board") BoardVO vo) {
    boardService.updateBoard(vo);
    return "getBoardList.do";
}
    
@RequestMapping("/deleteBoard.do")
public String deleteBoard(BoardVO vo) {
    boardService.deleteBoard(vo);
    return "getBoardList.do";
}
// 이하 동일
cs


그런데 실행해보면 NoSuchBeanDefinitionException이 뜨면서 실패한다. 예외명 그대로 빈이 생성되어있지 않기 때문이다. @Autowired가 걸려있는 비즈니스 클래스가 말이다.


이유는 이렇다. 현재 web.xml에 연결 되어있는 xml 설정 파일은 presentation-layer.xml 뿐이다. 그리고 이 녀석은 컨트롤러가 있는 패키지 com.springbook.view에만 component-scan이 적용되어있다. 따라서 비즈니스 클래스에 걸린 어노테이션은 적용되지 않는다. 그렇다면 그 설정이 되어있는 applicationContext.xml을 web.xml에 연결해주면 되는 것이다!


한 가지 알아둬야 하는 사실은 스프링 컨테이너가 두 개 만들어진다는 것이다. 실행 순서상 비즈니스 클래스와 DAO 클래스의 객체가 먼저 로딩 되어야 컨트롤러가 그것들을 가져올 수 있다. 이 때 먼저 생성되는 스프링 컨테이너를 “루트 컨테이너”라고 부른다.


비즈니스 레이어의 객체를 먼저 생성(pre-loading)하기 위해서는 Listener가 필요하다. 스프링에서 제공하는 ContextloaderListener를 사용하자.

이렇게 applicationContext.xml을 연결시켜주면 설정이 완료된다.

1
2
3
4
5
6
7
8
9
10
<!-- needed for ContextLoaderListener -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>
 
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
cs


검색 기능 구현

게시판에서 제목, 내용으로 검색하는 UI를 만들었었다. 이것들을 실제로 사용할 수 있도록 해보자. 우선 BoardVO에 검색 분류와 검색어에 해당하는 멤버 변수들을 추가한다. 그 후에 null guard도 해준다. @RequestParam으로 변수를 따로 받을 수도 있지만 깔끔한 코드를 위해서 VO를 수정하는 방향을 선택했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 생략
public class BoardVO {
    private int seq;
    private String title;
    private String writer;
    private String content;
    private Date regDate;
    private int cnt;
    private String searchCondition;
    private String searchKeyword;
 
// 생략
 
public String getSearchCondition() {
    return searchCondition;
}
 
public void setSearchCondition(String searchCondition) {
    this.searchCondition = searchCondition;
}
 
public String getSearchKeyword() {
    return searchKeyword;
}
 
public void setSearchKeyword(String searchKeyword) {
    this.searchKeyword = searchKeyword;
}
cs

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/getBoardList.do")
public String getBoardList(BoardVO vo, Model model) {
    if(vo.getSearchCondition() == null) {
        vo.setSearchCondition("TITLE");
    }
    if(vo.getSearchKeyword() == null) {
        vo.setSearchKeyword("");
    }
    model.addAttribute("boardList", boardService.getBoardList(vo));
    return "getBoardList.jsp";
}
cs


다음은 DAO 클래스를 수정한다. 게시글 목록 쿼리를 두 가지로 나누고, JDBC에서 분기를 만들어준 뒤 검색어를 삽입한다.

1
2
private final String BOARD_LIST_T = "select * from board where title like '%'||?||'%' order by seq desc";
private final String BOARD_LIST_C = "select * from board where content like '%'||?||'%' order by seq desc";
cs


1
2
3
4
5
6
7
8
    conn = JDBCUtil.getConnection();
    if(vo.getSearchCondition().equals("TITLE")) {
        stmt = conn.prepareStatement(BOARD_LIST_T);
    } else if(vo.getSearchKeyword().equals("CONTENT")) {
        stmt = conn.prepareStatement(BOARD_LIST_C);
    }
    stmt.setString(1, vo.getSearchKeyword());
    rs = stmt.executeQuery();
cs

파일 업로드

글을 등록할 때 파일도 같이 올릴 수 있도록 해보자. 우선 BoardVO에 멤버 변수를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BoardVO {
    
    //생략
 
    private MultipartFile uploadFile;
 
    //생략
 
    public MultipartFile getUploadFile() {
        return uploadFile;
    }
 
    public void setUploadFile(MultipartFile multipartFile) {
        this.uploadFile = multipartFile;
    }
cs


그리고 글 등록 메소드에서 파일 업로드 처리를 해주면 된다.

1
2
3
4
5
6
7
8
@RequestMapping("/insertBoard.do")
public String inseretBoard(BoardVO vo) throws IOException {
    MultipartFile uploadFile = vo.getUploadFile();
    if(!uploadFile.isEmpty()) {
        String fileName = uploadFile.getOriginalFilename();
        uploadFile.transferTo(new File("/Users/paella/Desktop/" + fileName));
    }
    //생략
cs

예외 처리

지금까지 예외가 발생하면 별도의 에러 페이지 없이 에러 코드와 그 내용을 노출시켰다. 매우 좋지 않은 현상이기 때문에 보완해주기로 했다.

스프링에서 제공해주는 @ControllerAdivce와 @ExceptionHandler 어노테이션을 사용하면 원하는 부분에서 발생하는 예외를 종류 별로 묶어서 처리해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.springbook.biz.common;
 
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
 
@ControllerAdvice("com.springbook.view")
public class CommonExceptionHandler {
    @ExceptionHandler(ArithmeticException.class)
    public ModelAndView handleArithmeticException(Exception e) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.setViewName("/common/arithmeticError.jsp");
        return mav;
    }
    
    @ExceptionHandler(NullPointerException.class)
    public ModelAndView handleNullPointerException(Exception e) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.setViewName("/common/nullPointerError.jsp");
        return mav;
    }
    
    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(Exception e) {
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.setViewName("/common/error.jsp");
        return mav;
    }
}
cs


그리고 에러 페이지를 만들면 끝이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//생략
<title>기본 에러 화면</title>
</head>
<body bgcolor="#ffffff" text="#000000">
<!-- 타이틀 시작 -->
<table width="100%" border="1" cellspacing="0" cellpadding="0">
    <tr>
        <td align="center" bgcolor="orange"><b>기본 에러 화면입니다.</b></td>
    </tr>
</table>
<br>
<!-- 에러 메시지 -->
<table width="100%" border="1" cellspacing="0" cellpadding="0" align="center">
    <tr>
        <td align="center">
        <br><br><br><br><br>
        Message: ${exception.message}
        <br><br><br><br><br>
        </td>
    </tr>
</table>
//
cs

다국어 처리

웹의 사용자는 한국 사람만 있는 것이 아니다. 다른 언어를 사용하는 사람들도 잘 사용할 수 있도록 다국어 지원을 추가했다. 이번에는 영어만 추가하는 것으로 한다.

먼저 메시지 파일을 작성한다. 파일명은 messageSource_(국가 영문 2글자).properties로 해주었다.


messageSource_en.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# login.jsp
message.user.login.title=LOGIN
message.user.login.id=ID
message.user.login.password=PASSWORD
message.user.login.loginBtn=LOG-IN
 
message.user.login.language.en=English
message.user.login.language.ko=Korean
 
# getBoardList.jsp
message.board.list.mainTitle=BOARD LIST
message.board.list.welcomeMsg=! Welcome to my BOARD
message.board.list.search.condition.title=TITLE
message.board.list.search.condition.content=CONTENT
message.board.list.search.condition.btn=Search
message.board.list.table.head.seq=SEQ
message.board.list.table.head.title=TITLE
message.board.list.table.head.writer=WRITER
message.board.list.table.head.regDate=REGDATE
message.board.list.table.head.cnt=CNT
message.board.list.link.insertBoard=Insert Board
cs

messageSource_ko.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# login.jsp
message.user.login.title=\uB85C\uADF8\uC778
message.user.login.id=\uC544\uC774\uB514
message.user.login.password=\uBE44\uBC00\uBC88
message.user.login.loginBtn=\uB85C\uADF8\uC778
 
message.user.login.language.en=\uC601\uC5B4
message.user.login.language.ko=\uD55C\uAE00
 
# getBoardList.jsp
message.board.list.mainTitle=\uAC8C\uC2DC\uAE00 \uBAA9\uB85D
message.board.list.welcomeMsg=\uB2D8! \uAC8C\uC2DC\uD310\uC5D0 \uC624\uC2E0\uAC78 \uD658\uC601\uD569\uB2C8\uB2E4.
message.board.list.search.condition.title=\uC81C\uBAA9
message.board.list.search.condition.content=\uB0B4\uC6A9
message.board.list.search.condition.btn=\uAC80\uC0C9
message.board.list.table.head.seq=\uBC88\uD638
message.board.list.table.head.title=\uC81C\uBAA9
message.board.list.table.head.writer=\uC791\uC131\uC790
message.board.list.table.head.regDate=\uB4F1\uB85D\uC77C
message.board.list.table.head.cnt=\uC870\uD68C\uC218
message.board.list.link.insertBoard=\uC0C8 \uAE00 \uB4F1\uB85D
cs


그 다음 스프링에서 이 파일들을 읽을 수 있도록 messageSource 클래스를 빈으로 등록한다. 그리고 localeResolver와 localeChangeInterceptor를 등록해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 다국어 설정 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
        <list>
            <value>message.messageSource</value>
        </list>
    </property>
</bean>
 
<!-- LocaleResolver 등록 -->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
 
<mvc:interceptors>
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
        <property name="paramName" value="lang"></property>
    </bean>
</mvc:interceptors>
cs


마지막으로 jsp 페이지의 플레인 텍스트 부분을 태그로 처리해주면 끝이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 생략 -->
<h1><spring:message code="message.user.login.title" /></h1>
<a href="login.do?lang=en">
    <spring:message code="message.user.login.language.en" /></a>&nbsp;&nbsp;
<a href="login.do?lang=ko">
    <spring:message code="message.user.login.language.ko" /></a>&nbsp;&nbsp;
    
<hr>
<form action="login.do" method="post">
<table border="1" cellpadding="0" cellspacing="0">
    <tr>
        <td bgcolor="orange"><spring:message code="message.user.login.id" /></td>
        <td><input type="text" name="id" value="${userVO.id }" /></td>
    </tr>
<!-- 생략 -->
cs

결과는 다음과 같다.

데이터 변환

다른 시스템과 통신을 하면서 많이 사용하는 데이터 포맷 중 하나가 바로 json이다. 데이터 출력을 json 형식으로 바꾸어보자.

먼저 jackson 라이브러리를 디펜던시에 추가한다.

1
2
3
4
5
6
    <!-- Jackson2 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.8.8</version>
    </dependency>
cs


그 다음 컨트롤러 메소드에 @ReponseBody 어노테이션을 추가한다. 이렇게 하면 자바 객체를 http 응답 프로토콜의 몸체로 변환시켜준다. 그리고 그 과정에서 jackson이 실행 결과를 json으로 매핑한다.

1
2
3
4
5
6
7
8
@RequestMapping("/dataTransform.do")
@ResponseBody
public List<BoardVO> dataTransform(BoardVO vo) {
    vo.setSearchCondition("TITLE");
    vo.setSearchKeyword("");
    List<BoardVO> boardList = boardService.getBoardList(vo);
    return boardList;
}
cs

결과는 다음과 같다.

(내용은 무시하자)


보이고 싶지 않은 변수의 getter에 @JsonIgnore 어노테이션을 사용하여 숨길 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
@JsonIgnore
public String getSearchKeyword() {
    return searchKeyword;
}
 
...
 
@JsonIgnore
public MultipartFile getUploadFile() {
    return uploadFile;
}
cs



이번에는 꽤 다양한 기능들을 추가해보았다. 프레임워크적 요소들을 많이 사용해볼 수 있어서 유익했고 써봤던 기능들을 상기시켜줘서 반가웠다.

'공부 > 스프링' 카테고리의 다른 글

스프링 퀵 스타트 : chap 5-2  (0) 2018.02.05
스프링 퀵 스타트 : chap 5-1  (0) 2018.02.04
스프링 퀵 스타트 : chap 4-1  (1) 2018.01.31
스프링 퀵 스타트 : chap 3-2  (1) 2018.01.30
스프링 퀵 스타트 : chap 3-1  (379) 2018.01.29
Comments