BlueNyang
[멀티캠퍼스] 풀스택 개발자 아카데미 (20) - Servlet/JSP(4)
Java 풀스택 아카데미
· 멀티캠퍼스 JAVA 풀스택 개발자 아카데미 6회차 (20편)

[멀티캠퍼스] 풀스택 개발자 아카데미 (20) - Servlet/JSP(4)

BlueNyangBlueNyang
·
·
약 6분
·
# 부트캠프후기# [현대이지웰] JAVA 풀스택 개발자 아카데미 6회차# 멀티캠퍼스it부트캠프# connection-pool# dbcp# jndi
시리즈·멀티캠퍼스 JAVA 풀스택 개발자 아카데미 6회차(30개의 글)
  • ···
  • 19.[멀티캠퍼스] 풀스택 개발자 아카데미(19) - Servlet/JSP(3)
  • 20.[멀티캠퍼스] 풀스택 개발자 아카데미 (20) - Servlet/JSP(4)현재
  • 21.[멀티캠퍼스] 풀스택 개발자 아카데미 (21) - Spring Framework(1)
  • ···

1. 연결의 비효율

이전 글에서는 MVC Model 2 아키텍처와 프론트 컨트롤러 패턴을 도입하여 웹 애플리케이션의 구조를 체계적으로 분리하는 방법을 알아보았다. 이를 통해 유지보수성과 확장성이 높은 코드 구조의 기반을 마련했다. 하지만 지금까지의 데이터베이스(DB) 연동 방식에는 아직 해결해야 할 중요한 성능 문제가 남아있다.

현재 우리의 DAO(Data Access Object)는 DB 작업이 필요할 때마다 DriverManager.getConnection()을 호출하여 새로운 연결을 맺고, 작업이 끝나면 연결을 끊는 방식을 사용한다. 사용자가 거의 없는 소규모 애플리케이션에서는 문제가 되지 않지만, 수많은 사용자가 동시에 접속하는 실제 서비스 환경에서는 치명적인 성능 저하의 원인이 된다. DB 연결은 생각보다 무겁고 시간이 많이 소요되는 작업이기 때문이다.

이번 글에서는 이러한 비효율을 해결하고 웹 애플리케이션의 성능을 극적으로 향상시키는 커넥션 풀(Connection Pool, DBCP) 기법을 도입한다. 커넥션 풀의 개념과 장점을 이해하고, 최신 아파치 톰캣(Apache Tomcat) 환경에서 **JNDI(Java Naming and Directory Interface)**를 이용해 커넥션 풀을 설정하고 DAO에 적용하는 구체적인 방법을 실습을 통해 알아본다.

2. DB 커넥션, 빌려 쓰고 반납

2.1. 매번 DB 연결 시 발생하는 문제점

사용자의 모든 요청이 DB 접근을 필요로 한다고 가정해보자. 기존 방식은 다음과 같은 과정을 거친다.

  1. 사용자 요청 발생
  2. JDBC 드라이버 로드
  3. DB와 Connection(연결) 생성 (TCP/IP 소켓 연결, DB 인증 등)
  4. SQL 실행
  5. Connection 종료
  6. 사용자에게 응답

이 과정에서 가장 큰 병목 지점은 3번, DB와 새로운 연결을 생성하는 단계이다. 네트워크 통신과 DB 인증 과정은 상당한 시스템 자원을 소모한다. 불특정 다수의 사용자가 동시에 접속하여 이 과정을 반복하면 서버는 금세 과부하에 걸리게 되고, 이는 서비스 전체의 응답 속도 저하로 이어진다.

기존-JDBC-연결-방식의-문제점
기존-JDBC-연결-방식의-문제점

2.2. 커넥션 풀(Connection Pool)의 개념과 장점

이러한 문제를 해결하기 위해 등장한 것이 바로 커넥션 풀(Connection Pool) 이다. 이름 그대로 '연결(Connection)을 모아두는 창고(Pool)'라고 생각할 수 있다.

커넥션 풀의 동작 원리는 간단하다. WAS(Web Application Server)가 시작될 때, 미리 정해진 개수만큼의 DB 커넥션을 생성하여 '풀(Pool)'에 저장해 둔다. 그리고 애플리케이션(DAO)에서 DB 연결이 필요하면, 풀에서 이미 생성된 커넥션 객체를 '대여'해서 사용한다. 사용이 끝나면 커넥션을 끊는 것이 아니라, 다시 풀에 '반납'하여 다른 요청이 재사용할 수 있도록 한다.

커넥션-풀의-동작-원리-흐름도
커넥션-풀의-동작-원리-흐름도

커넥션 풀 사용의 장점:

  • 성능 향상: 커넥션을 생성하고 닫는 데 드는 비용이 사라져 애플리케이션의 전반적인 응답 속도가 크게 향상된다.
  • 자원 관리 효율성: 생성할 수 있는 총 커넥션 수를 제어함으로써 DB 서버에 가해지는 부하를 관리하고, 시스템 자원이 고갈되는 것을 방지할 수 있다.
  • 연결 안정성: 유휴 커넥션을 검사하고 비정상적인 커넥션을 풀에서 제거하는 등 연결 관리를 WAS가 대신 해주므로 안정성이 높아진다.

3. JNDI를 이용한 톰캣 커넥션 풀 설정

커넥션 풀을 애플리케이션에 적용하는 방법은 다양하지만, 여기서는 WAS가 제공하는 기능을 활용하는 가장 표준적인 방법인 JNDI를 사용한다. JNDI는 자바 애플리케이션이 이름(Name)을 통해 데이터나 객체(Object)를 찾을 수 있도록 해주는 디렉터리 서비스이다.

우리는 톰캣에 "이 이름으로 DB 커넥션 풀을 관리해줘"라고 설정을 등록해두고, 자바 코드에서는 그 이름을 불러다 쓰는 방식으로 커넥션 풀을 이용하게 된다.

3.1. context.xml 파일 설정

IntelliJ IDEA에서 생성한 Dynamic Web Project(Jakarta EE) 기준으로, src/main/webapp/META-INF 디렉터리 안에 context.xml 파일이 있다. 이 파일에 우리가 사용할 커넥션 풀의 상세 정보를 JNDI 리소스로 등록한다.

XML

text
<?xml version="1.0" encoding="UTF-8"?>
<Context path="/ProjectName">

    <Resource name="jdbc/oracle"
              auth="Container"
              type="javax.sql.DataSource"
              username="your_db_username"
              password="your_db_password"
              driverClassName="oracle.jdbc.driver.OracleDriver"
              url="jdbc:oracle:thin:@localhost:1521:XE"
              maxTotal="20"
              maxIdle="10"
              maxWaitMillis="-1"/>

</Context>
IntelliJ-IDEA-프로젝트-구조와-context-xml-위치
IntelliJ-IDEA-프로젝트-구조와-context-xml-위치

각 속성의 의미는 다음과 같다.

  • name: JNDI 이름이다. 코드에서 이 이름을 사용하여 리소스를 찾게 된다.
  • auth: 인증 주체를 의미하며, 일반적으로 Container로 설정한다.
  • type: 리소스의 자바 클래스 타입을 지정한다. 커넥션 풀은 javax.sql.DataSource 인터페이스를 사용한다.
  • username, password, driverClassName, url: 기존 JDBC 연결에 사용했던 DB 접속 정보와 동일하다.
  • maxTotal: 풀에서 동시에 유지할 수 있는 최대 커넥션 수이다.
  • maxIdle: 풀에서 사용되지 않고 대기 상태로 있을 수 있는 최대 커넥션 수이다.
  • maxWaitMillis: 풀의 모든 커넥션이 사용 중일 때, 새로운 커넥션을 얻기 위해 대기하는 최대 시간(밀리초)이다. -1은 무한 대기를 의미한다.
Warning
Apache Tomcat 10.0 이상과 Jakarta EE 9+ 환경에서도 javax를 사용한다. Connection Pool은 Jakarta EE 사용의 일부가 아니라, 핵심 Java SE 플랫폼에 포함된 API이기 때문이다. Jakarta EE는 Java EE (Enterprise Edition)가 Oracle에서 Eclipse 재단으로 이전되면서 상표권 문제로 패키지 이름은 변경한 것이다. 하지만, javax.sql.* 등 몇몇 패키지는 처음부터 JDK에 포함되어 있던 표준 라이브러리이기 때문에, javax를 사용하는 것이다.
Note
context.xml 파일은 CATALINA_HOME/conf 디렉터리에도 존재하는데, 여기에 설정하면 해당 톰캣 서버의 모든 웹 애플리케이션에 적용된다. 프로젝트 내의 META-INF/context.xml에 설정하면 해당 프로젝트에만 적용된다. IntelliJ IDEA - Jakarta EE 프로젝트에는 없을 수 있는데, 파일을 만들면 바로 적용된다.

3.2. JDBC 드라이버 배치

톰캣이 커넥션 풀을 생성하려면 당연히 해당 DB의 JDBC 드라이버가 필요하다. 이전에 프로젝트의 Build Path에만 추가했다면, 이제는 톰캣이 직접 인식할 수 있는 위치에 드라이버 .jar 파일을 복사해야 한다.

ojdbc11.jar (또는 사용하는 DB에 맞는 드라이버. 필자의 경우 ojdbc17) 파일을 APACHE_TOMCAT_HOME/lib 디렉터리에 복사해 넣는다. IntelliJ IDEA를 사용한다면, Run/Debug Configurations의 Tomcat 설정에서 'Deployment' 탭으로 이동하여 외부 라이브러리를 직접 추가할 수도 있다.

만약, 필자와 같이 IntelliJ IDEA와 더불어 JDK21, JakartaEE (Maven)를 사용하고 있다면, 프로젝트의 루트 디렉터리에 있는 pom.xml에 다음 태그를 추가하여 손 쉽게 추가할 수 있다.

xml
<dependency>  
    <!-- 또는 사용하는 DB에 맞는 드라이버 -->
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc17</artifactId>
    <version>23.9.0.25.07</version>  
</dependency>
IDEA-Maven-Dependency-성공적으로-추가된-사진
IDEA-Maven-Dependency-성공적으로-추가된-사진

4. DAO에서 커넥션 풀 사용하기

이제 모든 설정이 끝났으니, DAO 코드를 수정하여 커넥션 풀을 사용하도록 변경할 차례이다. 기존의 DriverManager 관련 코드를 모두 제거하고, JNDI를 통해 DataSource 객체를 찾아와 사용한다.

4.1. MemberDAO.java 수정

DAO 클래스의 생성자에서 JNDI lookup을 통해 context.xml에 등록한 DataSource를 가져오고, getConnection() 메소드에서는 이 DataSource를 통해 풀에서 커넥션을 얻어오도록 수정한다.

Java

java
package com.example.model;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class MemberDAO {
    private DataSource dataSource;

    // 생성자에서 JNDI를 통해 DataSource 객체를 얻어온다.
    public MemberDAO() {
        try {
            Context initContext = new InitialContext();
            
            // JNDI는 'java:/comp/env/' 경로 아래에서 리소스를 찾는다.
            Context envContext = (Context) initContext.lookup("java:/comp/env");
            
            // context.xml에 설정한 name("jdbc/oracle")으로 DataSource를 찾는다.
            this.dataSource = (DataSource) envContext.lookup("jdbc/oracle");
            
            System.out.println("DBCP DataSource 로드 성공!");
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("DBCP DataSource 로드 실패!");
        }
    }

    // 회원 목록을 조회하는 메소드
    public List<MemberVO> getAllMembers() {
        List<MemberVO> memberList = new ArrayList<>();
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        String sql = "SELECT * FROM t_member ORDER BY joinDate DESC";

        try {
            // DriverManager가 아닌 DataSource로부터 Connection을 얻어온다.
            conn = dataSource.getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();

            while (rs.next()) {
                MemberVO vo = new MemberVO();
                vo.setId(rs.getString("id"));
                vo.setPwd(rs.getString("pwd"));
                vo.setName(rs.getString("name"));
                vo.setEmail(rs.getString("email"));
                vo.setJoinDate(rs.getDate("joinDate"));
                memberList.add(vo);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 자원 반납 (Connection은 여기서 close() 호출 시 풀에 반납된다)
            try {
                if (rs != null) rs.close();
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close(); // 중요: 연결 종료가 아닌 풀에 반납!
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return memberList;
    }
    
    // (addMember, findMember 등 다른 CRUD 메소드들도 동일하게 수정)
}
JNDI-lookup을-통한-DataSource-획득-과정
JNDI-lookup을-통한-DataSource-획득-과정

가장 중요한 부분은 finally 블록의 conn.close()이다. 커넥션 풀을 사용할 때 close() 메소드를 호출하면, 물리적인 연결을 끊는 것이 아니라 사용하던 커넥션을 다시 풀로 반납하는 동작을 수행한다. 따라서 자원 누수를 막기 위해 반드시 close()를 호출해주어야 한다.

DAO에서-커넥션-풀-사용-흐름
DAO에서-커넥션-풀-사용-흐름

이제 애플리케이션을 다시 실행하고 회원 목록 조회 기능을 사용해보면, 이전과 동일하게 동작하지만 내부적으로는 훨씬 효율적인 커넥션 풀 방식으로 DB와 통신하게 된다. 톰캣의 로그나 콘솔에 DAO 생성자에서 출력하도록 한 "DBCP DataSource 로드 성공!" 메시지가 나타나는지 확인해보자.

톰캣-콘솔에-출력된-DataSource-로드-성공-메시지
톰캣-콘솔에-출력된-DataSource-로드-성공-메시지

5. 결론

이번 글을 통해 우리는 동시 접속 환경에서 웹 애플리케이션의 성능을 좌우하는 중요한 요소인 커넥션 풀에 대해 학습했다. WAS가 제공하는 JNDI 서비스를 이용하여 커넥션 풀을 설정하고, DAO에서 이를 활용하는 방법을 익힘으로써 이제 우리의 웹 애플리케이션은 실제 서비스 환경에서도 안정적으로 동작할 수 있는 중요한 발판을 마련했다.

이로써 우리는 웹의 기본 동작 원리부터 서블릿과 JSP, MVC 패턴, 그리고 커넥션 풀에 이르기까지 순수 자바 웹 기술의 핵심 요소들을 차근차근 밟아왔다.

하지만 context.xml 설정, JNDI lookup, 반복적인 JDBC 자원 반납 코드 등은 여전히 번거롭고 개발자의 실수를 유발할 수 있는 부분이다. 이러한 기반 설정과 반복적인 작업을 프레임워크가 대신 처리해주고, 개발자는 오직 핵심 비즈니스 로직에만 집중할 수 있게 해주는 것이 바로 스프링 프레임워크이다.

다음 글에서는 왜 수많은 개발자가 스프링에 열광하는지, 스프링의 핵심 철학인 DI(의존성 주입)IoC(제어의 역전) 는 무엇인지 알아보며 현대적인 엔터프라이즈 개발의 세계로 본격적인 첫발을 내디딜 것이다.

BlueNyang
작성자BlueNyang
라이선스
CC BY NC
BlueNyang

BlueNyang

BlueNyang의 개발 log

카테고리

  • Development
  • Framework
  • Language
  • Dev Tools
  • DevOps & Infra
  • Studies

페이지

© 2026 BlueNyang. All rights reserved.

Made with Nuxt.js and Directus