구리

TIL_210623_SpringMVC_비즈니스 컴포넌트 사용, 비즈니스 컴포넌트 로딩, 검색 기능 구현 본문

SPRING FRAMEWORK

TIL_210623_SpringMVC_비즈니스 컴포넌트 사용, 비즈니스 컴포넌트 로딩, 검색 기능 구현

guriguriguri 2021. 6. 22. 23:47

목차

더보기

비즈니스 컴포넌트 사용
1. DAO 클래스 교체
2. AOP 설정

비즈니스 컴포넌트 로딩
1. 2-Layered 아키텍처
2. ContextLoaderListener 등록
3. 스프링 컨테이너의 관계

검색 기능 구현
1-1. 검색 정보 추출
1-2. Command 객체 수정
2. Controller 구현
3. DAO 클래스 수정
- BoardDAOSpring 클래스 수정

 

사실 Controller는 비즈니스 컴포넌트를 이용해 사용자의 요청을 처리해야 하며, 이때 컴포넌트가 제공하는 Service 인터페이스를 사용해야 합니다. 지금껏 사용했던 방법처럼 Controller가 직접 DAO 객체의 메소드 호출시 여러 문제를 야기하기 때문입니다.

문제

1. 유지보수 과정에서 DAO 클래스를 다른 클래스로 쉽게 교체하기 위해서

 

비즈니스 컴포넌트 사용

1. DAO 클래스 교체

아래와 같이 코드 변경시 BoardService 인터페이스를 구현한 BoardServiceImpl 클래스의 내부 코드를 변경해도 BoardController는 수정할 필요가 없어집니다. (BoardService 타입 멤버변수 선언)

이와 같이 클라이언트(Controller)가 인터페이스를 통해 비즈니스 컴포넌트를 이용하면 컴포넌트의 구현 클래스를 수정하여도 이를 사용하는 클라이언트(Controller)는 수정하지 않아도 됩니다.

package com.springbook.view.board;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

import com.springbook.biz.board.BoardService;
import com.springbook.biz.board.BoardVO;
import com.springbook.biz.board.impl.BoardDAO;

@Controller
@SessionAttributes("board")
public class BoardController {
	@Autowired
	private BoardService boardService;
	
	@RequestMapping("/insertBoard.do")
	public String insertBoard(BoardVO vo){
		System.out.println("글 등록 처리");
		boardService.insertBoard(vo);
		return "getBoardList.do";
	}
	
	@ModelAttribute("conditionMap")
	public Map<String, String> searchConditionMap(){
		Map<String, String> conditionMap =new HashMap<String, String>();
		conditionMap.put("제목", "TITLE");
		conditionMap.put("내용", "CONTENT");
		return conditionMap;
	}
	
	@RequestMapping("/getBoardList.do")
	public String getBoardList(BoardVO vo,Model model) {
		// model 정보 저장
		model.addAttribute("boardList", boardService.getBoardList(vo));
		return "getBoardList.jsp";
	}
	
	@RequestMapping("/getBoard.do")
	public String getBoard(BoardVO vo, Model model) {
		System.out.println("글 상세 조회 처리");
		model.addAttribute("board", boardService.getBoard(vo));
		return "getBoard.jsp";
	}
	
	@RequestMapping("/updateBoard.do")
	public String updateBoard(@ModelAttribute("board") BoardVO vo){
		boardService.updateBoard(vo);
		return "getBoardList.do";
	}
	
	@RequestMapping("/deleteBoard.do")
	public String deleteBoard(BoardVO vo) {
		System.out.println("글 삭제 처리");
		boardService.deleteBoard(vo);
		return "getBoardList.do";
	}
}

 

2. AOP 설정

클라이언트는 인터페이스의 추상 메서드를 호출하여 인터페이스를 구현한 Service 구현 객체의 메소드를 실행할 수 있습니다.

따라서 Controller 클래스는 비즈니스 컴포넌트의 인터페이스 타입의 멤버변수를 가지고 있어야 하며, 이 변수에 비즈니스 객체를 의존성 주입해야 합니다. 그러면 Controller 메소드에서는 인터페이스를 통해 비즈니스 객체의 메소드를 호출할 수 있고 결국 AOP로 설정한 어드바이스들이 동작합니다.

 

비즈니스 컴포넌트 로딩

- Controller보다 의존성 주입 대상이 되는 비즈니스 컴포넌트를 먼저 생성하기 위한 다른 컨테이너 필요

  따라서 Controller를 메모리에 생성하는 컨테이너보다 먼저 로딩하는 컨테이너가 존재해야 함

 

1. 2-Layered 아키텍처

 아래의 그림처럼 2개의 레이어로 나누어 시스템을 개발하는 것을 '2-Layered' 아키텍처 스타일이라고 합니다.

 

 

2. ContextLoaderListener 등록

- Controller 객체가 생성되기 전 src/main/resources 소스 폴더에 있는 applicationContext.xml 파일을 읽어    비즈니스 컴포넌트를  메모리에 생성하기 위한 역할을 하는 스프링 제공 클래스

- web.xml 파일에 등록하고 <listener> 의 하위 태그인 <listener-class>를 이용해 해당 클래스를 등록

- 서블릿 컨테이너가 web.xml 파일 읽어 구동될 때, 자동으로 메모리 생성 => 클라이언트 요청 없이Pre-Loading되는 객체

 

  <context-param>
  	<param-name>contextConfigLocation</param-name>
  	<param-value>classpath:applicationContext_aop.xml</param-value>
  </context-param>
  
  <listener>
  	<listener-class>
  		org.springframework.web.context.ContextLoaderListener
  	</listener-class>
  </listener>

- ContextLoaderListener는 기본적으로 /WEB-INF/applicationContext.xml 파일을 읽어 스프링 컨테이너를 구동하기에       src/main/resources 소스 폴더에 있는 스프링 설정 파일을 읽게 하기 위해선 <context-param> 설정을 추가합니다.

(WEB-INF에 파일을 복사해서 사용해도 되지만 나중에 관리상 문제가 발생할 수도 있기에 이 방법은 지양합니다.)

 

 

 

3. 스프링 컨테이너의 관계

스프링 컨테이너의 관계

 

① 톰캣 서버 최초 구동시 web.xml 파일 로딩 => 서블릿 컨테이너 구동

② 서블릿 컨테이너는 web.xml에 등록된 ContextLoaderListener 객체 생성

     이때 ContextLoaderListener 객체는 src/main/resources 소스 폴더에 있는 applicationContext.xml 파일을 로딩하여

     스프링 컨테이너(Root 컨테이너 = 부모 컨테이너)를 구동, Service 구현 클래스나 DAO 객체들이 메모리에 생성

     클라이언트 요청이 전달되면 서블릿 컨테이너는 DispatcherServlet 객체를 생성

③  DispatcherServlet 객체는 /WEB-INF/config 폴더에 있는 presentation-layer.xml 파일을 로딩해 2번째 스프링 컨테이너 구동

       => 2번째 컨테이너가 Controller 객체를 메모리에 생성

 

즉, ContextLoaderListener, DispatcherServlet 이 각각 XmlWebApplicationContext를 생성

ContextLoaderListener가 생성하는 스프링 컨테이너는 Root 컨테이너 = 부모 컨테이너

DispatcherServlet이 생성한 스프링 컨테이너(Root 컨테이너가 생성한 객체를 이용) = 자식 컨테이너

따라서 부모 컨테이너가 생성한 비즈니스 객체를 자식 컨테이너가 생성한 Controller에서 참조하여 사용합니다.


검색 기능 구현

1-1.  검색 정보 추출

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>


<!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>글 목록</title>
</head>
<body>
	<center>
		<h1>글 목록</h1>
		<h3>${userName }님 환영합니다 ! <a href="logout.do">Log-out</a></h3>
		
		<!-- 검색 시작 -->
		<form action="getBoardList.do" method="post">
			<table border="1" cellpadding="0" cellspacing="0" width="700">
				<tr>
					<td align="right">
						<select name="searchCondition">
						<c:forEach items="${conditionMap }" var="option">
							<option value="${option.value }" />${option.key}
						</c:forEach>
						</select>
						<input name="searchKeyword" type="text" />
						<input value="검색" type="submit" />
					</td>
				</tr>
				
			</table>
		</form>
		
		<table border="1" cellpadding="0" cellspacing="0" width="700">
			<tr>
				<th bgcolor="orange" width="100">번호</th>
				<th bgcolor="orange" width="200">제목</th>
				<th bgcolor="orange" width="150">작성자</th>
				<th bgcolor="orange" width="150">등록일</th>
				<th bgcolor="orange" width="100">조회수</th>
			</tr>
			
			<c:forEach items="${boardList }" var="board">
				<tr>
					<td>${board.seq }</td>
					<td align="left"><a href="getBoard.do?seq=${board.seq }">${board.title }</a></td>
					<td>${board.writer }</td>
					<td>${board.regDate }</td>
					<td>${board.cnt }</td>
				</tr>
			</c:forEach>
			
		</table>
		<br />
		<a href="insertBoard.jsp">새글 등록</a>
	</center>
</body>
</html>

searchCondition 파라미터값은 검색 조건을 제목으로 선택시 "TITLE", 내용으로 선택시 "CONTENT"로 설정되며 검색 키워드 설정 후 검색 버튼 클릭시 "/getBoardList.do" 요청을 서버에 전달하게 됩니다.

 

1-2 Command 객체 수정

"/getBoardList.do" 요청이 서버에 전달되면 BoardController의 getBoardList() 메소드 실행이 이뤄지는데 이때 사용자가 입력한 파라미터값들을 BoardVO인 Command 객체에 자동으로 채우기 위해 멤버변수 추가 선언을 합니다.

package com.springbook.biz.board;

import java.sql.Date;

// VO
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 BoardVO() {}
	
	public int getSeq() {
		return seq;
	}
	public void setSeq(int seq) {
		this.seq = seq;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public Date getRegDate() {
		return regDate;
	}
	public void setRegDate(Date regDate) {
		this.regDate = regDate;
	}
	public int getCnt() {
		return cnt;
	}
	public void setCnt(int cnt) {
		this.cnt = cnt;
	}
	
	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;
	}

	@Override
	public String toString(){
		return "BoardVO [seq=" + seq + ", title=" + title + ", writer=" + writer + ",content= " + content + ", regDate = " + regDate + ", cnt = " + cnt +"]";	
	}
}

 

 

2. Controller 구현

BoardController.java
@RequestMapping("/getBoardList.do")
	public String getBoardList(BoardVO vo,Model model) {
		// Null check
		if(vo.getSearchCondition() == null) vo.setSearchCondition("TITLE");
		if(vo.getSearchKeyword() == null) vo.setSearchKeyword("");
		
		// model 정보 저장
		model.addAttribute("boardList", boardService.getBoardList(vo));
		return "getBoardList.jsp";
	}

검색 조건과 키워드가 전달되지 않을때를 대비하여 null값에 대한 체크 로직을 추가합니다.

이때, 클라이언트가 로그인 후 getBoardList.do 요청으로 인해 해당 메소드가 호출되면 검색 조건,키워드 정보는 전달되지 않기에 BoardVO 객체의 searchCondition과 searchKetword 변수에 null 값이 설정되어 모든 정보를 조회할 수 있게 됩니다.이런 상황에선 기본값을 적절히 설정해 비즈니스 컴포넌트에 전달해야 합니다.

 

3. DAO 클래스 수정

package com.springbook.biz.board.impl;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Repository;

import com.springbook.biz.board.BoardVO;
import com.springbook.biz.common.JDBCUtil;

@Repository("boardDAO")
public class BoardDAO {
	// JDBC 관련 변수
	private Connection conn = null;
	private PreparedStatement stmt = null;
	private ResultSet rs = null;
	
	// sql 명령어들
	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";

	// 글 목록 조회
	public List<BoardVO> getBoardList(BoardVO vo){
		System.out.println("===> JDBC로 getBoardList() 기능 처리");
		List<BoardVO> boardList = new ArrayList<BoardVO>();
		try{
			conn = JDBCUtil.getConnection();
			if(vo.getSearchCondition().equals("TITLE")){
				stmt = conn.prepareStatement(BOARD_LIST_T);
			}else if(vo.getSearchCondition().equals("CONTENT")){
				stmt = conn.prepareStatement(BOARD_LIST_C);
			}
			stmt.setString(1, vo.getSearchKeyword());
			rs = stmt.executeQuery();
			while(rs.next()){
				BoardVO board = new BoardVO();
				board.setSeq(rs.getInt("SEQ"));
				board.setTitle(rs.getString("TITLE"));
				board.setWriter(rs.getString("WRITER"));
				board.setContent(rs.getString("CONTENT"));
				board.setRegDate(rs.getDate("REGDATE"));
				board.setCnt(rs.getInt("CNT"));
				boardList.add(board);
			}
		}catch(Exception e){
			System.err.println("getBoardList() ERR : " + e.getMessage());
		}finally{
			JDBCUtil.close(rs, stmt, conn);
		}
		return boardList;
	}
}

 

BoardDAOSpring 클래스 수정

package com.springbook.biz.board.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.springbook.biz.board.BoardVO;

// DAO
@Repository
public class BoardDAOSpring{
	@Autowired
	private JdbcTemplate jdbcTemplate;

	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";

		
	// 글 목록 조회
	public List<BoardVO> getBoardList(BoardVO vo){
		System.out.println("===>Spring JDBC로 getBoardList() 기능 처리");
		Object[] args = {vo.getSearchKeyword()};
		if(vo.getSearchCondition().equals("TITLE")){
			return jdbcTemplate.query(BOARD_LIST_T,args, new BoardRowMapper());
		}else if(vo.getSearchCondition().equals("CONTENT")){
			return jdbcTemplate.query(BOARD_LIST_C,args, new BoardRowMapper());
		}
		return null;
	}
}

기본적인 코드 구성은 BoardDAO 클래스와 동일하지만 검색 키워드 설정을 위해 Object 배열을 사용합니다.