Programming/JPA ⁄Spring

JDBC 와 MYSQL 연동하기 6) MVC 패턴 적용하기

Shane_Park 2021. 4. 17. 13:03
반응형

1. 필요한 VO(Value Object) 객체를 제일 먼저 만들어 줬습니다.

package kr.or.ddit.vo;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Data
@EqualsAndHashCode(of="al_id")
@ToString
public class AlbaVO {
	private String al_id;
	private String al_name;
	private Integer al_age;
	private String al_zip;
	private String al_addr1;
	private String al_addr2;
	private String al_hp;
	private String gr_code;
	private String al_gen;
	private String al_mail;
	private String al_career;
	private String al_spec;
	private String al_desc;
	private String al_img;
}

 

페이징 처리와 검색을 위해서 미리 만둘어둔 PagingVO와 SearchVO 도 복사해서 넣어뒀습니다.

package kr.or.ddit.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 *	검색 조건과 검색 키워드를 이용한 단순 검색에 사용.
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchVO {
	private String searchType;
	private String searchWord;
}
package kr.or.ddit.vo;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
public class PagingVO<T> implements Serializable{
	
	public PagingVO(int screenSize, int blockSize) {
		super();
		this.screenSize = screenSize;
		this.blockSize = blockSize;
	}

	private int screenSize = 10 ;
	private int blockSize = 5;

	private int totalRecord;
	private int currentPage;
	private int totalPage;
	private int startRow;
	private int endRow;
	private int startPage;
	private int endPage;
	private List<T> dataList;
	
	private SearchVO simpleSearch;
	private T detailSearch;
	private Map<String, Object> searchMap;
	
	public void setTotalRecord(int totalRecord) {
		this.totalRecord = totalRecord;
		totalPage = totalRecord % screenSize == 0 ? totalRecord / screenSize : totalRecord / screenSize + 1;
	}
	
	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;
		startRow = (currentPage - 1) * screenSize;
		endRow = currentPage * screenSize;
		
		endPage = (currentPage + (blockSize -1)) / blockSize * blockSize;
		startPage = endPage - (blockSize - 1);
		
	}
	
	private static String aPattern = "<a href='#' data-page='%d'>[%s]</a>";
	private static String currentPagePtrn = "<a href='#'>[%s]</a>";
	
	public String getPagingHTML() {
		StringBuffer html = new StringBuffer();
		if(startPage > 1 ) {
			html.append(String.format(aPattern, (startPage-1), "이전"));
		}
		endPage = endPage < totalPage ? endPage : totalPage;
		for(int page = startPage; page<=endPage; page++) {
			if(page == currentPage) {
				html.append(String.format(currentPagePtrn, page+""));
			}else {
				html.append(String.format(aPattern, page, page+""));
			}
		}
		if(endPage < totalPage) {
			html.append(String.format(aPattern, (endPage+1), "다음"));
		}
		return html.toString();
	}
	
}

 

2. DAO Interface를 만들어줬습니다.

package kr.or.ddit.dao;

import java.util.List;

import kr.or.ddit.vo.AlbaVO;
import kr.or.ddit.vo.PagingVO;

public interface AlbaDAO {
	/**
	 * @param pagingVO
	 * @return selected albaList
	 */
	public List<AlbaVO> selectAlbaList(PagingVO<AlbaVO> pagingVO);
	/**
	 * @param al_id
	 * @return selected AlbaVO
	 */
	public AlbaVO selectAlba(String al_id);
	/**
	 * @param alba
	 * @return inserted row count
	 */
	public int insertAlba(AlbaVO alba);
	/**
	 * @param alba
	 * @return inserted row count
	 */
	public int updateAlba(AlbaVO alba);
	/**
	 * @param pagingVO
	 * @return total record count
	 */
	public int selectTotalRecord(PagingVO<AlbaVO> pagingVO);
}

 

3. DAO 객체를 하나 구현했습니다.

package kr.or.ddit.dao;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import kr.or.ddit.utils.db.ConnectionFactory;
import kr.or.ddit.vo.AlbaVO;
import kr.or.ddit.vo.PagingVO;

public class AlbaDAOImpl implements AlbaDAO {
	private static AlbaDAOImpl self;
	private AlbaDAOImpl() {};
	
	public static AlbaDAOImpl getInstance() {
		if(self == null) self = new AlbaDAOImpl();
		return self;
	}
	
	@Override
	public List<AlbaVO> selectAlbaList(PagingVO<AlbaVO> pagingVO) {
		List<AlbaVO> albaList = new ArrayList<>();
		try(
			Connection conn = ConnectionFactory.getConnection();
		){
			String sql = "select * from alba";
			Statement statement = conn.createStatement();
			ResultSet result = statement.executeQuery(sql);

			while(result.next()) {
				AlbaVO alba = new AlbaVO();
				
				String al_id = result.getString(1);
				String al_name = result.getString(2);
				int al_age = result.getInt(3);
				String al_zip = result.getString(4);
				String al_addr1 = result.getString("al_addr1");
				String al_addr2 = result.getString("al_addr2");
				String al_hp = result.getString("al_hp");
				String gr_code = result.getString("gr_code");
				String al_gen = result.getString("al_gen");
				String al_mail =result.getString("al_mail");
				String al_career = result.getString("al_career");
				String al_spec = result.getString("al_spec");
				String al_desc = result.getString("al_desc");
				String al_img = result.getString("al_img");
				
				alba.setAl_id(al_id);
				alba.setAl_name(al_name);
				alba.setAl_age(al_age);
				alba.setAl_zip(al_zip);
				alba.setAl_addr1(al_addr1);
				alba.setAl_addr2(al_addr2);
				alba.setAl_hp(al_hp);
				alba.setGr_code(gr_code);
				alba.setAl_gen(al_gen);
				alba.setAl_mail(al_mail);
				alba.setAl_career(al_career);
				alba.setAl_spec(al_spec);
				alba.setAl_desc(al_desc);
				alba.setAl_img(al_img);
				
				albaList.add(alba);
				
			}
		} catch (SQLException e) {
			e.printStackTrace();

		}
		return albaList;
	}

	@Override
	public AlbaVO selectAlba(String al_id) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int insertAlba(AlbaVO alba) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int updateAlba(AlbaVO alba) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public int selectTotalRecord(PagingVO<AlbaVO> pagingVO) {
		// TODO Auto-generated method stub
		return 0;
	}

}

 

DataMapper의 힘을 빌리지 않고 객체에 매핑시키려니 여간 귀찮은게 아닙니다.

4. 테스트를 해봐야겠죠? 얼마전 학원에서 배운 JUnit을 활용해보았습니다.

위에서 Source folder 가 alba/src/test/resource로 되어 있었는데 그걸 확인 안하고 그냥 했다가 'no junit tests found' 에러로 애먹었었습니다. 혹시 jUnit test를 찾지 못한다면 경로를 한번 확인해보세요.

새로 구현한 메서드만 테스트 해 봅니다.

package kr.or.ddit.dao;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.util.List;

import org.junit.Test;

import kr.or.ddit.vo.AlbaVO;

public class AlbaDAOImplTest {
	private AlbaDAO dao = AlbaDAOImpl.getInstance();
	
	@Test
	public void testSelectAlbaList() {
		List<AlbaVO> list = dao.selectAlbaList(null);
		assertNotNull(list);
		assertEquals(2, list.size());
	}

}

위와 같이 테스트 코드를 작성했습니다. DB에 데이터가 2개 있으니 정상적으로 작동한다면 list의 size가 2가 될겁니다.

테스트 결과가 정상으로 나옵니다! list size 비교를 3으로 한다면?

3과 비교하면 위와 같이 3을 예상했지만 2의 결과가 나왔다는 Failure를 확인할 수 있습니다. Dao는 정상적으로 잘 만들어 진 것으로 보입니다. 확실히 하기 위해 Console에 출력도 해 보았습니다.

한땀한땀 찍어낸 보람이 있습니다.

 

5. 이제 Service를 만들어야 합니다. 그에 앞서 서비스 결과에 대한 간단한 ServiceResult enum을 만들어 주었습니다.

package kr.or.ddit.enumpkg;

public enum ServiceResult {
	OK, FAIL, NOTEXIST, INVALIDPASSWORD, PKDUPLICATED
}

 

AlbaService interface도 간단하게 만들어줍니다.

package kr.or.ddit.service;

import java.util.List;

import kr.or.ddit.enumpkg.ServiceResult;
import kr.or.ddit.vo.AlbaVO;
import kr.or.ddit.vo.PagingVO;

public interface AlbaService {
	/**
	 * @param pagingVO
	 * @return selected albaList
	 */
	public List<AlbaVO> retrieveAlbaList(PagingVO<AlbaVO> pagingVO);
	/**
	 * @param al_id
	 * @return selected AlbaVO
	 */
	public AlbaVO retrieveAlba(String al_id);
	/**
	 * @param alba
	 * @return OK, FAIL, PKDUPLICATED
	 */
	public ServiceResult insertAlba(AlbaVO alba);
	/**
	 * @param alba
	 * @return OK, FAIL, NOTEXIST
	 */
	public ServiceResult updateAlba(AlbaVO alba);
	public int selectTotalRecord(PagingVO<AlbaVO> pagingVO);
}

 

구현체도 만들어줍니다. 아직은 딱히 할게 없습니다.

나중에 ORM(object-relational mapping) Framework 를 추가하더라도 (Mybatis 등...) Persistence Layor인 DAO만 손보면 됩니다. Service는 Logic Layer, 혹은 Business Layer로 불립니다.

package kr.or.ddit.service;

import java.util.List;

import kr.or.ddit.dao.AlbaDAO;
import kr.or.ddit.dao.AlbaDAOImpl;
import kr.or.ddit.enumpkg.ServiceResult;
import kr.or.ddit.vo.AlbaVO;
import kr.or.ddit.vo.PagingVO;

public class AlbaServiceImpl implements AlbaService {
	private AlbaDAO dao = AlbaDAOImpl.getInstance();
	private static AlbaServiceImpl self;
	
	private AlbaServiceImpl() {}
	public static AlbaServiceImpl getInstance() {
		if(self == null) new AlbaServiceImpl();
        // 위의 코드에 버그가 숨겨져 있습니다. 그냥 복사 붙여넣기 하면 안돼요! //
		return self;
	}

	@Override
	public List<AlbaVO> retrieveAlbaList(PagingVO<AlbaVO> pagingVO) {
		return dao.selectAlbaList(pagingVO);
	}

	@Override
	public AlbaVO retrieveAlba(String al_id) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public ServiceResult insertAlba(AlbaVO alba) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public ServiceResult updateAlba(AlbaVO alba) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int selectTotalRecord(PagingVO<AlbaVO> pagingVO) {
		// TODO Auto-generated method stub
		return 0;
	}

}

 

6. 테스트도 굳이 필요하진 않겠지만 연습삼아 해봅니다.

package kr.or.ddit.service;

import static org.junit.Assert.assertEquals;

import java.util.List;

import org.junit.Test;

import kr.or.ddit.vo.AlbaVO;

public class AlbaServiceImplTest {
	AlbaService service = AlbaServiceImpl.getInstance();
	@Test
	public void testRetrieveAlbaList() {
		List<AlbaVO> list = service.retrieveAlbaList(null);
		assertEquals(list.size(),2);
	}

}

테스트가 필요하지 않다구요..? NullPointException이 발생했습니다.

일단 service 객체를 잘 받아오는지 먼저 확인해 보았습니다.

service 객체를 제대로 받아오지 못합니다. 원인이 여기에 있습니다.

singleton으로 service 객체를 관리하는 코드를 살펴보니 new AlbaServiceImpl()만 해두고 할당을 안해줬습니다. 예전에도 단위 테스트를 아직 배우지 않았을때 뷰, 컨트롤러, 로직, persistence, DB 다 붙여놓고 같은 실수로 500 에러가 떠서 찾느라 엄청 고생했던 기억이 납니다.

package kr.or.ddit.service;

import java.util.List;

import kr.or.ddit.dao.AlbaDAO;
import kr.or.ddit.dao.AlbaDAOImpl;
import kr.or.ddit.enumpkg.ServiceResult;
import kr.or.ddit.vo.AlbaVO;
import kr.or.ddit.vo.PagingVO;

public class AlbaServiceImpl implements AlbaService {
	private AlbaDAO dao = AlbaDAOImpl.getInstance();
	private static AlbaServiceImpl self;
	
	private AlbaServiceImpl() {}
	
	public static AlbaServiceImpl getInstance() {
		if(self == null) self = new AlbaServiceImpl();
		return self;
	}

	@Override
	public List<AlbaVO> retrieveAlbaList(PagingVO<AlbaVO> pagingVO) {
		return dao.selectAlbaList(pagingVO);
	}

	@Override
	public AlbaVO retrieveAlba(String al_id) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public ServiceResult insertAlba(AlbaVO alba) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public ServiceResult updateAlba(AlbaVO alba) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int selectTotalRecord(PagingVO<AlbaVO> pagingVO) {
		// TODO Auto-generated method stub
		return 0;
	}

}

ServiceImpl에서 문제되었던 부분을 수정하고 다시 테스트 해봅니다.

테스트를 이제야 통과합니다. TDD ( Test Driven Development ) 개념을 이래서 배우나 봅니다..

 

7. 이제 Controller를 작성합니다. 단순하게 service만 호출해서 Request Scope에 알바생 리스트를 담아 보내고, ViewResolver에 논리적 뷰 네임 "test/test06" 을 보내는 컨트롤러 입니다.

package kr.or.ddit.test;

import java.io.IOException;
import java.util.List;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;

import kr.or.ddit.mvc.annotation.Controller;
import kr.or.ddit.mvc.annotation.RequestMapping;
import kr.or.ddit.service.AlbaService;
import kr.or.ddit.service.AlbaServiceImpl;
import kr.or.ddit.vo.AlbaVO;

@Controller
public class Test06MVC extends HttpServlet{
	private static final long serialVersionUID = 1L;
	AlbaService service = AlbaServiceImpl.getInstance();

	@RequestMapping("/test06.do")
	public String test05(HttpServletRequest req) throws IOException{
		List<AlbaVO> albaList = service.retrieveAlbaList(null);
		req.setAttribute("albaList", albaList);
		String view = "test/test06";
		
		return view;
	}
	
}

 

논리적 뷰네임을 저렇게 반환 하면, FrontController에서 request인지 foward인지 판단한 뒤에 , forward 이므로 ( redirect시에는 뷰네임 앞에 redirect:를 붙입니다) ViewRevolser에서 prefix(ServletContextPath/WEB-INF/views)와 suffix (.jsp)를 붙여 forwarding 시킵니다.

 

test06.jsp 도 간단하게 만들어봅니다.

EL (Expression Language)과 JSTL을 배웠으니,  활용해봅니다.

<%@ 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>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<jsp:include page="/includee/preScript.jsp" />
</head>
<body>
<h4>Test 06</h4>
<table class="table table-dark table-striped">
	<c:forEach items="${albaList }" var="alba" varStatus="vs" >
		<tr>
			<th>알바후보 ${vs.count}번</th>
			<td>${alba}</td>
		</tr>
	</c:forEach>
</table>
<jsp:include page="/includee/postScript.jsp"/>
</body>
</html>

 

Jquery와 BootStrap을 활용하기 위해 preScript와 postScript를 넣었는데요, 굳이 넣지 않아도 테스트에는 영향이 없습니다.

성공적으로 MVC 패턴을 적용했습니다! 

반응형