Java 웹 개발 핵심 가이드

애플리케이션 아키텍처 패턴

데스크톱 소프트웨어는 크게 두 가지 구조로 분류됩니다.

  • B/S 구조: 브라우저가 서버와 통신하는 방식
  • C/S 구조: 전용 클라이언트 프로그램이 서버와 통신하는 방식

Tomcat 서버 디렉토리 구조

경로용도
/bin시작 및 종료 스크립트
/conf설정 파일
/lib필요한 JAR 라이브러리
/logs로그 파일
/temp임시 파일
/webapps배포된 웹 애플리케이션
/workJSP에서 변환된 서블릿

포트 설정 변경

conf/server.xml에서 Connector 설정을 수정합니다.

<Connector port="9090" protocol="HTTP/1.1"
           connectionTimeout="30000"
           redirectPort="8443" />

JSP 동작 원리

JSP는 서버에서 Java 소스로 변환되고, 클래스 파일로 컴파일되어 실행됩니다.

JSP 스크립트 요소

  • <% %> - Java 코드 블록
  • <%= %> - 표현식 출력
  • <% out.write("내용") %> - 출력 객체 사용
  • <%@ page import="java.util.ArrayList" %> - 패키지 임포트

HTTP 메서드 비교

특성GETPOST
캐싱가능불가
북마크가능불가
데이터 크기URL 길이 제한제한 없음
인코딩단일 형식다중 형식 지원
문자 제한ASCII만모든 문자, 바이너리
보안URL에 노출요청 본문에 숨김

내장 객체 활용

출력 객체

out.write("화면에 표시될 내용");

요청 객체

HTML 폼:

<input type="text" name="nickname"/>

단일 값 수신:

String nickname = request.getParameter("nickname");

다중 값 수신:

String[] interests = request.getParameterValues("interests");

인코딩 문제 해결

POST 방식:

request.setCharacterEncoding("UTF-8");

GET 방식 - 수동 변환:

nickname = new String(nickname.getBytes("ISO-8859-1"), "UTF-8");

GET 방식 - 서버 설정:

<Connector port="9090" 
           protocol="HTTP/1.1"
           connectionTimeout="30000"
           redirectPort="8443"
           URIEncoding="UTF-8"
           useBodyEncodingForURI="true"/>

응답 객체

페이지 이:

response.sendRedirect("/main.jsp");

요청 전달 방식

포워딩

request.getRequestDispatcher("/target.jsp").forward(request, response);

리다이렉트

response.sendRedirect("/target.jsp");

차이점

  1. URL 변경: 포워딩은 유지, 리다이렉트는 변경
  2. 요청 횟수: 포워딩은 1회, 리다이렉트는 2회
  3. 데이터 전달: 포워딩은 가능, 리다이렉트는 불가
  4. 이동 범위: 포워딩은 내부만, 리다이렉트는 외부도 가능

시작 페이지 설정

<welcome-file-list>
    <welcome-file>index.html</welcome-file>
</welcome-file-list>

자주 발생하는 오류

코드원인
404경로 오류, WEB-INF 위치, 미배포
500JSP 코드 오류
연결 불가서버 미실행

에러 페이지 지정

<error-page>
    <error-code>404</error-code>
    <location>/error/notfound.html</location>
</error-page>
<error-page>
    <error-code>500</error-code>
    <location>/error/servererror.html</location>
</error-page>

JSP 내장 객체 9가지

객체타입범위
requestHttpServletRequest요청 단위
responseHttpServletResponse페이지 단위
sessionHttpSession사용자 세션
applicationServletContext애플리케이션 전체
outJspWriter출력 버퍼 관리
pageContextPageContext모든 객체 접근
configServletConfig설정 정보
pageObject현재 페이지
exceptionThrowable에러 페이지

영역 객체 4단계

페이지 영역

pageContext.setAttribute("key", "값");
String val = (String) pageContext.getAttribute("key");

// 서블릿에서
PageContext ctx = JspFactory.getDefaultFactory().getPageContext(
    this, request, response, null, true, 8192, true);

요청 영역

request.setAttribute("user", member);
Member m = (Member) request.getAttribute("user");

세션 영역

session.setAttribute("loginUser", member);
out.print(session.getId());
Member m = (Member) session.getAttribute("loginUser");

session.removeAttribute("loginUser");
session.setMaxInactiveInterval(120);
session.invalidate();

// 서블릿에서
HttpSession s = request.getSession();

애플리케이션 영역

application.setAttribute("config", settings);
Settings s = (Settings) application.getAttribute("config");

// 서블릿에서
ServletContext ctx = this.getServletContext();

쿠키 활용

// 저장
String id = URLEncoder.encode(userId, "UTF-8");
Cookie c = new Cookie("uid", id);
c.setPath("/");
c.setMaxAge(7200);
response.addCookie(c);

// 조회
Cookie[] arr = request.getCookies();
if (arr != null) {
    for (Cookie c : arr) {
        if ("uid".equals(c.getName())) {
            String v = URLDecoder.decode(c.getValue(), "UTF-8");
        }
    }
}

데이터베이스 연결

필요 라이브러리: mysql-connector-java-8.0.20.jar

기본 연결 흐름

  1. 드라이버 로드
  2. Connection 획득
  3. Statement 생성
  4. SQL 실행
  5. 자원 해제
Connection conn = null;
Statement stmt = null;
try {
    Class.forName("com.mysql.cj.jdbc.Driver");
    String url = "jdbc:mysql://localhost:3306/mall?serverTimezone=Asia/Seoul";
    conn = DriverManager.getConnection(url, "admin", "secret");
    stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT member_no, nickname FROM members");
    while (rs.next()) {
        long no = rs.getLong(1);
        String name = rs.getString(2);
        System.out.println(no + ": " + name);
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    try { stmt.close(); conn.close(); } 
    catch (SQLException e) { e.printStackTrace(); }
}

주요 드라이버 클래스

  • SQL Server: com.microsoft.sqlserver.jdbc.SQLServerDriver
  • MySQL: com.mysql.cj.jdbc.Driver
  • Oracle: oracle.jdbc.OracleDriver

Statement 메서드

메서드반환용도
executeQueryResultSetSELECT
executeUpdateintINSERT, UPDATE, DELETE
executeboolean모든 SQL

URL 파라미터

?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&zeroDateTimeBehavior=CONVERT_TO_NULL

PreparedStatement 사용

public Member authenticate(String email, String password) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        conn = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/mall", "admin", "secret");
        String sql = "SELECT member_no, email FROM members WHERE email=? AND passwd=?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, email);
        pstmt.setString(2, password);
        ResultSet rs = pstmt.executeQuery();
        Member m = null;
        while (rs.next()) {
            m = new Member();
            m.setNo(rs.getLong("member_no"));
            m.setEmail(rs.getString("email"));
        }
        return m;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try { pstmt.close(); conn.close(); } 
        catch (SQLException e) { e.printStackTrace(); }
    }
    return null;
}

목록 조회

List<Product> list = new ArrayList<>();
while (rs.next()) {
    Product p = new Product();
    p.setCode(rs.getLong("product_code"));
    p.setTitle(rs.getString("title"));
    p.setDesc(rs.getString("description"));
    list.add(p);
}
return list;

정보 수정

String sql = "UPDATE products SET title=?, view_count=?, status=? WHERE code=?";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, product.getTitle());
pstmt.setInt(2, product.getViewCount());
pstmt.setInt(3, product.getStatus());
pstmt.setLong(4, product.getCode());
return pstmt.executeUpdate();

DAO 패턴 구조

  • com.store.dao - 인터페이스 (MemberDao)
  • com.store.dao.impl - 구현체 (MemberDaoImpl)
  • com.store.entity - 도메인 객체 (Member)
  • com.store.util - 공통 클래스 (DbUtil)

공통 유틸리티 클래스

public class DbUtil {
    private Connection conn = null;
    private PreparedStatement pstmt = null;
    
    public boolean open() {
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mall", "admin", "secret");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    
    public int modify(String sql, Object[] params) {
        int result = 0;
        try {
            if (open()) {
                pstmt = conn.prepareStatement(sql);
                if (params != null) {
                    for (int i = 0; i < params.length; i++) {
                        pstmt.setObject(i + 1, params[i]);
                    }
                }
                result = pstmt.executeUpdate();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close();
        }
        return result;
    }
    
    public ResultSet query(String sql, Object[] params) {
        try {
            if (open()) {
                pstmt = conn.prepareStatement(sql);
                if (params != null) {
                    for (int i = 0; i < params.length; i++) {
                        pstmt.setObject(i + 1, params[i]);
                    }
                }
                return pstmt.executeQuery();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    public void close() {
        try {
            if (pstmt != null) pstmt.close();
            if (conn != null) conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

설정 파일 외부화

database.properties:

db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mall
db.user=admin
db.password=secret
Properties props = new Properties();
InputStream is = DbUtil.class.getClassLoader()
    .getResourceAsStream("database.properties");
props.load(is);
String driver = props.getProperty("db.driver");

싱글톤 패턴

지연 초기화 방식

public class ConfigHolderLazy {
    private static ConfigHolderLazy instance = null;
    private static Properties props = new Properties();
    
    public static synchronized ConfigHolderLazy getInstance() {
        if (instance == null) {
            instance = new ConfigHolderLazy();
        }
        return instance;
    }
    
    private ConfigHolderLazy() {
        try (InputStream is = getClass().getClassLoader()
                .getResourceAsStream("database.properties")) {
            props.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public String get(String key) {
        return props.getProperty(key);
    }
}

즉시 초기화 방식

public class ConfigHolderEager {
    private static ConfigHolderEager instance = new ConfigHolderEager();
    private static Properties props = new Properties();
    
    static {
        try (InputStream is = ConfigHolderEager.class.getClassLoader()
                .getResourceAsStream("database.properties")) {
            props.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private ConfigHolderEager() {}
    
    public static ConfigHolderEager getInstance() {
        return instance;
    }
    
    public String get(String key) {
        return props.getProperty(key);
    }
}

적용 예시

public Connection obtainConnection() throws Exception {
    Properties p = new Properties();
    try (InputStream is = DbUtil.class.getClassLoader()
            .getResourceAsStream("database.properties")) {
        p.load(is);
    }
    Class.forName(p.getProperty("db.driver"));
    return DriverManager.getConnection(
        p.getProperty("db.url"),
        p.getProperty("db.user"),
        p.getProperty("db.password"));
}

서블릿 개요

JSP는 내부적으로 서블릿으로 변환됩니다.

생명주기

인스턴스 생성 → init() 초기화 → service() → doGet/doPost → destroy()

단계주체시점
인스턴스 생성컨테이너시작 또는 첫 요청
초기화컨테이너생성 직후
요청 처리컨테이너요청 수신 시
소멸컨테이너애플리케이션 종료

상속 계층

  • Servlet 인터페이스 - 기본 규약
  • GenericServlet - 프로토콜 독립적 구현
  • HttpServlet - HTTP 프로토콜 전용

web.xml 매핑

<context-param>
    <param-name>encoding</param-name>
    <param-value>UTF-8</param-value>
</context-param>

<servlet>
    <servlet-name>AuthServlet</servlet-name>
    <servlet-class>com.store.web.AuthServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>AuthServlet</servlet-name>
    <url-pattern>/login</url-pattern>
</servlet-mapping>

어노테이션 매핑

@WebServlet("/login")
public class AuthServlet extends HttpServlet {
    private String encoding;
    
    @Override
    public void init(ServletConfig cfg) throws ServletException {
        encoding = cfg.getServletContext().getInitParameter("encoding");
    }
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        req.setCharacterEncoding(encoding);
        // ...
    }
}

태그: java JSP Servlet JDBC Tomcat

6월 3일 18:09에 게시됨