[웹 풀스택] 부스트코스 - 웹 프로그래밍 - 5. 웹 앱 개발: 예약서비스 3/4

June·2021년 8월 14일
0

1. UI Component module - FE

1) 생성자 패턴

자바스크립트 객체 다시 이해하기

우리는 아래와 같은 코드를 객체리터럴(Object literal)이라고 알고 있습니다.

var healthObj = {
  name : "달리기",
  lastTime : "PM10:12",
  showHealth : function() {
    console.log(this.name + "님, 오늘은 " + this.lastTime + "에 운동을 하셨네요");
  }
}

healthObj.showHealth();

그런데 healthObj의 형태를 가진 여러개의 객체가 필요하다면 어떻게 할까요?
healthObj2, healthObj3...
그렇게 구현해도 되지만, 비슷한 객체를 계속 중복해서 만들어 두는 건 이상해보이죠.
더군다나 각 객체마다 showHealth라는 메서드는 중복으로 들어가 있을 겁니다.
따라서 이를 좀 더 개선할 필요가 있어보이네요.

객체를 동적으로 생성하는 방법

먼저 객체를 동적으로 생성하는 방법을 알아보겠습니다.
방법은 간단한데 아래처럼 함수를 이용하는 것입니다.
먼저, 아래 코드를 실행해보세요.

function Health(name, lastTime) {
  this.name = name;
  this.lastTime = lastTime;
  this.showHealth = function(){...}
}
const h = new Health("달리기", "10:12");

내부적으로 안에 return this가 생략되어있다고 생각하면 좋다

h는 객체입니다.
h안을 들여다보면 어떻게 구성되어 있는지 알 수 있습니다.
Health함수를 한 번 더 불러서 h2객체를 만듭니다.

h2 = new Health("걷기", "20:11"); 

계속 이런식으로 객체를 만들어낼 수 있습니다.
Health함수는 new키워드로 불리면서, 객체를 생성하는 함수 역할을 합니다.
그래서 Health를 생성자(constructor)라고 합니다.

하지만 아직 문제가 하나 존재합니다.
아직 h와 h2를 열어보면 showHealth메서드가 여전히 중복해서 존재합니다.
이처럼 동일한 메서드가 여기저기 메모리 공간을 차지하는 것은 분명 자원 낭비입니다.

prototype으로 메서드를 같이 사용해보자

자바스크립트는 prototype이라는 공간을 통해서 객체간의 재사용 되는 것을 바라보게 할 수 있습니다.

개념적으로는 이렇습니다.

prototype타입이라는 것은 신기합니다.
객체지향 언어를 배운 분들은 '이것이 상속(inheritance)인가?'싶을 겁니다.
실제로 비슷한 개념이라 할 수도 있습니다.
위 그림에서는 각 인스턴스(h, h2, h3)가 prototype이라는 같은 참조객체를 바라보고 있는 것입니다.

따라서 prototype의 어떤 속성을 변경하면 모든 인스턴스에게 공유됩니다.
prototype에 어떠한 속성을 추가하면서 실제 코드로 확인해보겠습니다.
아래 코드를 보면 Health 함수 아래 prototype 객체가 존재하고, 이것에 showHealth 메서드를 속성으로 추가했습니다.
이런 식으로 prototype객체 안에 여러 가지 속성을 추가할 수 있습니다.

function Health(name, lastTime) {
  this.name = name;
  this.lastTime = lastTime;

}

Health.prototype.showHealth = function() {
    console.log(this.name + "," + this.lastTime);
}

const h = new Health("달리기", "10:12");
console.log(h);  //크롬개발자도구를 열고 이 부분이 어떻게 출력되는지 확인해보세요
h.showHealth();

그럼 아래처럼 여러 인스턴스를 만들어도 prototype안의 showHealth는 같은 참조점을 바라보고 있는 것을 알 수 있습니다.

const h = new Health("달리기", "10:12");
const h2 = new Health("걷기", "14:20");
console.log(h.showHealth === h2.showHealth); //true

2) 생성자패턴으로 TabUI만들기

TabUI 동작 코드를 생성자패턴으로 구현해볼 수 있습니다.
이렇게 구현한 코드는 UI Component라고 말하곤 합니다.

자바스크립트 객체 다시 이해하기

이전에 구현했던 코드는 아래와 같습니다.

function makeTemplate(data, clickedName) {
            var html = document.getElementById("tabcontent").innerHTML;
            var resultHTML = "";
            for (var i = 0; i < data.length; i++) {
                if (data[i].name === clickedName) {
                    resultHTML = html.replace("{name}", data[i].name)
                        .replace("{favorites}", data[i].favorites.join(" "));
                    break;
                }
            }
            document.querySelector(".content").innerHTML = resultHTML;
        }
        function sendAjax(url, clickedName) {
            var oReq = new XMLHttpRequest();
            oReq.addEventListener("load", function () {
                var data = JSON.parse(oReq.responseText);
                makeTemplate(data, clickedName);
            });
            oReq.open("GET", url);
            oReq.send();
        }
        var tabmenu = document.querySelector(".tabmenu");
        tabmenu.addEventListener("click", function (evt) {
            sendAjax("./json.txt", evt.target.innerText);
        });

전역공간에 구현된 코드를 prototype기반의 클래스로 구현해볼 겁니다.

prototype 기반 클래스 코드

prototype기반 코드는 하나의 클래스(모듈)로 만드는 것으로, 기존코드의 큰 수정 없이 변경할 수 있습니다.
우리는 지금처럼 비슷한 기능 덩어리를 하나의 객체, 즉 클래스 형태로 만들 수가 있습니다.
영상에 노출된 코드는 아래에서도 확인할 수 있습니다.

<html>
<header>
    <link rel="stylesheet" href="tabui.css">
    <style>
    h2 {
    text-align: center
}

h2,
h4 {
    margin: 0px;
}

.tab {
    width: 600px;
    margin: 0px auto;
}

.tabmenu {
    background-color: bisque;
}

.tabmenu>div {
    display: inline-block;
    width: 146px;
    height: 50px;
    line-height: 50px;
    text-align: center;
    cursor: pointer;
}

.content {
    padding: 5%;
    background-color: antiquewhite;
}
</style>
</header>

<body>
    <h2> TAB UI TEST</h2>

    <div class="tab">
        <div class="tabmenu">
            <div>crong</div>
            <div>jk</div>
            <div>pobi</div>
            <div>honux</div>
        </div>
        <section class="content">
            <h4>hello jk</h4>
            <p>golf, facebook</p>
        </section>
    </div>
    <script>

        function Tab(tabElement) {
            this.tabmenu = tabElement;
            this.registerEvents();
        }

        Tab.prototype = {
            registerEvents : function() {
                this.tabmenu.addEventListener("click", function (evt) {
                    this.sendAjax("./json.txt", evt.target.innerText);
                }.bind(this));
            },
            sendAjax : function(url, clickedName) {
                var oReq = new XMLHttpRequest();
                oReq.addEventListener("load", function () {
                    var data = JSON.parse(oReq.responseText);
                    this.makeTemplate(data, clickedName);
                }.bind(this));
                oReq.open("GET", url);
                oReq.send(); 
            },
            makeTemplate : function(data, clickedName) {
                var html = document.getElementById("tabcontent").innerHTML;
                var resultHTML = "";

                for (var i = 0; i < data.length; i++) {
                    if (data[i].name === clickedName) {
                        resultHTML = html.replace("{name}", data[i].name)
                            .replace("{favorites}", data[i].favorites.join(" "));
                        break;
                    }
                }
                document.querySelector(".content").innerHTML = resultHTML
            }
        }

        var tabmenu = document.querySelector(".tabmenu");
        var o = new Tab(tabmenu);

/*
        function makeTemplate(data, clickedName) {
            var html = document.getElementById("tabcontent").innerHTML;
            var resultHTML = "";
            for (var i = 0; i < data.length; i++) {
                if (data[i].name === clickedName) {
                    resultHTML = html.replace("{name}", data[i].name)
                        .replace("{favorites}", data[i].favorites.join(" "));
                    break;
                }
            }
            document.querySelector(".content").innerHTML = resultHTML;
        }
        function sendAjax(url, clickedName) {
            var oReq = new XMLHttpRequest();
            oReq.addEventListener("load", function () {
                var data = JSON.parse(oReq.responseText);
                makeTemplate(data, clickedName);
            });
            oReq.open("GET", url);
            oReq.send();
        }
        var tabmenu = document.querySelector(".tabmenu");
        tabmenu.addEventListener("click", function (evt) {
            sendAjax("./json.txt", evt.target.innerText);
        });
        */
    </script>

    <script id="tabcontent" type="my-template">
            <h4>hello {name}</h4>
            <p>{favorites}</p>
       </script>
</body>

</html>

2. JavaScript Regular expression - FE

1) 정규표현식이란?-1

숫자 하나 찾기

ar result = "abc3zzz".match(/\d/)[0];

console.log(result);

숫자 두 개 찾기

var result = "abc32zzz".match(/\d\d/)[0];

console.log(result);

1) 정규표현식이란?-2

우편번호 구/신

var result1 = "19323".match(/(\d{3}-\d{3}|\d{5})/)[0];

console.log (result1);

var result2 = "193-123".match(/(\d{3}-\d{3}|\d{5})/)[0];

console.log (result2);
"92405".match(/(\d{3}-\d{3}|[0-46-8]\d{4}/)[0];

대쉬로 나뉘거나 안나뉜 우편번호를 나타내되, 안나뉜 경우 뒤에 첫 숫자는 5와 9는 안되는 경우

핸드폰 전화번호 규칙

var result = "010-9021-0011".match("/01[01789]-\d{3,4}-\d{4}/")

개발도구에서의 함수 선택

\(?function\s+[a-zA-Z_$]+

치환

var result = "011-021-0011".replace(/(\d{2})\d/, "$10");;

console.log(result);
>"010-021-0011"

$1은괄호를 나타내고, 그 뒤에 숫자를 0으로 바꿔라라는 의미다.

탐욕적(greedy), 게으른(lazy) 수량자
뒤에서부터 찾거나, 앞에서부터 찾거나 (lazy를 써서 앞에서부터 찾도록 합니다.)
greedy : , +, {n,}
lazy :
?, +?, {n,}?

탐욕적으로 최대한 길게 잡는다

물음표를 붙이면 게으르게 하나만 찾는다

3. form 데이터 보내기 - FE

1) form 데이터 보내기

form 태그를 사용한 html

<form action="/join" method="post">
    <div class="inputWrap">
        <div class="email">
            <span> Email </span> <input type="text" name="email"><br/>
        </div>
        <div class="password">
            <span> Password </span> <input type="password" name="password"><br/>
        </div>
    </div>
    <input class="sendbtn" type="submit">
</form>

name이 서버에 보낼 때 키 값이 되는 것이다.

2) form 데이터 유효성 검증하기

form 태그를 사용한 html

다음의 form 코드에서 email 정보가 올바른지 유효성검증을 하고 싶습니다.

<form action="/join" method="post">
    <div class="inputWrap">
        <div class="email">
            <span> Email </span> <input type="text" name="email"><br/>
        </div>
        <div class="password">
            <span> Password </span> <input type="password" name="password"><br/>
        </div>
    </div>
    <input class="sendbtn" type="submit">
</form>

이 부분처리를 서버에서 한다면 사용자는 꽤 답답할 겁니다.
왜냐하면, 서버에 갈 때까지 email 정보가 틀렸는지 알 수가 없기 때문입니다.
예를 들어 다른 값을 모두 다 넣고 확인을 눌러서 서버로 데이터를 보냈는데, email 정보가 틀렸다고 메시지가 뒤늦게 나온다면 사용자는 당황할 겁니다.

좀 더 좋은 UX를 제공하기 위해서는 에러 메시지를 더 빨리 사용자에게 노출해주는 것이 좋습니다. form 검증방법은 아래와 같이 구현할 수가 있습니다.

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title> Join !</title>
        <link rel="stylesheet" href="/css/ui.css">
    </head>
    <body>
        <h1>Join my website!</h1>
        <div class="formWrap">
            <form action="/join" method="post" id="myform">
                <div class="inputWrap">
                    <div class="email">
                        <span> Email </span> <input type="text" name="email"><br/>
                    </div>
                    <div class="password">
                        <span> Password </span> <input type="password" name="password"><br/>
                    </div>
                </div>
                <input class="sendbtn" type="submit">
           </form>
        </div>

        <section class="result"></section>
        <script>
		var btn = document.querySelector(".sendbtn");
		var result = document.querySelector(".result");
		btn.addEventListener("click", function(evt) {
		    evt.preventDefault();
		    var emailValue = document.querySelector("[name='email']").value;
		    var bValid = (/^[\w+_]\w+@\w+\.\w+$/).test(emailValue);
		    if(!bValid)  { 
  		      result.innerHTML = "올바르지 않은 이메일입니다";
		    } else {
        		result.innerHTML = "이메일정보가 좋아요~";
        		document.querySelector("#myform").submit();
 		   }
		});
        </script>
    </body>
</html>

preventDefault()를 이용해서 클릭하자마자 서버로 전송되는 것을 막았다.

정규식을 통해서 이메일 형식을 정의한다. 시작은 문자 또는 언더바, 그리고 끝은 무자열이게 정의했다.

위 코드에서는 addEventListener에서 click 이벤트를 사용했습니다. 다른 방법도 있습니다.
'submit'이벤트를 통해서 역시 동일하게 form체크와 데이터 전송을할 수있습니다.
아래 방법은 위 방법보다 좀더 의미적으로 와닿을 겁니다.

document.querySelector("#myform").addEventListener("submit", function(evt) {
  console.log(evt.target);
});

4. 상태유지기술(Cookie & Session) - BE

1) 상태정보란?

웹에서의 상태 유지 기술

HTTP프로토콜은 상태 유지가 안되는 프로토콜입니다.

  • 이전에 무엇을 했고, 지금 무엇을 했는지에 대한 정보를 갖고 있지 않습니다.
  • 웹 브라우저(클라이언트)의 요청에 대한 응답을 하고 나면 해당 클라이언트와의 연결을 지속하지 않습니다.
    상태 유지를 위해 Cookie와 Session기술이 등장합니다.

상태유지가 안되는 프로토콜은 무상태 프로토콜(stateless protocol)이라 하고 HTTP, UDP, DNS가 있습니다.

상태를 유지하는 프로토콜은 상태 프로토콜(stateful protocol)이라 하고 FTP, Telnet이 있습니다.

쿠키(Cookie)와 세션(Session)

  • 쿠키
    • 사용자 컴퓨터에 저장
    • 저장된 정보를 다른 사람 또는 시스템이 볼 수 있는 단점
    • 유효시간이 지나면 사라짐
  • 세션
    • 서버에 저장
    • 서버가 종료되거나 유효시간이 지나면 사라집니다.

쿠키(Cookie) 동작 이해

세션 동작 이해

2) 쿠키란?

쿠키 정의

  • 클라이언트 단에 저장되는 작은 정보의 단위입니다.
  • 클라이언트에서 생성하고 저장될 수 있고, 서버 단에서 전송한 쿠키가 클라이언트에 저장될 수 있습니다.

이용 방법

  • 서버에서 클라이언트의 브라우저로 전송되어 사용자의 컴퓨터에 저장합니다.
  • 저장된 쿠키는 다시 해당하는 웹 페이지에 접속할 때, 브라우저에서 서버로 쿠키를 전송합니다.
  • 쿠키는 이름(name)과 값(value) 쌍으로 정보를 저장합니다.
    • 이름-값 쌍 외에도 도메인(Domain), 경로(Path), 유효기간(Max-Age, Expires), 보안(Secure), HttpOnly 속성을 저장할 수 있습니다.

쿠키는 그 수와 크기에 제한

javax.servlet.http.Cookie

서버에서 쿠키 생성, Reponse의 addCookie메소드를 이용해 클라이언트에게 전송

Cookie cookie = new Cookie(이름,);
response.addCookie(cookie);
  • 쿠키는 (이름, 값)의 쌍 정보를 입력하여 생성합니다.
  • 쿠키의 이름은 일반적으로 알파벳과 숫자, 언더바로 구성합니다. 정확한 정의를 알고 싶다면 RFC 6265(https://tools.ietf.org/html/rfc6265) 문서 [4.1.1 Syntax] 항목을 참조하세요.

클라이언트가 보낸 쿠키 정보 읽기

Cookie[] cookies = request.getCookies();
  • 쿠키 값이 없으면 null이 반환됩니다.
  • Cookie가 가지고 있는 getName()과 getValue()메소드를 이용해서 원하는 쿠키정보를 찾아 사용합니다.

클라이언트에게 쿠키 삭제 요청

  • 쿠키를 삭제하는 명령은 없고, maxAge가 0인 같은 이름의 쿠키를 전송합니다.
Cookie cookie = new Cookie("이름", null);
cookie.setMaxAge(0);
response.addCookie(cookie);

같은 이름을 가진 두 쿠키가 존재할 수 없기 때문에 이렇게 보내면 삭제 된다.

쿠키의 유효기간 설정

  • 메소드 setMaxAge()
    • 인자는 유효기간을 나타내는 초 단위의 정수형
    • 만일 유효기간을 0으로 지정하면 쿠키의 삭제
    • 음수를 지정하면 브라우저가 종료될 때 쿠키가 삭제
  • 유효기간을 10분으로 지정하려면
    - cookie.setMaxAge(10 60); //초 단위 : 10분
    - 1주일로 지정하려면 (7
    246060)로 설정합니다.
  • @CookieValue 애노테이션 사용

    • 컨트롤러 메소드의 파라미터에서 CookieValue애노테이션을 사용함으로써 원하는 쿠키정보를 파라미터 변수에 담아 사용할 수 습니다.
  • 컨트롤러메소드(@CookieValue(value="쿠키이름", required=false, defaultValue="기본값") String 변수명)

3) 쿠키를 이용한 상태정보 유지하기-1

package kr.or.connect.guestbook.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;

@Controller
public class GuestbookController {
	@Autowired
	GuestbookService guestbookService;

	@GetMapping(path="/list")
	public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
					   ModelMap model,
                       HttpServletRequest request,
					   HttpServletResponse response) {

        
		String value = null;
		boolean find = false;
		Cookie[] cookies = request.getCookies();
		if(cookies != null) {
			for(Cookie cookie : cookies) {
				if("count".equals(cookie.getName())) {
					find = true;
					value = cookie.getValue();
				}
			}
		}
		
      
		if(!find) {
			value = "1";
		}else { // 쿠키를 찾았다면.
			try {
				int i = Integer.parseInt(value);
				value = Integer.toString(++i);
			}catch(Exception ex) {
				value = "1";
			}
		}
		
   
		Cookie cookie = new Cookie("count", value);
		cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지.
		cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용. 
		response.addCookie(cookie);
		
		
		List<Guestbook> list = guestbookService.getGuestbooks(start);
		
		int count = guestbookService.getCount();
		int pageCount = count / GuestbookService.LIMIT;
		if(count % GuestbookService.LIMIT > 0)
			pageCount++;
		
		List<Integer> pageStartList = new ArrayList<>();
		for(int i = 0; i < pageCount; i++) {
			pageStartList.add(i * GuestbookService.LIMIT);
		}
		
		model.addAttribute("list", list);
		model.addAttribute("count", count);
		model.addAttribute("pageStartList", pageStartList);
		model.addAttribute("cookieCount", value); // jsp에게 전달하기 위해서 쿠키 값을 model에 담아 전송한다.
		
		return "list";
	}
	
	@PostMapping(path="/write")
	public String write(@ModelAttribute Guestbook guestbook,
						HttpServletRequest request) {
		String clientIp = request.getRemoteAddr();
		System.out.println("clientIp : " + clientIp);
		guestbookService.addGuestbook(guestbook, clientIp);
		return "redirect:list";
	}

list.jsp 에서 방명록 전체 수 옆에 방문한 수를 출력하는 el 코드를 추가합니다.

  • 방명록 전체 수 : ${count }
  • 방문한 수 : ${cookieCount }<br><br>

3) 쿠키를 이용한 상태정보 유지하기-2

package kr.or.connect.guestbook.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;

@Controller
public class GuestbookController {
	@Autowired
	GuestbookService guestbookService;

  	@GetMapping(path="/list")
	public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
					   ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value,
					   HttpServletResponse response) {
		
        // 쿠키 값을 1증가 시킨다.
		try {
			int i = Integer.parseInt(value);
			value = Integer.toString(++i);
		}catch(Exception ex){
			value = "1";
		}
		
        // 쿠키를 전송한다.
		Cookie cookie = new Cookie("count", value);
		cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지.
		cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용. 
		response.addCookie(cookie);
		
		List<Guestbook> list = guestbookService.getGuestbooks(start);
		
		int count = guestbookService.getCount();
		int pageCount = count / GuestbookService.LIMIT;
		if(count % GuestbookService.LIMIT > 0)
			pageCount++;
		
		List<Integer> pageStartList = new ArrayList<>();
		for(int i = 0; i < pageCount; i++) {
			pageStartList.add(i * GuestbookService.LIMIT);
		}
		
		model.addAttribute("list", list);
		model.addAttribute("count", count);
		model.addAttribute("pageStartList", pageStartList);
		model.addAttribute("cookieCount", value); // 쿠키를 추가한다.
		
		return "list";
	}
	
	@PostMapping(path="/write")
	public String write(@ModelAttribute Guestbook guestbook,
						HttpServletRequest request) {
		String clientIp = request.getRemoteAddr();
		System.out.println("clientIp : " + clientIp);
		guestbookService.addGuestbook(guestbook, clientIp);
		return "redirect:list";
	}
}

4) Session이란?

세션

정의

  • 클라이언트 별로 서버에 저장되는 정보

이용 방법

  • 웹 클라이언트가 서버측에 요청을 보내게 되면 서버는 클라이언트를 식별하는 session id를 생성합니다.
  • 서버는 session id를 이용해서 key와 value를 이용한 저장소인 HttpSession을 생성합니다.
  • 서버는 session id를 저장하고 있는 쿠키를 생성하여 클라이언트에 전송합니다.
  • 클라이언트는 서버측에 요청을 보낼때 session id를 가지고 있는 쿠키를 전송합니다.
  • 서버는 쿠키에 있는 session id를 이용해서 그 전 요청에서 생성한 HttpSession을 찾고 사용합니다.

세션 생성 및 얻기

HttpSession session = request.getSession();
HttpSession session = request.getSession(true);
  • request의 getSession()메소드는 서버에 생성된 세션이 있다면 세션을 반환하고 없다면 새롭게 세션을 생성하여 반환합니다.
  • 새롭게 생성된 세션인지는 HttpSession이 가지고 있는 isNew()메소드를 통해 알 수 있습니다.
HttpSession session = request.getSession(false);
  • request의 getSession()메소드에 파라미터로 false를 전달하면, 이미 생성된 세션이 있다면 반환하고 없으면 null을 반환합니다.

세션에 값 저장

setAttribute(String name, Object value)
  • name과 value의 쌍으로 객체 Object를 저장하는 메소드입니다.
  • 세션이 유지되는 동안 저장할 자료를 저장합니다.
session.setAttribute(이름,)

세션에 값 조회

getAttribute(String name) 메소드

  • 세션에 저장된 자료는 다시 getAttribute(String name) 메소드를 이용해 조회합니다.
  • 반환 값은 Object 유형이므로 저장된 객체로 자료유형 변환이 필요합니다.
  • 메소드 setAttribute()에 이용한 name인 “id”를 알고 있다면 바로 다음과 같이 바로 조회합니다.
String value = (String) session.getAttribute("id");

세션에 값 삭제

  • removeAttribute(String name) 메소드
    • name값에 해당하는 세션 정보를 삭제합니다.
  • invalidate() 메소드
    • 모든 세션 정보를 삭제합니다.

javax.servlet.http.HttpSession

세션은 클라이언트가 서버에 접속하는 순간 생성

  • 특별히 지정하지 않으면 세션의 유지 시간은 기본 값으로 30분 설정합니다.
  • 세션의 유지 시간이란 서버에 접속한 후 서버에 요청을 하지 않는 최대 시간입니다.
  • 30분 이상 서버에 전혀 반응을 보이지 않으면 세션이 자동으로 끊어집니다.
  • 이 세션 유지 시간은 web.xml파일에서 설정 가능합니다.
<session-config>
  <session-timeout>30</session-timeout>
</session-config>

5) Session을 이용한 상태정보 유지하기

실습코드

  • /guess로 요청을 하면 컴퓨터가 1부터 100 사이의 임의의 값 중의 하나를 맞춰보라는 메시지가 출력합니다.
  • 해당 값은 세션에 저장합니다.
  • 사용자는 1부터 100 사이의 값을 입력합니다.
  • 입력한 값이 세션 값보다 작으면, 입력한 값이 작다고 출력합니다.
  • 입력한 값이 세션 값보다 크면, 입력한 값이 크다고 출력합니다.
  • 입력한 값이 세션 값과 같다면 몇 번째에 맞췄다고 출력합니다.

GuessNumberController

package kr.or.connect.guestbook.controller;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class GuessNumberController {

	@GetMapping("/guess")
	public String guess(@RequestParam(name="number", required=false) Integer number,
			HttpSession session,
			ModelMap model) {
		
		String message = null;

		// get방식으로 /guess 를 요청하는데 파라미터 number가 없을 경우에는 session에 count를 0으로 randomNumber엔 1부터 100사이의 값을 저장합니다.
		if(number == null) {
			session.setAttribute("count", 0);
			session.setAttribute("randomNumber", (int)(Math.random() * 100) + 1); // 1 ~ 100사이의 random값
			message = "내가 생각한 숫자를 맞춰보세요.";
		}else {

			// number파라미터가 있을 경우 세션에서 값을 읽어들인 후, number와 세션에 저장된 값을 비교합니다.
			// 값을 비교해서 작거나 크다면 카운트를 1증가시켜주고
			// 값이 같다면 세션 정보를 삭제합니다.
			// 각 상황에 맞는 메시지를 message변수에 저장을 한 후 jsp에게 전달하기 위해서 ModelMap의 addAttribute메소드를 통해 전달하게 됩니다.
			int count = (Integer)session.getAttribute("count");
			int randomNumber = (Integer)session.getAttribute("randomNumber");
		
			
			if(number < randomNumber) {
				message = "입력한 값은 내가 생각하고 있는 숫자보다 작습니다.";
				session.setAttribute("count", ++count);
			}else if(number > randomNumber) {
				message = "입력한 값은 내가 생각하고 있는 숫자보다 큽니다.";
				session.setAttribute("count", ++count);
			}else {
				message = "OK " + ++count + " 번째 맞췄습니다. 내가 생각한 숫자는 " + number + " 입니다.";
				session.removeAttribute("count");
				session.removeAttribute("randomNumber");
			}
		}
		
		model.addAttribute("message", message);
		
		return "guess";
	}
}

guess.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>       
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>숫자 맞추기 게임</title>
</head>
<body>
<h1> 숫자 맞추기 게임.</h1>
<hr>
<h3>${message }</h3>

<c:if test="${sessionScope.count != null}">
<form method="get" action="guess">
1부터 100사이의 숫자로 맞춰주세요.<br>
<input type="text" name="number"><br>
<input type="submit" value="확인">
</form>
</c:if>

<a href="guess">게임 다시 시작하기.</a>
</body>
</html>

5. Spring 에서의 Session 사용법 - BE

1) Spring MVC에서 Session사용하기

@SessionAttributes & @ModelAttribute

@SessionAttributes 파라미터로 지정된 이름과 같은 이름이 @ModelAttribute에 지정되어 있을 경우 메소드가 반환되는 값은 세션에 저장됩니다.
아래의 예제는 세션에 값을 초기화하는 목적으로 사용되었습니다.

@SessionAttributes("user")
public class LoginController {
  @ModelAttribute("user")
  public User setUpUserForm() {
  return new User();
  }
}

@SessionAttributes의 파라미터와 같은 이름이 @ModelAttribute에 있을 경우 세션에 있는 객체를 가져온 후, 클라이언트로 전송받은 값을 설정합니다.

@Controller
@SessionAttributes("user")
public class LoginController {
......
  @PostMapping("/dologin")
  public String doLogin(@ModelAttribute("user") User user, Model model) {
......
  }
}

@SessionAttribute

메소드에 @SessionAttribute가 있을 경우 파라미터로 지정된 이름으로 등록된 세션 정보를 읽어와서 변수에 할당합니다.

@GetMapping("/info")
public String userInfo(@SessionAttribute("user") User user) {
//...
//...
return "user";
}

SessionStatus

SessionStatus 는 컨트롤러 메소드의 파라미터로 사용할 수 있는 스프링 내장 타입입니다.
이 오브젝트를 이용하면 현재 컨트롤러의 @SessionAttributes에 의해 저장된 오브젝트를 제거할 수 있습니다.

@Controller
@SessionAttributes("user")
public class UserController {
...... 
    @RequestMapping(value = "/user/add", method = RequestMethod.POST)
    public String submit(@ModelAttribute("user") User user, SessionStatus sessionStatus) {
  ......
  sessionStatus.setComplete();
                                   ......

   }
 }

Spring MVC - form tag 라이브러리

modelAttribute속성으로 지정된 이름의 객체를 세션에서 읽어와서 form태그로 설정된 태그에 값을 설정합니다.

<form:form action="login" method="post" modelAttribute="user">
Email : <form:input path="email" /><br>
Password : <form:password path="password" /><br>
<button type="submit">Login</button>
</form:form>

실습코드

  • 관리자는 /loginform에서 암호를 입력해 로그인을 한다.
  • 관리자가 암호를 맞게 입력할 경우 세션에 로그인 정보가 저장된다.
  • 세션에 로그인 정보가 있을 경우 방명록에는 "삭제" 링크가 보여진다.
  • 삭제 링크를 누르면 삭제가 된다. 삭제 작업에서도 로그인 정보가 있는지를 검사해야 한다.

GuestbookAdminController.java

package kr.or.connect.guestbook.controller;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class GuestbookAdminController {

       @GetMapping(path="/loginform")
		public String loginform() {
			return "loginform";
		}
		
        @PostMapping(path="/login")
		public String login(@RequestParam(name="passwd", required=true) String passwd, 
				HttpSession session,
				RedirectAttributes redirectAttr) {
			
			if("1234".equals(passwd)) {
				session.setAttribute("isAdmin", "true");
			}else {
				redirectAttr.addFlashAttribute("errorMessage","암호가 틀렸습니다.");
				return "redirect:/loginform";
			}
			return "redirect:/list";
		}
		
       @GetMapping(path="/logout")
		public String login(HttpSession session) {
			session.removeAttribute("isAdmin");
			return "redirect:/list";
		}

}

loginform.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>loginform</title>
</head>
<body>
<h1>관리자 로그인</h1>
<br><br>
${errorMessage}<br>

<form method="post" action="login">
	암호 : <input type="password" name="passwd"><br>
	<input type="submit">
</form>

</body>
</html>

기존 코드에서 /delete 삭제 부분을 추가합니다.
세션에 isAdmin이름의 값이 있을 경우에만 삭제 처리를 하도록 합니다.

GuestbookController.java

package kr.or.connect.guestbook.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;

@Controller
public class GuestbookController {
	@Autowired
	GuestbookService guestbookService;

	@GetMapping(path="/list")
	public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
					   ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value,
					   HttpServletResponse response) {
		
		try {
			int i = Integer.parseInt(value);
			value = Integer.toString(++i);
		}catch(Exception ex){
			value = "1";
		}
		
		Cookie cookie = new Cookie("count", value);
		cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지.
		cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용. 
		response.addCookie(cookie);
		
		List<Guestbook> list = guestbookService.getGuestbooks(start);
		
		int count = guestbookService.getCount();
		int pageCount = count / GuestbookService.LIMIT;
		if(count % GuestbookService.LIMIT > 0)
			pageCount++;
		
		List<Integer> pageStartList = new ArrayList<>();
		for(int i = 0; i < pageCount; i++) {
			pageStartList.add(i * GuestbookService.LIMIT);
		}
		
		model.addAttribute("list", list);
		model.addAttribute("count", count);
		model.addAttribute("pageStartList", pageStartList);
		model.addAttribute("cookieCount", value);
		
		return "list";
	}
	
	@PostMapping(path="/write")
	public String write(@ModelAttribute Guestbook guestbook,
						HttpServletRequest request) {
		String clientIp = request.getRemoteAddr();
		System.out.println("clientIp : " + clientIp);
		guestbookService.addGuestbook(guestbook, clientIp);
		return "redirect:list";
	}
	
   
	@GetMapping(path="/delete")
	public String delete(@RequestParam(name="id", required=true) Long id, 
			             @SessionAttribute("isAdmin") String isAdmin,
			             HttpServletRequest request,
			             RedirectAttributes redirectAttr) {
		if(isAdmin == null || !"true".equals(isAdmin)) { // 세션값이 true가 아닐 경우
			redirectAttr.addFlashAttribute("errorMessage", "로그인을 하지 않았습니다.");
			return "redirect:loginform";
		}
		String clientIp = request.getRemoteAddr();
		guestbookService.deleteGuestbook(id, clientIp);
		return "redirect:list";		
	}
}

기존 list.jsp에서 isAdmin세션값이 있을 경우 삭제 링크를 걸업줍니다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>방명록 목록</title>
</head>
<body>

<h1>방명록</h1>
<br>
방명록 전체 수 : ${count }, 방문한 수 : ${cookieCount }<br><br>


<c:forEach items="${list}" var="guestbook">

${guestbook.id }<br>
${guestbook.name }<br>
${guestbook.content }<br>
${guestbook.regdate }<br>
<c:if test="${sessionScope.isAdmin == 'true'}"><a href="delete?id=${guestbook.id}">삭제</a><br><br></c:if>
</c:forEach>
<br>

<c:forEach items="${pageStartList}" var="pageIndex" varStatus="status">
<a href="list?start=${pageIndex}">${status.index +1 }</a>&nbsp; &nbsp;
</c:forEach>

<br><br>
<form method="post" action="write">
name : <input type="text" name="name"><br>
<textarea name="content" cols="60" rows="6"></textarea><br>
<input type="submit" value="등록">
</form>
</body>
</html>

6. 인터셉터 - BE

1) 인터셉터란?

InterceptorDispatcher servlet에서 Handler(Controller)로 요청을 보낼 때, Handler에서 Dispathcer servlet으로 응답을 보낼 때 동작합니다.

인터셉터 작성법

  • org.springframework.web.servlet.HandlerInterceptor 인터페이스를 구현합니다.
  • org.springframework.web.servlet.handler.HandlerInterceptorAdapter 클래스를 상속받습니다.
  • Java Config를 사용한다면, WebMvcConfigurerAdapter가 가지고 있는 addInterceptors 메소드를 오버라이딩하고 등록하는 과정을 거칩니다.
  • xml 설정을 사용한다면, <mvc:interceptors> 요소에 인터셉터를 등록합니다.

2) 인터셉터를 이용해 Controller 공통 로직 처리하기

LogInterceptor.java

package kr.or.connect.guestbook.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class LogInterceptor extends HandlerInterceptorAdapter{

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		System.out.println(handler.toString() + " 가 종료되었습니다.  " + modelAndView.getViewName() + "을 view로 사용합니다.");
	}

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println(handler.toString() + " 를 호출했습니다.");
		return true;
	}	
}

WebMvcContextConfiguration 에 addInterceptors()메소드를 추가합니다.

인자로 넘어온 InterceptorRegistry의 addInterceptor에 앞에서 만든 인터셉터 객체를 넣어주면 추가가 됩니다.

@Override
	public void addInterceptors(InterceptorRegistry registry) {
    		registry.addInterceptor(new LogInterceptor());
	}

7. 아규먼트 리졸버 - BE

1) 아규먼트 리졸버란?

이번 시간엔 컨트롤러의 메소드의 인자값으로 사용자가 임의의 값을 전달할 수 있도록 도와주는 아규먼트 리졸버(Argument Resolver)에 대해 알아보도록 하겠습니다.

아규먼트 리졸버란?

  • 컨트롤러의 메소드의 인자로 사용자가 임의의 값을 전달하는 방법을 제공하고자 할 때 사용됩니다.
  • 예를 들어, 세션에 저장되어 있는 값 중 특정 이름의 값을 메소드 인자로 전달합니다.

아규먼트 리졸버 작성방법 1/2

  • org.springframework.web.method.support.HandlerMethodArgumentResolver를 구현한 클래스를 작성합니다.
  • supportsParameter메소드를 오버라이딩 한 후, 원하는 타입의 인자가 있는지 검사한 후 있을 경우 true가 리턴되도록 합니다.
  • resolveArgument메소드를 오버라이딩 한 후, 메소드의 인자로 전달할 값을 리턴합니다.

아규먼트 리졸버 작성방법 2/2

  • Java Config에 설정하는 방법
    • WebMvcConfigurerAdapter를 상속받은 Java Config 파일에서 addArgumentResolvers 메소드를 오버라이딩 한 후 원하는 아규먼트 리졸버 클래스 객체를 등록합니다.
  • xml 파일에 설정하는 방법
  <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="아규먼트리졸버클래스"></bean>      
        </mvc:argument-resolvers>
    </mvc:annotation-driven>

Spring MVC의 기본 ArgumentResolver들

getDefaultArgumentResolvers()메소드를 보면 기본으로 설정되는 아규먼트 리졸버에 어떤 것이 있는지 알 수 있습니다.

Map객체나 Map을 상속받은 객체는 Spring에서 이미 선언한 아규먼트 리졸버가 처리하기 때문에 전달 할 수 없습니다.

Map객체를 전달하려면 Map을 필드로 가지고 있는 별도의 객체를 선언한 후 사용해야 합니다.

2) 아규먼트 리졸버를 이용해 HTTP Header정보를 Map객체에 담아서 Controller에게 전달하기

HeaderInfo.java

package kr.or.connect.guestbook.argumentresolver;

import java.util.HashMap;
import java.util.Map;

public class HeaderInfo {
	private Map<String, String> map;
	
	public HeaderInfo() {
		map = new HashMap<>();
	}

	public void put(String name, String value) {
		map.put(name,  value);
	}
	
	public String get(String name) {
		return map.get(name);
	}

}

HeaderMapArgumentResolver.java

package kr.or.connect.guestbook.argumentresolver;

import java.util.Iterator;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class HeaderMapArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.getParameterType() == HeaderInfo.class;
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

		HeaderInfo headerInfo = new HeaderInfo();
		
		Iterator<String> headerNames = webRequest.getHeaderNames();
		while(headerNames.hasNext()) {
			String headerName = headerNames.next();
			String headerValue = webRequest.getHeader(headerName);
//			System.out.println(headerName + " , " + headerValue);
			headerInfo.put(headerName, headerValue);
		}
		
		return headerInfo;

	}

}

supportsParameter에서 true가 반환되는 경우에만 resolveArgument가 호출이 된다. resolveArgument가 반환한 값은 컨트롤러 메서드의 인자로 전달 된다.

아규먼트 리졸버를 적용하려면 WebMvcContextConfiguration 클래스에 addArgumentResolvers메소드를 오버라이딩 하고, 인자로 넘어온 argumentResolvers에 앞에서 생성한 아규먼트 리졸버를 넘겨줘야 합니다.

@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    		System.out.println("아규먼트 리졸버 등록..");
		argumentResolvers.add(new HeaderMapArgumentResolver());
	}

GuestbookController 의 메소드인 list메소드의 인자로 HeaderInfo headerInfo를 추가합니다.

콘솔에 headerInfo의 get메소드에 user-agent를 넘겨서 값이 잘 출력되는지 확인할 수 있도록 코드를 추가합니다.

@GetMapping(path="/list")
	public String list(@RequestParam(name="start", required=false, defaultValue="0") int start,
					   ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value,
					   HttpServletResponse response,
					   HeaderInfo headerInfo) {
		System.out.println("-----------------------------------------------------");
		System.out.println(headerInfo.get("user-agent"));
		System.out.println("-----------------------------------------------------");

0개의 댓글