이전까지는 웹쪽을 중점으로 리팩토링을 해왔다.
이번에는 데이터베이스 접근 관련해서 리팩토링을 진행해보도록하겠다.
✅ 기존 구조에서의 데이터 베이스 접근 방식의 단점(순수 JDBC)
현재 구조에서는 순수한 JDBC를 사용해서 MySQL에 접근했다.
그러다보니 여러 가지 불편한 점이 있었다.
1️⃣ SQL 쿼리 작성의 어려움
우선 순수 JDBC의 경우 String을 통해 쿼리를 작성해야 했다.
@Override
public int update(Board board) {
int result = 0;
try {
String sql = "UPDATE board SET ";
if(board.getTitle()!=null && board.getTitle().length()>0){
sql += " TITLE='"+board.getTitle()+"', ";
}
if(board.getWriter()!=null && board.getWriter().length()>0){
sql += " WRITER='"+board.getWriter()+"', ";
}
if(board.getContent()!=null && board.getContent().length()>0){
sql += " CONTENT='"+board.getContent()+"', ";
}
if(board.getReadCount() != null && board.getReadCount() > 0)
sql += " READ_COUNT='"+board.getReadCount()+"', ";
sql = sql.substring(0, sql.length()-2);
sql += " WHERE id = "+board.getId();
conn = DBUtil.getConnection();
ps = conn.prepareStatement(sql);
result = ps.executeUpdate();
}catch (SQLException ex){
throw new RuntimeException(ex);
}finally {
DBUtil.close(ps, conn);
}
return result;
}
이렇게 쿼리를 동적으로 만들어야 하는 경우 코드가 매우 복잡해진다.
2️⃣ 커넥션 관리
순수 JDBC를 사용하면 개발자가 직접 Connection을 관리해야 했다.
직접 Conneciton을 생성하고 닫아줘야 했다.
public static Connection getConnection() throws SQLException {
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
}catch (SQLException e) {
System.out.println("커넥션 생성 오류");
e.printStackTrace();
}
return conn;
}
public static void close(AutoCloseable... closeables){
for(AutoCloseable closeable : closeables){
if(closeable != null){
try {
closeable.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
3️⃣ 객체 매핑의 불편함
JDBC에서 객체 단위로 데이터를 불러오려면 일일이 객체에 해당 값들을 ResultSet을 통해 매핑을 시켜줘야 했었다.
@Override
public Optional<Board> selectOne(Long id) {
Board board = null;
try {
String sql = "SELECT id, title, writer,content, read_count, created_at FROM board WHERE id=?";
conn = DBUtil.getConnection();
ps = conn.prepareStatement(sql);
ps.setLong(1, id);
rs = ps.executeQuery();
if(rs.next()){ // 글번호 이상하면 없을수는 있음.
board = makeBoardDTO(rs);
}
}catch (SQLException ex){
throw new RuntimeException(ex);
}finally {
DBUtil.close(rs, ps, conn);
}
return Optional.ofNullable(board);
}
// 데이터베이스에서 데이터를 받아와 객체의 필드 값에 하나하나 매핑
private Board makeBoardDTO(ResultSet rs) throws SQLException {
Board boardDTO = new Board();
boardDTO.setId(rs.getLong("id"));
boardDTO.setTitle(rs.getString("title"));
boardDTO.setWriter(rs.getString("writer"));
boardDTO.setContent(rs.getString("content"));
boardDTO.setReadCount(rs.getInt("read_count"));
boardDTO.setCreatedAt(rs.getString("created_at"));
return boardDTO;
}
이러한 문제점을 해결해주는 것이 MyBatis이다.
✅ MyBatis란?
<MyBatis 공식 문서>
개발자가 지정한 SQL, 저장프로시저 그리고 몇가지 고급 매핑을 지원하는 퍼시스턴스 프레임워크이다. 마이바티스는 JDBC로 처리하는 상당부분의 코드와 파라미터 설정및 결과 매핑을 대신해준다. 마이바티스는 데이터베이스 레코드에 원시타입과 Map 인터페이스 그리고 자바 POJO 를 설정해서 매핑하기 위해 XML과 애노테이션을 사용할 수 있다.
💡 쉽게 말해서 JDBC를 쉽게 사용할 수 있게 도와주는 프레임워크라고 보면 된다.
xml로 쿼리를 작성하는 것이 가장 큰 특징이다.
MyBatis 동작 방식
별도의 xml 파일에 sql을 작성하는데 매우 편리하게 작성이 가능하다. 앞서 JDBC를 사용하면서 느꼈던 모든 불편함을 해결해준다.
✅ MyBatis 의존성 추가하기
새롭게 추가한 의존성은 다음과 같다.
1️⃣ mybatis
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
2️⃣ mybatis - spring
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
3이상의 버전을 사용하자
3️⃣ spring - jdbc
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.1.8</version>
</dependency>
4️⃣ (선택) 커넥션 풀
커넥션 풀은 쉽게말하면 데이터베이스 커넥션을 관리해주는 라이브러리이다.
당장 테스트 환경에서는 크게 필요가 없지만 추후에 정말로 서비스를 하게 된다면 도입해야하는 라이브러리이다.
수 많은 라이브러리가 있지만 그중에서도 dbcp2를 사용했다.
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
</dependency>
✅ MyBatis 관련 객체들 빈으로 등록하기
Spring을 사용하지 않고도 MyBatis를 사용할 수 있긴 하지만 Spring을 사용하면 더 쉽게 설정이 가능하다.
MyBatis 설정 정보를 Spring Bean으로 등록하는 방법은 크게 두 가지가 있다.
xml 파일 방식과 자바 클래스 방식이다.
1️⃣ XML로 등록하기
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 데이터베이스 커넥션 관리 (POOL)-->
<bean id="basicDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/simple_board_project"/>
<property name="username" value="#{systemEnvironment['DB_USER']}"/>
<property name="password" value="#{systemEnvironment['DB_PASSWORD']}"/>
</bean>
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="basicDataSource"/>
<property name="mapperLocations" value="classpath:/mapper/*.xml"/>
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true"/>
</bean>
</property>
</bean>
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.example.board.mvc.repository"/>
</bean>
</beans>
하나하나 세부적으로 알아보자.
(1) 데이터베이스 커넥션 풀 등록
<bean id="basicDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/simple_board_project"/>
<property name="username" value="#{systemEnvironment['DB_USER']}"/>
<property name="password" value="#{systemEnvironment['DB_PASSWORD']}"/>
</bean>
외부에 데이터베이스와의 커넥션을 커넥션 풀을 통해 생성하기 위해 등록하는 정보이다.
앞서 dbcp2를 의존성에 추가해줬는데 그걸 사용하는 부분이다.
참고로 xml에서 시스템 상에 저장된 환경변수를 사용하려면 #{환경변수명}으로 하면 된다
(2) MyBatis 기본 설정 정보 등록
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="basicDataSource"/>
<property name="mapperLocations" value="classpath:/mapper/*.xml"/>
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true"/>
</bean>
</property>
</bean>
- <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
- SqlSessionFactoryBean은 MyBatis에 대한 다양한 설정정보를 담고 있는 클래스이다.
- <property name="dataSource" ref="basicDataSource"/>
- 앞서 등록한 데이터 소스 풀을 등록한다.
- <property name="mapperLocations" value="classpath:/mapper/*.xml"/>
- 앞서 작성한 mapper xml 파일의 위치를 지정한다.
- <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property>
- 데이터베이스의 컬럼과 데이터베이스 필드의 변수 명명 컨벤션의 차이를 자동으로 변환시켜줌 데이터베이스에선 snake_case(ex - member_id) 객체에선 camelCase(ex - memberId)
(3) 매핑 인터페이스 패키지 지정
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.example.board.mvc.repository"/>
</bean>
- mapper xml과 매핑될 인터페이스의 위치를 지정한다.
2️⃣ Java 클래스로 등록하기
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
public class AppConfig {
@Bean
public DataSource basicDataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/simple_board_project");
dataSource.setUsername(System.getenv("DB_USER")); // 환경변수에서 사용자명 가져오기
dataSource.setPassword(System.getenv("DB_PASSWORD")); // 환경변수에서 비밀번호 가져오기
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer scannerConfigurer = new MapperScannerConfigurer();
scannerConfigurer.setBasePackage("org.example.board.mvc.repository");
return scannerConfigurer;
}
}
✅ 리포지토리 인터페이스를 매핑시켜줄 xml파일 작성
💡 xml파일은 src>main>resources폴더 아래에 만들어야 한다. 그래야 MyBatis가 인식한다.
1️⃣ BoardRepository.java ↔ BoardRepository.xml
BoardRepository.java
public interface BoardRepository {
int insert(@Param("board") Board board);
int update(Board board);
int delete(Long id);
List<Board> selectAll();
Optional<Board> selectOne(Long id);
}
BoardRepository.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.board.mvc.repository.BoardRepository">
<!-- insert 메서드에 대한 매핑 -->
<insert id="insert" parameterType="org.example.board.domain.Board">
INSERT INTO board (title, writer, content)
VALUES (#{title}, #{writer}, #{content});
</insert>
<!-- update 메서드에 대한 매핑 -->
<update id="update" parameterType="org.example.board.domain.Board">
UPDATE board
<set>
<if test="title != null and title.length() > 0">title = #{title},</if>
<if test="writer != null and writer.length() > 0">writer = #{writer},</if>
<if test="content != null and content.length() > 0">content = #{content},</if>
<if test="readCount != null">read_count = #{readCount},</if>
</set>
WHERE id = #{id};
</update>
<!-- delete 메서드에 대한 매핑 -->
<delete id="delete" parameterType="long">
DELETE FROM board WHERE id = #{id};
</delete>
<!-- selectAll 메서드에 대한 매핑 -->
<select id="selectAll" resultType="org.example.board.domain.Board">
SELECT id, title, writer, content, read_count, created_at
FROM board;
</select>
<!-- selectOne 메서드에 대한 매핑 -->
<select id="selectOne" parameterType="long" resultMap="rrr">
SELECT id, title, writer, content, read_count, created_at
FROM board
WHERE id = #{id};
</select>
<resultMap id="rrr" type="org.example.board.domain.Board">
<result property="readCount" column="read_count" />
<result property="createdAt" column="created_at"/>
</resultMap>
</mapper>
2️⃣ MemberRepository.java ↔ MemberRepository.xml
MemberRepository.java
public interface MemberRepository {
int insert(Member member);
List<Member> findAll();
Optional<Member> findById(Long id);
Optional<Member> findByMemberId(String memberId);
}
MemberRepository.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.board.mvc.repository.MemberRepository">
<!-- insert 메서드에 대응하는 SQL -->
<insert id="insert" parameterType="org.example.board.domain.Member">
INSERT INTO member (member_id, password)
VALUES (#{memberId}, #{password})
</insert>
<!-- findAll 메서드에 대응하는 SQL -->
<select id="findAll" resultType="org.example.board.domain.Member">
SELECT id, member_id, password
FROM member
</select>
<!-- findById 메서드에 대응하는 SQL -->
<select id="findById" parameterType="long" resultType="org.example.board.domain.Member">
SELECT id, member_id, password
FROM member
WHERE id = #{id}
</select>
<!-- findByMemberId 메서드에 대응하는 SQL -->
<select id="findByMemberId" parameterType="string" resultType="org.example.board.domain.Member">
SELECT id, member_id, password
FROM member
WHERE member_id = #{memberId}
</select>
</mapper>
✅ 코드로 보는 MyBatis의 장점(순수 JDBC의 단점 극복)
1️⃣ SQL 쿼리 작성이 쉬워짐
MyBatis를 사용해보면 가장 처음 느껴지는 장점이 쿼리 작성이 매우 직관적이고 쉽다는 것이다.
특히 순수 JDBC의 가장 큰 단점이었던 동적쿼리 문제도 비교적 매우 간결하게 작성할 수 있게 됐다.
int update(Board board);
순수 JDBC
@Override
public int update(Board board) {
int result = 0;
try {
String sql = "UPDATE board SET ";
if(board.getTitle()!=null && board.getTitle().length()>0){
sql += " TITLE='"+board.getTitle()+"', ";
}
if(board.getWriter()!=null && board.getWriter().length()>0){
sql += " WRITER='"+board.getWriter()+"', ";
}
if(board.getContent()!=null && board.getContent().length()>0){
sql += " CONTENT='"+board.getContent()+"', ";
}
if(board.getReadCount() != null && board.getReadCount() > 0)
sql += " READ_COUNT='"+board.getReadCount()+"', ";
sql = sql.substring(0, sql.length()-2);
sql += " WHERE id = "+board.getId();
conn = DBUtil.getConnection();
ps = conn.prepareStatement(sql);
result = ps.executeUpdate();
}catch (SQLException ex){
throw new RuntimeException(ex);
}finally {
DBUtil.close(ps, conn);
}
return result;
}
MyBatis
<!-- update 메서드에 대한 매핑 -->
<update id="update" parameterType="org.example.board.domain.Board">
UPDATE board
<set>
<if test="title != null and title.length() > 0">title = #{title},</if>
<if test="writer != null and writer.length() > 0">writer = #{writer},</if>
<if test="content != null and content.length() > 0">content = #{content},</if>
<if test="readCount != null">read_count = #{readCount},</if>
</set>
WHERE id = #{id};
</update>
2️⃣ 더이상 커넥션을 열고 닫는 것을 신경쓰지 않아도 됨
순수 JDBC
public static Connection getConnection() throws SQLException {
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
}catch (SQLException e) {
System.out.println("커넥션 생성 오류");
e.printStackTrace();
}
return conn;
}
public static void close(AutoCloseable... closeables){
for(AutoCloseable closeable : closeables){
if(closeable != null){
try {
closeable.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
이렇게 메서드를 만들고 코드에서 직접 호출해야 했음
MyBatis
<bean id="basicDataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/simple_board_project"/>
<property name="username" value="#{systemEnvironment['DB_USER']}"/>
<property name="password" value="#{systemEnvironment['DB_PASSWORD']}"/>
</bean>
MyBatis에선 설정정보에 해당 부분만 추가하면 더 이상 신경쓰지 않아도 됨
3️⃣ 객체 매핑이 편리해짐
순수 JDBC
@Override
public Optional<Board> selectOne(Long id) {
Board board = null;
try {
String sql = "SELECT id, title, writer,content, read_count, created_at FROM board WHERE id=?";
conn = DBUtil.getConnection();
ps = conn.prepareStatement(sql);
ps.setLong(1, id);
rs = ps.executeQuery();
if(rs.next()){ // 글번호 이상하면 없을수는 있음.
board = makeBoardDTO(rs);
}
}catch (SQLException ex){
throw new RuntimeException(ex);
}finally {
DBUtil.close(rs, ps, conn);
}
return Optional.ofNullable(board);
}
// 데이터베이스에서 데이터를 받아와 객체의 필드 값에 하나하나 매핑
private Board makeBoardDTO(ResultSet rs) throws SQLException {
Board boardDTO = new Board();
boardDTO.setId(rs.getLong("id"));
boardDTO.setTitle(rs.getString("title"));
boardDTO.setWriter(rs.getString("writer"));
boardDTO.setContent(rs.getString("content"));
boardDTO.setReadCount(rs.getInt("read_count"));
boardDTO.setCreatedAt(rs.getString("created_at"));
return boardDTO;
}
이전에는 이렇게 하나하나 세터 메서드를 사용해서 매핑시켜줘야 했다.
MyBatis
<!-- selectOne 메서드에 대한 매핑 -->
<select id="selectOne" parameterType="long" resultType="org.example.board.domain.Board">
SELECT id, title, writer, content, read_count, created_at
FROM board
WHERE id = #{id};
</select>
MyBatis에게 객체만 명시하면 알아서 매핑시켜준다.
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
게시판 만들기 - 기능 추가: 댓글(대댓글) 기능 추가 (0) | 2024.08.30 |
---|---|
게시판 만들기 - 기능추가: Pagination(페이지네이션) (0) | 2024.08.27 |
게시판 만들기 - 웹 계층 리팩토링(Spring MVC 도입) (0) | 2024.08.22 |
게시판 만들기 - 웹 계층 리팩토링(FrontController 도입) (0) | 2024.08.21 |
게시판 만들기 - 구현 (0) | 2024.08.20 |