2-1 JDBC 프로그래밍 준비
데이터베이스를 생성하기 위해 새로운 데이터베이스를 생성합니다. 데이터베이스의 이름을 설정하고 생성합니다.
[도구] - [사용자 관리자] 메뉴로 이동하면 새로운 사용자를 추가할 수 있습니다. 추가 버튼을 눌러 이름과 암호를 설정합니다. 실습을 위해 호스트 설정은 %(모든곳에서 접근)으로 설정합니다.
객체 추가 버튼을 클릭하여 사용할 수 있는 대상을 지정해줍니다.
생성된 계정 확인을 위해 [파일] - [세션 관리자] 메뉴를 통해서 점검합니다. 사용자와 암호, 데이터베이스를 설정하고 문제가 없는지 확인합니다.
select now();
데이터베이스 연동 설정이 완료되면 SQL을 실행할 수 있는 창이 하나 생성됩니다. 현재 시간을 가져오는 SQL을 작성해 테스트해봅니다.
(1) 테스트 프로그램 작성하기
public class ConnectTests {
@Test
public void test1() {
int v1=10;
int v2=10;
Assertions.assertEquals(v1,v2);
}
}
@Test 어노테이션을 사용하는 메소드를 테스트 코드라고 합니다. 메소드는 public으로 선언되어야 하며 파라미터나 리턴타입 없이 작성합니다.
assertEquals()는 두 변수의 내용이 같아야 성공합니다.
@Test
public void testConnection() throws Exception {
Class.forName("org.mariadb.jdbc.Driver");
Connection connection = DriverManager.getConnection(
"jdbc:mariadb://localhost:3306/webdb",
"webuser",
"webuser");
Assertions.assertNotNull(connection);
connection.close();
}
Class.forName(): JDBC 드라이버 클래스를 메모리상으로 로딩하는 역할을 합니다.
Connection connection: java.sql패키지의 Connection 인터페이스 타입의 변수입니다. 데이터베이스와 네트워크 연결을 의미합니다.
DriverManager.getConnection(): 데이터베이스 내에 있는 여러 정보들을 통해서 특정 데이터베이스에 연결을 시도합니다.
jdbc:mariadb://localhost:3306/webdb: jdbc 프토토콜을 이용한다는 의미, localhost3306은 네트워크 연결 정보를, webdb는 연결하려는 데이터베이싀 정보를 의미합니다.
webuser: 연결을 위해 사용하는 계정과 패스워드가 필요합니다.
Assertions.assertNotNull(): 데이터베이스와 정상적으로 연결이 된다면 Connection 타이븨 객체는 null이 아니라는 것을 확신한다는 의미입니다.
connection.close(): 데이터베이스와 연결을 종료합니다. 작업이 완료되면 반드시 연결을 종료해야합니다.
(2) 데이터베이스 테이블 생성
create table tbl_todo(
tno int auto_increment primary key ,
title varchar(100) not null ,
dueDate date not null ,
finished tinyint default 0
);
Database 항목의 [New -> Query Console]을 이용해서 SQL 테이블을 생성합니다.
auto_increment는 식별키를 지정하기 위해서 사용합니다. 새로운 데이터가 추가될 때 자동으로 새로운 번호가 생성됩니다. 같은 번호가 생성되지 않아 고유한 식벽을 위해 사용됩니다.
(3) 데이터 insert
insert into tbl_todo(title, dueDate, finished)
values( 'Test...', '2002-12-31', 1);
insert 문은 특정한 테이블에 데이터를 추가하기 위해서 사용합니다. 칼럼을 지정하고 values를 이용해서 처리합니다.
(4) 데이터 select
데이터를 조회하는 SQL은 쿼리라고 하며 from절을 이용해서 가져오려는 데이터의 대상을 지정하고 where을 이용해서 필터링을 합니다.
select * from tbl_todo where tno<10;
(5) 데이터 update
기존 데이터를 수정하려면 update를 이용해서 처리할 수 있습니다. set을 이용해서 칼럼의 내용을 수정하고 where을 이용해서 대상을 지정할 수 있습니다.
update tbl_todo set finished=0 where tno=1;
(5) 데이터 delete
데이터 삭제는 delete문을 이용해서 작성할 수 있습니다. where 조건이 없는 경우 모든 데이터를 삭제할 수 있는 위험이 있어 경고 메시지를 보여주고 실행되지 않도록 되어 있습니다.
delete from tbl_todo where tno>3;
DML과 쿼리(select)의 차이
DML과 쿼리문에는 다음과 같은 차이가 있습니다.
DML은 몇개의 데이터가 처리되었는지 숫자로 결과 반환
select문은 데이터를 반환
insert/update/delete는 실행하고 나면 추가된 데이터를 보여주는 것이 아니라 몇개의 row가 추가/변경/삭제 되었는지를 알려줍니다. 반면 select는 실제 데이터들을 반환합니다.
!자바를 이용해서 프로그램을 작성하면 결과화면이 없기때문에 항상 SQL문을 미리 실행해서 올바르게 결과가 나오는 것을 확인하고 코드를 작성하는 것이 좋습니다.
JDBC 프로그래밍을 위한 API와 용어들
java.sql.Connection
Connection 인터페이스는 데이터베스와 네트워크상의 연결을 의미합니다. 개발자들은 Connection이라는 인터페이스를 활용하고 실제 구현 클래스는 JDBC 드라이버와 파일 내부의 클래스를 이용합니다. 반드시 Connection은 close()해야합니다. 데이터베이스는 많은 연결을 처리해야 하는데 연결이 종료가 되지 않으면 새로운 연결을 받을 수 없는 상황이 발생할 수 있습니다. Connection 종료를 위해서는 코드 내에서 tyr~catch~finally를 이용해서 종료하거나 try-with-resource방식을 이용합니다.
Statement, PreparedStatement
SQL을 실행할 수 있는 객체를 생성하는 기능으로 다음 방식이 가장 많이 사용됩니다.
Connection connection= . . .
PreparedStatement preparedStatement = connection.preparedStatement("select * from tbl_todo");
JDBC에서 데이터베이스를 보내기 위해서는 Statement/PreparedStatement타입을 이용합니다.
SQL을 전달한다는 점에서 같지만 SQL문을 미리 전달하고 나중에 데이터를 보내는 방식(PreparedStatement)과 SQL문 내부에 모든 데이터를 전송하는 방식(Statement)라는 차이가 있습니다. 실제 개발에서는 PreparedStatement만을 이용하는 것이 관례입니다.(SQL 내부에 고의적으로 다른 처리가 가능한 SQL문자열을 심어서 보내는 SQL injection 공격을 막기 위해)
중요 기능들은 다음과 같습니다.
- setXXX(): setInt(), setString(), setDate()와 같이 다양한 타입에 맞게 데이터를 세팅합니다.
- executeUpdate(): DML(insert/update/delete)을 실행하고 결과를 int 타입으로 반환합니다. 결과는 몇개의 행이 영향을 받았는가 입니다.
- executeQuery(): 말 그대로 쿼리를 실행할 때 사용합니다. ResultSet이라는 리턴타입을 이용합니다.
Statement 역시 마지막에 close()를 통해서 종료해주어야합니다.
java.sql.Result
PreparedStatement를 이용해서 처리하는 DML의 경우에는 int로 반환되는 것과 달리 쿼리를 실행했을 때 데이터베이스에서 반환되는 데이터를 읽어 들이기 위해서는 특별하게 ResultSet이라는 인터페이스를 이용합니다. ResultSet은 자바 코드에서 데이터를 읽어들이기 때문에 getInt(), getString() 등의 메소드를 이용해서 필요한 데이터를 읽어 들입니다.
next()라는 메소드가 존재합니다. ResultSet은 데이터를 순차적으로 읽는 방식으로 구성되기 때문에 next()를 이용해서 다음 행의 데이터를 읽을 수 있도록 이동하는 작업이 필요합니다.
Result 역시 작업후에 close()를 해주어야 합니다.
Connection Pool과 DataSource
JDBC는 기본적으로 필요한 순간에 잠깐 데이터베이스와 네트워크로 연결하고 데이터를 보내고 받는 방식으로 구성됩니다. 이 과정에서 연결을 맺는 작업은 시간과 자원을 쓰기에 성능저하가 발생합니다. JDBC는 보통 Connection Pool이라는 것을 이용해서 이 문제를 해결합니다.
-Connection Pool은 미리 Connection들을 생성해서 보관하고 필요할 때마다 꺼내서 쓰는 방식입니다. Connection Pool은 데이터베이스와 연결된 Connection들을 보관하기 때문에 연결에 걸리는 시간과 자원을 절약할 수 있습니다.
DAO(Data Access Object)
DAO는 데이터를 전문적으로 처리하는 객체를 의미합니다. 데이터베이스의 접근과 처리를 전담하는 객체를 의미하는데 DAO는 주로 VO를 단위로 처리합니다. DAO를 호출하는 객체는 내부에서 어떤식으로 데이터를 처리하는지 알 수 없도록 구성합니다. 이 때문에 JDBC프로그램을 작성한다는 의미는 실제로는 DAO를 작성한다는 의미입니다.
VO(Value Object) 혹은 엔티티(Entity)
데이터베이스에서는 하나의 데이터를 엔티티라고 하는데 자바는 이를 처리하기 위해 테이블과 유사한 구조의 클래스를 만들어서 객체를 처리하는 방식을 사용합니다. 이때 만든 객체는 값을 보관하라는 의미에서 VO(Value Object)라고 합니다.
VO는 DTO와 유사해 보이지만 DTO가 각 계층을 오고 가는데 사용되는 택배상자라면 VO는 데이터베이스의 엔티티를 자바 객체로 표현한 것이라고 생각할 수 있습니다.
DTO는 getter/setter을 이용해서 자유롭게 데이터를 가공할 수 있는 반면 VO는 주로 데이터 자체를 의미하기 때문에 getter만을 이용하는 경우가 대부분입니다.
2-2 프로젝트 내 JDBC 구현
Lombok라이브러리
- @Getter, @Setter: @Data 등을 이용해서 자동 생성
- toString(): @ToString을 이용한 toString() 메소드 자동 생성
- equals()/hashCode(): @EqualsAndHashCode를 이용한 자동 생성
- 생성자 자동 생성: @AllArgsConstructor, @NoArgsConstructor 등을 이용한 생성자 자동 생성
- 빌더 생성: @Builder를 이용한 빌더 패턴 코드 생성
(1) TodoVO 클래스
@Getter
@Builder
@ToString
public class TodoVO {
private long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
}
VO는 주로 읽기 전용으로 사용하므로 @Getter를 추가했고 객체 생성시에 빌더 패턴을 이용하기 위해 @Builder를 추가했습니다.
(2) Connection Pool
@Test
public void testHikariCP() throws Exception {
HikariConfig config = new HikariConfig();
config.setDriverClassName("org.mariadb.jdbc.Driver");
config.setJdbcUrl("jdbc:mariadb://localhost:3306/webdb");
config.setUsername("webuser");
config.setPassword("webuser");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
HikariDataSource ds = new HikariDataSource(config);
Connection connection = ds.getConnection();
System.out.println(connection);
connection.close();
}
실행결과는 동일하게 connection을 얻어 내지만 HikariCP를 통해서 얻어온 것이라는 것을 출력을 통해 알 수 있습니다.
TodoDAO와 @Clean up
public enum ConnectionUtil {
INSTANCE;
private HikariDataSource ds;
ConnectionUtil() {
HikariConfig config = new HikariConfig();
config.setDriverClassName("com.mysql.jdbc.Driver");
config.setJdbcUrl("jdbc:mariadb://localhost:3306/webdb");
config.setUsername("user");
config.setPassword("user");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
ds = new HikariDataSource(config);
}
public Connection getConnection() throws Exception {
return ds.getConnection();
}
}
HikariConfig를 이용해서 하나의 HikariDataSource를 구성했습니다. getConnection()을 통해서 사용하게 되는데 외부에서는 ConnectionUtil.INSTNACE.getConnection()을 통해서 Connection을 얻을 수 있도록 구성된 코드입니다.
public class TodoDAO {
public String getTime(){
String now=null;
try (Connection connection=ConnectionUtil.INSTANCE.getConnection();
PreparedStatement preparedStatement=connection.prepareStatement("select now()");
ResultSet resultSet=preparedStatement.executeQuery();
){
resultSet.next();
now=resultSet.getString(1);
}catch(Exception e){
e.printStackTrace();
}
return now;
}
}
ConnectionUtil을 사용하는 코드를 추가했습니다.
public class todoDAOTests {
private TodoDAO todoDAO;
@BeforeEach
public void ready(){
todoDAO=new TodoDAO();
}
@Test
public void testTime() throws Exception{
System.out.println(todoDAO.getTime());
}
}
@BeforeDach를 이용하는 ready()를 통해서 모든 테스트 전에 TodoDAO타입의 객체를 생성하도록 하고 testTime()을 이용해서 getTime()이 작동하는지 확인합니다.
Lomobk의 @Cleanup
@Cleanup이 추가된 변수는 해당 메소드가 끝날 때 close()가 호출되는 것을 보장합니다. Lombok라이브러리에 종속적인 코드를 작성하게 되지만 close()가 보장되는 장점이 있습니다.
(3) TodoDAO 등록 기능 구현
public void insert(TodoVO vo) throws Exception{
String sql="insert into tbl_todo(title,dueDate,finished) values(?,?,?)";
@Cleanup Connection connection=ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setString(1,vo.getTitle());
preparedStatement.setDate(2, Date.valueOf(vo.getDueDate()));
preparedStatement.setBoolean(3, vo.isFinished());
preparedStatement.executeUpdate();
}
파라미터로 입력된 TodoVO객체의 정보를 이용해서 DML을 실행하기 때문에 executeUpdate()를 실행합니다.
PreparedStatement는 ?를 이용해서 나중에 전달할 데이터를 지정하는데 setXXX를 이용해서 값을 지정합니다.
@Test
public void testInsert() throws Exception{
TodoVo todoVo=TodoVo.builder()
.title("Sample Title")
.dueDate(LocalDate.of(2025,1,22))
.build();
todoDAO.insert(todoVo);
}
TodoVO에 선언한 빌더를 어떻게 사용하는지 보여줍니다. 실행 후 tbl_todo 테이블에 새로운 데이터가 추가되었는지 확인합니다.
public List<TodoVO> selectAll() throws Exception{
String sql = "select * from tbl_todo";
@Cleanup Connection connection=ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement preparedStatement=connection.prepareStatement(sql);
@Cleanup ResultSet resultSet=preparedStatement.executeQuery();
List<TodoVO> list= new ArrayList<>();
while(resultSet.next()){
TodoVO vo= TodoVO.builder()
.tno(resultSet.getLong("tno"))
.title(resultSet.getString("title"))
.dueDate(resultSet.getDate("dueDate").toLocalDate())
.finished(resultSet.getBoolean("finished"))
.build();
list.add(vo);
}
return list;
}
ResultSet으로 각행을 이동하면서 각 행의 데이터를 TodoVo로 변환합니다.
@Test
public void testList() throws Exception{
List<TodoVO> list =todoDAO.selectAll();
list.forEach(vo->System.out.println(v0));
}
등록과 마찬가지로 테스트합니다.
(5) TodoDAO의 조회 기능
public TodoVO selectOne(Long tno) throws Exception{
String sql="select * from tbl_todo where tno=?";
@Cleanup Connection connection=ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setLong(1, tno);
@Cleanup ResultSet resultSet=preparedStatement.executeQuery();
resultSet.next();
TodoVO vo=TodoVO.builder()
.tno(resultSet.getLong("tno"))
.title(resultSet.getString("title"))
.dueDate(resultSet.getDate("dueDate").toLocalDate())
.finished(resultSet.getBoolean("finished"))
.build();
return vo;
}
여러 데이터가 나오는 selectAll()과 달리 selectOne()은 한 행의 데이터만 나오기 때문에 while 없이 resultSet.net()만 실행하면 됩니다.
@Test
public void testSelect() throws Exception{
Long tno=1L;
TodoVO vo= todoDAO.selectOne(tno);
System.out.println(vo);
}
selectOne() 기능의 동작을 확인합니다.
(6) 삭제/수정
public void deleteOne(Long tno) throws Exception{
String sql="delete from tbl_todo where tno=?";
@Cleanup Connection connection=ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setLong(1, tno);
preparedStatement.executeUpdate();
}
삭제는 조회와 비슷하지만 쿼리가 아니라는 점이 다릅니다. 특정한 번호가 필요합니다.
public void updateOne(TodoVO todoVO) throws Exception{
String sql="update tbl_todo set title=?, dueDate=?, finished=? where tno=?";
@Cleanup Connection connection=ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement preparedStatement=connection.prepareStatement(sql);
preparedStatement.setString(1,todoVO,getTitle());
preparedStatement.setDate(2,Date.valueOf(todoVO.getDueDate()));
preparedStatement.setBoolean(3,todoVO.isFinished());
preparedStatement.setLong(4,todoVO.getTno());
preparedStatement.executeUpdate();
}
수정 기능은 특정한 번호를 가진 데이터의 제목과 만료일, 완료여부를 update하도록 구성해야합니다.
updateOne()은 파라미터로 모든 정보가 담겨있는 TodoVO를 받아서 executeUpdate를 실행합니다.
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoDTO {
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
}
@DAta는 getter/setter/toString/equals/hashCode 등을 모두 컴파일할 때 생성해줍니다.
public enum MapperUtil {
INSTANCE;
private ModelMapper modelMapper;
MapperUtil() {
this.modelMapper = new ModelMapper();
this.modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies. STRICT);
}
public ModelMapper get(){
return modelMapper;
}
}
(7) TodoService와 ModelMapper 테스트
DTO와 VO를 둘 다 이용해야 하는 TodoService를 구성하고 ModelMapper의 동작을 확인합니다.
public enum TodoService {
INSTANCE;
private TodoDAO dao;
private ModelMapper modelMapper;
TodoService(){
dao=new TodoDAO();
modelMapper = MapperUtil.INSTANCE.get();
}
public void register(TodoDTO todoDTO)throws Exception{
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
System.out.println("todoVO"+ todoVO);
dao.insert(todoVO);
}
}
register은 TodoDTO 파라미터를 받아서 TodoVO로 변환하는 과정이 필요합니다. 이를 확인하기 위해서 ModelMapper로 처리된 TodoVO를 출력을 이용해 확인중입니다.
Log4j2와 Log4j2
프로젝트를 개발하다 보면 많은 System.out.println()을 이용하게 됩니다. 문제는 개발이 끝난고 난 후에는 대부분의 System.out.println()은 필요없는 경우가 많아 코드상에서 해당 부분을 모두 삭제하거나 주석 처리를 해야 하는 경우가 많습니다. 로그(log)기능은 이러한 문제를 해결하기 위한 기능입니다.
Log4j2는 레벨이라는 설정이 있어 개발할 때 필요한 레벨의 로그와 실제 운영 시에 필요한 로그를 쉽게 구분할 수 있습니다. Log4j2에서 가장 핵심적인 개념은 로그의 레벨과 어펜더입니다. 어펜더(Appender)는 로그를 어떤 방식으로 기록할 것인지를 의미하는데 콘솔창에 출력할 것인지, 파일로 출력할 것인지 등을 결정합니다. 레벨(level)은 로그의 '중요도'개념입니다. 로그의 레벨을 지정하면 해당 레벨 이상의 로그들만 출력되기 때문에 개발할 때는 로그의 레벨을 많이 낮게 설정하고 운영할 때는 중요한 로그들만 기록하게 설정합니다.
'자바웹개발 워크북' 카테고리의 다른 글
자바웹개발 워크북(6) (1) | 2025.01.20 |
---|---|
자바웹개발 워크북(5) (0) | 2025.01.16 |
자바웹개발 워크북(4) (0) | 2025.01.05 |
자바웹개발 워크북(3) (0) | 2025.01.02 |
자바웹개발 워크북(1) (1) | 2024.12.30 |