[Java/MySQL] MySQL, Java Servlet 데이터 베이스 연동 테스트하기 약간의 삽질도 같이

sbj·2023년 12월 5일

Java

목록 보기
13/15

누군가 시키는 대로 받아쓰는 건 내 것이 아니다.
스프링 공부 전 Servlet + Java + DB 연동 다시 테스트 해보기.


01. Overview

A database connection is built by combining Servlet and JDBC MySQL.

Servlet, JDBC MySQL을 결합해 데이터 베이스와 연결한다.


02. Flow (흐름)

  1. 연결할 DB 생성하기
  2. POST 요청 및 URL (/InsertData)를 포함해 username, password를 제출한다. → /InsertData
  3. 해당 양식 제출 이후, 해당 서블릿이 호출된다. → InsertData.java
  4. InsertData 서블렛 클래스는 모든 req 요청 매개변수를 처리하고, 데이터 베이스에 인풋으로 들어온 데이터를 저장한다.

03. Tools and technologies used

  • JDK - 21
  • Apache Tomcat - 10.1
  • Servlet API
  • MySQL - mysql-connector-j-8.2.0.jar
  • JSP-API

0. Add Dependencies

  • mysql-connector-j-8.2.0.jar
  • jsp-api.jar
  • servlet-api.jar

1. Test용 DB 생성하기

CREATE TABLE `login` (
  `username` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

2. Implementation of required Web-pages

DB에 입력할 Input을 받아올 HTML 파일을 생성하자.

<!DOCTYPE html>
<html>

<head>
	<meta charset="UTF-8">
	<title>Insert Data</title>
</head>

<body>
	<form action="./InsertData" method="post"> //폼에 Servlet 참조를 인스턴스로 제공한다.
		<p>ID:</p>
		<input type="text" name="id" />
		<br />
		<p>String:</p>
		<input type="text" name="string" /> 
		<br /><br /><br />
		<input type="submit" />
	</form>
</body>

</html>

Output

HTML form 요소

<form action="./InsertData" method="post"> 

HTTP POST 메소드를 통해 서버에 데이터를 전송할 수 있다.

HTML Input 요소

<p>ID:</p>
<input type="text" name="id" />

ID를 입력하는 text input field이다. name 속성은 폼이 제출될 때, input을 식별하는 . 데에 사용된다.

<p>String:</p>
<input type="text" name="string" /> 

상기 ID 인풋 요소와 동일하게 작동한다.

<input type="submit" />

해당 버튼이 클릭되면 폼이 제출되고, action속성에 지정된 서버에 데이터를 전송한다.


3. Creation of Java Servlet program with JDBC Connection

JDBC 연결을 통한 Java Servlet 프로그램을 생성해보자.

JDBC 연결을 구축하기 위한 간략한 과정은 아래와 같다.
1. 모든 패키지 Import하기
2. JDBC Driver 등록하기
3. 연결하기
4. 쿼리를 실행하고, 결과를 검색하기
5. JDBC 환경 정리하기

동일한 코드 스니펫을 각 프로그램에서 생성하는 것은 비효율적이므로, DB 연결만을 담당하는 별도의 클래스를 생성했다.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseConnection {
	protected static Connection initializeDatabase() throws SQLException, ClassNotFoundException {

		// Database Connection
		String dbDriver = "com.mysql.jdbc.Driver";
		String dbURL = "jdbc:mysql://localhost:3306/";
		// Database name to access
		String dbName = "demo"; //DB명
		String dbUsername = "{user명}";
		String dbPassword = "{비밀번호}";

		Class.forName(dbDriver);
		Connection con = DriverManager.getConnection(dbURL + dbName, dbUsername, dbPassword);

		return con;
	}
}

코드를 하나 하나 분석해보자.

3.1. Method Declation

protected static Connection initializeDatabase() throws SQLException, ClassNotFoundException {
  • protected static
    • protected으로 접근제한 해줌으로서 → 동일한 패키지 내의 하위 클래스에 의해 접근될 수 있고
    • static으로 선언해줌으로서 → 해당 클래스의 인스턴스를 생성하지 않고도 사용할 수 있는 메소드가 되는 것이다.

DatabaseConnection 클래스의 인스턴스를 생성하지 않고도 호출할 수 있는 것이다.

3.2. Database Connection Details

		// Database Connection
		String dbDriver = "com.mysql.jdbc.Driver";
		String dbURL = "jdbc:mysql://lcalhost:3306/";
		// Database name to access
		String dbName = "demo"; //DB명
		String dbUsername = "{user명}";
		String dbPassword = "{비밀번호}";

MySQL DB 연동을 위해 입력해줘야 하는 정보이다.

  • dbDriver: MySQL 연결을 위한 JDBC 드라이버의 정규화된 이름
  • dbURL: MySQl 데이터베이스의 URL
  • dbName: 접근할 데이터베이스 명 (나는 ‘demo’라는 이름의 DB와 연동할 것)
  • dbUsername: MySQL user 이름
  • dbPassword: MySQL user 비밀번호

3.3. Load JDBC Driver

Class.forName(dbDriver);

dbDriver에 저장된 JDBC 드라이버 클래스를 동적으로 로딩한다.

“동적으로 로딩한다?”
런타임(프로그램 실행 중)에 특정 클래스 or 자원을 메모리에 로드하는 것을 의미한다. 이를 통해 프로그램은 필요에 따라 특정 기능을 사용할 수 있게 되고, 사용하지 않는 기능은 메모리에 로드하지 않아 ⇒ 메모리 효율성을 높일 수 있다.

3.4. Establish Database Connection

Connection con = DriverManager.getConnection(dbURL + dbName, dbUsername, dbPassword);

DriverManager.getConnection을 사용하여 MySQL에 연결한다.

  • 매개변수로 (dbURL+ dbName), 사용자 이름, 비밀번호를 가진다.

JDBC URL: jdbc:mysql://localhost:3306/ + demo; 이 된다.
그래서 DB 명을 필요에따라 변경할 수도 있다!

3.5 Return Connection

return con;

Connection 객체를 반환한다.

세션(Session), SQL 실행문이 될 수 있다.

세션(Session)은 사용자가 DB에 연결될 때부터 끊어질 때까지 시간 동안의 상호작용을 의미하는데, 즉 DB와의 통신을 관리하는 하나의 작업단위를 말한다.

즉, 코드의 Connection 객체는 이 세션의 시작과 끝을 관리하게 되는 것이다.


4. To use this class method , create an object in Java Servlet program

try {
			Connection con = DatabaseConnection.initializeDatabase();
			PreparedStatement st = con.prepareStatement("insert into login values(?, ?)");

			st.setInt(1, Integer.valueOf(req.getParameter("username")));
			st.setString(2, req.getParameter("password"));

“subeen”을 username에 입력했고, 당연히 NumberFormat Exception 발생했다. 그래서 코드를 아래처럼 수정을 했는데

(일단 급한 불 끄고 실수한 거 이후 복기)

NumberFormat Exception

프로그램이 숫자를 파싱하려고 할 때 발생하는 에러다.
즉, 나의 경우 "subeen"은 Integer이 아닌 String 인데, 이를 Integer 로 변환하려고 시도했기 때문임.

Integer.valueOf : 문자열을 정수로 변환해준다.
문자열은 숫자로 변환하는 건 가능한데, 해당 String이 숫자로 표현될 수 있는 형식이어야 한다. 에를 들어 "123"이라는 String은 Integer로 변환할 수 있지만 "subeen"이라는 String은 Integer로 변환될 수 없는 문자열 형식이다. 그래서 NumberFormatException이 발생했다.

그럼 다른 상황에서, 정수형태로 입력값을 받을 경우 사용자가 문자열 입력하면 다 터지겠네?

아니. 이럴 땐 사용자 입력값 처리하기 전 DataType을 검증하는 단계를 추가해줄 수 있다.

예를 들면, 사용자가 입력한 값이 1. 숫자로 변환 가능한지 확인 만약 그렇지 않다면, 사용자에게 유효한 값을 입력하도록 요청하거나, 에러 메시지를 표시할 수 있는 것이다.


만약 사용자가 정수를 입력하면 예외처리를 구현하고, 문자열 입력에 대해서는 정상적으로 처리되도록 하고싶었다.

try {
				st.setString(1, req.getParameter("username"));
				st.setString(2, req.getParameter("password"));
			} catch (NumberFormatException nfe) {
				System.out.println("NumberFormat Exception: invalid Input String");
			}

내 예상

  1. req.getParameter("username")), req.getParameter("password")) 로 문자열을 얻어오려고 시도한다.
  2. setString 은 문자열 데이터를 설정하는 메소드이다. 따라서 사용자가 정수를 입력한다면 NumberFormatException이 발생할 것이다.

테스트

당연히 에러가 나겠지?

테스트 결과

?..

왜 정상적으로 진행되었나?

  • 데이터베이스 타입 호환성
    • DB 컬럼이 문자열로 정의되었더라도, RDBMS는 자동 형 변환하여 저장할 수 있다.

      묵시적 형 변환
      조건절 데이터 타입이 다르면 “우선 순위가 있는 쪽으로 형 변환”이 내부적으로 발생하는 것이다.
      예를 들면 정수 값과 문자열을 비교하는 경우, 정수형이 문자열보다 우선 순위에 있기 때문에 문자열은 자동으로 형 변환 되는 것이다.

      즉, MySQL이 내 1000이라는 정수를 자동 형 변환해서 저장했다.

그럼 DB에서 묵시적 형변환 금지처리 하기 vs 코드에서 정수 입력 시 에러 처리 어떤게 더 효율적인가?

사용자와의 상호작용이 많은 경우나, 코드의 가독성이 중요한 경우 코드에서 검증하고 예외 처리하는 것이 낫지 않을까?

왜냐면

  1. 입력 데이터 제어: 무효한 데이터를 더 쉽게 감지하고 처리할 수 있으니까.
  2. 코드 가독성 및 유지보수성: 형변환 오류 처리 로직을 명시적으로 작성해두면 코드 의도가 명확해지고, 유지보수가 용이해지니까.

이제는 Duplicate entry 'subeen' for key 'login.PRIMARY’

이라는 에러가 뜬다. username은 PRIMARY KEY로 설정되어있고, subeen 이라는 username이 이미 DB에 있는데. 거기다 동일한 값을 전송했으니 당연히 에러가 발생했다.


최종 코드

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/InsertData") // 서블렛
public class InsertData extends HttpServlet {
	private static final long serialVersionUID = 1L;

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

		try {
			// DB 초기화
			Connection con = DatabaseConnection.initializeDatabase();
			// SQL 쿼리문 생성: insert data into demo table
			// login 테이블은 2개의 열로 구성되어 있기에, 두 개의 ?를 사용
			PreparedStatement st = con.prepareStatement("insert into login values(?, ?)");

			try {
				String username = req.getParameter("username");
				String password = req.getParameter("password");
				if (isNumeric(username) && isNumeric(password)) {
					throw new NumberFormatException("Invalid Input:  Should be a String");
				}
				st.setString(1, username);
				st.setString(2, password);

				// Insert 쿼리문 실행
				int rowsAffected = st.executeUpdate();

				st.close();
				con.close();

				PrintWriter out = resp.getWriter();
				out.println("<html><body><b> Successfully Inserted" + "</b></body></html>");
			} catch (NumberFormatException nfe) {
				System.out.println(nfe.getMessage());
			}

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

	// 문자열이 정수로 변환가능한지 확인, 변환 성공하면 true 실패하면 에러처리 false 리턴
	private boolean isNumeric(String str) {
		try {
			Integer.parseInt(str); // 문자열을 정수로 변환하는데 사용하는데, 변환 불가능한 문자열 주어지면 NumberFormayException이 발생하게 된다.
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

	public static void main(String[] args) {
	}

}

테스트 (2)

테스트 결과 (2)

에러처리 완료다.


부족한 점 복기

  • HTTP, 요청 메소드 다시 공부 (POST/GET etc.)
  • NumberFormat Exception
  • Duplicate entry ‘subeen’ for key ‘login.PRIMARY’
  • HTTP 405 에러

이건 기초중의 기초인 거 아는데 그래서 더 꼼꼼히 짚고 넘어가고 싶었다. 여기에서 나아가 빨리 코드를 확장해보고 싶다.

profile
Strong men believe in cause and effect.

0개의 댓글