새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

8 분 소요

목표 D-day : 92 일
리마인드 카운트 : 14번

오늘 학습 목표

  • 최범균의 JSP 프로그래밍 ch 4

학습 방법

  • 키워드 정리
  • 키워드 위주로 학습

키워드

  • Redirect
    • URLEncoder.encode()가 왜 필요한지 알아야합니다.

학습내용

rediect 기능

웹 서버가 웹 브라우저에게 다른 페이지로 이동하라고 응답하는 기능입니다.

특정 페이지를 실행 후 지정한 페이지로 이동하기 원할 때 리다이렉트 기능을 하용한다.

실질적으로 웹 브라우저는 2번의 웹 요청을 시도한다.

웹 서버가 웹 브라우저에게 다음 행선지를 알려준다.

reponse.rediect("이동할 페이지");

Redirect 주의사항

웹 서버에 전송할 파라미터 값은 알맞은 인코딩을 해야합니다.

www.naver.com?name=자바로 웹 서버에 요청할 때 웹 서버가 사용하는 캐릭터 셋으로 인코딩 후 전송해야합니다.

www.naver.com?name=%EC%9E%90%EB%B0%94로 변경하여 Rediect를 시도해합니다.

여기서 주의사항이 있습니다.

response.sendRediect(url) url에서 쿼리 스트링으로 한글이 들어갈 경우 자동으로 인코딩되지 않습니다.

왜 자동 인코딩이 되지 않을까?

웹 브라우저는 리디렉션 응답을 받을 때, 해당 URL을 그대로 사용합니다. 만약 URL에 한글이나 특수 문자가 인코딩되지 않은 채로 포함되어 있다면, 브라우저가 이를 올바르게 처리하지 못할 수 있습니다. 따라서, 서버 측에서 URL을 수동으로 인코딩해주어야 합니다.

따라서, 자바가 제공하는 URLEncoder.encode()을 사용하여 인코딩이 필요합니다.

String encode = URLEncoder.encode('자바','utf-8');
response.sendRediect("www.naver.com?name="+encode);

필수 이해 요소

키워드
  • JSP의 요청 처리 과정
  • 출력 버퍼
  • 웹 어플리케이션 폴더 구조
  • war 파일

catalina.sh

JAVA_HOME 환경 변수를 설정하여 JDK의 설치 경로를 지정하는 것은 필수입니다.

내부에서 .java.class파일로 컴파일을 하며, .class파일을 실행하기 위해 필요하기 때문입니다.

#   JAVA_HOME       Must point at your Java Development Kit installation.
#                   Required to run the with the "debug" argument.
WAS 실행 과정
  1. /startup.bat로 톰캣을 실행합니다.

  2. 내부에서 JVM을 실행하여 main-class로 작성된 클래스(TomcatServer)의 main()을 실행하여 톰캣이 실행됩니다.

    public class TomcatServer {
        public static void main(String[] args) {
         	ServerSocket serverSocket = new ServerSocket(8080); // 서버 소켓 생성
               
            ExecutorService executor = Executors.newFixedThreadPool(10); // 스레드 풀 생성
    		while (true) {
    		    Socket clientSocket = serverSocket.accept();
        		executor.submit(new RequestHandler(clientSocket)); // 스레드 풀에서 스레드 할당	
    		}
        }
    }
       
    
  3. 내부에 connection pool을 만들고 웹 요청을 대기합니다.

  4. 웹 요청이 들어오면 소켓을 읽고HTTP규칙에 맞게 정보를 분리합니다.

    public class RequestHandler implements Runnable {
        private Socket clientSocket;
       
        public RequestHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }
        @Override
        public void run() {
            try {
                // 요청 읽기
                InputStream input = clientSocket.getInputStream();
                // 응답 쓰기
                OutputStream output = clientSocket.getOutputStream();
                // 서블릿 요청 처리
                HttpServletRequest request = createRequest(input);
                HttpServletResponse response = createResponse(output);
                // 서블릿 매핑 및 호출
                HttpServlet servlet = getServlet(request);
                servlet.service(request, response);
            } catch (IOException | ServletException e) {
                e.printStackTrace();
            } finally {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        private HttpServletRequest createRequest(InputStream input) {
            // 요청 객체 생성 코드
        }
        private HttpServletResponse createResponse(OutputStream output) {
            // 응답 객체 생성 코드
        }
        private HttpServlet getServlet(HttpServletRequest request) {
            // 서블릿 매핑 코드
        }
    }
    
  5. 추상화 객체 Request,Reponse 인스턴스를 생성합니다.

JSP 처리 과정

WAS는 요청 URL을 확인하고 관련된 JSP 파일이나 서블릿 객체를 찾습니다.

JSP에 해당하는 서블릿이 존재하지 않는 경우

  1. JSP 페이지를 WAS는 자바 코드와 문자를 분리하여 서블릿 클래스.java파일로 생성합니다.
    import java.io.IOException;
    import java.io.PrintWriter;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
       
    public class SampleServlet extends HttpServlet {
        private static final long serialVersionUID = 1L;
       
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            processRequest(request, response);
        }
       
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            processRequest(request, response);
        }
       
        private void processRequest(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            // 응답 콘텐츠 타입 설정
            response.setContentType("text/html; charset=UTF-8");
            PrintWriter out = response.getWriter();
       
            try {
                // 동적 콘텐츠 생성
                out.println("<!DOCTYPE html>");
                out.println("<html>");
                out.println("<head>");
                out.println("<title>Sample JSP Page</title>");
                out.println("</head>");
                out.println("<body>");
                out.println("<h1>Hello, JSP!</h1>");
                // 기본 객체 사용 예시
                String name = request.getParameter("name");
                if (name == null) {
                    name = "World";
                }
                out.println("안녕하세요, " + name + "!");
       
                // 세션 객체 사용 예시
                HttpSession session = request.getSession();
                session.setAttribute("user", name);
                out.println("<br>세션에 저장된 사용자 이름: " + session.getAttribute("user"));
       
                out.println("</body>");
                out.println("</html>");
            } finally {
                out.close();
            }
        }
    }
    
  2. SampleServlet.javaJDKSampleServlet.class 컴파일 합니다.
  3. 톰캣은 해당 .class파일로 서블릿 객체를 생성합니다.
  4. 톰캣은 리플렉션을 사용하여 서블릿 클래스를 동적으로 인스턴스화 합니다..
  5. 서블릿에게 파라미터로 기본 객체를 전달하고 서블릿은 결과를 respone에 저장합니다.
  6. 톰캣은 reponse객체를 응답으로 웹브라우저에게 전송합니다

JSP에 해당하는 서블릿이 존재하는 경우

  1. 서블릿에 클라이언트 요청을 전달합니다.
  2. 서블릿이 요청을 처리한 결과를 응답으로 생성합니다.
  3. 응답을 웹 브라우저에 전달합니다.
정리

JVM 내에 메인 쓰레드에서 WAS가 실행이 됩니다.

WAS는 제한된 자원 내에 최대한 많은 웹 요청을 수용하기 위해 커넥션 풀을 생성합니다.

웹 요청이 오면 생성된 쓰레드 풀에서 하나씩 실행하여 결과를 다시 전달하는 과정이 반복되는 구조입니다.

.jsp파일을 .java로 변환하는 역할은 was이며, 이후 .class파일로 컴파일 하는건 jdk의 역할입니다.

was는 리플렉션으로 동적으로 클래스를 인스턴스화 하여 사용합니다.

결국 .jsp파일은 하나의 클래스로 관리된다고 생각하면 되며,

그 클래스를 서블릿이라고 합니다. 또 다른 말로는 웹 컴포넌트로 웹 요청에 대한 하나의 기능을 말합니다.

출력 버퍼와 응답

출력 버퍼가 왜 필요할까?

웹 브라우저와 웹 서버는 TCP/IP 통신으로 연결되면 소켓을 통해 데이터 통신이 가능합니다.

여기서 JSP 코드를 다시보면

PrintWriter out = response.getWriter();
try {
    // 동적 콘텐츠 생성
    out.println("<!DOCTYPE html>");
    out.println("<html>");
    out.println("<head>");
    out.println("<title>Sample JSP Page</title>");
    out.println("</head>");
    out.println("<body>");
    out.println("<h1>Hello, JSP!</h1>");
    // 기본 객체 사용 예시
    String name = request.getParameter("name");
    if (name == null) {
        name = "World";
    }
    out.println("안녕하세요, " + name + "!");

    // 세션 객체 사용 예시
    HttpSession session = request.getSession();
    session.setAttribute("user", name);
    out.println("<br>세션에 저장된 사용자 이름: " + session.getAttribute("user"));

    out.println("</body>");
    out.println("</html>");
} finally {
    out.close();
}

TCP/IP 웹 소켓 연결이 되어있는 상태입니다.

현재 상황

  • 웹 소켓에 I/O를 하면 웹 브라우저에 전송됩니다.
  • HTTP는 항상 BODY보다 Header가 도착해야합니다. 그래서 write하게되면 header는 먼저 전송됩니다.

TCP/IP 통신으로 정확한 전송과 수신을 보장하기 위해 여러가지 확인 절차가 필요합니다.

버퍼를 사용하는 이유

  1. 데이터 전송 성능 향상: 데이터 교환을 최대한 큰 단위로 묶어 불필요한 확인 절차를 최소화 하기 위해서
  2. JSP 실행동안 버퍼에 저장하다가 오류가 발생하거나 추가 파일이 생기면 새로운 내용으로 변경이 가능하다.
  3. 버퍼가 다 차기전까지 헤더 변경이 가능하다.

버퍼를 flush하기 전까지 내용물을 전송하지 않았으므로 중간 수정이 가능하고, 최적화를 할 수 있다는 의미입니다.

폴더 구성과 URL 매핑

서블릿/JSP로 구성된 웹 어플리케이션의 기본 구조는 다음과 같습니다.

WEB-INF/
├── web.xml                 # 웹 애플리케이션 배포 설명자
├── classes/                # 컴파일된 자바 클래스 파일들이 위치하는 디렉터리
│   └── com/
│       └── example/
│           └── MyServlet.class
├── lib/                    # 애플리케이션에 필요한 라이브러리(JAR 파일)들이 위치하는 디렉터리
│   ├── library1.jar
│   └── library2.jar
└── jsp/                    # JSP 파일들이 위치하는 디렉터리 (옵션)
    └── index.jsp
폴더와 URL 관계

tomcat/webapps/[웹경로] > http://host:port/[웹경로]로 매핑됩니다.

tomcat이 조회하는 방식은 다음과 같습니다.

만약 http://localhost:8080/views/app.jsp를 검색한다면 탐색 우선순위는 다음과 같습니다.

  1. webapps/views내에 app.jsp파일을 찾습니다

  2. ROOT/views/app.jsp 파일을 찾습니다.

하위 폴더 사용

.jsp파일을 모두 한 폴더 안에 관리하게 된다면 유지보수가 어렵습니다.

수십 수백개가 되는 .jsp파일을 찾아서 수정하고, 관련된 .jsp도 수정하기 힘들기 때문입니다.

기능 별로 분류하기

.jsp파일은 웹 컴포넌트로 웹 요청과 1:1 매칭이 됩니다.

웹 브라우저의 요청을 웹 서버는 웹 컴포넌트를 수행 결과를 응답해주는 방식입니다.

이렇게 생각해보면 이해가 잘되는 거같습니다.

클래스

회원 클래스가 있다면 회원 클래스내 메서드는 이런게 있을 겁니다.

  • 회원 생성 메서드
  • 회원 등급 수정 메서드
  • 회원 잔고 수정 메서드
  • 기타..

메서드는 클래스의 기능을 나타내며 클래스의 상태를 사용하여 기능을 수행합니다.

웹 컴포넌트인 .jsp파일도 하나의 기능을 하므로

public class Member {
	public Member(..){
		//..
	}
	public String incrementGrade(){
		//..
	}
}

이 구조를 다시 웹 컴포넌트로 표현하면 다음과 같이 표현할 수 있습니다.

Member/
├── IncrementGrade.jsp
└── changeBlance.jsp

웹 기능을 클래스의 메서드로 생각하여 추상화하여 폴더로 생성할 수 있습니다.

웹 어플리케이션 배포

  • 대상 폴더에 파일을 직접 복사
  • war 파일로 묶어서 배포

첫번째 방법은 실제 .jsp파일을 \wabapps 구조 안에 직접 넣는 방식입니다.

두번째 방식은 war(Web application archive) 구조로 구성을 묶는 것입니다.

JDK를 사용하여 .class의 묶음을 jar파일로 만들거나 웹 애플리케이션용인 war로 압축이 가능합니다.

jar,war모두 Manifest라는 파일에 실행할 main() 메서드가 있는 클래스를 입력해야합니다.

쿠키

쿠키를 사용하는 이유는 HTTP 의 특징인 Stateless 인 비상태을 극복하기 위해서 사용됩니다.

쿠키를 사용하여 서버와 클라이언트간 상태 유지하, 이를 통해 사용자 세션,사용자 인증,맞춤 설정을 저장할 수 있습니다.

키워드
  • 상태 저장
  • stateless 극복

쿠키를 구성하는 요소는 다음과 같습니다.

구성 요소 설명 예시
이름 (Name) 쿠키의 식별자. 각 쿠키는 고유한 이름을 가져야 합니다. sessionId
값 (Value) 쿠키에 저장되는 데이터. 값은 문자열 형태로 저장됩니다. abc123
도메인 (Domain) 쿠키가 유효한 도메인을 지정. 지정된 도메인에서만 쿠키가 전송됩니다. example.com
경로 (Path) 쿠키가 유효한 URL 경로를 지정. 지정된 경로에서만 쿠키가 전송됩니다. /app
유효기간 (Expires/Max-Age) 쿠키의 만료 시간을 지정. Expires는 절대 시간을, Max-Age는 현재 시간부터의 상대 시간을 지정합니다. Expires=Wed, 09 Jul 2024 10:18:14 GMT, Max-Age=3600
보안 (Secure) Secure 속성이 설정된 쿠키는 HTTPS를 통해서만 전송됩니다. Secure
HTTP 전용 (HttpOnly) HttpOnly 속성이 설정된 쿠키는 클라이언트 측 스크립트에서 접근할 수 없습니다. 이는 보안 목적으로 사용됩니다. HttpOnly
SameSite 쿠키가 크로스 사이트 요청에 대해 전송되는 방식을 지정합니다. 값으로 Strict, Lax, None을 가질 수 있습니다. SameSite=Strict

웹 브라우저는 여러 개의 쿠키를 갖을 수 있습니다.

웹 서버는 웹 브라우저가 전송한 쿠키중 쿠키의 이름을 사용하여 원하는 작업을 수행합니다.

쿠키 사용하기

쿠키 클래스는 아스키 문자로 구성됩니다. UTF-8로 작성하는 경우 인코딩이 필요합니다.

// 쿠키 값에 한국어가 포함된 경우 인코딩
String value = "홍길동";
String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
// URL 인코딩된 쿠키 설정
Cookie cookie = new Cookie("username", encodedValue);
쿠키 관리
  1. 쿠키 읽기
// 쿠키 배열을 가져옴
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("username".equals(cookie.getName())) {
            String value = cookie.getValue();
            // 쿠키 값 사용
        }
    }
}
  1. 쿠키 삭제
// 쿠키를 삭제하려면 동일한 이름과 경로로 만료 시간을 0으로 설정하여 추가
Cookie cookie = new Cookie("username", "");
cookie.setMaxAge(0); // 만료 시간 0으로 설정
cookie.setPath("/"); // 쿠키가 생성된 경로와 동일하게 설정
response.addCookie(cookie);
  1. 쿠키 변경

쿠키 변경시 주의사항

쿠키의 값을 변경한다는 것은 기존에 쿠키에 저장되어있다는 의미입니다.

변경하려는 쿠키 이름에 값이 있는지 확인하고 추가해야합니다.

// 쿠키를 변경하려면 동일한 이름으로 새 값을 설정하여 추가
Cookie cookie = new Cookie("username", "new_value");
cookie.setMaxAge(3600); // 새로운 유효 시간 설정
cookie.setPath("/"); // 쿠키가 생성된 경로와 동일하게 설정
response.addCookie(cookie);
쿠키 도메인

기본적으로 쿠키는 생성한 서버에만 전송됩니다.

www.naver.com/login.jsp에서 저장된 쿠키는 같은 호스트(www.naver.com)에만 전송됩니다.

만약 mail.naver.com 혹은 shop.naver.com등 같은 도메인을 사용하지만 다른 서버에도 쿠키를 보낼때 사용합니다.

// 쿠키 생성
Cookie cookie = new Cookie("username", "user_value");
// 도메인을 .naver.com으로 설정
cookie.setDomain(".naver.com");
// 쿠키를 응답에 추가
response.addCookie(cookie);

이제 username 의 쿠키는 naver.com 도메인을 사용하는 모든 서버에 요청을 보낼때 같이 따라갑니다.

도메인 주의사항

setDomain은 현재 서버의 도메인 및 상위 도메인만 전달 할 수 있습니다.

현재 연결 중인 서버의 호스트가 www.naver.com이라면 setDomainwww.naver.com.naver.com만 가능합니다.

cookie.setDomain(`mail.naver.com`)

이런 경우는 다른 주소의 값을 주는 경우는 웹 브라우저에서 저장하지 않습니다

왜?

웹 브라우저가 타 도메인으로 지정한 쿠키를 받지 않는 이유는 보안 문제입니다.

임의의 다른 서버에서 타 도메인의 쿠키를 사용하여 값을 변경한다면 보안이 뚫릴 수 있습니다.

댓글남기기