수업시간에 이어 Spring MVC를 이용한 주소록 만들기를 계속했다. 수업에서는 Select(조회)와 Insert(등록) 기능을 배웠는데, 어제 미리 공부한 덕분인지 금방 해결할 수 있었다.
코드가 DispatcherServlet부터 시작해서 Controller, DAO, Mapper까지 정해진 흐름대로 연결되다 보니, 마치 정해진 틀에 끼워 맞추기만 하면 되는 느낌이라 쉽고 재밌었다. 물론 그 과정에서 자잘한 오타 때문에 계속 오류가 나서 틀린 그림 찾기를 하는 기분도 들었지만 말이다.
수업 진도가 끝난 후, 남은 자습 시간을 활용해 Update(수정)와 Delete(삭제) 기능까지 직접 완성해 보았다. 역시나 기본적인 방식은 비슷했다.
Update 기능은 크게 두 단계로 나뉜다.
먼저, 리스트에서 특정 항목을 수정하기 위해 해당 데이터의 고유 ID(abId)를 받아 DB에서 정보를 가져온다. 이때 URL 경로에 변수를 포함시키는 @PathVariable을 사용했다.
AddrbookController.java
@RequestMapping("/addrbook_edit_form.do/abId={abId}")
public String edit(@PathVariable("abId") int abId, HttpServletRequest req) {
AddrBookVO edit = dao.getDB(abId);
req.setAttribute("ab", edit); // <--- 바로 이 부분!
return "addrbook_edit_form";
}
💡 TIP:
@PathVariable사용 컨벤션
위 코드처럼/path/key=value형식도 동작은 하지만, 보통은/path/{value}와 같이 URL 경로의 일부로 변수를 넣는 방식을 더 많이 사용합니다. 예를 들어@RequestMapping("/addrbook_edit_form/{abId}")와 같이 작성하면 좀 더 RESTful한 URL 디자인 컨벤션에 가까워집니다.
AddrBookDAO.java
public AddrBookVO getDB(int abId) {
return session.selectOne("getDB", abId);
}
여기서 생각지도 못한 문제에 부딪혀 거의 4시간을 헤맸다. 컨트롤러에서 분명히 데이터를 request에 담아 JSP로 넘겼는데, 화면은 정상적으로 전환되면서 값만 텅 비어서 나오는 것이었다.
원인은 아주 사소했다.
addrbook_edit_form.jsp
<%-- request scope에 담긴 "ab"라는 이름의 객체를 AddrBookVO 타입으로 사용하겠다. --%>
<jsp:useBean id="ab" scope="request" class="lx.edu.springmvc.AddrBookVO" />
<input type="text" name="abName" value="${ab.abName}">
...
바로 <jsp:useBean>의 id 속성값이었다. 이 id 값은 컨트롤러에서 req.setAttribute("여기!!", vo)로 지정한 첫 번째 인자(key 값)와 정확히 일치해야 했다. 이전 페이지에서 복사-붙여넣기 하면서 id가 data로 되어 있었던 것. id="data"를 id="ab"로 바꾸자마자 모든 것이 정상적으로 동작했다. 정말 허탈했지만, 덕분에 JSP와 Controller 간의 데이터 전달 원리를 확실히 몸에 새길 수 있었다.
수정 폼에서 '수정' 버튼을 누르면, 폼 데이터는 AddrBookVO 객체에 자동으로 매핑되어 update 메소드로 전달된다. 이후 DAO를 통해 DB를 업데이트하고, 다시 목록 페이지로 리다이렉트 시킨다.
AddrbookController.java
@RequestMapping("/update.do")
public String update(AddrBookVO vo) {
dao.updateDB(vo);
return "redirect:addrbook_list.do";
}
💡 TIP:
redirect:경로 지정
redirect:addrbook_list.do처럼 상대경로를 사용하면 현재 URL 위치에 따라 의도치 않은 경로로 이동할 수 있습니다.redirect:/addrbook_list.do와 같이 슬래시(/)로 시작하는 절대경로를 사용하면 이런 실수를 방지할 수 있어 더 안전합니다.
AddrBookDAO.java
public AddrBookVO updateDB(AddrBookVO ab){
return this.session.selectOne("updateDB", ab);
}
📝 NOTE: MyBatis의 반환 타입
위 코드는update후selectOne을 호출하도록 되어 있는데, 보통 MyBatis에서update,delete,insertSQL을 실행할 때는 반영된 행의 개수(int)를 반환하도록 하거나, 반환값이 굳이 필요 없다면void로 선언하는 경우가 더 일반적입니다. 예를 들어this.session.update("updateDB", ab);처럼요.
삭제는 업데이트보다 훨씬 간단했다. 수정 폼 페이지에 '삭제' 버튼을 만들고, 클릭 시 확인 창을 띄운 후 지정된 URL로 ID 값을 넘겨주기만 하면 된다.
JavaScript의 confirm() 함수로 사용자에게 재확인하고, location.href를 이용해 삭제를 처리할 컨트롤러의 URL을 호출했다.
addrbook_edit_form.jsp
<input type="button" value="삭제" onClick="delcheck()"></td>
<script>
function delcheck() {
result = confirm("정말로 삭제하시겠습니까 ?");
if (result == true) {
document.location.href = "/delete.do/abId=${ab.abId}";
document.form1.submit();
} else
return;
}
</script>
💡 TIP: JavaScript 동작 방식
위 스크립트에서document.location.href가 실행되는 순간 페이지는 즉시 다른 주소로 이동을 시작합니다. 그래서 바로 다음 줄에 있는document.form1.submit()코드는 실행될 기회조차 없습니다. 이런 GET 방식의 요청에서는location.href한 줄만으로 충분합니다!
@PathVariable로 ID를 받아 DAO에 전달하여 DB에서 해당 데이터를 삭제한다. 작업이 끝나면 목록 페이지로 리다이렉트한다.
AddrbookController.java
@RequestMapping("/delete.do/abId={abId}")
public String delete(@PathVariable("abId") int abId, HttpServletRequest req){
dao.deleteDB(abId);
return "redirect:addrbook_list.do";
}
AddrBookDAO.java
public AddrBookVO deleteDB(int abId){
return this.session.selectOne("deleteDB", abId);
}
HttpServletRequest vs Model재미나이가 내가 HttpServletRequest req를 사용하는 것을 보고, 그건 예전 방식이니 Model 인터페이스를 사용하는 게 더 세련된 방법이라고 조언해주었다. Model을 사용하면 Spring이 알아서 객체를 만들어 주입해주기 때문에 Servlet API에 대한 의존성을 낮출 수 있다고 한다. 확실히 더 좋은 방법인 것은 알겠지만, 지금은 일단 배운 대로 HttpServletRequest에 익숙해지는 시간을 가져보기로 했다. 차근차근 나아가자.
결국 자습 시간 동안 CRUD 기능을 모두 완성했다. 특히 4시간 동안 나를 괴롭혔던 문제를 해결했을 때의 쾌감은 정말 짜릿했다. Spring MVC의 구조가 점점 손에 익는 것이 느껴진다. 다음 기능 구현은 또 어떤 재미와 시련을 안겨줄지 기대된다.