05. 서블릿 비즈니스 로직 처리

05. 서블릿 비즈니스 로직 처리

서블릿이 클라이언트 요청을 받아 DB 연동 등 실제 작업을 수행하는 것을 비즈니스 로직 처리라 한다.


1. Statement vs PreparedStatement

Statement의 문제점

Statement는 SQL문을 문자열 그대로 DBMS에 전달한다. DBMS는 매번 SQL을 파싱하고 컴파일해야 하므로 반복 실행 시 성능이 떨어진다.

  Statement 실행 흐름
        │
        ▼
┌────────────────┐
│ SQL 문자열 전달  │
│ (매번 새로 전달) │
└───────┬────────┘
        │
        ▼
┌────────────────┐
│ DBMS           │
│ ① 구문 분석    │ ← 매번 반복
│ ② 컴파일      │ ← 매번 반복
│ ③ 실행        │
└────────────────┘
// Statement 방식 - 매번 SQL을 새로 컴파일
Statement stmt = conn.createStatement();
String sql = "SELECT * FROM member"
           + " WHERE id='" + id + "'";
ResultSet rs = stmt.executeQuery(sql);

PreparedStatement의 동작 원리

PreparedStatement는 SQL문을 미리 컴파일하여 DBMS에 캐싱한다. 이후 ?(바인드 변수)에 값만 바꿔 넣으므로 반복 실행 시 훨씬 빠르다.

  PreparedStatement 실행 흐름
        │
        ▼
┌────────────────┐
│ SQL 템플릿 전달  │
│ (? 포함)       │
└───────┬────────┘
        │ 최초 1회만
        ▼
┌────────────────┐
│ DBMS           │
│ ① 구문 분석    │ ← 최초 1회
│ ② 컴파일      │ ← 최초 1회
│ ③ 캐싱        │
└───────┬────────┘
        │ 이후 실행
        ▼
┌────────────────┐
│ ? 값만 바인딩   │
│ → 바로 실행    │
└────────────────┘
// PreparedStatement 방식 - 컴파일된 SQL 재사용
String sql = "SELECT * FROM member"
           + " WHERE id = ?";
PreparedStatement pstmt
    = conn.prepareStatement(sql);
pstmt.setString(1, id);  // ? 에 값 바인딩
ResultSet rs = pstmt.executeQuery();

비교

구분StatementPreparedStatement
SQL 컴파일매번 수행최초 1회만 수행
실행 속도느림빠름 (캐싱)
SQL Injection취약안전
파라미터 바인딩문자열 연결? + setter
가독성낮음높음
반복 실행비효율적효율적

PreparedStatement 주요 메서드

반환형메서드기능
ResultSetexecuteQuery()SELECT 실행
intexecuteUpdate()INSERT/UPDATE/DELETE 실행
voidsetString(idx, val)문자열 바인딩
voidsetInt(idx, val)정수 바인딩
voidsetDate(idx, val)날짜 바인딩
voidsetTimestamp(idx, val)타임스탬프 바인딩
voidsetNull(idx, type)NULL 바인딩

?의 인덱스는 1부터 시작한다.

SQL Injection 방어

Statement는 문자열 결합으로 SQL을 만들기 때문에 악의적 입력에 취약하다. PreparedStatement는 ?에 바인딩된 값을 데이터로만 처리하므로 SQL 구문으로 해석되지 않는다.

// 위험: Statement + 문자열 결합
// 입력값: ' OR '1'='1
String sql = "SELECT * FROM member"
    + " WHERE id='" + userInput + "'"
    + " AND pw='" + pwInput + "'";
// 결과: WHERE id='' OR '1'='1' AND pw=...
// → 모든 회원 정보가 노출됨

// 안전: PreparedStatement
String sql = "SELECT * FROM member"
           + " WHERE id = ? AND pw = ?";
PreparedStatement pstmt
    = conn.prepareStatement(sql);
pstmt.setString(1, userInput);
pstmt.setString(2, pwInput);
//  ? 값은 항상 데이터로만 처리됨

2. 커넥션풀 (Connection Pool)

기존 방식의 문제

요청마다 DB 연결을 생성하고 닫는 방식은 연결 생성 비용이 크고, 동시 요청이 많으면 DB에 과부하가 걸린다.

  기존 방식 (요청마다 연결)

  요청1 ──── open ──── DB
              close ───┘
  요청2 ──── open ──── DB
              close ───┘
  요청3 ──── open ──── DB
              close ───┘

  → 매번 연결/해제 반복 (느림)

커넥션풀 동작 과정

커넥션풀은 애플리케이션 시작 시 미리 DB 연결을 생성해두고, 요청이 올 때 빌려주고 반납받는 방식이다.

  커넥션풀 방식

┌────────────────┐
│ 톰캣 시작       │
│                │
│ Pool 생성      │
│ ┌──┐┌──┐┌──┐  │
│ │C1││C2││C3│  │ ← 미리 연결
│ └──┘└──┘└──┘  │
└───────┬────────┘
        │
        ▼
┌────────────────┐
│ 요청 발생       │
│                │
│ C1 빌려줌 →    │ 사용
│ C1 반납 ←      │ 완료
│                │
│ C2 빌려줌 →    │ 사용
│ C2 반납 ←      │ 완료
└────────────────┘
  1. 톰캣 컨테이너 실행 시 ConnectionPool 객체를 생성한다
  2. 설정된 수만큼 미리 DB와 연결(Connection)을 만들어 둔다
  3. 애플리케이션이 연결이 필요하면 풀에서 빌려 사용한다
  4. 작업이 끝나면 연결을 풀에 반납한다 (close가 아님)

커넥션풀의 이점

항목설명
성능 향상미리 만든 연결을 재사용하여 연결 생성 비용 제거
자원 관리최대 연결 수를 제한하여 DB 과부하 방지
안정성유휴 연결 검증, 타임아웃 등 자동 관리
응답 속도대기 없이 즉시 연결 획득 가능

3. DataSource

실제 웹 애플리케이션에서 커넥션풀을 사용할 때는 javax.sql.DataSource 인터페이스를 이용한다. DataSource는 커넥션풀을 관리하며, getConnection() 메서드로 풀에서 연결을 꺼내준다.

DataSource 접근 흐름

  서블릿
    │
    │ ① JNDI lookup
    ▼
┌────────────────┐
│ JNDI           │
│ key: jdbc/myDB │
│                │
│ → DataSource   │
└───────┬────────┘
        │ ② getConnection()
        ▼
┌────────────────┐
│ ConnectionPool │
│ ┌──┐┌──┐┌──┐  │
│ │C1││C2││C3│  │
│ └──┘└──┘└──┘  │
└───────┬────────┘
        │ ③ Connection 반환
        ▼
  서블릿에서 DB 작업

JNDI를 통한 DataSource 접근

JNDI(Java Naming and Directory Interface)는 필요한 자원을 key/value 쌍으로 저장한 후, key를 이용해 자원에 접근하는 방식이다. 톰캣이 생성한 ConnectionPool 객체에 JNDI 이름을 부여하고, 서블릿에서 이 이름으로 조회한다.

JNDI 사용 예설명
getParameter(name)name으로 파라미터 값 조회
HashMap.get(key)key로 value 조회
DNS 조회도메인명으로 IP 주소 조회
DataSource 조회JNDI 이름으로 커넥션풀 조회

톰캣 DataSource 설정

context.xml

<Context>
    <Resource
        name="jdbc/oracle"
        auth="Container"
        type="javax.sql.DataSource"
        driverClassName=
            "oracle.jdbc.OracleDriver"
        url=
            "jdbc:oracle:thin:@localhost:1521:XE"
        username="scott"
        password="tiger"
        maxTotal="20"
        maxIdle="10"
        maxWaitMillis="-1" />
</Context>
속성설명
nameJNDI 조회에 사용할 이름
auth인증 주체 (Container: 톰캣 관리)
type리소스 타입
driverClassNameJDBC 드라이버 클래스
urlDB 접속 URL
maxTotal풀의 최대 연결 수
maxIdle유휴 상태 최대 연결 수
maxWaitMillis연결 대기 최대 시간 (-1: 무제한)

Java 코드에서 DataSource 사용

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;

public class DBUtil {

    private static DataSource ds;

    static {
        try {
            Context ctx =
                new InitialContext();
            Context envCtx =
                (Context) ctx.lookup(
                    "java:comp/env");
            ds = (DataSource) envCtx.lookup(
                    "jdbc/oracle");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Connection getConnection()
        throws Exception {
        return ds.getConnection();
    }
}

4. 서블릿 DB 연동 예제

회원 조회 서블릿

@WebServlet("/members")
public class MemberServlet
    extends HttpServlet {

    @Override
    protected void doGet(
        HttpServletRequest req,
        HttpServletResponse resp)
        throws ServletException, IOException {

        resp.setContentType(
            "text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        try (
            Connection conn =
                DBUtil.getConnection();
            PreparedStatement pstmt =
                conn.prepareStatement(
                    "SELECT * FROM member");
            ResultSet rs =
                pstmt.executeQuery()
        ) {
            out.println("<html><body>");
            out.println("<h2>회원 목록</h2>");
            out.println("<table border='1'>");
            out.println("<tr>");
            out.println("<th>ID</th>");
            out.println("<th>이름</th>");
            out.println("<th>이메일</th>");
            out.println("</tr>");

            while (rs.next()) {
                out.println("<tr>");
                out.println("<td>"
                    + rs.getString("id")
                    + "</td>");
                out.println("<td>"
                    + rs.getString("name")
                    + "</td>");
                out.println("<td>"
                    + rs.getString("email")
                    + "</td>");
                out.println("</tr>");
            }

            out.println("</table>");
            out.println("</body></html>");

        } catch (Exception e) {
            e.printStackTrace();
            out.println("오류: "
                + e.getMessage());
        }
    }
}

회원 등록 서블릿

@WebServlet("/addMember")
public class AddMemberServlet
    extends HttpServlet {

    @Override
    protected void doPost(
        HttpServletRequest req,
        HttpServletResponse resp)
        throws ServletException, IOException {

        req.setCharacterEncoding("UTF-8");

        String id =
            req.getParameter("id");
        String name =
            req.getParameter("name");
        String email =
            req.getParameter("email");

        String sql =
            "INSERT INTO member"
          + " (id, name, email)"
          + " VALUES (?, ?, ?)";

        try (
            Connection conn =
                DBUtil.getConnection();
            PreparedStatement pstmt =
                conn.prepareStatement(sql)
        ) {
            pstmt.setString(1, id);
            pstmt.setString(2, name);
            pstmt.setString(3, email);

            int result =
                pstmt.executeUpdate();

            resp.setContentType(
                "text/html;charset=UTF-8");
            PrintWriter out =
                resp.getWriter();

            if (result > 0) {
                out.println("등록 성공");
            } else {
                out.println("등록 실패");
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

CRUD 패턴 요약

작업SQL메서드반환
조회SELECTexecuteQuery()ResultSet
등록INSERTexecuteUpdate()int (영향 행 수)
수정UPDATEexecuteUpdate()int (영향 행 수)
삭제DELETEexecuteUpdate()int (영향 행 수)

5. DAO 패턴

실무에서는 DB 접근 로직을 서블릿에 직접 작성하지 않고, DAO(Data Access Object) 클래스로 분리한다. 이렇게 하면 역할이 분리되어 유지보수가 용이하다.

  서블릿 → DAO → DB 구조

┌────────────────┐
│    Servlet     │
│ (요청/응답 처리) │
└───────┬────────┘
        │ 메서드 호출
        ▼
┌────────────────┐
│      DAO       │
│ (DB 접근 전담)  │
└───────┬────────┘
        │ SQL 실행
        ▼
┌────────────────┐
│   Database     │
└────────────────┘

MemberDAO 예제

public class MemberDAO {

    private DataSource ds;

    public MemberDAO() {
        try {
            Context ctx =
                new InitialContext();
            Context envCtx =
                (Context) ctx.lookup(
                    "java:comp/env");
            ds = (DataSource)
                envCtx.lookup(
                    "jdbc/oracle");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 전체 조회
    public List<MemberVO> selectAll() {
        List<MemberVO> list =
            new ArrayList<>();
        String sql =
            "SELECT * FROM member";

        try (
            Connection conn =
                ds.getConnection();
            PreparedStatement pstmt =
                conn.prepareStatement(sql);
            ResultSet rs =
                pstmt.executeQuery()
        ) {
            while (rs.next()) {
                MemberVO vo =
                    new MemberVO();
                vo.setId(
                    rs.getString("id"));
                vo.setName(
                    rs.getString("name"));
                vo.setEmail(
                    rs.getString("email"));
                list.add(vo);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }

    // 등록
    public int insert(MemberVO vo) {
        int result = 0;
        String sql =
            "INSERT INTO member"
          + " (id, name, email)"
          + " VALUES (?, ?, ?)";

        try (
            Connection conn =
                ds.getConnection();
            PreparedStatement pstmt =
                conn.prepareStatement(sql)
        ) {
            pstmt.setString(1,
                vo.getId());
            pstmt.setString(2,
                vo.getName());
            pstmt.setString(3,
                vo.getEmail());
            result =
                pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    // 수정
    public int update(MemberVO vo) {
        int result = 0;
        String sql =
            "UPDATE member"
          + " SET name = ?, email = ?"
          + " WHERE id = ?";

        try (
            Connection conn =
                ds.getConnection();
            PreparedStatement pstmt =
                conn.prepareStatement(sql)
        ) {
            pstmt.setString(1,
                vo.getName());
            pstmt.setString(2,
                vo.getEmail());
            pstmt.setString(3,
                vo.getId());
            result =
                pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    // 삭제
    public int delete(String id) {
        int result = 0;
        String sql =
            "DELETE FROM member"
          + " WHERE id = ?";

        try (
            Connection conn =
                ds.getConnection();
            PreparedStatement pstmt =
                conn.prepareStatement(sql)
        ) {
            pstmt.setString(1, id);
            result =
                pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}

MemberVO (Value Object)

public class MemberVO {
    private String id;
    private String name;
    private String email;

    // getter / setter
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    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;
    }
}

서블릿에서 DAO 사용

@WebServlet("/members")
public class MemberServlet
    extends HttpServlet {

    private MemberDAO dao =
        new MemberDAO();

    @Override
    protected void doGet(
        HttpServletRequest req,
        HttpServletResponse resp)
        throws ServletException, IOException {

        resp.setContentType(
            "text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();

        List<MemberVO> list =
            dao.selectAll();

        out.println("<html><body>");
        out.println("<h2>회원 목록</h2>");
        for (MemberVO vo : list) {
            out.println(
                vo.getId() + " / "
              + vo.getName() + " / "
              + vo.getEmail()
              + "<br>");
        }
        out.println("</body></html>");
    }
}

요약

핵심 개념

항목내용
PreparedStatementSQL 미리 컴파일, ? 바인딩, SQL Injection 방어
ConnectionPool미리 DB 연결을 만들어두고 재사용
DataSource커넥션풀을 관리하는 표준 인터페이스
JNDIkey/value로 자원을 조회하는 네이밍 서비스
DAO 패턴DB 접근 로직을 별도 클래스로 분리
VO 패턴데이터를 담아 전달하는 객체

서블릿 비즈니스 로직 처리 흐름

  클라이언트 (form)
        │
        │ HTTP 요청
        ▼
┌────────────────┐
│    Servlet     │
│ 파라미터 수신   │
└───────┬────────┘
        │
        ▼
┌────────────────┐
│      DAO       │
│ PreparedStmt   │
└───────┬────────┘
        │ JNDI → DataSource
        ▼
┌────────────────┐
│ ConnectionPool │
│ → DB 연동      │
└───────┬────────┘
        │ 결과 반환
        ▼
  클라이언트 (응답)