이번 장에서는 미니 프로젝트 형식으로 트위터 앱을 개발해 본다. 앞에서 배운 JSP, Servlet, MVC 패턴 등이 모두 적용된 종합 예제로 bootstrap을 이용한 화면 디자인도 포함한다. 여러 사용자들이 게시글을 공유할 수 있도록 구성해 실제 서비스와 같은 느낌을 받을 수 있으며 향후 데이터베이스, 구글파이어베이스 등과 연동할 수 있도록 확장 가능한 구조로 설계한다.

프로그램 구성

프로그램은 트위터와 유사한 간단한 SNS 프로그램으로 로그인과 함께 간단한 메시지를 남길수 있는 웹서비스 이다.

파일명

설명

error.jsp

에러 표시 화면

inc_header.jsp

bootstrap 등 공통 기본 헤더 파일

twitterLogin.jsp

로그인 화면

twittList.jsp

트윗목록 화면

TwitterService.java

데이터서비스 인터페이스

EmbeddedService.java

메모리 데이터 서비스 구현 클래스

TwitterController.java

메인 컨트롤러

컨트롤러는 서블릿으로 구현하고 데이터베이스 없이 application scope 즉 ServletContext 객체의 속성으로 데이터를 저장하도록 구성한다.

프로그램의 동작 시나리오는 다음과 같다.

  1. 로그인, 세션이 활성화 되어 있다면 게시글 목록으로 이동
  2. 간단한 메시지를 작성하면 로그인한 이름과 시간등의 정보와 함께 서버에 저장
  3. 작성한 메시지는 접속한 모든 사용자들이 공유. 단, 변경내용은 자동으로 갱신되지 않고 Refresh 버튼을 눌러 새로운 게시글 확인 가능.

단순한 프로그램이지만 간단한 채팅 용도로도 활용 가능하며 실시간 데이터 갱신을 위해 자바스크립트 코드를 추가하거나 Rest API 방식의 서버 프로그램을 포함해 비동기 방식으로 실시간 데이터 갱신을 구현할수도 있다.

실행 화면

프로그램의 실행화면은 다음과 같다. 시작화면은 로그인 화면으로 컨트롤러 서블릿을 호출했을때 세션이 활성화 되어 있지 않다면 로그인 화면으로 자동 전환된다.

로그인 화면

아이디를 입력하고 엔터를 치커나 Login 버튼을 누르면 목록 화면으로 이동된다. 톰캣을 처음 실행한 것이라면 목록이 비어 있고 이후 메시지를 작성하고 엔터를 치면 글이 등록되어 목록에 나타난다.

목록화면_시작

동시에 여러 사용자의 접속이 가능하다. 세션중복을 피하기 위해 다른 종류의 브라우저 혹은 네트워크의 다른 기기에서 접속해 볼 수 있다. 서로 다른 이름으로 로그인된 상태에서 글을 작성하면 내용이 공유되고 Refresh 버튼을 누르면 추가된 메시지의 확인이 가능하다.

목록화면_사용

다음은 아이패드에서 실행한 화면이다. 컴퓨터와 동일한 와이파이에 연결된 태블릿이나 스마트폰등에서 컴퓨터의 ip주소로 접속이 가능하며 스마트폰의 경우 화면크기로 인해 버튼이 아래로 밀려 보이니 참고 한다.

아이패드

먼저 서버 에러 발생시 별도의 화면에서 에러처리가 될 수 있도록 설정한다. 정확하게는 서버에러로 웹 애플리케이션 실행중 발생하는 에러는 콘솔과 브라우저 화면에 표시 되는데 브라우저에 코드 일부가 노출되는 부분도 문제가 있고 일반 사용자들에게 신뢰를 주기 위해서는 별도의 에러 처리가 반드시 필요하다.

프로그램 실행중 발생할 수 있는 문제들은 별도의 에러메시지를 화면에 출력할 수 있도로 구성할 것이다. 우선 서버 에러 처리를 위해 다음과 같이 WEB-INF/web.xml 파일에 에러 처리 화면을 추가한다. 만일 web.xml 이 존재 하지 않다면 파일을 생성하고 내용을 복사해 넣도록 한다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" id="WebApp_ID" version="4.0">
  <display-name>jwprj</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
  <error-page>
  	<error-code>500</error-code>
  	<location>/common/error.jsp</location>
  </error-page>
</web-app>

다음으로 WebContent 아래에 common 폴더를 만들고 error.jsp 파일을 생성한 다음 다음과 같이 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" isErrorPage="true"%>    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
    
<%
	//이 부분이 있어야 브라우저 에러 화면이 아닌 서버 에러페이지가 표시됨.
	response.setStatus(HttpServletResponse.SC_OK);
%>
<c:set var="exception" value="${requestScope['javax.servlet.error.exception']}"/>		

<!DOCTYPE html>
<html>
<head>
<%@include file="inc_header.html" %>
<title>에러 페이지</title>
</head>
<body>
<div class="container shadow w-75 p-3 mt-5">
	<H2>에러 페이지</H2>
	<HR>
	<div class="bg-warning p-3">
		<strong>다음의 에러 메시지를 확인하세요!!</strong>
		<hr>
		Error : ${exception} <button class="ml-3 btn btn-outline-danger" data-toggle="collapse" data-target="#msg">Show/Hide</button>
		<hr>
		<div id="msg" class="collapse">		
		  <c:forEach var="stackTraceElem" items="${exception.stackTrace}">
		    <c:out value="${stackTraceElem}"/><br/>
		  </c:forEach>		
		  <hr>
		</div>
		<button class="btn btn-success" onclick="history.go(-1)">&lt;&lt; Back</button>
	</div>
</div>
</body>
</html>

bootstrap을 사용해 화면을 디자인 하고 서버로 부터 받은 exception 객체를 통해 필요한 부분만 에러를 표시하고 있다.

공통 css, js 포함을 위해 inc_header.html 를 include 하고 있다. 같은 폴더에 inc_header.html 파일을 다음과 같이 생성한다.

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

<!-- jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<!-- Popper JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>

<!-- Latest compiled JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>

inc_header.html 파일은 공통 meta 태그및 css, js 를 포함하는 파일이다. 이후 모든 뷰페이지에서도 사용된다.

트위터앱은 로그인 화면과 목록 화면으로 구성된다. 목록 화면에는 게시글 작성이 포함되며 v2에서 게시글 삭제 기능이 추가될 예정이다.

파일 생성과 기본 코드 작성

먼저 로그인 화면을 만들어 보자. WebContent 폴더에 twitter 폴더를 생성하고 twitterLogin.jsp 파일을 만든다.

JSTL 사용을 위해 taglib 지시어를 추가하고 기본 URL 경로를 baseUrl 로 선언해 둔다.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- 기본 URL 경로 지정 -->
<c:set var="baseUrl" value="${pageContext.request.contextPath}/twitter" />

jsp내에서 <a> 혹은 <form> 에서 서블릿이나 jsp 를 호출할때 직접 경로를 작성할 경우 환경변화에 따라 수정해야 하는 부분이 늘어나게 된다. 예를 들면 현재 이클립스 프로젝트가 jwprj 인데 이는 컨텍스트 루트 이름도 되며 모든 서블릿이나 jsp 호출시 /jwprj/로 시작해야 한다. 만일 소스를 복사해서 다른 프로젝트에서 실행할 경우(혹은 다른 사람에게 복사해 주는 경우) 프로젝트 이름이 다를 수 있기 때문에 그냥 실행하면 404 Not Found 에러가 발생하게 된다. 이경우 컨텍스트 이름을 수정하거나 소스 파일을 수정해야 하는데 jsp 파일이 많은 경우 상당히 문제가 될 수 있다. 따라서 컨텍스트 이름을 고정하지 않고 현재 서버 설정을 참조할 수 있도록 해야하며 여기서는 jstl 을 이용해 baseUrl 이라는 속성으로 참조할 수 있도록 지정한 것이다.

다음은 HTML 기본 코드 부분이다.

<!DOCTYPE html>
<html>
<head>
<%@ include file="/common/inc_header.html"%>
<title>MyTwitter 로그인</title>
</head>
<body>

</body>
</html>

특이 사항은 없으며 공통 헤더파일을 include 하고 있다.

메인 콘텐츠 영역 설정

다음으로 bootstrap을 이용해 메인 화면을 구성하기 위해 전체 영역을 <div>로 묶는다.

<div class="container mx-auto mt-5 p-5 w-50 shadow bg-info">

</div>

다음은 div 에 사용된 bootstrap 클래스들에 대한 설명이다.

다음으로 로그인 폼을 구성하는 부분이다. 화면 디자인을 위해 bootstrap의 input-groupform-control 클래스를 사용했다.

		<H2>Twitter::Login</H2>
		<form name="form1" method="post" action="${baseUrl}">
			<input type="hidden" name="action" value="login" />
			<div class="input-group">
				<input class="form-control" type="text" name="username" placeholder="아이디를 입력하세요.." /> 
				<input class="btn btn-warning" type="submit" value="Login" />
			</div>
		</form>

마지막으로 아이디를 입력하지 않고 로그인하는 경우 컨트롤러에서 해당 에러메시지를 포함시켜 다시 로그인 화면으로 돌려보내도록 할 것이기 때문에 에러메시지를 포함하고 있는 경우라면 에러메시지가 출력될 수 있도록 form 아래쪽에 메시지 박스를 만든다.

		<c:if test="${error != null}">
			<div class="alert alert-danger mt-3">
				<button type="button" class="close" data-dismiss="alert">&times;</button>
				에러발생: ${error}
		</div>

tweetList.jsp 는 로그인후 보여질 화면으로 게시글 목록을 보여주고 새로운 게시글 작성을 위한 입력양식을 포함한다.

taglib 지시어, include, baseUrl 등 기본 문서 구조는 동일하게 작성하고 메인 <div> 역시 동일하다. 다만 로그인 화면보다 가로로 넓어야 하므로 width 만 좀 더 넓게 설정하고 배경색은 넣지 않는다.

	<div class="container shadow mx-auto mt-5 p-5 w-60">

기본 화면 구성

tweetList.jsp 는 로그인 아이디를 표시하고 게시글을 입력할 수 있도록 되어 있다. 로그인과 유사하게 하나의 입력만 가진다.

		<H3>My Simple Twitter!!</H3>
		<HR>
		<form action="${baseUrl}"
			method="post">
			<input type="hidden" name="action" value="tweet">

			<div class="input-group">
				<button type="button" class="btn btn-outline-success">@${user}</button>
				<input class="form-control" type="text" name="msg">
				<input class="btn btn-warning ml-2" type="submit" value="Tweet"> 
				<a href="${baseUrl}?action=list" class="btn btn-success ml-2">Refresh</a> 
				<a href="${baseUrl}?action=logout" class="btn btn-secondary ml-2">Sign out</a>
			</div>

		</form>

에러 메시지 박스

다음으로 로그인에서와 마찬가지로 게시글을 작성하지 않고 엔터를 치거나 Tweet 버튼을 누른 경우 에러 메시지 출력을 위한 메시지 박스를 만듬.

		<c:if test="${error != null}">
			<div class="alert alert-danger mt-3">
				<button type="button" class="close" data-dismiss="alert">&times;</button>
				에러발생: ${error}
			</div>
		</c:if>

게시글 목록 출력

게시글은 컨트롤러에 의해 목록의 형태로 전달되며 jstl 을 이용해 출력하는 것으로 구현한다. 각각의 게시글은 <li> 를 이용해 구성하며 bootstrap의 list-group-item 을 사용해 박스 형태로 출력한다.

		<div align="left">
			<ul class="list-group">
				<c:forEach var="msg" items="${tweetlist}">
					<li class="list-group-item list-group-item-action">${msg}</li>
				</c:forEach>
			</ul>
		</div>

이것으로 필요한 뷰의 구성을 마치고 모델 영역의 구현으로 넘어간다.

모델영역에서는 다양한 형태로 구현될 수 있는 데이터 서비스를 정의하기 위해 TwitterService 인터페이스를 먼저 정의 한다. 실제 개발에는 구현에 사용되는 데이터베이스에 따라 Oracle, MySQL, MongoDB 등 다양한 데이터베이스에 최적화된 DAO 혹은 서비스 클래스의 구현이 필요하며 이들을 동일한 규격으로 사용할 수 있도록 해야 하기 때문에 먼저 인터페이스 정의가 필요한 것이다.

향후 v2에서는 H2 데이터베이스와 Map 형태의 서비스가 추가되고 v4 에서는 구글 firebase 연동이 추가되기 때문에 실제 인터페이스 설계는 이들을 고려해야 한다.

일반적인 데이터베이스의 경우 자동으로 증가되는 정수값(1~n)을 주키로 하고 자동증가 옵션 혹은 시퀀스객체등을 사용해 관리하게 되지만 MongoDB나 firebase realtime database 와 같은 NoSQL 데이터베이스의 경우 숫자가 아닌 좀 더 복잡한 문자열로 구성된 고유키를 생성하게 된다.

우선 v1 에서는 최대한 단순한 구조의 인터페이스를 설계하고 v2에서 이들을 고려한 구조로 개선하게 된다.

또한 게시글에 포함된 정보를 구조화한 엔티티 클래스도 필요하지만 여기서는 전체 시스템 구조 설계와 동작원리 이해를 주 목적으로 하기 때문에 단순하게 문자열로 처리하므로 별도의 엔티티 클래스도 생략 된다.

package jwprj.twitter;
public interface TwitterService {
	public void write(String msg);
	public List<String> getList();
}

v1 에서는 실제 데이터베이스가 아니고 문자열 리스트를 만들어 application scope 에 저장해 일종의 메모리 DB처럼 구현된 서비스를 사용할 것이다. 따라서 데이터는 접속한 모든 사용자들이 공유할 수 있지만 톰캣을 종료하게 되면 데이터가 보존되지 않기 때문에 테스트용으로만 사용해야 한다.

게시글은 설계에 따라 작성자명, 게시글내용, 날짜 시간 을 포함하는 문자열로 구성된다. 데이터 저장을 위해서는 ArrayList를 사용한다.

public class EmbeddedService implements TwitterService {
	ServletContext application;
	List<String> tweetdb;

생성자

생성자에서는 데이터 공유를 위해 ServletContext 참조를 인자로 받아오며 tweetdb 라는 이름으로 저장된 객체가 있다면 가지고 온다. 즉, 이미 저장된 객체가 있다면 새로 생성하지 않고 사용하는 것이다.

만일 생성된 객체가 없다면 새로운 ArrayList를 생성하고 ServletContext 에 저장한다.

	public EmbeddedService(ServletContext application) {
		this.application = application;
    	// 메시지 저장을 위해 application 에서 msgs 로 저장된 ArrayList 가지고 옴
    	tweetdb = (List<String>) application.getAttribute("tweetdb");
    	
    	// null 인 경우 새로운 ArrayList 객체를 생성
    	if(tweetdb == null) {
    		tweetdb = new ArrayList<String>();
    		// application 에 ArrayList 저장
    		application.setAttribute("tweetdb",tweetdb);
    	}
	}

write()

v1 의 write() 메서드는 컨트롤러에서 완성된 게시글의 형태로 문자열을 전달받기 때문에 받은 값을 ArrayList 에 추가만 하면 된다.

	@Override
	public void write(String msg)  {
    	tweetdb.add(msg);
	}

getList()

목록을 제공하는 getList() 메서드 역시 tweetdb 를 그대로 리턴하기만 하면 된다.

	@Override
	public List<String> getList() {
		return tweetdb;
	}

이제 트위터 v1 의 마지막인 컨트롤러 구현이다. 서블릿으로 구현할 것이며 최대한 기능변화나 재활용에 유리한 형태의 구조를 만들게 된다.

기본 코드 구성

서블릿으로 클래스를 생성하되 doGet(), doPost()는 사용하지 않는다. 대신 service() 메서드를 오버라이딩해서 사용하게 된다.

@WebServlet(urlPatterns = "/twitter")
public class TwitterController extends HttpServlet {
	private static final long serialVersionUID = 1L;
	
	private ServletContext application;
	
	// 웹 리소스 기본 경로 지정 
	private final String BASE_DIR = "/twitter/";
	private final String START_PAGE = "twitterLogin.jsp";
	
	// 데이터 서비스 인터페이스
	TwitterService service;

init() 과 service()

init() 은 서블릿이 처음 컨테이너에 로드될때 한번만 실행되는 메서드 이다. 즉 init()에서 초기화한 객체들은 모든 접속 사용자들에 공유된다. 여기서는 TwitterService 와 ServletContext 의 초기화 작업을 수행한다.

service()는 사용자 요청시 마다 호출되는 메서드로 컨테이너에 의해 스레드로 실행되며 오버라이딩하지 않을 경우 사용자 요청메서드를 구분해 doGet(), doPost()등을 자동으로 호출하게 되어 있다. 여기서는 processRequest() 라고 하는 메서드를 따로 만들어 모든 요청을 처리하도록 수정한다.

	// 서블릿이 초기화될때 서비스 객체 생성
	@Override
	public void init() throws ServletException {
		// 데이터 서비스 선택 지정.
		service = new EmbeddedService(getServletContext());
		application = getServletContext();
	}

	// 모든 처리를 processRequest 에서 처리하도록 함.
	@Override
	protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
		processRequest(arg0, arg1);
	}

processRequest()

컨트롤러의 메인 부분이며 action 파라미터로 전달된 사용자 요청을 분석해 처리 메서드를 호출하고 호출결과 지정된 뷰로 이동할 수 있는 구조를 만들어야 한다.

기본적으로 한글 인코딩 설정과 필요한 객체들을 참조한 다음 action 인자 없이 호출된 경우 세션을 초기화 하고 로그인 화면으로 이동하게 한다.

	private void processRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
		// 한글 인코딩 설정
		request.setCharacterEncoding("utf-8");
		
		// 서블릿 컨텍스트경로
		String ctxPath = request.getContextPath();

		String action = request.getParameter("action");
		
		// action 파라미터 없이 접근한 경우 세션을 초기화 하고 로그인 화면으로 이동
		if (action == null) {
			request.getSession().invalidate();
			response.sendRedirect(ctxPath+BASE_DIR+START_PAGE);
			// 로그인 화면으로 리디렉션후 현재 메서드를 종료해야함.
			return;
		}

다음으로 action 인자값에 따라 메서드를 분기하는 코드가 필요하다. 간단하게는 switch 문을 사용해 요청에 따라 메서드를 호출하도록 하면된다. 화면의 이동역시 요청처리 메서드에서 하면 된다.

그러나 이런 접근은 요청이 수정/추가 되거나 코드가 복잡해지는 경우 관리에 어려움이 발생한다. 따라서 좀 더 단순하면서도 확실하게 동작하는 구조가 필요한데 여기서는 자바의 리플렉션 API를 사용해 요청이름과 동일한 메서드를 자동으로 호출하도록 구현한다.

이렇게 구현하면 요청/기능 추가시 메서드만 만들면 되고 별도의 switch 문 추가가 필요 없다. 또한 모든 요청처리 메서드의 리턴으로 뷰를 받아 이동할 수 있도록 구성하였다.

이해를 돕기 위해 예외처리를 생략한 코드를 보도록 한다.

		// 자바 리플렉션을 사용해 if, switch 없이 요청에 따라 구현 메서드가 실행되도록 함.
		Method m;
		String view = null;

		// 현재 클래스에서 action 이름과 HttpServletRequest 를 파라미터로 하는 메서드 찾음
		m = this.getClass().getMethod(action, HttpServletRequest.class);
		// 메서드 실행후 리턴값 받아옴
		view = (String)m.invoke(this, request);

Method m 은 현재 클래스(this.getClass())로 부터 action 파라미터로 전달된 값(write,login,delte,list,logout 등)을 메서드 이름으로 하고 HttpServletRequest 타입의 인자를 가진 메서드를 찾아 Method 타입으로 받아온다.

invoke() 메서드로 실행후(이때 인자로 request를 반드시 전달해야 함) 이동할 뷰 파일명을 리턴 받는다. action 으로 전달된 이름과 일치하는 메서드가 없을 경우 NoSuchMethodException이 발생하게 되고 이 경우에는 에러메시지를 request 에 저장해서 로그인 화면으로 보낸다.

		} catch (NoSuchMethodException e) {
			e.printStackTrace();
			// 에러 로그를 남기고 view 를 로그인 화면으로 지정, 앞에서와 같이 redirection 사용도 가능.
			application.log("요청 action 없음!!");
			request.setAttribute("error", "정상적인 요청이 아닙니다. 다시 로그인 하세요!!");
			view = START_PAGE;
		}

메서드 호출 이후에는 받아온 뷰페이지로 포워딩 한다.

		// 지정된 뷰로 포워딩, 포워딩시 컨텍스트경로는 필요없음.
		RequestDispatcher dispatcher = request.getRequestDispatcher(BASE_DIR+view);
		dispatcher.forward(request, response);

컨트롤러의 기본 구조가 준비 되었으면 이제 action 에 따라 처리 메서드를 만들어 보도록 한다.

login()

로그인 화면에서 아이디를 넣고 로그인할때 호출되는 메서드 이다. 이름을 가져와 세션에 저장하고 없을 경우 아이디를 입력하라는 메시지를 request에 넣고 twitterLogin.jsp 를 리턴한다.

아이디가 입력된 경우에는 list() 메서드를 호출해 목록을 가져온 다음 리턴된 뷰페이지(tweetList.jsp)를 최종 리턴 한다.

// 로그인 처리
public String login(HttpServletRequest request) {
	// HTML 폼에서 username으로 전달된 값을 가지고 옴
	String username = request.getParameter("username");
	
	// username이 입력된 경우에만 세션에 값을 저장
	if (username != "") {
		request.getSession().setAttribute("user", username);
	} else {
		request.setAttribute("error", "로그인 아이디를 입력하세요!!");
		return "twitterLogin.jsp";
	}
	// 로그인후 게시글 목록을 포함해 목록화면으로 이동해야 하므로 list()메서드 호출 
	return(list(request));
}

logout()

로그아웃의 경우 세션을 무효화 하고 다시 로그인 페이지로 돌아 간다.

// 로그아웃 처리
public String logout(HttpServletRequest request) {
	request.getSession().invalidate();
	
	return "twitterLogin.jsp";
}

list()

게시글 목록을 보여주는 메서드로 로그인 및 게시글 작성후에 호출된다.

// 게시글 목록을 포함해 목록화면으로 이동하는 메서드로 로그인 및 게시글 작성에서 공통으로 호출
public String list(HttpServletRequest request) {
	List<String> tweetlist = new ArrayList<String>();
	tweetlist = service.getList();
	request.setAttribute("tweetlist", tweetlist);
	return "tweetList.jsp";
}

tweet()

게시글 작성시 호출되는 메서드이다. 내용적으로는 세션으로부터 사용자 이름을 가지고오고 입력된 메시지와 현재 날짜를 조합해 메시지 형식을 만든다음 서비스를 통해 저장하고 다시 목록을 호출하는 구조이다.

// 게시글을 저장하기위한 메서드로 입력한 메시지에 이름, 날짜등으로 메시지를 조합하고 application scope 에 저장후 list() 호출
public String tweet(HttpServletRequest request) throws IOException {
	// HTML 폼에서 전달된 msg 값을 가지고 옴
	String msg = request.getParameter("msg");

	// 메시지 없는 경우 에러 처리
	if(msg == "") {
		request.setAttribute("error", "메시지를 입력하세요!!");
		return(list(request));
	}
	
	// 세션에 저장된 로그인 사용자 이름을 가지고 옴
	String username = (String) request.getSession().getAttribute("user");

	// 사용자 이름, 메시지, 날짜 정보를 포함하여 ArrayList에 추가
	LocalDateTime date = LocalDateTime.now();
	DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
	msg = username + " :: " + msg + " , " + date.format(f);

	service.write(msg);

	// 톰캣 콘솔을 통한 로깅
	application.log(msg + "추가됨");

	// 목록 화면 데이터 로딩
	return(list(request));
}

실행은 jwprj.twitter 패키지의 TwitterController 서블릿을 실행하면 된다. 성공이라면 로그인 화면이 보일것이다.

정상적인 기능 동작 확인을 위해 다음과 같이 테스트를 진행한다.

  1. 아이디를 입력하지 않고 로그인 했을때 에러 메시지가 나오는지 확인.
  2. 아이디를 입력하고 로그인한 다음 메시지를 입력하지 않고 등록시 에러 메시지 확인.
  3. 메시지를 입력하고 정상 등록 되는지 확인.
  4. 로그아웃해서 다른 아이디로 입력후 메시지 등록.
  5. 톰캣을 실행한 컴퓨터의 ip주소를 확인하고 다른컴퓨터에서도 접속후 사용.

이상 모든 기능이 정상 동작하면 v1 버전이 완성된 것이다.