-월요일부터 어제까지는 스프링 프레임워크의 환경과 동작 순서 환경설정 그리고 기능들에 대해 살펴봤고 스프링 프레임워크의 장점을 개념적으로만 살펴봤다. 그리고 오늘은 회원가입 페이지를 작성하면서 실제로 jsp에서 작성했던 것을 스프링 프레임워크에서는 어떤식으로 작성하는지, 또 어떤 부분이 편하고 좋은지 직접 경험해봤다. 물론 아직은 조금 낯설기 때문에 굉장히 어려웠지만 그래도 엄청나게 이해하기 어렵진 않았다. 아무래도 jsp에서 조금 겪었던 로직이기 때문이 아닐까 싶다.
-우선 regForm.jsp 화면을 작성해봤다. 그런데 스프링 프레임워크에서는 url과 포트번호를 작성하고 /regForm을 했을 때 어떤 파일을 찾아가는지 따로 알려줘야 한다. 그래서 우선은 해당 jsp파일을 작성하기 전에 컨트롤러에서 보여줘야 하는 view의 이름을 알려줘야 한다.
@RequestMapping(value="/user/regForm", method=RequestMethod.GET)
public String regForm(HttpServletRequest request, HttpServletResponse response)
{
//쿠키 값 가져옴
String cookieUserId = CookieUtil.getHexValue(request, AUTH_COOKIE_NAME);
if(StringUtil.isEmpty(cookieUserId))
{
//쿠키가 비어있음
return "/user/regForm";
}
else
{
CookieUtil.deleteCookie(request, response, AUTH_COOKIE_NAME);
return "redirect:/";
}
}
위에서 쿠키명인 AUTH_COOKIE_NAME은 해당 클래스의 아래처럼 정의되어 있다.
// 쿠키명
@Value("#{env['auth.cookie.name']}")
private String AUTH_COOKIE_NAME;
-오늘 확실하게 깨달은 것인데, 보여주는 페이지에 대한 부분은 리턴타입이 해당 jsp파일의 이름인 String이 된다. 그 String이 viewResolver가 가지고 있는 접두어 접미어와 합쳐져서 실제 파일의 경로를 찾아서 해당 jsp파일을 화면으로 보여주게 되는 것이다.
이제 진짜 regForm.jsp를 작성한다. 전과는 다르게, 아이디 중복체크를 먼저 작성했다. 아이디 중복체크는 기존의 페이지에서 새로운 페이지를 보여줄 필요가 없다. 즉 보여줄 view가 없이 그냥 db에만 접속해서 해당 결과를 요청부에 알려주면 된다. 화면은 보여줄 것이 없고 그냥 그 화면 그대로 사용하기 때문에 비동기 통신인 ajax통신을 활용해서 작성해줬다.
//ajax
//xhr.setRequestHeader("AJAX", "true"); 요거는 이제 헤더부분에 이건 AJAX 통신이야 알려주는 것
$.ajax({
type: "POST",
url: "/user/idCheck",
data:
{
userId: $("#userId").val()
},
datatype: "JSON",
beforeSend:function(xhr)
{
xhr.setRequestHeader("AJAX", "true");
},
success:function(response)
{
if(response.code == 0)
{
//정상일 때
fn_userReg();
}
else if(response.code == 100)
{
//중복 아이디 존재
alert("중복된 아이디 입니다.");
$("#userId").focus();
}
else if(response.code == 400)
{
//파라미터 값이 넘어가지 않음.
alert("인수 값이 잘못되었습니다.");
$("#userId").focus();
}
else
{
alert("오류가 발생했습니다.");
$("#userId").focus();
}
},
complete:function(data)
{
icia.common.log(data); //잘 사용하지 않으나, 로그용임.
},
error:function(xhr, status, error)
{
icia.common.error(error);
}
});
-ajax통신의 url: "/user/idCheck"
부분을 보면 해당 url로 보낸다는 뜻인데, 스프링 프레임워크에서는 디스패쳐가 컨트롤러를 찾을 때 해당 url의 값을 가지고 RequestMapping어노테이션을 찾아간다. 따라서 유저 컨트롤러에 대항 부분을 작성해준다. 단, 만약 인터셉터가 존재한다면 해당 인터셉터를 먼저 거치는게 순서이다. 다만 현재의 코드에서는 인터셉터를 거치지 않는 경로들을 미리 설정했는데, regForm이 이에 해당된다.
-이제 유저 컨트롤러쪽에 어노테이션과 함께 코드를 작성해준다.
//아이디 체크, 중복확인
//response를 받아야 하기 때문에 어노테이션과 리턴값을 저렇게 함.
@RequestMapping(value="/user/idCheck", method=RequestMethod.POST)
@ResponseBody
public Response<Object> idCheck(HttpServletRequest request, HttpServletResponse response)
{
String userId = HttpUtil.get(request, "userId");
Response<Object> ajaxResponse = new Response<Object>();
if(!StringUtil.isEmpty(userId))
{
//값 있음
if(userService.userSelect(userId) == null )
{
//아이디가 없음
ajaxResponse.setResponse(0 , "Success");
}
else
{
ajaxResponse.setResponse(100 , "Duplicate ID");
}
}
else
{
//값 없음
ajaxResponse.setResponse(400 , "Bad request");
}
return ajaxResponse;
}
-위의 코드를 보면 view를 보여주는 String 리턴타입을 가진 위와 다른 것을 볼 수 있다. 왜냐하면 비동기 통신으로 보여줄 페이지가 없고 그냥 결과만 알려주면 되기 때문에 리턴타입은 Response객체가 된다. 그리고 @ResponseBody는 바로 뷰리졸버를 거치지 않고 서버의 결과만 응답으로 보내는 비동기 통신에 대해서 해주는 어노테이션이라고 생각하면 편하다.
-위의 userSelect메소드는 userService에 정의되어 있는 것을 알 수 있다. 컨트롤러가 서비스를 호출해서 해당 서비스의 메소드를 사용하는 것이다.
public User userSelect(String userId)
{
User user = null;
try
{
user = userDao.userSelect(userId);
}
catch(Exception e)
{
logger.error("[UserService] userSelect Exception", e);
}
return user;
}
-유저 셀렉트 메소드는 이런식으로 간단하게 정의되어 있다. 유저 서비스에서는 유저 다오를 통해 해당 쿼리를 날리게 되는데, 실제로 유저 다오는 아무것도 정의되어 있지 않은 인터페이스이다.
public User userSelect(String userId);
-그러면 실제로 해당 쿼리문은 어디에 작성이 되어 있냐? 바로 userDao.xml에 작성이 되어 있다.
<resultMap type="com.icia.web.model.User" id="userResultMap">
<id column="USER_ID" property="userId" />
<result column="USER_PWD" property="userPwd" />
<result column="USER_NAME" property="userName" />
<result column="USER_EMAIL" property="userEmail" />
<result column="STATUS" property="status" />
<result column="REG_DATE" property="regDate" />
</resultMap>
<select id="userSelect" parameterType="string" resultMap="userResultMap">
SELECT
USER_ID,
NVL(USER_PWD, '') AS USER_PWD,
NVL(USER_NAME, '') AS USER_NAME,
NVL(USER_EMAIL, '') AS USER_EMAIL,
NVL(STATUS, 'N') AS STATUS,
NVL(TO_CHAR(REG_DATE, 'YYYY.MM.DD HH24:MI:SS'), '') AS REG_DATE
FROM
TBL_USER
WHERE
USER_ID = #{value}
</select>
-여기서 resultMap은 바로 ResultSet객체와 비슷한 역할을 한다고 보면 된다. 그리고 해당 resultMap의 컬럼명은 쿼리문의 알리아스 명과 일치해야 하고, resultMap의 property는 resultMap type에 적혀있는 User.java의 변수명과 일치해야 한다. 그래야 쿼리의 결과와 해당 유저 객체의 각각의 변수와 컬럼값을 매핑해줄 수 있기 때문에 이름이 맞아야 한다. 또한 결과적으로 userDao.xml은 userDao.java 인터페이스의 추상메소드를 오버라이딩한 것이기 때문에 반드시 리턴타입과 메소드명이 parameterType과 id와 일치해야한다.
-그렇다면 왜 이렇게 해놓는 것일까?
-나도 처음엔 이렇게 나누는 이유를 명확하게 알지 못했지만 오늘 코딩을 해보니, 이렇게 구분해놓으면 쿼리문에 집중하는 사람은 정말 쿼리문만 집중하고 쿼리결과로 로직 처리에 대한 작성을 하는 사람은 해당 로직에만 신경쓸 수 있고, 화면을 보여주는 사람들은 .jsp에만 집중하면 되기때문에 분업이 굉장히 편하게 될 것 같다고 느꼈다. 또한, 어떤 오류가 났을 때, 로그를 보면 어느 부분이 확실하게 문제가 있구나를 확실하게 알 수 있고 해당 부분을 빠르게 수정할 수 있다. 그리고 뭔가 로직의 변화나 쿼리의 변화가 생긴다면 그냥 그 부분만 수정하면 되니까 전체적인 흐름에 큰 영향이 안간다는 것도 굉장한 장점이라고 생각한다. 그렇게 생각하니 왜 이런식으로 분할을 했는지 어느정도는 이해가 갔다.
-이제 아이디 중복체크가 끝났으니 다음으로 회원가입을 실제로 진행해야 한다. 그래서 다시 regForm.jsp에서 ajax통신을 통해서 해당 form태그의 입력한 값들을 비동기 통신으로 보낸다.
//ajax
$.ajax({
type: "POST",
url: "/user/regProc",
data:
{
userId: $("#userId").val(),
userPwd: $("#userPwd").val(),
userName: $("#userName").val(),
userEmail: $("#userEmail").val()
},
datatype: "JSON",
beforeSend:function(xhr)
{
xhr.setRequestHeader("AJAX", "true");
},
success:function(response)
{
if(response.code == 0)
{
//정상일 때
alert("회원가입이 되었습니다.");
location.href = "/board/list";
}
else if(response.code == 100)
{
//중복 아이디 존재
alert("중복된 아이디 입니다.");
$("#userId").focus();
}
else if(response.code == 400)
{
//파라미터 값이 넘어가지 않음.
alert("인수 값이 잘못되었습니다.");
$("#userId").focus();
}
else if(response.code == 500)
{
alert("회원가입 중 오류가 발생했습니다.");
$("#userId").focus();
}
else
{
alert("오류가 발생했습니다.");
$("#userId").focus();
}
},
complete:function(data)
{
icia.common.log(data); //잘 사용하지 않으나, 로그용임.
},
error:function(xhr, status, error)
{
icia.common.error(error);
}
});
-위에서도 언급했다시피 이제 디스패쳐가 해당 url의 값을 가지고 컨트롤러를 찾아가기 때문에 컨트롤러에 해당 url을 어노테이션으로 작성해준다.
//회원가입
@RequestMapping(value="/user/regProc", method=RequestMethod.POST)
@ResponseBody
public Response<Object> regProc(HttpServletRequest request, HttpServletResponse response)
{
Response<Object> ajaxResponse = new Response<Object>();
//우선 HTTP프로토콜로 넘어온 값들을 각각 받음
String userId = HttpUtil.get(request, "userId");
String userPwd = HttpUtil.get(request, "userPwd");
String userName = HttpUtil.get(request, "userName");
String userEmail = HttpUtil.get(request, "userEmail");
if(!StringUtil.isEmpty(userId) && !StringUtil.isEmpty(userPwd) && !StringUtil.isEmpty(userName) && !StringUtil.isEmpty(userEmail))
{
//넘어온 값이 다 있어야 함.
if(userService.userSelect(userId) == null)
{
//아이디 중복이 없음
//사이트 url로 바로 넘어온 경우에 이렇게 또 거르기 위해서 또 체크하는 거. 정상적인 경로라면 이미 걸러져있어야 함.
User user = new User();
user.setUserId(userId);
user.setUserPwd(userPwd);
user.setUserName(userName);
user.setUserEmail(userEmail);
user.setStatus("Y");
if( userService.userInsert(user) > 0)
{
//성공
ajaxResponse.setResponse(0, "Success");
}
else
{
//실패
ajaxResponse.setResponse(500, "Internal Server Error");
}
}
else
{
//아이디 중복, 오류코드 100번
ajaxResponse.setResponse(100, "Duplicate ID");
}
}
else
{
ajaxResponse.setResponse(400, "Bad Request");
}
return ajaxResponse;
}
-당연히 회원가입 여부는 따로 페이지를 보여줄 필요가 없고 비동기 통신으로 서버에서 결과만 얻어오면 되기 때문에 @ResponseBody어노테이션을 적어준다. 이제 유저 서비스를 통해서 해당 쿼리를 날려주는 메소드를 사용해준다. 물론 이미 이 시점에서는 userDao.xml과 userDao.java가 이미 매핑되어 있는 상태여야 한다. 하지만 양쪽에서 번갈아가며 중간으로 오는 것보단 한쪽에서 다른쪽 끝으로 가는 순서가 더 명확하기 때문에 후자의 순서로 작성을 했다.
//사용자 등록 서비스
public int userInsert(User user)
{
int count = 0;
try
{
count = userDao.userInsert(user);
}
catch(Exception e)
{
logger.error("[UserService] userInsert Exception", e);
}
return count;
}
-userDao.java는 인터페이스이기 때문에 추상메소드만 정의되어 있어서 굳이 여기다 적진 않겠다. 다만 메소드명과 xml의 id명이 같아야 한다는 점은 항상 명심해야 한다.
<insert id="userInsert" parameterType="com.icia.web.model.User">
INSERT INTO TBL_USER (
USER_ID,
USER_PWD,
USER_NAME,
USER_EMAIL,
STATUS,
REG_DATE
) VALUES (
#{userId},
#{userPwd},
#{userName},
#{userEmail},
#{status},
SYSDATE
)
</insert>
-인서트는 jsp에서 했던 것과 비슷하게 resultset객체가 필요하지 않다. 그래서 따로 ResultMap을 사용하지 않는다.
-아직 스프링 프레임워크에서 코드를 작성하는 것이 뭔가 불편하긴 한 상태이다. 안익숙하고 좀 더 복잡한 느낌이 물씬 들기 때문에 조금 어렵지만 그래도 완전히 이해가 가지 않는 것은 아니기 때문에 따라갈만은 한 것 같다. 지금 시간이 12시 25분이 넘었다는 것만 빼면 아주 이해를 못할 정도는 아닌 것 같다. 내일 수업을 위해서 빨리 운동하고 씻고 자야할 것 같다.
오늘도 고생했다 파이팅!!