JAVA/JAVA | Spring 학습기록

[Spring] 스프링이 태어난 이유_서블릿, JSP로 만든 MVC의 한계

kth990303 2021. 7. 6. 21:55
반응형

김영한님의 스프링 입문과 기본편을 듣고,

스프링 mvc편을 들으며 Servlet, JSP를 하면서 느낀 점은 

요즘은 스프링 프레임워크가 도입돼서 정말 다행이라는 점이다.

 

이렇게 느낀 이유를 아래에 포스팅해보겠다.

 


Servlet을 하면서

사실 나는 6개월 정도 전에 Spring을 아예 모르고 단순히 웹개발을 해보고 싶다는 마음에 커리큘럼을 알아보다가 완전 쌩 Servlet부터 배워보려고 Servlet을 처음 접한 적이 있다.

 

그 때 당시 내가 짜던 코드는 회원가입 서비스를 만들어보려고 책을 참고해서 변형해 만들어본 코드였는데,

MemberDAO, MemberVO, MemberServlet 세 개의 클래스 코드를 열심히 짠 기록이 깃헙에 남아있어 코드를 가져와보았다.

 

아래 코드는 HttpServlet을 상속받은 MemberServlet 클래스 코드이며, 필드로는 name, tier, password, joinDate가 있다.

백준 알고리즘 소그룹 회원명단을 만들어보려고 짜본 코드이다.

@WebServlet("/member2")
public class MemberServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
    	response.setContentType("text/html;charset=utf-8");
    	PrintWriter out=response.getWriter();	
    	MemberDAO dao=new MemberDAO();
    	List list=dao.listMembers();

    	out.print("<html><body>");
    	out.print("<table border=1><tr align='center' bgcolor='lightgreen'>");
    	out.print("<td>아이디</td><td>비밀번호</td><td>티어</td><td>가입일</td></tr>");
    
    
    	for (int i=0; i<list.size();i++){
    		MemberVO memberVO=(MemberVO) list.get(i);
    		String id=memberVO.getId();
    		String pwd = memberVO.getPwd();
    		String tier=memberVO.getTier();
			Date joinDate = memberVO.getJoinDate();
			out.print("<tr><td>"+id+"</td><td>"+
			                pwd+"</td><td>"+
			                tier+"</td><td>"+
			                joinDate+"</td></tr>");		
    	}
    	out.print("</table></body></html>");
    }
}

이 코드에는 가장 치명적인 불편함이 있다.

바로, Html 코드가 클래스 안에 존재하여 작성하기 매우 불편하다는 점이다!

당시 나는 이 점이 너무 불편하여 html 코드를 따로 VS Code에 작성하고 Eclipse(당시 난 흑클립스를  사용중이었다..)에 옮겨 작성하였다.

 

그리고 이 코드 뿐만 아니라, 다른 코드를 보면 불편한 점이 몇가지 더 생기는데,

바로 중복되는 코드를 항상 일일이 작성해야 된다는 점이다.

response에서 ContentType 설정과 PrintWriter 설정은 클래스마다 매번 해주어야 했다.

또한, MemberDAO 코드도 굉장히 불편했다.

아래 코드를 보자.

	private Connection con;
	private PreparedStatement pstmt;
	private DataSource dataFactory;

	public MemberDAO() {
		try {
			Context ctx = new InitialContext();
			Context envContext = (Context) ctx.lookup("java:/comp/env");
			dataFactory = (DataSource) envContext.lookup("jdbc/oracle");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public List<MemberVO> listMembers() {
		List<MemberVO> list = new ArrayList<MemberVO>();
		try {
			// connDB();
			con = dataFactory.getConnection();
			String query = "select * from t_member ";
			pstmt = con.prepareStatement(query);
			ResultSet rs = pstmt.executeQuery();
			while (rs.next()) {
				String id = rs.getString("id");
				// pwd, tier, joinDate도 마찬가지로 가져오기
				MemberVO vo = new MemberVO();
				vo.setId(id);
                		// pwd, tier, joinDate도 마찬가지로 설정하기
				list.add(vo);
			}
			rs.close();
			pstmt.close();
			con.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return list;
	}

원래 엄청 긴 코드인데, 너무 글이 길어질까봐 DB와 연동할 수 있게 기본세팅하고 회원 리스트 출력하는 부분의 코드만 가져와본 것이다(...) 실제로는 회원추가 메소드 등도 있다.

굉장히 긴 코드임을 알 수 있다.

 

저 MemberDAO 생성자 부분은 모든 DAO 클래스마다 반드시 존재해야 했으며,

메소드들을 작성할 때마다 쿼리문을 일일이 작성하고(이 점은 JPA로 해결가능), 이상한 prepareStatement, dataFactory 설정을 매번 해주어야 하는 불편함이 존재했다.

즉, 중복되는 코드를 너무 많이 작성해야 했다는 점이다.

 

사실 이 때는 MVC 패턴을 적용하지 않았기도 하고, 

Repository, Service, Controller 분리를 따로 하지 않아 더더욱 코드가 길었다.

만약 이 코드들을 유지보수해야 됐다면 굉장히 끔찍했을 것이다.


다행히 김영한님께 Servlet을 배울 때는 domain과 Service를 분리하고, JSON형태의 데이터를 주고받으면서 좀 더 간편하게 작업할 수 있었다.

@WebServlet(name = "responseJsonServlet", urlPatterns = "/response-json")
public class ResponseJsonServlet extends HttpServlet {

 	private ObjectMapper objectMapper = new ObjectMapper();
    
 	@Override
 	protected void service(HttpServletRequest req, HttpServletResponse res) 
    throws ServletException, IOException {
 		response.setHeader("content-type", "application/json");
 		response.setCharacterEncoding("utf-8");
        
        	data.setUsername("kth990303");
       	 	data.setAge(20);
        
 		String result = objectMapper.writeValueAsString(data);
 		response.getWriter().write(result);
	}
}

맨 위의 MemberServlet 코드와 다른 점은, @WebServlet 코드에 urlPatterns로 따로 api 주소명을 매핑을 해줬다는 점이다.

 

urlPatterns에서 맨 앞에 "/"를 빼먹는 경우가 많은데, 이렇게 작성하면  invalid url-pattern in servlet mapping

에러가 뜨므로 주의, 또 주의하자! (요즘은 서블릿 많이 안쓰니까 딱히 이런 에러를 볼 일이 없을 것 같긴 하지만)

 

만약 JSON 데이터를 주고받는 것이 아닌, html에 직접 데이터를 출력해주고 싶으면,

아까 봤던 MemberServlet 클래스 코드와 같이 Java 코드 안에 html을 삽입하면 된다. 물론 중간중간 큰따옴표와 +기호를 적어주는 것 잊지 말고 말이다^^...


JSP와 MVC를 하면서

다른 점은 다 참을만 했지만, 개발하면서 수천 줄의 코드를 짜면서 Java 코드 안에 html을 일일이 넣어야 되는 현상은 개발자 입장에서 굉장히 불편했을 것 같다.

고작 몇십줄 코드를 봤는데도 굉장히 불편하게 느껴지지 않는가? 큰따옴표와 +를 일일이 쳐가면서 html을 쳐야 한다니...

프론트 개발자든 백엔드 개발자든 굉장히 울화통이 터져버렸을 것 같다.

 

JSP는 이러한 단점을 개선해준다! 

Html 코드 안에 Java 코드가 존재하기 때문이다!

그러나 중복된 코드를 계속 작성해야 한다는 점, JSP 코드가 너무 방대해져버린다는 점은 여전히 큰 단점이다. 특히나 controller, domain, service를 분리하지 않는다면 더더욱이다.

 

Html코드 안에 Java 코드가 존재한다는 점은, Java코드를 작성함으로써 데이터도 관리하고, 클라이언트에게 데이터 내용들을 보여주기 위해 뷰 렌더링 역할도 맡는다는 점이다. 즉, 비즈니스 로직, 뷰 렌더링을 모두 맡으면서 코드가 방대해지고, 유지보수가 굉장히 어려워진다는 단점은 그대로 존재하는 셈이다.

따라서 하나의 코드에 부담을 덜어주는 역할을 해주기 위해 여기서 MVC가 도입된다.

 

또한, MVC 패턴이란 Model과 View, Controller로 나누어 작업하는 패턴을 말한다.

 

쉽게 말하자면 MVC 패턴이란,

Model에서 백엔드 데이터를 Controller에 보내주고,

Controller에서 매핑 작업을 담당하여 클라이언트에서 사이트 주소를 입력하면

Controller에서 View에게 전달해준 데이터를 띄워주는 패턴이다.

 

비즈니스 로직은 Servlet으로, 

뷰 렌더링은 JSP로 코드를 작성해보았다.

 

아래 코드는 회원을 저장하는 Servlet 비즈니스 로직이다.

@WebServlet(name="mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
    private MemberRepository memberRepository=MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse res) 
    throws ServletException, IOException {
        String username = req.getParameter("username");
        int age = Integer.parseInt(req.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        req.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, res);
    }
}

MemberRepository를 싱글톤 패턴으로 따로 만들었음을 확인할 수 있다.

싱글톤 패턴은 구현량이나 객체지향 관계적으로 단점들이 존재하여 좋지는 않은 패턴이다.

이 부분은 스프링으로 대체할 수 있으나, 현재 스프링을 사용하지 않고 있으므로 싱글톤 패턴으로 생성해주었다.

 

싱글톤 패턴 단점은 아래 블로그에서 볼 수 있다.

https://ssoco.tistory.com/65

 

깊이 있는 Singleton

Singleton의 개념을 알 수 있다 : 무엇이며 언제, 어떻게 사용하는가? Singleton의 단점을 알 수 있다 : 왜 안티패턴이라 불리는가? Spring에서 Singleton의 의미를 알 수 있다 : Spring bean, Single Object, Sin..

ssoco.tistory.com

viewPath를 JSP 코드 위치로 설정하여 뷰 렌더링 역할은 JSP로 넘겨줌을 알 수 있다.

dispatcher.forward는 리다이렉트 역할을 수행해주는데, 대신 리다이렉트와 다르게 서버 내부에서 JSP를 호출하기 때문이 URL 변경이 일어나지 않고, 클라이언트 또한 이를 인지할 수 없다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
성공
<ul>
    <li>id=${member.id}</li>
    <li>username=${member.username}</li>
    <li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

html 코드가 JSP 파일 안에 존재하며, html 문법이 아닌 JSP 문법 또한 존재함을 알 수 있다.

<%@ %>, ${} 와 같은 문법들이 바로 JSP 문법이다.

request에서 setAttribute로 데이터 객체 이름을 member로 넘겨줬기 때문에

JSP 파일에서 member 도메인 객체에 접근할 수 있는 것이다.

 

확실히 JSP로 옮기면서 Servlet만 사용했을 때보단 매우 편리함을 알 수 있다.

그러나, 이것도 여러 번 코드를 작성하다 보면 꽤 불편한 점들이 많이 보인다.

Dispatcher.forward는 매 클래스마다 작성해야 하며, 생산성 또한 좋지 않다. 이러한 중복되는 코드는 유지보수 면에서 큰 단점으로 작용한다.

그리고 HttpServletRequest, HttpServletResponse중 하나만 사용될 경우에도 반드시 HttpServlet을 상속받아 사용하기 때문에 두 코드를 모두 작성해놓아야 한다는 불편함이 있다. 또한, 이 코드들은 테스트 코드를 작성할 때 불편하다고 하기도 한다.


뭔가 점점 만들면서 스프링과 비슷하게 변화되고 있는 느낌이다.

Servlet에서의 불편함을 개선하기 위해 JSP가 생겨났고,

JSP에서의 불편함을 개선하기 위해 MVC 패턴을 도입했으며,

이 때 스프링에서의 Controller과 같이 매핑 작업을 따로 담당해주는 메소드가 있다는 점 등등. 아직 한참 부족하지만, 많이 닮아가고 있다는 느낌을 받지 않을 수 없을 듯하다.

 

MVC 패턴을 계속 공부하면서 

회원-게시판-댓글 기능 + restTemplate을 이용한 API 호출을 통해 여러가지 스프링 웹사이트를 만들어보고 싶다.

반응형