
먼저 문제 원본은 아래와 같다.
문제에서 실제로 구현해야 하는 조건만 다시 정리하면 이렇다.
- 기존
SpringMVC 실습 2에서 만든 로또 기능을 복사해서 수정한다.- 로또 번호 이미지를 클릭하면 해당 숫자가
GET방식으로 컨트롤러에 전달되어야 한다.- 사용자가 고른 숫자와 서버에서 만든 랜덤 숫자가 같으면 당첨 결과를 보여준다.
- 숫자가 다르면 낙첨 결과와 재시도 링크를 보여준다.
- 낙첨된 경우 로또 응모는 3번까지만 가능하게 만든다.
- 3번을 초과하면 더 이상 응모하지 못하게 막고 안내 메시지를 보여준다.
- 초과 안내 메시지에는
<br>태그를 포함해서 두 줄로 출력한다.- 결과 화면에서는 메시지, 이미지, 재시도 링크를 상황에 맞게 출력한다.
이 문제의 핵심은 단순히 랜덤 숫자를 비교하는 것이 아니라, 브라우저가 유지되는 동안 사용자의 응모 횟수를 기억하고 3번 이후부터는 응모를 막는 것이다.
이 문제는 기존 로또 실습에 응모 횟수 제한을 추가하는 문제다.
기존 기능은 사용자가 숫자 이미지를 클릭하면 컨트롤러로 숫자가 넘어가고, 컨트롤러에서 랜덤 숫자와 비교한 뒤 결과 화면으로 이동하는 구조였다.
이번에는 여기에session을 추가해서 사용자가 몇 번 응모했는지 기억해야 한다.
요청은 한 번 처리되고 끝나지만,session은 같은 브라우저를 사용하는 동안 값을 계속 유지할 수 있다.
그래서 로또 응모 횟수처럼 여러 요청 사이에서 유지해야 하는 값은session에 저장하는 것이 자연스럽다.
이 문제에서 중요한 흐름은 아래와 같다.
이미지 클릭 → GET 요청 → num 전달 → session에서 응모 횟수 확인 → 3번 초과 여부 검사 → 랜덤 숫자 비교 → 결과 데이터 생성 → Model에 저장 → Thymeleaf 화면 출력
lottoForm2.html에서 숫자 이미지 클릭 시num값이 전달되는 구조 확인LottoVO2에서 결과 메시지와 이미지 이름을 담는 구조 확인LottoController2에서 기존 로또 처리 흐름 확인- 컨트롤러에
HttpSession추가session에서 현재 응모 횟수인lottoCount가져오기- 응모 횟수가 3번 이상이면 초과 안내 결과를 바로 반환하기
- 3번 미만이면 기존처럼 랜덤 숫자와 사용자가 고른 숫자 비교하기
- 낙첨이면 응모 횟수를 1 증가시켜
session에 다시 저장하기- 결과 화면에서
<br>태그가 적용되도록th:utext로 변경하기- 최종 코드 확인하기
이번 실습은 기존 로또 기능에
session기반 횟수 제한을 추가하는 흐름이다.
따라서 처음부터 새로 만드는 것이 아니라, 기존 파일들이 각각 어떤 역할을 하는지 먼저 확인한 뒤 컨트롤러와 화면을 수정하면 된다.
1. lottoForm2.html에서 숫자 전달 구조 확인
lottoForm2.html은 사용자가 로또 번호 이미지를 클릭하는 화면이다.
각 이미지는 단순 이미지가 아니라button안에 들어 있다.
그래서 이미지를 클릭하면 버튼이 눌리고, 버튼의name="num"과value값이 서버로 전달된다.<form method="get" action="/lotto2"> <button type="submit" name="num" value="1"> <img src="../images/1.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> <button type="submit" name="num" value="2"> <img src="../images/2.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> </form>여기서 중요한 부분은
method="get"과name="num"이다.
사용자가 1번 이미지를 클릭하면/lotto2?num=1형태로 요청이 전달된다.
2번 이미지를 클릭하면/lotto2?num=2형태로 요청이 전달된다.
즉, 이미지 자체가 값을 보내는 것이 아니라 이미지를 감싸고 있는 버튼이 값을 보내는 구조다.
데이터 흐름
사용자 3번 이미지 클릭 ↓ <button name="num" value="3"> 버튼 제출 ↓ GET /lotto2?num=3 요청 발생 ↓ 컨트롤러의 @RequestParam("num") int num 으로 3 전달이 흐름을 알면 컨트롤러에서 왜
@RequestParam("num")으로 값을 받는지 이해할 수 있다.
화면에서는num이라는 이름으로 값을 보내고, 컨트롤러에서는 같은 이름인num으로 값을 꺼내는 것이다.
2. LottoVO2에서 결과 데이터 담기
LottoVO2는 결과 화면에 넘길 데이터를 하나로 묶는 객체다.
로또 결과 화면에는 결과 메시지와 이미지가 필요하다.
그래서result에는 출력할 문장을 저장하고,imgName에는 출력할 이미지 경로를 저장한다.package com.example.springlab.domain; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public class LottoVO2 { private String result; private String imgName; }
@AllArgsConstructor가 있기 때문에new LottoVO2(result, imgName)처럼 생성자를 직접 호출할 수 있다.
@Getter가 있기 때문에Thymeleaf화면에서${lotto.result},${lotto.imgName}형태로 값을 읽을 수 있다.
데이터 흐름
LottoVO2 vo = new LottoVO2("추카추카!!", "/images/sun.png");result → "추카추카!!" imgName → "/images/sun.png"이렇게 만들어진 객체를
model.addAttribute("lotto", vo)로 저장하면 화면에서는lotto라는 이름으로 이 객체에 접근할 수 있다.
즉, 컨트롤러에서 만든 결과 데이터가Model을 통해 결과 화면으로 넘어가는 구조다.
3. 기존 컨트롤러 흐름 확인
기존 컨트롤러는 사용자가 고른 숫자와 랜덤 숫자를 비교한다.
두 숫자가 같으면 당첨 메시지와 해 이미지를 보여주고, 다르면 낙첨 메시지와 비 이미지를 보여준다.@RequestMapping("/lotto2") public String lotto(@RequestParam("num") int num, HttpServletRequest request, Model model) { int randomNum = new Random().nextInt(6) + 1; String result = ""; String imgName = ""; String referer = ""; if (num == randomNum) { result = "추카추카"; imgName = "/images/sun.png"; } else { result = "아쉽네요.. 다음 기회를 !!"; imgName = "/images/rain.png"; referer = request.getHeader("Referer"); } LottoVO2 vo = new LottoVO2(result, imgName); model.addAttribute("lotto", vo); model.addAttribute("previousPage", referer); return "lottoView2"; }여기서
request.getHeader("Referer")는 사용자가 이전에 있던 페이지 주소를 가져온다.
낙첨된 경우에만 이 값을previousPage로 넘기면 결과 화면에서 재시도 링크를 만들 수 있다.
이제 여기서 추가해야 하는 것은 응모 횟수다.
응모 횟수는 한 번의 요청 안에서만 필요한 값이 아니라, 여러 번 요청해도 계속 기억되어야 하는 값이다.
그래서 지역변수가 아니라session에 저장해야 한다.
4. 컨트롤러에 HttpSession 추가하기
응모 횟수를 기억하려면
HttpSession이 필요하다.
session은 같은 브라우저 안에서 여러 요청 사이에 값을 유지할 수 있는 저장 공간이다.@RequestMapping("/lotto2") public String lotto(@RequestParam("num") int num, HttpServletRequest request, HttpSession session, Model model) { // 로또 처리 코드 작성 }기존 매개변수에
HttpSession session만 추가하면 된다.
그러면 스프링이 현재 요청에 연결된 세션 객체를 자동으로 넣어준다.
데이터 흐름
첫 번째 요청 GET /lotto2?num=3 ↓ session에 lottoCount 없음 ↓ lottoCount를 0으로 시작두 번째 요청 GET /lotto2?num=5 ↓ 같은 브라우저라면 session 유지 ↓ 이전에 저장한 lottoCount 다시 꺼내기 가능이 문제에서
session을 쓰는 이유는 명확하다.
응모 횟수는 한 번 요청하고 끝나는 값이 아니라, 사용자가 몇 번 응모했는지 계속 누적해서 확인해야 하는 값이기 때문이다.
5. session에서 응모 횟수 가져오기
처음 응모하는 사용자는 아직
session에 응모 횟수가 없다.
그래서 먼저session.getAttribute("lottoCount")로 값을 꺼내고, 값이 없으면 0으로 시작해야 한다.Integer lottoCount = (Integer) session.getAttribute("lottoCount"); if (lottoCount == null) { lottoCount = 0; }
getAttribute()의 반환 타입은Object다.
그래서 숫자로 사용하려면(Integer)로 형변환해야 한다.
처음에는 값이 없기 때문에null이 나온다.
그 상태에서 바로 숫자 비교를 하면 문제가 생기므로,null이면 0으로 바꿔준다.
데이터 흐름
처음 응모 session.getAttribute("lottoCount") → null lottoCount = 0이전에 2번 낙첨된 상태 session.getAttribute("lottoCount") → 2 lottoCount = 2즉,
lottoCount는 현재 사용자가 이 브라우저에서 몇 번 낙첨 응모를 했는지 기억하는 값이다.
6. 응모 횟수가 3번 이상이면 바로 차단하기
응모 횟수가 이미 3번 이상이면 더 이상 랜덤 숫자를 뽑을 필요가 없다.
이미 응모 제한을 넘었기 때문에 바로 안내 메시지와 이미지를 만들어 결과 화면으로 이동시키면 된다.if (lottoCount >= 3) { LottoVO2 vo = new LottoVO2( "로또 응모는 낙첨된 경우에 한하여 3번 까지만 가능합니다.<br>브라우저를 재기동한 후에 응모해 주세요", "/images/snow.png" ); model.addAttribute("lotto", vo); model.addAttribute("previousPage", ""); return "lottoView2"; }여기서는 메시지 안에
<br>태그가 들어간다.
문제에서 두 줄 안내 메시지를 요구했기 때문이다.
또한previousPage에는 빈 문자열을 넣는다.
응모 제한을 넘은 상태에서는 재시도 링크가 나오면 안 되기 때문이다.
데이터 흐름
현재 session 값 lottoCount = 3조건 검사 lottoCount >= 3 → true결과 화면으로 보낼 데이터 result → "로또 응모는 낙첨된 경우에 한하여 3번 까지만 가능합니다.<br>브라우저를 재기동한 후에 응모해 주세요" imgName → "/images/snow.png" previousPage → ""이 흐름에서는 랜덤 숫자 비교를 하지 않는다.
이미 응모 제한을 넘었으므로 로또 결과가 아니라 제한 안내 화면을 보여주는 것이 맞다.
7. 3번 미만이면 랜덤 숫자와 비교하기
응모 횟수가 3번 미만이면 기존 로또 기능을 그대로 실행한다.
서버에서 1부터 6 사이의 랜덤 숫자를 만들고, 사용자가 고른 숫자와 비교한다.int randomNum = new Random().nextInt(6) + 1; String result = ""; String imgName = ""; String referer = ""; System.out.println("내가 누른 번호: " + num); System.out.println("랜덤 번호: " + randomNum);
new Random().nextInt(6)은 0부터 5까지의 숫자 중 하나를 만든다.
여기에 1을 더하면 1부터 6까지의 숫자가 된다.
즉, 사용자가 고른 숫자도 1부터 6이고, 서버가 만든 랜덤 숫자도 1부터 6이다.
두 숫자를 비교해서 같으면 당첨, 다르면 낙첨이다.
데이터 흐름
사용자 클릭 num = 4서버 랜덤 숫자 생성 randomNum = 2비교 num == randomNum 4 == 2 → false이 경우에는 낙첨 결과로 이동한다.
반대로num과randomNum이 같으면 당첨 결과로 이동한다.
8. 당첨과 낙첨 결과 처리하기
당첨되면 성공 메시지와 해 이미지를 보여준다.
낙첨되면 실패 메시지와 비 이미지를 보여주고, 응모 횟수를 1 증가시킨다.if (num == randomNum) { result = "추카추카!!"; imgName = "/images/sun.png"; session.removeAttribute("lottoCount"); } else { lottoCount++; session.setAttribute("lottoCount", lottoCount); result = "아쉽네요.. 다음 기회를 !!"; imgName = "/images/rain.png"; referer = request.getHeader("Referer"); }당첨된 경우에는
session.removeAttribute("lottoCount")로 응모 횟수를 지웠다.
당첨 결과가 나왔으면 더 이상 낙첨 재시도 횟수를 유지할 필요가 없기 때문이다.
낙첨된 경우에는lottoCount++로 횟수를 1 증가시킨 뒤session.setAttribute()로 다시 저장한다.
이렇게 해야 다음 요청에서도 증가된 횟수를 기억할 수 있다.
데이터 흐름
낙첨 전 lottoCount = 1낙첨 처리 lottoCount++낙첨 후 lottoCount = 2 session.setAttribute("lottoCount", 2)즉, 지역변수 값만 증가시키면 끝이 아니다.
반드시 다시session에 저장해야 다음 요청에서 증가된 값이 유지된다.
9. Model에 결과 데이터 저장하기
컨트롤러에서 만든 결과 메시지와 이미지 경로는
LottoVO2객체에 담는다.
그리고 그 객체를Model에 저장해서 결과 화면으로 넘긴다.LottoVO2 vo = new LottoVO2(result, imgName); model.addAttribute("lotto", vo); model.addAttribute("previousPage", referer); return "lottoView2";
lotto에는 결과 메시지와 이미지 경로가 들어간다.
previousPage에는 재시도 링크로 사용할 이전 페이지 주소가 들어간다.
당첨된 경우에는referer가 빈 문자열이므로 재시도 링크가 나오지 않는다.
낙첨된 경우에는referer에 이전 페이지 주소가 들어가므로 재시도 링크가 나온다.
데이터 흐름
컨트롤러 model.addAttribute("lotto", vo) model.addAttribute("previousPage", referer)화면 ${lotto.result} ${lotto.imgName} ${previousPage}컨트롤러에서 저장한 이름과 화면에서 꺼내는 이름이 같아야 한다.
lotto로 저장했으면 화면에서도${lotto.result}처럼 접근해야 한다.
10. 결과 화면에서 br 태그 적용하기
기존 화면에서
th:text를 사용하면<br>태그가 문자 그대로 출력될 수 있다.
이번 문제는 전달하는 메시지에<br>태그를 포함해서 줄바꿈해야 하므로th:utext를 사용한다.<h2 th:utext="${lotto.result}" style="color:red; text-align: center">결과화면</h2>
th:text는 태그를 문자로 안전하게 출력한다.
반면th:utext는 태그를 HTML로 해석해서 출력한다.
이번 실습에서는 컨트롤러에서 직접 만든 안내 문장만 출력하므로<br>을 적용하기 위해th:utext를 사용한다.
데이터 흐름
컨트롤러에서 보낸 값 "로또 응모는 낙첨된 경우에 한하여 3번 까지만 가능합니다.<br>브라우저를 재기동한 후에 응모해 주세요"<h2 th:utext="${lotto.result}">화면 출력 로또 응모는 낙첨된 경우에 한하여 3번 까지만 가능합니다. 브라우저를 재기동한 후에 응모해 주세요즉,
<br>을 문자열로 보여주는 것이 아니라 실제 줄바꿈으로 적용하려면th:utext가 필요하다.
아래 코드는 이번 문제를 반영한 최종 코드다.
핵심 수정은LottoController2에HttpSession을 추가하고,lottoView2.html에서th:text를th:utext로 바꾼 부분이다.
lottoForm2.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Document</title> </head> <body> <h1>Lotto Game</h1> <hr/> <form method="get" action="/lotto2"> <button type="submit" name="num" value="1"> <img src="../images/1.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> <button type="submit" name="num" value="2"> <img src="../images/2.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> <button type="submit" name="num" value="3"> <img src="../images/3.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> <button type="submit" name="num" value="4"> <img src="../images/4.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> <button type="submit" name="num" value="5"> <img src="../images/5.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> <button type="submit" name="num" value="6"> <img src="../images/6.png" alt="로또 추첨" style="width: 70px; height: 70px"/> </button> </form> </body> </html>
LottoVO2.java
package com.example.springlab.domain; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public class LottoVO2 { private String result; private String imgName; }
LottoController2.java
package com.example.springlab.controller; import com.example.springlab.domain.LottoVO2; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.Random; @Controller public class LottoController2 { @RequestMapping("/lotto2") public String lotto(@RequestParam("num") int num, HttpServletRequest request, HttpSession session, Model model) { Integer lottoCount = (Integer) session.getAttribute("lottoCount"); // 추가: 세션에서 기존 응모 횟수 가져오기 if (lottoCount == null) { // 추가: 처음 응모한 경우 0으로 시작 lottoCount = 0; } if (lottoCount >= 3) { // 추가: 3번 이상 낙첨 응모한 경우 더 이상 응모하지 못하게 처리 LottoVO2 vo = new LottoVO2( "로또 응모는 낙첨된 경우에 한하여 3번 까지만 가능합니다.<br>브라우저를 재기동한 후에 응모해 주세요", "/images/snow.png" ); model.addAttribute("lotto", vo); model.addAttribute("previousPage", ""); return "lottoView2"; } int randomNum = new Random().nextInt(6) + 1; String result = ""; String imgName = ""; String referer = ""; System.out.println("내가 누른 번호: " + num); System.out.println("랜덤 번호: " + randomNum); if (num == randomNum) { result = "추카추카!!"; imgName = "/images/sun.png"; session.removeAttribute("lottoCount"); // 추가: 당첨되면 낙첨 응모 횟수 초기화 } else { lottoCount++; // 추가: 낙첨된 경우 응모 횟수 1 증가 session.setAttribute("lottoCount", lottoCount); // 추가: 증가된 응모 횟수를 세션에 다시 저장 result = "아쉽네요.. 다음 기회를 !!"; imgName = "/images/rain.png"; referer = request.getHeader("Referer"); } LottoVO2 vo = new LottoVO2(result, imgName); model.addAttribute("lotto", vo); model.addAttribute("previousPage", referer); return "lottoView2"; } }
lottoView2.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1 style="text-align: center">로또 결과</h1> <hr/> <h2 th:utext="${lotto.result}" style="color:red; text-align: center">결과화면</h2> <img th:src="${lotto.imgName}" style="display: block; margin: 0 auto;"> <hr/> <h2 th:if="${previousPage != null and previousPage != ''}" style="text-align: center"> <a th:href="${previousPage}">재시도</a> </h2> </body> </html>
GET방식에서는 버튼의name과value가 쿼리스트링으로 전달된다.@RequestParam("num")은 요청으로 전달된num값을 컨트롤러 매개변수로 받는다.- 한 번의 요청 안에서만 쓰는 값은 지역변수로 처리할 수 있다.
- 여러 요청 사이에서 유지해야 하는 값은
session에 저장해야 한다.session.getAttribute()의 반환 타입은Object이므로 필요한 타입으로 형변환해야 한다.- 처음 세션 값은 없을 수 있으므로
null체크가 필요하다.- 세션 값을 증가시킨 뒤에는
setAttribute()로 다시 저장해야 다음 요청에서 유지된다.th:text는 HTML 태그를 문자로 출력하고,th:utext는 HTML 태그를 해석해서 출력한다.- 이번 문제에서는
<br>태그를 실제 줄바꿈으로 적용해야 하므로th:utext를 사용해야 한다.