오늘 우리는 서블릿과 JSP를 사용하여 Tic-Tac-Toe 게임을 작성할 것입니다.

이 프로젝트는 이전 프로젝트와 조금 다를 것입니다. 작업뿐만 아니라 수행 방법에 대한 설명도 포함됩니다. 즉, "HOW TO ..."시리즈의 프로젝트가 될 것입니다.

지침:

  1. 저장소에서 포크: https://github.com/CodeGymCC/project-servlet.git
  2. 프로젝트 버전을 컴퓨터에 다운로드합니다.
  3. IDEA에서 애플리케이션 실행 설정:
    • Alt + Shift + F9 -> 구성 편집… -> Alt + 삽입 -> tom(검색 표시줄에) -> Local.
    • 그런 다음 "구성"을 클릭하고 Tomcat이 있는 아카이브를 다운로드하고 압축을 푼 위치를 지정해야 합니다.
    • "배포" 탭에서: Alt + 삽입 -> Artifact… -> tic-tac-toe:war exploded -> 확인.
    • "응용 프로그램 컨텍스트" 필드에서 "/"(슬래시)만 남겨 둡니다.
    • "적용"을 누르십시오.
    • 설정 창을 닫습니다.
    • 사용자 정의된 구성의 첫 번째 테스트 실행을 수행하십시오. 모든 것이 올바르게 완료되면 다음과 같은 기본 브라우저가 열립니다.
  4. "pom.xml" 파일을 엽니다 . "종속성" 블록 에는 2개의 종속성이 있습니다 .
    • javax.servlet-api서블릿 사양을 담당합니다. "제공된" 범위는 개발 중에 필요하지만 런타임에는 필요하지 않습니다(Tomcat은 이미 lib 폴더에 이 종속성을 가지고 있습니다).
    • jstl– 템플릿 엔진으로 간주할 수 있습니다.
  5. "webapp" 폴더 에는 3개의 파일이 있습니다 .
    • index.jsp- 이것은 템플릿입니다(HTML 페이지와 유사). 여기에는 마크업과 스크립트가 포함됩니다. 3단계에서 보았던 구성이 없을 경우 초기 페이지로 주어지는 것은 “index” 라는 파일입니다 .
    • /static/main.css- 스타일 파일. 이전 프로젝트에서와 마찬가지로 여기에 있는 모든 것은 원하는 대로 페인트할 수 있습니다.
    • /static/jquery-3.6.0.min.js- 서버가 정적으로 배포할 프런트엔드 종속성.
  6. "com.tictactoe" 패키지에는 모든 Java 코드가 포함됩니다. 현재 2개의 클래스가 있습니다.
    • Sign- enum은 "cross/zero/void"를 담당합니다 .
    • Field우리 분야입니다. 이 클래스에는 "필드" 맵이 있습니다 . 데이터 저장 원리는 다음과 같습니다. tic-tac-toe 필드의 셀은 0부터 번호가 매겨집니다. 첫 번째 줄에는 0, 1, 2, 두 번째 줄에는 3, 4, 5 등이 있습니다. 3가지 방법도 있습니다. "getEmptyFieldIndex"는 첫 번째 빈 셀을 찾습니다(예, 상대방은 그다지 똑똑하지 않습니다). "checkWin"은 게임이 끝났는지 확인합니다. 세 개의 십자 표시가 있는 행이 있으면 십자 표시를 반환하고 3개의 0이 있는 행이 있으면 0을 반환합니다. 그렇지 않으면 비어 있습니다. "getFieldData" - "필드" 맵 의 값을 인덱스 오름차순으로 정렬된 목록으로 반환합니다.
  7. 템플릿에 대한 설명이 끝났습니다. 이제 작업을 시작할 수 있습니다. 3 x 3 테이블을 그리는 것으로 시작하겠습니다. 이렇게 하려면 "index.jsp" 에 다음 코드를 추가합니다 .
    <table>
    	<tr>
    		<td>0</td>
    		<td>1</td>
    		<td>2</td>
    	</tr>
    	<tr>
    		<td>3</td>
    		<td>4</td>
    		<td>5</td>
    	</tr>
    	<tr>
    		<td>6</td>
    		<td>7</td>
    		<td>8</td>
    	</tr>
    </table>
    그런 다음 테이블에서 숫자를 제거하고 십자, 0 또는 빈 필드로 바꿉니다. 또한 "head" 태그 안에 스타일 파일을 포함합니다. 이렇게 하려면 다음 행을 추가하십시오.<link href="static/main.css" rel="stylesheet">

    스타일 파일의 내용은 귀하에게 달려 있습니다. 나는 이것을 사용했다 :
    td {
        border: 3px solid black;
        padding: 10px;
        border-collapse: separate;
        margin: 10px;
        width: 100px;
        height: 100px;
        font-size: 50px;
        text-align: center;
        empty-cells: show;
    }
    실행 후 내 결과는 다음과 같습니다.
  8. 이제 다음 기능을 추가해 보겠습니다. 셀을 클릭하면 클릭한 셀의 인덱스를 매개변수로 전달하는 요청이 서버로 전송됩니다. 이 작업은 전면에서 요청을 보내고 서버에서 요청을 수락하는 두 부분으로 나눌 수 있습니다. 변화를 위해 정면에서 시작합시다.

    각 "d" 태그 에 "onclick" 매개변수를 추가해 보겠습니다 . 값에서 현재 페이지가 지정된 URL로 변경되었음을 나타냅니다. 논리를 담당할 서블릿에는 URL "/logic" 이 있습니다 . 그리고 "click" 이라는 매개변수를 사용합니다 . 따라서 사용자가 클릭한 셀의 인덱스를 전달합니다.
    <table>
        <tr>
            <td onclick="window.location='/logic?click=0'">0</td>
            <td onclick="window.location='/logic?click=1'">1</td>
            <td onclick="window.location='/logic?click=2'">2</td>
        </tr>
        <tr>
            <td onclick="window.location='/logic?click=3'">3</td>
            <td onclick="window.location='/logic?click=4'">4</td>
            <td onclick="window.location='/logic?click=5'">5</td>
        </tr>
        <tr>
            <td onclick="window.location='/logic?click=6'">6</td>
            <td onclick="window.location='/logic?click=7'">7</td>
            <td onclick="window.location='/logic?click=8'">8</td>
        </tr>
    </table>
    브라우저의 개발자 패널을 통해 모든 것이 올바르게 수행되었는지 확인할 수 있습니다. 예를 들어 Chrome에서는 F12 버튼 으로 열립니다 . 인덱스가 4인 셀을 클릭한 결과 그림은 다음과 같습니다. "logic" 주소로 서버를 보낼 수 있는 서블릿을 아직 생성하지 않았기 때문에 오류가 발생합니다 .
  9. "com.tictactoe" 패키지에서 "javax.servlet.http.HttpServlet " 클래스에서 파생되어야 하는 "LogicServlet" 클래스를 만듭니다 . 클래스에서 "doGet" 메서드를 재정의합니다 .

    그리고 클릭한 셀의 인덱스를 가져오는 메서드를 추가해 보겠습니다. 또한 매핑(이 서블릿이 요청을 가로챌 주소)을 추가해야 합니다. 주석을 통해 이 작업을 수행하는 것이 좋습니다(어려움이 마음에 들면 web.xml을 사용할 수도 있음). 일반 서블릿 코드:
    package com.tictactoe;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    
    @WebServlet(name = "LogicServlet", value = "/logic")
    public class LogicServlet extends HttpServlet {
        @Override
    	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            int index = getSelectedIndex(req);
            resp.sendRedirect("/index.jsp");
        }
    
    
        private int getSelectedIndex(HttpServletRequest request) {
            String click = request.getParameter("click");
            boolean isNumeric = click.chars().allMatch(Character::isDigit);
            return isNumeric ? Integer.parseInt(click) : 0;
        }
    
    }
    이제 임의의 셀을 클릭하면 서버에서 이 셀의 인덱스를 가져옵니다(디버그에서 서버를 실행하여 확인할 수 있음). 클릭이 발생한 동일한 페이지로 리디렉션됩니다.
  10. 이제 클릭할 수 있지만 아직 게임이 아닙니다. 게임에 논리가 있으려면 요청 사이에 게임 상태(십자가 있는 곳, 0이 있는 곳)를 저장해야 합니다. 이를 수행하는 가장 쉬운 방법은 이 데이터를 세션에 저장하는 것입니다. 이 접근 방식을 사용하면 세션이 서버에 저장되고 클라이언트는 "JSESSIONID" 라는 쿠키에 세션 ID를 받습니다 . 그러나 세션은 매번 생성할 필요가 없으며 게임 시작 시에만 생성됩니다. 이를 위해 "InitServlet" 이라고 하는 다른 서블릿을 시작하겠습니다 . "doGet" 메서드를 재정의하여 새 세션을 만들고, 경기장을 만들고, 이 경기장과 Sign 유형 목록을 세션 속성에 넣고 " forward" 를 index.jsp로 보냅니다. 페이지. 암호:
    package com.tictactoe;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.io.IOException;
    import java.util.List;
    import java.util.Map;
    
    @WebServlet(name = "InitServlet", value = "/start")
    public class InitServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // Create a new session
            HttpSession currentSession = req.getSession(true);
    
            // Create a playing field
            Field field = new Field();
            Map<Integer, Sign> fieldData = field.getField();
    
            // Get a list of field values
            List<Sign> data = field.getFieldData();
    
            // Adding field parameters to the session (needed to store state between requests)
            currentSession.setAttribute("field", field);
            // and field values ​​sorted by index (required for drawing crosses and zeroes)
            currentSession.setAttribute("data", data);
    
            // Redirect request to index.jsp page via server
            getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
        }
    }
    잊지 말고 서버를 시작한 후 브라우저에서 열리는 시작 페이지를 "/start" 로 변경하십시오 . 이제 서버를 다시 시작하고 "요청 헤더" 섹션의 브라우저 개발자 메뉴에서 필드의 아무 셀이나 클릭한 후 , 세션 ID가 있는 쿠키가 있습니다.
  11. 클라이언트(브라우저)의 요청 간에 상태를 저장할 수 있는 저장소가 있으면 게임 로직 작성을 시작할 수 있습니다. 우리가 가지고 있는 로직은 "LogicServlet" 에 있습니다 . "doGet" 메서드 로 작업해야 합니다 . 이 동작을 메서드에 추가해 보겠습니다.
    • 우리는 세션에서 Field 유형의 "field" 개체를 가져올 것입니다( "extractField" 메서드 로 가져갈 것입니다 ).
    • 사용자가 클릭한 곳에 십자 표시를 합니다(지금까지 확인하지 않음).
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();
    
        // Get the playfield object from the session
        Field field = extractField(currentSession);
    
        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
    
        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);
    
        // Read the list of icons
        List<Sign> data = field.getFieldData();
    
        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);
    
        resp.sendRedirect("/index.jsp");
    }
    
    
    
    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
    동작은 아직 변경되지 않았지만 디버그에서 서버를 시작하고 리디렉션이 전송되는 줄에 중단점을 설정하면 "data" 객체 의 "innards"를 볼 수 있습니다 . 실제로 클릭한 색인 아래에 "CROSS"가 표시됩니다 .
  12. 이제 프런트엔드에 십자가를 표시할 차례입니다. 이를 위해 "index.jsp" 파일 과 "JSTL" 기술을 사용합니다 .
    • <head> 섹션에 다음을 추가합니다.<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    • 각 <td> 블록 내부의 테이블에서 인덱스를 값을 계산할 수 있는 구문으로 변경합니다. 예를 들어 인덱스 0의 경우: <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td> 이제 셀을 클릭하면 십자 표시가 나타납니다.
  13. 우리는 움직였습니다. 이제 "제로"의 차례입니다. 그리고 이미 점유된 셀에 표지판이 배치되지 않도록 여기에 몇 가지 확인 사항을 추가해 보겠습니다.
    • 클릭한 셀이 비어있는지 확인해야 합니다. 그렇지 않으면 아무것도 하지 않고 세션 매개변수를 변경하지 않고 사용자를 동일한 페이지로 보냅니다.
    • 필드의 셀 수가 홀수이므로 십자가가 배치되었을 가능성이 있지만 0이 들어갈 여지가 없습니다. 따라서 십자 표시를 한 후 비어 있는 셀의 인덱스를 가져오려고 합니다(Field 클래스의 getEmptyFieldIndex 메서드). 인덱스가 음수가 아니면 거기에 0을 넣으십시오. 암호:
      package com.tictactoe;
      
      import javax.servlet.RequestDispatcher;
      import javax.servlet.ServletException;
      import javax.servlet.annotation.WebServlet;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import javax.servlet.http.HttpSession;
      import java.io.IOException;
      import java.util.List;
      
      @WebServlet(name = "LogicServlet", value = "/logic")
      public class LogicServlet extends HttpServlet {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              // Get the current session
              HttpSession currentSession = req.getSession();
      
              // Get the playfield object from the session
              Field field = extractField(currentSession);
      
              // get the index of the cell that was clicked
              int index = getSelectedIndex(req);
              Sign currentSign = field.getField().get(index);
      
              // Check if the clicked cell is empty.
              // Otherwise, we do nothing and send the user to the same page without changes
              // parameters in the session
              if (Sign.EMPTY != currentSign) {
                  RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
                  dispatcher.forward(req, resp);
                  return;
              }
      
              // put a cross in the cell that the user clicked on
              field.getField().put(index, Sign.CROSS);
      
              // Get an empty field cell
              int emptyFieldIndex = field.getEmptyFieldIndex();
      
              if (emptyFieldIndex >= 0) {
                  field.getField().put(emptyFieldIndex, Sign.NOUGHT);
              }
      
              // Read the list of icons
              List<Sign> data = field.getFieldData();
      
              // Update field object and icon list in session
              currentSession.setAttribute("data", data);
              currentSession.setAttribute("field", field);
      
              resp.sendRedirect("/index.jsp");
          }
      
          private int getSelectedIndex(HttpServletRequest request) {
              String click = request.getParameter("click");
              boolean isNumeric = click.chars().allMatch(Character::isDigit);
              return isNumeric ? Integer.parseInt(click) : 0;
          }
      
          private Field extractField(HttpSession currentSession) {
              Object fieldAttribute = currentSession.getAttribute("field");
              if (Field.class != fieldAttribute.getClass()) {
                  currentSession.invalidate();
                  throw new RuntimeException("Session is broken, try one more time");
              }
              return (Field) fieldAttribute;
          }
      }
  14. 이 단계에서 십자 표시를 하면 AI가 0으로 대답합니다. 그러나 언제 게임을 중지할지 확인하지 않습니다. 다음 세 가지 경우일 수 있습니다.
    • 다음 십자가 이동 후 세 개의 십자가 선이 형성되었습니다.
    • 0으로 다음 리턴 이동 후 세 개의 0이 형성되었습니다.
    • 십자가의 다음 이동 후 빈 셀이 끝났습니다.
    행에 세 개의 십자/0이 있는지 확인하는 메서드를 추가해 보겠습니다.
    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);
    
            // Read the list of icons
            List<Sign> data = field.getFieldData();
    
            // Update this list in session
            currentSession.setAttribute("data", data);
    
            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }
    이 방법의 특이점은 승자가 발견되면 세션에 다른 매개변수를 추가하여 다음 단락에서 "index.jsp" 의 표시를 변경한다는 것입니다 .
  15. “checkWin ” 메서드에 대한 호출을 “doGet” 메서드에 두 번 추가해 봅시다 . 십자가를 설정 한 후 처음으로, 두 번째는 0을 설정 한 후입니다.
    // Check if the cross won after adding the user's last click
    if (checkWin(resp, currentSession, field)) {
        return;
    }
    if (emptyFieldIndex >= 0) {
        field.getField().put(emptyFieldIndex, Sign.NOUGHT);
        // Check if the zero won after adding the last zero
        if (checkWin(resp, currentSession, field)) {
            return;
        }
    }
  16. 동작 측면에서 거의 아무것도 변경되지 않았습니다(사인 중 하나가 이기면 더 이상 0이 배치되지 않는다는 점만 제외). "index.jsp" 에서 "winner" 매개변수를 사용 하고 승자를 표시해 보겠습니다. 테이블 c:set다음에 지시문을 사용합니다.c:if
    <hr>
    <c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
    <c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>
    
    <c:if test="${winner == CROSSES}">
        <h1>CROSSES WIN!</h1>
    </c:if>
    <c:if test="${winner == NOUGHTS}">
        <h1>NOUGHTS WIN!</h1>
    </c:if>
    크로스가 이기면 "CROSSES WIN!" , 0이 "NOUGHTS WIN!" 인 경우 . 결과적으로 다음 두 비문 중 하나를 얻을 수 있습니다.
  17. 승자가 있으면 복수할 수 있어야 합니다. 이렇게하려면 서버에 요청을 보내는 버튼이 필요합니다. 그리고 서버는 현재 세션을 무효화하고 요청을 "/start" 로 다시 리디렉션합니다 .
    • "head" 섹션 의 "index.jsp" 에서 스크립트 "jquery" 를 작성합니다 . 이 라이브러리를 사용하여 서버에 요청을 보냅니다.
      <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    • "script" 섹션 의 "index.jsp" 에서 서버에 POST 요청을 보낼 수 있는 기능을 추가합니다. 함수를 동기식으로 만들고 서버에서 응답이 오면 현재 페이지를 다시 로드합니다.
      <script>
          function restart() {
              $.ajax({
                  url: '/restart',
                  type: 'POST',
                  contentType: 'application/json;charset=UTF-8',
                  async: false,
                  success: function () {
                      location.reload();
                  }
              });
          }
      </script>
    • "c:if" 블록 안에 클릭하면 방금 작성한 함수를 호출하는 버튼을 추가합니다.
      <c:if test="${winner == CROSSES}">
          <h1>CROSSES WIN!</h1>
          <button onclick="restart()">Start again</button>
      </c:if>
      <c:if test="${winner == NOUGHTS}">
          <h1>NOUGHTS WIN!</h1>
          <button onclick="restart()">Start again</button>
      </c:if>
    • "/restart" URL을 제공할 새 서블릿을 만들어 봅시다 .
      package com.tictactoe;
      
      import javax.servlet.annotation.WebServlet;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      
      @WebServlet(name = "RestartServlet", value = "/restart")
      public class RestartServlet extends HttpServlet {
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
              req.getSession().invalidate();
              resp.sendRedirect("/start");
          }
      }
      승리 후 "다시 시작" 버튼이 나타납니다 . 그것을 클릭하면 필드가 완전히 지워지고 게임이 다시 시작됩니다.
  18. 마지막 상황을 고려하는 것이 남아 있습니다. 사용자가 십자가를 놓으면 승리가 없고 0이 들어갈 자리가 없다면? 그러면 이것은 무승부이며 지금 처리합니다.
    • "LogicServlet" 세션 에서 다른 매개변수 "draw"를 추가하고 "data" 필드를 업데이트한 다음 "index.jsp" 로 리디렉션을 보냅니다 .
      // If such a cell exists
      if (emptyFieldIndex >= 0) {}
      // If there is no empty cell and no one wins, then it's a draw
      else {
          // Add a flag to the session that signals that a draw has occurred
          currentSession.setAttribute("draw", true);
      
          // Read the list of icons
          List<Sign> data = field.getFieldData();
      
          // Update this list in session
          currentSession.setAttribute("data", data);
      
          // helmet redirect
          response.sendRedirect("/index.jsp");
          return;
      }
    • "index.jsp" 에서 이 매개변수를 처리합니다.
      <c:if test="${draw}">
          <h1>IT'S A DRAW</h1>
          <br>
          <button onclick="restart()">Start again</button>
      </c:if>
      무승부의 결과로 해당 메시지와 다시 시작하라는 제안을 받게 됩니다.

이것으로 게임 작성이 완료되었습니다.

작업한 클래스 및 파일 코드

초기화 서블릿

package com.tictactoe;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;
import java.util.Map;

@WebServlet(name = "InitServlet", value = "/start")
public class InitServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Create a new session
        HttpSession currentSession = req.getSession(true);

        // Create a playing field
        Field field = new Field();
        Map<Integer, Sign> fieldData = field.getField();

        // Get a list of field values
        List<Sign> data = field.getFieldData();

        // Adding field parameters to the session (needed to store state between requests)
        currentSession.setAttribute("field", field);
        // and field values ​​sorted by index (required for drawing crosses and zeroes)
        currentSession.setAttribute("data", data);

        // Redirect request to index.jsp page via server
        getServletContext().getRequestDispatcher("/index.jsp").forward(req, resp);
    }
}

로직서블릿

package com.tictactoe;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;

@WebServlet(name = "LogicServlet", value = "/logic")
public class LogicServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Get the current session
        HttpSession currentSession = req.getSession();

        // Get the playfield object from the session
        Field field = extractField(currentSession);

        // get the index of the cell that was clicked
        int index = getSelectedIndex(req);
        Sign currentSign = field.getField().get(index);

        // Check if the clicked cell is empty.
        // Otherwise, we do nothing and send the user to the same page without changes
        // parameters in the session
        if (Sign.EMPTY != currentSign) {
            RequestDispatcher dispatcher = getServletContext().getRequestDispatcher("/index.jsp");
            dispatcher.forward(req, resp);
            return;
        }

        // put a cross in the cell that the user clicked on
        field.getField().put(index, Sign.CROSS);

        // Check if the cross has won after adding the user's last click
        if (checkWin(resp, currentSession, field)) {
            return;
        }

        // Get an empty field cell
        int emptyFieldIndex = field.getEmptyFieldIndex();

        if (emptyFieldIndex >= 0) {
            field.getField().put(emptyFieldIndex, Sign.NOUGHT);
            // Check if the zero won after adding the last zero
            if (checkWin(resp, currentSession, field)) {
                return;
            }
        }
        // If there is no empty cell and no one wins, then it's a draw
        else {
            // Add a flag to the session that signals that a draw has occurred
            currentSession.setAttribute("draw", true);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            resp.sendRedirect("/index.jsp");
            return;
        }

        // Read the list of icons
        List<Sign> data = field.getFieldData();

        // Update field object and icon list in session
        currentSession.setAttribute("data", data);
        currentSession.setAttribute("field", field);

        resp.sendRedirect("/index.jsp");
    }

    /**
     * The method checks if there are three X/O's in a row.
     * returns true/false
     */
    private boolean checkWin(HttpServletResponse response, HttpSession currentSession, Field field) throws IOException {
        Sign winner = field.checkWin();
        if (Sign.CROSS == winner || Sign.NOUGHT == winner) {
            // Add a flag to indicate that someone has won
            currentSession.setAttribute("winner", winner);

            // Read the list of icons
            List<Sign> data = field.getFieldData();

            // Update this list in session
            currentSession.setAttribute("data", data);

            // helmet redirect
            response.sendRedirect("/index.jsp");
            return true;
        }
        return false;
    }

    private int getSelectedIndex(HttpServletRequest request) {
        String click = request.getParameter("click");
        boolean isNumeric = click.chars().allMatch(Character::isDigit);
        return isNumeric ? Integer.parseInt(click) : 0;
    }

    private Field extractField(HttpSession currentSession) {
        Object fieldAttribute = currentSession.getAttribute("field");
        if (Field.class != fieldAttribute.getClass()) {
            currentSession.invalidate();
            throw new RuntimeException("Session is broken, try one more time");
        }
        return (Field) fieldAttribute;
    }
}

RestartServlet

package com.tictactoe;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "RestartServlet", value = "/restart")
public class RestartServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        req.getSession().invalidate();
        resp.sendRedirect("/start");
    }
}

index.jsp_ _

<%@ page import="com.tictactoe.Sign" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<!DOCTYPE html>
<html>
<head>
    <link href="static/main.css" rel="stylesheet">
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <script src="<c:url value="/static/jquery-3.6.0.min.js"/>"></script>
    <title>Tic-Tac-Toe</title>
</head>
<body>
<h1>Tic-Tac-Toe</h1>

<table>
    <tr>
        <td onclick="window.location='/logic?click=0'">${data.get(0).getSign()}</td>
        <td onclick="window.location='/logic?click=1'">${data.get(1).getSign()}</td>
        <td onclick="window.location='/logic?click=2'">${data.get(2).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=3'">${data.get(3).getSign()}</td>
        <td onclick="window.location='/logic?click=4'">${data.get(4).getSign()}</td>
        <td onclick="window.location='/logic?click=5'">${data.get(5).getSign()}</td>
    </tr>
    <tr>
        <td onclick="window.location='/logic?click=6'">${data.get(6).getSign()}</td>
        <td onclick="window.location='/logic?click=7'">${data.get(7).getSign()}</td>
        <td onclick="window.location='/logic?click=8'">${data.get(8).getSign()}</td>
    </tr>
</table>

<hr>
<c:set var="CROSSES" value="<%=Sign.CROSS%>"/>
<c:set var="NOUGHTS" value="<%=Sign.NOUGHT%>"/>

<c:if test="${winner == CROSSES}">
    <h1>CROSSES WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${winner == NOUGHTS}">
    <h1>NOUGHTS WIN!</h1>
    <button onclick="restart()">Start again</button>
</c:if>
<c:if test="${draw}">
    <h1>IT'S A DRAW</h1>
    <button onclick="restart()">Start again</button>
</c:if>

<script>
    function restart() {
        $.ajax({
            url: '/restart',
            type: 'POST',
            contentType: 'application/json;charset=UTF-8',
            async: false,
            success: function () {
                location.reload();
            }
        });
    }
</script>

</body>
</html>

메인 .css_

td {
    border: 3px solid black;
    padding: 10px;
    border-collapse: separate;
    margin: 10px;
    width: 100px;
    height: 100px;
    font-size: 50px;
    text-align: center;
    empty-cells: show;
   }