✅ Spring MVC
기존 구조
나는 중간에서 모든 걸 처리해주는 FrontController, 적절한 컨트롤러를 찾아주는 ControllerMapper, 정확한 뷰의 경로로 변환시켜주는 ViewResolver를 모두 내가 직접 작성했다. 이 구조는 분명 좋은 구조이긴 했지만 일일이 개발자가 직접 구현해야 한다는 부분이 번거로웠다.
스프링은 이러한 부분을 모두 자동으로 처리해준다.
Spring MVC의 구조
기본적인 구조는 거의 동일한 것을 알 수 있다. 하지만 Spring MVC는 개발자에게 있어 상당한 편리함을 제공해준다.
자동화
FrontController - DispatcherServlet
HandlerMapping - ControllerMapper
ViewResolver - ViewResolver
라고 보면 된다.
Spring은 이러한 부분들을 모두 제공해 요청을 자동으로 처리해준다.
Model 개념의 변화
또한 Model을 이전까지는 비즈니스 로직이 들어있는 Service 계층과 데이터베이스에 접근하는 Repository 계층이라고 봤다면 Spring MVC에선 Service 계층과 Repository 계층을 통해 전달되는 데이터를 포함한 뷰에게 전달되야 하는 모든 데이터를 의미한다.
이를 통해,
개발자는 Controller, Service, Repository 개발에만 집중할 수 있게 되었다.
✅ Spring이 제공하는 편리함
그러면 구체적으로 Spring MVC가 어떤식으로 편리함을 제공하는지 내가 게시판 기능을 마이그레이션 하면서 느꼈던 점을 정리해 보고자 한다.
1️⃣ 서블릿 경로 분기가 편리하다.
Spring MVC를 도입하기 전 회원 컨트롤러
가독성을 위해 세부 로직은 생략했다.
public class MemberController implements MyController{
MemberService memberService = MemberService.getInstance();
@Override
public String process(HttpServletRequest req, HttpServletResponse resp) {
String action = Optional.ofNullable(req.getPathInfo()).orElse("");
System.out.println(action);
String viewName = "";
if(action.equals("/signup")) {
if(req.getMethod().equals("GET"))
viewName = getSignUpForm(); // GET: 회원가입 폼 요청
else if(req.getMethod().equals("POST"))
viewName = signup(req); // POST: 회원가입 요청
} else if(action.equals("/login")) {
if(req.getMethod().equals("GET"))
viewName = getLoginForm(); // GET: 로그인 폼 요청
else if(req.getMethod().equals("POST"))
viewName = login(req, resp); // POST: 회원가입 요청
} else if (action.equals("/logout")) {
viewName = logout(req); // POST: 로그아웃 요청
}
return viewName;
}
private String getSignUpForm() {
}
private String signup(HttpServletRequest req) {
}
/**
* [GET] /members/login: 로그인 폼 요청
* @return
*/
private String getLoginForm() {
}
/**
* [POST] /members/login: 로그인 요청
* @param req
* @param resp
* @return
*/
private String login(HttpServletRequest req, HttpServletResponse resp) {
}
/**
* [POST]/members/logout: 로그아웃 요청
* @param req
* @return
*/
private String logout(HttpServletRequest req) {
}
}
해당 코드에서는 process() 메서드에서 일일이 하나하나 문자열을 파싱해서 분기해줬다.
단순 경로 뿐만 아니라 메서드까지 나누어서 분기했다.
이는 코드를 매우 복잡하게 만든다.
Spring MVC는 이러한 문제를 해결해준다.
Spring MVC 기반 회원 컨트롤러
가독성을 위해 내부 로직은 생략했다.
@Controller
@RequestMapping("/members")
public class MemberController {
private MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/signup")
public String signupForm() {
// 회원가입 폼 제공
}
@PostMapping("/signup")
public ModelAndView signup(@ModelAttribute SignUpDTO signUpDTO) {
// 회원 가입 요청
}
@GetMapping("/login")
public String loginForm() {
// 로그인 폼 제공
}
@PostMapping("/login")
public ModelAndView login(@ModelAttribute LoginDTO loginDTO, HttpServletRequest req, HttpServletResponse resp) {
// 로그인 요청
}
@PostMapping("/logout")
public ModelAndView logout(HttpSession session) {
// 로그아웃
}
}
@RequestMapping(””)을 통해 컨트롤러의 공통 서블릿 경로를 지정하고
@GetMapping(), @PostMapping()을 통해 컨트롤러 내부 세부 경로를 나눔과 동시에 메서드까지 나누어준다.
이를 통해 더욱 깔끔하게 코드를 작성할 수 있게 해준다.
2️⃣ 경로에 변수가 있는 경우 그 변수에 접근하기가 편하다.
Spring MVC 도입 전 글 상세정보 요청 처리
가독성을 위해 일부 코드를 생략했다.
@Override
public String process(HttpServletRequest req, HttpServletResponse resp) {
String action = Optional.ofNullable(req.getPathInfo()).orElse("");
String viewName = "";
if (action.isEmpty()) {
} else if (action.startsWith("/write")) {
} else if (action.startsWith("/edit")) {
} else if (action.startsWith("/delete")) {
} else {
// 게시판 글 상세정보
Long viewId = Long.valueOf(action.substring(1));
viewName = viewBoard(req, viewId); // GET: 글 상세정보 요청
}
return viewName;
}
/**
* [GET] /boards/*: 글 상세정보 요청
* @param req
* @param viewId
* @return
*/
private String viewBoard(HttpServletRequest req, Long viewId) {
try {
Board board = boardService.read(viewId);
boardService.addReadCount(board); // 조회수 1 증가 시키기
req.setAttribute("board", board);
return "board/board";
} catch (BoardNotFoundException e) {
Alert.setAlertInfo(req, e.getMessage(), req.getContextPath() + "/boards");
return "alert";
}
}
일일이 경로 문자열을 파싱해서 해당 변수를 얻어냈다.
Spring MVC는 경로 문자열을 매우 편리하게 얻을 수 있게 해준다.
Spring MVC 기반 글 상세정보 요청 처리
@GetMapping("/{board_id}")
public ModelAndView getBoardInfo(@PathVariable("board_id") Long board_id) {
System.out.println("여기");
ModelAndView modelAndView = new ModelAndView();
try {
Board board = boardService.read(board_id);
boardService.addReadCount(board); // 조회수 1 증가 시키기
modelAndView.addObject("board", board);
modelAndView.setViewName("board/board");
} catch (BoardNotFoundException e) {
setAlertInfo(modelAndView, e.getMessage(), CONTEXT_PATH + "/boards");
modelAndView.setViewName("alert");
}
return modelAndView;
}
Spring MVC는 @PathVariable()이라는 어노테이션을 통해 이를 너무나도 편리하게 처리한다.
3️⃣ 넘어오는 파라미터를 객체 단위로 받을 수 있다.
Spring MVC를 도입하기 전 로그인 요청 처리
원래대로라면 다음과 같이 파라미터들을 req.getParameter()를 통해 받아왔어야 했다.
private String login(HttpServletRequest req, HttpServletResponse resp) {
String memberId = req.getParameter("memberid");
String password = req.getParameter("password");
String rememberMe = req.getParameter("rememberMe");
}
이러한 방법의 단점은 추후에 파라미터가 추가되거나 변경될 때 유지보수가 어렵고 테스트를 할 때 일일이 하나하나 값을 세팅해줬어야 한다.
또한 이 경우에는 해당이 안되긴하지만, 만약 파라미터의 타입이 String 이외의 int 혹은 boolean 값 등 다른 타입이 온다면, 그 타입에 대한 안정성이 보장되지 않는다
Spring MVC는 이러한 문제를 해결한다.
Spring MVC 구조에서의 로그인 요청 처리
가독성을 위해 일부 코드를 생략했다.
@PostMapping("/login")
public ModelAndView login(@ModelAttribute LoginDTO loginDTO, HttpServletRequest req, HttpServletResponse resp) {
String memberId = loginDTO.getMemberId();
String password = loginDTO.getPassword();
String rememberMe = loginDTO.getRememberMe();
// 기타 로직 생략
}
@ModelAttribute를 통해 파라미터들을 객체에 자동으로 매핑시켜준다.
이렇게 되면 추후에 파라미터가 추가되더라도 유지보수가 쉽고
테스트할 때도 그냥 DTO 객체를 만들기만 하면되기 때문에 훨씬 쉬워진다.
또한 String이외 타입이 파라미터로 와도 객체의 타입을 보고 직접 매핑을 시켜주기 때문에 타입에 대한 안정성이 올라간다.
4️⃣ 메서드 파라미터에 내가 원하는 객체를 넣을 수 있다.
Spring MVC 도입 이전의 로그아웃 처리 코드
가독성을 위해 일부 코드를 생략했다.
public String process(HttpServletRequest req, HttpServletResponse resp) {
String action = Optional.ofNullable(req.getPathInfo()).orElse("");
System.out.println(action);
String viewName = "";
if(action.equals("/signup")) {
} else if(action.equals("/login")) {
} else if (action.equals("/logout")) {
viewName = logout(req); // POST: 로그아웃 요청
}
return viewName;
}
/**
* [POST]/members/logout: 로그아웃 요청
* @param req
* @return
*/
private String logout(HttpServletRequest req) {
HttpSession session = req.getSession();
session.invalidate();
setAlertInfo(req, "로그아웃 되었습니다.", req.getContextPath());
return "alert";
}
해당 구조에서는 logout처리 과정에서 세션을 삭제하기 위해 HttpSevletRequest에 의존했다.
물론 메서드의 매개변수를 HttpSession으로 해서 호출 시 넣어줘도 되지만 이렇게 되면 process()메서드는 너무 복잡해진다.
Spring MVC는 이런 문제를 해결해준다.
Spring MVC 기반 로그아웃 처리
@PostMapping("/logout")
public ModelAndView logout(HttpSession session) {
ModelAndView modelAndView = new ModelAndView("alert");
session.invalidate();
setAlertInfo(modelAndView, "로그아웃 되었습니다", CONTEXT_PATH);
return modelAndView;
}
그냥 매개변수에 필요한 객체인 HttpSession을 넣어놓으면 알아서 해당 객체를 주입시켜준다.
이를 통해 코드를 더욱 직관적으로 짤 수 있게 된다.
✅ 느낀점
게시판의 기능이 단순하기 때문에 Spring MVC의 장점을 온전히 다 느끼진 못했지만 왜 개발자들이 Spring 프레임워크를 도입하는지를 깨닫기에는 충분했다.
물론 프레임워크를 도입하기 전에 어떤 과정으로 해당 프레임워크가 등장하게 됐는지는 이해할 필요가 있다고 생각한다. 그래야 프레임워크를 잘 활용할 수 있다고 생각한다.
더 열심히 공부해야겠다.
https://github.com/jhk01007/simple_board/tree/main/very-simple-board_ver3
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
게시판 만들기 - 기능추가: Pagination(페이지네이션) (0) | 2024.08.27 |
---|---|
게시판 만들기 - 데이터 계층 리팩토링(MyBatis 도입) (0) | 2024.08.26 |
게시판 만들기 - 웹 계층 리팩토링(FrontController 도입) (0) | 2024.08.21 |
게시판 만들기 - 구현 (0) | 2024.08.20 |
게시판 만들기 - 개요 (0) | 2024.08.20 |