Java : JSP DB 연동(postgreSQL)

NuyHes·2025년 8월 22일
0

[Study]

목록 보기
70/71
post-thumbnail

🕵️ JSP DB(PostgreSQL) 연동

JSP 프로젝트를 하게 되어 학습을 하고있는 상황이다. 간단하게 해당 프로젝트에 DB연동을 위해 작성하게 되었다. 현재 환경은 IntelliJ + Maven + JSP + PostgreSQL 이다.


📌 1. Maven 의존성 추가

  1. pom.xml에 PostgreSQL JDBC 드라이버를 넣는다.
<dependencies>  
	...
	...
	...
    <dependency>  
        <groupId>org.postgresql</groupId>  
        <artifactId>postgresql</artifactId>  
        <version>42.7.3</version>  
    </dependency>  
</dependencies>
  1. IntelliJViewTool WindowsMaven 클릭 후 상단 Sync 아이콘을 클릭하여 Reload All Maven Projects를 클릭한다.

📌 2. JSP에서 JDBC를 이용한 DB 연동

1️⃣ URL & SQL 문자열

  1. URL : DB URL 문자열을 만든다.
<%
	String url = "jdbc:postgresql://localhost:5432/project";
	...
	...
%>
  • 형식 : jdbc:postgresql://<host>:<port>/<database>?<params>
  • 예 : jdbc:postgresql://localhost:5432/project?currentSchema=public&ApplicationName=myapp

🕵️ 자주 쓰는 Postgres 파라미터

  • currentSchema=스키마명 : 기본 스키마 지정 (예 : app, public 다중도 가능)
  • sslmode= : disable|allow|prefer|require|verify-ca|verify-full
  • connectTimeout=10 : 접속 타임아웃(초)
  • socketTimeout=30 : 쿼리 실행 대기 타임아웃(초)
  • ApplicationName=myapp : 접속 식별용 이름
  • reWriteBatchedInserts=true : 배치 INSERT 최적화
  • stringtype=unspecified : 문자열 파라미터 타입 유연화(특정 케이스에 유용)

  1. SQL : 실행할 쿼리
<%
	String sql = "SELECT * FROM NOTICE";
	...
	...
%>
  • 베스트 : SELECT 컬럼명만 지정 (와일드카드 * 지양) 스키마 명시 ex) public.notice
  • 동적 값이 섞이면 PreparedStatement로 파라미터 바인딩

🕵️ PreparedStatement란?

SQL문을 미리 준비하고 컴파일하여 캐시된 실행 계획을 반복적으로 재사용하는 데이터베이스 객체로, SQL쿼리 실행 속도를 향상시키고 SQL Injection 공격을 방지하는 보안 기능도 제공한다. Statement와 달리 물음표(?)와 같은 플레이스 홀더를 사용해 동적인 데이터 바인딩하는 방식으로 데이터 베이스에서 동일하거나 비슷한 SQL문을 효율적으로 실행할 때 사용된다.

주요 특징 및 동작 방식

  1. 미리 컴파일 및 캐싱 : SQL문을 처음 실행할 때 구문 분석(parse) 및 컴파일 과정을 거쳐 실행 계획을 데이터베이스의 캐시에 저장
  2. 플레이스홀더 사용 : SQL문에 직접 데이터를 넣는 대신 플레이스홀더를 사용하여 변수 값을 전달할 위치를 지정
  3. 데이터 바인딩 : 이후 데이터가 필요할 때는 플레이스홀더에 해당 데이터를 바인딩하여 미리 컴파일된 실행 계획을 재사용
  4. 성능 향상 : 캐싱된 실행 계획 덕분에 동일한 SQL문을 여러번 실행할 때 구문 분석 및 컴파일 과정을 생략할 수 있어 실행 속도가 빨라진다.
  5. SQL 삽입 방지 : 개발자가 SQL문과 데이터를 분리하여 처리하므로 악의적인 사용자가 SQL 코드를 삽입하여 데이터를 조작하는 SQL Injection 공격을 효과적으로 막을 수 있다.

Statement와 차이

  1. 성능 : PreparedStatement는 컴파일된 실행 계획을 캐시하여 재사용하므로 Statement보다 일반적으로 성능이 우수
  2. 사용 : PreparedStatement는 동일한 SQL 쿼리를 반복적으로 실행해야 할 때 유리하며 INSERT, UPDATE, DELETE 문에서 자주 사용된다. Statement는 고정된 SQL문을 실행하거나 SQL 구문 자체가 동적으로 변경될 때 적합하다.

2️⃣ 계정 정보

  1. 단순 사용자/비밀번호 문자열을 적었다 실제 프로젝트에서는 다른 방식을 사용해야한다. (예 : 환경변수, properties, YAML 분리) 운영에서는 커넥션 풀(DataSource)로 관리
<%
	String user = "postgres";
	String pass = "비밀번호";
	...
	...
%>

💡 하드코딩은 지양


3️⃣ 드라이버 로딩

  1. 드라이버 클래스 로딩 및 DriverManager등록
<%
	Class.forName("org.postgresql.Driver");
	...
	...
%>
  • JDBC 4.0+ (PostgreSQL JDBC 8.x/42.x 계열)에서는 생략해도 자동 로딩되는 경우가 많음
  • ClassNotFoundException 발생 시: Maven 의존성 확인

4️⃣ 연결 생성

  1. DriverManager.getConnection 오버로드
<%
	Connection con = DriverManager.getConnection(url, user, pass);
	...
	...
%>
  • (url, user, pass) — 지금 방식
  • (url) — URL에 user/password 파라미터 포함 시
  • (url, Properties props) — 세부 옵션을 Properties로 전달
  1. 유용한 Connection 설정

    • con.setAutoCommit(false); 트랜잭션 수동제어 (INSERT/UPDATE/DELETE 후 commit/rollback)
    • con.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); 등 격리수준 설정
  2. 더 좋은 실무 방식

    • 커넥션 풀(DataSource) 사용 (예 : HikariCP) → 성능/안정성 ↑
    • 서블릿 컨테이너(Tomcat)라면 JNDI로 DataSource 바인딩해서 꺼내 쓰기

5️⃣Statement 생성

<%
	Statement st = con.createStatement();
	...
	...
%>
  • 기본(전방만, 읽기 전용) ResultSet을 생성하는 Statement
  • 오버로드(스크롤/동시성/홀더빌리티 지정)
Statement st = con.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE,   // 스크롤 가능(원본 변경 반영 X)
    ResultSet.CONCUR_READ_ONLY,          // 읽기 전용
    ResultSet.HOLD_CURSORS_OVER_COMMIT   // COMMIT 후에도 커서 유지(드물게 사용)
);
  • 권장 : Statement 대신 PreparedStatement(파라미터 바인딩 + SQL 인젝션 방지 + 캐시)

6️⃣SQL 실행

<%
	ResultSet rs = st.executeQuery(sql);
	...
	...
%>
  • executeQuery(sql) : SELECT 전용, 결과는 ResultSet
  • 다른 실행 메서드
    - executeUpdate(sql) : INSERT/UPDATE/DELETE 반환값은 영향 받은 행 수 int
    - execute(sql) : 결과가 ResultSet인지 업데이트 카운트인지 모를 때
    - 배치 : addBatch()/executeBatch() - 대량 INSERT/UPDATE최적화

9️⃣🔟

7️⃣ResultSet 사용 요령(핵심)

읽기 패턴

while (rs.next()) {               // 다음 행으로 이동, 없으면 false
    int id = rs.getInt("id");     // 컬럼명 또는 1부터 시작하는 인덱스
    String title = rs.getString("title");
}
  • 컬럼명/라벨
    - SELECT id AS notice_id 라벨 지정 시 rs.getInt("notice_id")
    - Postgres는 따옴표 없이 만든 컬럼은 소문자 라벨로 취급(대소문자 헷갈리면 컬럼 라벨 확인)
  • NULL 처리
    - rs.getInt(...)뒤에 rs.wasNull()로 방금 읽은 값이 NULL인지 확인 가능
    - 객체형(getObject, getString, getTimestamp)은 그대로 null 반환
  • 스크롤 가능 ResultSet(필요 시)

🕵️ ResultSet에서 스크롤이란?

JDBC에서 ResultSet을 스크롤 가능하게 만들 수 있는 옵션ResultSet이 기본적으로 순방향(Forward-only) 으로만 레코드를 읽을 수 있기 때문이다.

즉, 기본 ResultSet

  • .next() 메서드로 앞으로 한 칸씩만 이동 가능
  • 이미 지난 행으로 뒤로 돌아갈 수 없음
  • 특정 행 번호로 바로 이동도 불가능

이를 해결하기 위해 스크롤 가능한 ResultSet 옵션을 사용하면,
next() 뿐만 아니라 previous(), absolute(n), relative(n) 같은 메서드로 앞뒤 이동임의 위치 이동이 가능해진다.


8️⃣예외/자원 정리(매우 중요)

  • 예외
    - ClassNotFoundException : 드라이버 없음 → 의존성 점검
    - SQLException : 인증 실패, URL 오타, 권한, 타임아웃 등
  • 자원 닫기 순서 : ResultSetStatementConnection
  • try-with-resources 권장
String sql = "SELECT id, title FROM notice";
try (Connection con = DriverManager.getConnection(url, user, pass);
     PreparedStatement ps = con.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    while (rs.next()) {
        ...
    }
} catch (SQLException e) {
    // 로깅/처리
}

🕵️ JSP에 JDBC를 직접 넣는 것에 대해

  • 학습 수준에선 괜찮으나 실무에선 MVC 분리 권장
    - DAO/Service(자바 클래스)에 DB코드 → 서블릿/컨트롤러에서 호출 → JSP는 출력만(JSTL/EL)

실전형 리팩터링 예시

자바(서블릿/DAO) - 데이터 읽기

// NoticeDao.java
public class NoticeDao {
    private final DataSource ds; // HikariCP or JNDI로 주입

    public NoticeDao(DataSource ds) { this.ds = ds; }

    public List<Notice> findAll() throws SQLException {
        String sql = "SELECT id, title FROM public.notice ORDER BY id ASC";
        List<Notice> list = new ArrayList<>();
        try (Connection con = ds.getConnection();
             PreparedStatement ps = con.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                Notice n = new Notice();
                n.setId(rs.getInt("id"));
                n.setTitle(rs.getString("title"));
                list.add(n);
            }
        }
        return list;
    }
}

💡 컨트롤러(서블릿)에서 request.setAttribute("noticeList", dao.findAll()); 후 JSP로 forward.

JSP - 출력만(JSTL)

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<table>
  <thead><tr><th>ID</th><th>Title</th></tr></thead>
  <tbody>
    <c:forEach var="n" items="${noticeList}">
      <tr>
        <td>${n.id}</td>
        <td>${n.title}</td>
      </tr>
    </c:forEach>
  </tbody>
</table>

0개의 댓글