누군가 시키는 대로 받아쓰는 건 내 것이 아니다.
스프링 공부 전 Servlet + Java + DB 연동 다시 테스트 해보기.
A database connection is built by combining Servlet and JDBC MySQL.
Servlet, JDBC MySQL을 결합해 데이터 베이스와 연결한다.
/InsertDataInsertData.javaCREATE 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
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>

<form action="./InsertData" method="post">
HTTP POST 메소드를 통해 서버에 데이터를 전송할 수 있다.
<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속성에 지정된 서버에 데이터를 전송한다.
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;
}
}
코드를 하나 하나 분석해보자.
protected static Connection initializeDatabase() throws SQLException, ClassNotFoundException {
protected staticprotected으로 접근제한 해줌으로서 → 동일한 패키지 내의 하위 클래스에 의해 접근될 수 있고static으로 선언해줌으로서 → 해당 클래스의 인스턴스를 생성하지 않고도 사용할 수 있는 메소드가 되는 것이다.
DatabaseConnection클래스의 인스턴스를 생성하지 않고도 호출할 수 있는 것이다.
// 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 연동을 위해 입력해줘야 하는 정보이다.
Class.forName(dbDriver);
dbDriver에 저장된 JDBC 드라이버 클래스를 동적으로 로딩한다.
“동적으로 로딩한다?”
런타임(프로그램 실행 중)에 특정 클래스 or 자원을 메모리에 로드하는 것을 의미한다. 이를 통해 프로그램은 필요에 따라 특정 기능을 사용할 수 있게 되고, 사용하지 않는 기능은 메모리에 로드하지 않아 ⇒ 메모리 효율성을 높일 수 있다.
Connection con = DriverManager.getConnection(dbURL + dbName, dbUsername, dbPassword);
DriverManager.getConnection을 사용하여 MySQL에 연결한다.
JDBC URL:jdbc:mysql://localhost:3306/ + demo; 이 된다.
그래서 DB 명을 필요에따라 변경할 수도 있다!
return con;
Connection 객체를 반환한다.
세션(Session), SQL 실행문이 될 수 있다.
세션(Session)은 사용자가 DB에 연결될 때부터 끊어질 때까지 시간 동안의 상호작용을 의미하는데, 즉 DB와의 통신을 관리하는 하나의 작업단위를 말한다.
즉, 코드의 Connection 객체는 이 세션의 시작과 끝을 관리하게 되는 것이다.
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");
}
req.getParameter("username")), req.getParameter("password")) 로 문자열을 얻어오려고 시도한다.setString 은 문자열 데이터를 설정하는 메소드이다. 따라서 사용자가 정수를 입력한다면 NumberFormatException이 발생할 것이다.
당연히 에러가 나겠지?

?..
DB 컬럼이 문자열로 정의되었더라도, RDBMS는 자동 형 변환하여 저장할 수 있다.
묵시적 형 변환
조건절 데이터 타입이 다르면 “우선 순위가 있는 쪽으로 형 변환”이 내부적으로 발생하는 것이다.
예를 들면 정수 값과 문자열을 비교하는 경우, 정수형이 문자열보다 우선 순위에 있기 때문에 문자열은 자동으로 형 변환 되는 것이다.
즉, MySQL이 내 1000이라는 정수를 자동 형 변환해서 저장했다.
사용자와의 상호작용이 많은 경우나, 코드의 가독성이 중요한 경우 코드에서 검증하고 예외 처리하는 것이 낫지 않을까?
왜냐면
이제는 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) {
}
}


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