[JSP] DAO 패턴과 커넥션 풀을 활용한 회원가입 및 로그인 기능 구현

김태희·2025년 7월 10일
0

JSP

목록 보기
6/6
post-thumbnail

DAO 패턴과 커넥션 풀

비즈니스 로직을 JSP에서 분리했더라도, 데이터베이스 연결(JDBC) 코드가 서블릿이나 JSP에 직접 포함되어 있다면 심각한 문제를 야기한다.

// 서블릿이나 JSP 파일 내부 - 좋지 않은 예시
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

try {
    // 1. 드라이버 로딩
    Class.forName("oracle.jdbc.driver.OracleDriver");
    // 2. 커넥션 생성
    conn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:XE", "user", "pass");
    // 3. SQL 작성 및 실행
    String sql = "SELECT * FROM member WHERE id = ?";
    pstmt = conn.prepareStatement(sql);
    pstmt.setString(1, request.getParameter("id"));
    rs = pstmt.executeQuery();
    // 4. 결과 처리...
    // 이 모든 코드가 DB 작업이 필요한 모든 파일에 중복되어 삽입된다.
} catch(Exception e) {
    // 예외 처리
} finally {
    // 5. 자원 해제 (매번 반복)
    if(rs != null) rs.close();
    if(pstmt != null) pstmt.close();
    if(conn != null) conn.close();
}

이러한 구조는 코드의 중복이 극심하고, DB 정보(URL, ID, PW)가 변경되면 관련된 모든 파일을 수정해야 하는 유지보수 재앙을 초래한다.

또한, 매 요청마다 DB 커넥션을 생성하고 해제하는 과정은 시스템에 엄청난 부하를 주어 성능 저하의 주범이 된다.

해결책으로 DAO(Data Access Object) 패턴을 도입하여 데이터 로직을 분리하고, 커넥션 풀(Connection Pool)을 함께 사용하여 시스템의 효율을 극대화해야 한다.


DAO 패턴

DAO는 데이터베이스에 접근하고 처리하는 로직(CRUD: Create, Read, Update, Delete)을 별도의 클래스로 완전히 분리하여 캡슐화하는 디자인 패턴이다.

이는 '관심사의 분리(Separation of Concerns)' 원칙을 충실히 따르는 방법이다.

서블릿이나 다른 서비스 클래스들은 복잡한 JDBC 코드나 SQL 문을 전혀 알 필요가 없다.

그저 DAO 객체에게 "회원 정보 저장", "ID가 'user1'인 회원 정보 조회" 와 같이 의미 있는 기능 단위로 요청만 하면 된다.


DTO, VO 그리고 자바빈(JavaBean)

DTO (Data Transfer Object) / VO (Value Object)
계층 간(Controller-Service-DAO) 데이터를 전달하기 위한 목적으로 만들어진 객체이다.
예를 들면 MemberDTO는 회원 한 명의 정보를 온전히 담아 운반하는 '데이터 상자' 역할을 한다.

자바빈 (JavaBean)
앞서 다뤘던 자바빈은 특정 규칙을 따르는 자바 클래스를 지칭하는 기술 규약이다.
1. private 접근 제한자로 필드를 선언한다.
2. public 접근자로 getter/setter 메서드를 가진다.
3. 인자 없는 기본 생성자(default constructor)를 가진다.

하지만 여기서 DTO는 자바빈 규약에 따라 만드는 것이 일반적이다.

즉, MemberDTO는 '데이터 전달'이라는 역할(패턴)을 수행하며, 그 구현은 자바빈(기술 규약)을 따른다.

따라서 "DTO는 자바빈이다"라고 말할 수 있다.

JSP에서 <jsp:useBean> 액션 태그가 DTO 객체를 쉽게 다룰 수 있는 것도 DTO가 자바빈 규약을 따르기 때문이다.


DAO (Data Access Object)

실제 DB 작업을 수행하는 객체이다.

모든 JDBC 코드는 DAO 클래스 내부에만 존재한다.

// MemberDAO.java 의 예시
public class MemberDAO {
    // ... Connection Pool을 통해 Connection을 얻어오는 로직 ...

    public int insertMember(MemberDTO dto) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        String sql = "INSERT INTO member (id, password, name, email) VALUES (?, ?, ?, ?)";
        int result = 0;
        try {
            conn = getConnection(); // 커넥션 풀에서 하나 빌려오기
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, dto.getId());
            pstmt.setString(2, dto.getPassword());
            pstmt.setString(3, dto.getName());
            pstmt.setString(4, dto.getEmail());
            result = pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(pstmt, conn); // 자원 반납
        }
        return result;
    }
    // getMember(id), updateMember(dto), deleteMember(id) 등 다른 메서드들...
}

커넥션 풀(Connection Pool)

데이터베이스 커넥션을 맺고 끊는 작업은 네트워크 통신, 사용자 인증, 세션 설정 등이 포함된, 시스템에 상당한 부하를 주는 고비용 작업이다.

매 요청마다 이 작업을 반복하는 것은 극심한 성능 저하를 유발한다.

커넥션 풀은 이 문제를 해결하기 위해 미리 일정 개수의 DB 커넥션을 생성하여 '풀(Pool)'이라는 저장 공간에 보관해두고, 필요할 때마다 빌려 쓰고 반납하는 기법이다.

동작 원리:

  1. WAS 시작
    웹 애플리케이션 서버(EX:Tomcat)가 시작될 때, context.xml에 설정된 정보를 읽어 지정된 개수만큼 커넥션을 미리 생성하여 풀에 저장한다.

  2. DAO의 요청
    MemberDAO가 DB 작업이 필요하면, DriverManager.getConnection()을 직접 호출하는 대신, WAS가 제공하는 JNDI(Java Naming and Directory Interface) 서비스를 통해 커넥션 풀을 찾는다.

  3. 대여(Borrow)
    JNDI를 통해 찾은 커넥션 풀(DataSource 객체)에게 dataSource.getConnection()을 호출하여 커넥션을 요청한다. 풀은 유휴 상태의 커넥션 하나를 빌려준다.

  4. 작업 수행
    DAO는 빌린 커넥션을 사용하여 SQL 작업을 수행한다.

  5. 반납(Return)
    작업이 끝나면 connection.close()를 호출한다.
    커넥션 풀을 통해 얻은 커넥션은 이 때 실제로 닫히는 것이 아니라, 풀에 반납되어 '대기' 상태로 돌아간다.

  6. 재사용
    다른 요청이 들어오면, 풀에 대기 중인 커넥션을 다시 빌려주어 커넥션 생성 비용 없이 즉시 DB 작업을 시작할 수 있다.

Tomcat context.xml 설정 예시
META-INF/context.xml 파일에 다음과 같이 DB 정보를 설정한다.

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource name="jdbc/OracleDB" auth="Container"
              type="javax.sql.DataSource"
              driverClassName="oracle.jdbc.driver.OracleDriver"
              url="jdbc:oracle:thin:@localhost:1521:XE"
              username="myuser"
              password="mypassword"
              maxTotal="20"   <!-- 최대 커넥션 개수 -->
              maxIdle="10"    <!-- 유휴 상태로 유지할 최대 커넥션 개수 -->
              maxWaitMillis="5000" /> <!-- 커넥션을 기다리는 최대 시간(ms) -->
</Context>

커넥션 풀 사용 코드 (DAO 내부)

// DAO에서 커넥션을 얻어오는 메서드
private Connection getConnection() {
    Connection conn = null;
    try {
        Context initContext = new InitialContext();
        // context.xml 에 설정한 "jdbc/OracleDB" 이름을 찾아 DataSource 객체를 얻어온다.
        DataSource ds = (DataSource) initContext.lookup("java:/comp/env/jdbc/OracleDB");
        conn = ds.getConnection(); // DataSource를 통해 풀에서 커넥션을 대여한다.
    } catch (Exception e) {
        e.printStackTrace();
    }
    return conn;
}

DAO 패턴과 커넥션 풀의 조합은 현대적인 웹 애플리케이션 구축의 표준적인 방법이다.


회원가입 및 로그인 기능 구현

JSP/Servlet 기반 MVC 패턴을 사용하여 회원가입 및 로그인 기능을 구현하는 코드이다.

DB 테이블

데이터베이스에 회원 정보를 저장할 member 테이블을 생성한다.

-- member 테이블 생성 SQL
CREATE TABLE member (
    id VARCHAR2(20) PRIMARY KEY,
    password VARCHAR2(20) NOT NULL,
    name VARCHAR2(30) NOT NULL,
    email VARCHAR2(50),
    regdate DATE DEFAULT SYSDATE
);

Model (데이터 및 로직 처리)

Member.java (DTO)

회원 정보를 담아 계층 간에 전달하는 객체. 자바빈 규약에 따라 작성한다.

// Member.java
package com.example.model;

import java.io.Serializable;
import java.sql.Date;

public class Member implements Serializable {
    private static final long serialVersionUID = 1L;

    // 필드 (DB 컬럼과 일치)
    private String id;
    private String password;
    private String name;
    private String email;
    private Date regdate;

    // 기본 생성자
    public Member() {
    }

    // Getter and Setter
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public Date getRegdate() { return regdate; }
    public void setRegdate(Date regdate) { this.regdate = regdate; }
}

MemberDAO.java (DAO)

실제 데이터베이스에 접근하여 CRUD 작업을 수행하는 객체.

커넥션 풀을 사용한다.

// MemberDAO.java
package com.example.model;

import java.sql.*;
import javax.naming.*;
import javax.sql.*;

public class MemberDAO {

    private DataSource dataSource;

    // 생성자에서 커넥션 풀(DataSource)을 찾아 초기화
    public MemberDAO() {
        try {
            Context context = new InitialContext();
            dataSource = (DataSource) context.lookup("java:comp/env/jdbc/OracleDB");
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    // 회원가입 (Create)
    public int join(Member member) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int result = 0;
        String sql = "INSERT INTO member (id, password, name, email) VALUES (?, ?, ?, ?)";
        
        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, member.getId());
            pstmt.setString(2, member.getPassword());
            pstmt.setString(3, member.getName());
            pstmt.setString(4, member.getEmail());
            result = pstmt.executeUpdate(); // 성공 시 1 반환
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close(); // 커넥션을 풀에 반납
            } catch (SQLException e) {}
        }
        return result;
    }

    // 로그인 인증 (Read)
    public int loginCheck(String id, String password) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        int result = -1; // -1: 에러, 0: 비밀번호 불일치, 1: 성공
        String sql = "SELECT password FROM member WHERE id = ?";

        try {
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, id);
            rs = pstmt.executeQuery();

            if (rs.next()) { // 아이디가 존재할 경우
                if (rs.getString("password").equals(password)) {
                    result = 1; // 로그인 성공
                } else {
                    result = 0; // 비밀번호 불일치
                }
            } else {
                result = -1; // 아이디 존재 안함
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) rs.close();
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {}
        }
        return result;
    }
}

View (사용자 인터페이스)

join.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입</title>
</head>
<body>
    <h2>회원가입</h2>
    <form action="join.do" method="post">
        <label for="id">아이디:</label><br>
        <input type="text" id="id" name="id" required><br><br>
        <label for="password">비밀번호:</label><br>
        <input type="password" id="password" name="password" required><br><br>
        <label for="name">이름:</label><br>
        <input type="text" id="name" name="name" required><br><br>
        <label for="email">이메일:</label><br>
        <input type="email" id="email" name="email"><br><br>
        <input type="submit" value="가입하기">
    </form>
</body>
</html>

login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
    <h2>로그인</h2>
    <form action="login.do" method="post">
        <label for="id">아이디:</label><br>
        <input type="text" id="id" name="id" required><br><br>
        <label for="password">비밀번호:</label><br>
        <input type="password" id="password" name="password" required><br><br>
        <input type="submit" value="로그인">
    </form>
    <br>
    <%-- 로그인 실패 시 에러 메시지 출력 --%>
    <%
        String error = (String) request.getAttribute("error");
        if (error != null) {
    %>
        <p style="color: red;"><%= error %></p>
    <%
        }
    %>
    <a href="join.jsp">회원가입</a>
</body>
</html>

main.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    // 페이지 상단에서 세션 검사
    String userId = (String) session.getAttribute("userId");
    if (userId == null) { // 세션에 로그인 정보가 없으면
        response.sendRedirect("login.jsp"); // 로그인 페이지로 강제 이동
        return; // 현재 페이지의 나머지 코드 실행 중단
    }
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>메인 페이지</title>
</head>
<body>
    <h1><%= userId %>, 환영합니다!</h1>
    <p>성공적으로 로그인되었습니다.</p>
    <a href="logout.do">로그아웃</a>
</body>
</html>

Controller (요청 처리 및 흐름 제어)

JoinServlet.java

// JoinServlet.java
package com.example.controller;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

import com.example.model.Member;
import com.example.model.MemberDAO;

@WebServlet("/join.do")
public class JoinServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 파라미터 인코딩 설정
        request.setCharacterEncoding("UTF-8");

        // 2. 파라미터 추출 및 Member 객체 생성
        Member member = new Member();
        member.setId(request.getParameter("id"));
        member.setPassword(request.getParameter("password"));
        member.setName(request.getParameter("name"));
        member.setEmail(request.getParameter("email"));
        
        // 3. DAO를 통해 DB에 회원 정보 저장
        MemberDAO dao = new MemberDAO();
        int result = dao.join(member);

        // 4. 결과에 따른 페이지 이동
        if (result == 1) { // 회원가입 성공
            response.sendRedirect("login.jsp");
        } else { // 회원가입 실패
            // 실제로는 에러 페이지로 보내거나 알림을 띄우는 것이 좋음
            response.sendRedirect("join.jsp");
        }
    }
}

LoginServlet.java

// LoginServlet.java
package com.example.controller;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

import com.example.model.MemberDAO;

@WebServlet("/login.do")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 파라미터 추출
        String id = request.getParameter("id");
        String password = request.getParameter("password");

        // 2. DAO를 통해 인증 시도
        MemberDAO dao = new MemberDAO();
        int result = dao.loginCheck(id, password);

        // 3. 결과에 따른 처리
        if (result == 1) { // 로그인 성공
            // 세션 생성 및 사용자 정보 저장
            HttpSession session = request.getSession();
            session.setAttribute("userId", id);
            response.sendRedirect("main.jsp");
        } else { // 로그인 실패
            // 에러 메시지를 request에 저장
            request.setAttribute("error", "아이디 또는 비밀번호가 올바르지 않습니다.");
            // 포워드를 통해 login.jsp로 이동하여 에러 메시지 출력
            RequestDispatcher dispatcher = request.getRequestDispatcher("login.jsp");
            dispatcher.forward(request, response);
        }
    }
}

LogoutServlet.java

// LogoutServlet.java
package com.example.controller;

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/logout.do")
public class LogoutServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 세션 가져오기 (없으면 null 반환)
        HttpSession session = request.getSession(false);

        // 2. 세션이 존재하면 무효화
        if (session != null) {
            session.invalidate();
        }

        // 3. 로그인 페이지로 리다이렉트
        response.sendRedirect("login.jsp");
    }
}

이처럼 JSP/Servlet 기반 MVC 웹 애플리케이션에서는 각 구성 요소가 명확한 역할을 수행한다.

요청은 컨트롤러(서블릿)가 받아서 흐름을 제어한다.
데이터 처리와 비즈니스 로직은 모델(DAO, DTO)이 담당한다.
화면 표시는 뷰(JSP)가 담당한다.

이러한 역할 분담은 코드의 재사용성을 높이고 유지보수를 용이하게 만든다.

0개의 댓글