JDBC, Spring JDBC, MyBatis, JPA 의 DB 접근 방식 알아보기
제일 처음 웹 개발 공부를 할 때 가장 먼저 배웠던 건 JDBC를 사용해 DB와 연결하는 방법이었다. 그 다음 MyBatis, JDBC Template을 사용해 각각 프로젝트를 만들어보며 DB에 접근하는 방법이 어떻게 다른지 공부했고 최근에는 intellij IDE와 JPA를 사용한 Spring Boot 프로젝트를 제작해 보기 전에 DB 접근 기술들을 전체적으로 정리 해보려고 한다.
JDBC
JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API로 JDBC를 통해서 DBMS의 종류에 상관없이 데이터베이스를 연결하고 작업을 처리할 수 있다.
[UserVO.java]
public class UserVO {
private String userID;
private String userName;
private String userEmail;
private String userBirth;
private String userPhone;
private String userPassword;
private int userAvailable;
private String userEmailHash;
private int userEmailChecked;
public UserVO() {}
public UserVO(String userID, String userName, String userEmail, String userBirth, String userPhone,String userPassword) {
this.userID = userID;
this.userName = userName;
this.userEmail = userEmail;
this.userBirth = userBirth;
this.userPhone = userPhone;
this.userPassword = userPassword;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getUserEmail() {
return userEmail;
}
public void setUserEmail(String userEmail) {
this.userEmail = userEmail;
}
public String getUserBirth() {
return userBirth;
}
public void setUserBirth(String userBirth) {
this.userBirth = userBirth;
}
public String getUserPhone() {
return userPhone;
}
public void setUserPhone(String userPhone) {
this.userPhone = userPhone;
}
public String getUserPassword() {
return userPassword;
}
public void setUserPassword(String userPassword) {
this.userPassword = userPassword;
}
public int getUserAvailable() {
return userAvailable;
}
public void setUserAvailable(int userAvailable) {
this.userAvailable = userAvailable;
}
public String getUserEmailHash() {
return userEmailHash;
}
public void setUserEmailHash(String userEmailHash) {
this.userEmailHash = userEmailHash;
}
public int getUserEmailChecked() {
return userEmailChecked;
}
public void setUserEmailChecked(int userEmailChecked) {
this.userEmailChecked = userEmailChecked;
}
@Override
public String toString() {
return "UserVO [userID=" + userID + ", userName=" + userName + ", userEmail=" + userEmail + ", userBirth="
+ userBirth + ", userPhone=" + userPhone + ", userPassword=" + userPassword + ", userAvailable="
+ userAvailable + ", userEmailHash=" + userEmailHash + ", userEmailChecked=" + userEmailChecked + "]";
}
}
[UserDAO.java]
public class UserDAO {
private Connection conn;
private ResultSet rs;
public UserDAO() {
try {
String dbURL = "jdbc:mysql://데이터베이스 url/ 데이터베이스 이름?useUnicode=true&characterEncoding=UTF-8";
String dbID = "아이디";
String dbPassword = "비밀번호";
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(dbURL, dbID, dbPassword);
} catch (Exception e) {
e.printStackTrace();
}
}
// 회원 가입
public int join(UserDTO user) {
try {
String SQL = "INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(SQL);
// pstmt = conn.prepareStatement(SQL);
pstmt.setString(1, user.getUserID());
pstmt.setString(2, user.getUserName());
pstmt.setString(3, user.getUserEmail());
pstmt.setString(4, user.getUserBirth());
pstmt.setString(5, user.getUserPhone());
pstmt.setString(6, hashPW);
pstmt.setInt(7, 1);
pstmt.setString(8, hashEmail);
pstmt.setBoolean(9, false);
return pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
}
위와 같이 대부분 데이터를 관리하기 위해 VO(DTO), DAO 클래스를 생성하는데 그 중 DAO 클래스에서 DB와 연결해 사용한다. JDBC API는 코드를 이해하기 쉽고 가독성이 좋다는 장점이 있지만 메서드마다 DB와 일일히 연결해 사용하기 때문에 중복된 코드가 늘어나고 Connection 관리를 계속 해줘야 한다는 단점이 있다.
이런 단점을 개선하기 위해 발전된 방법이 SQLMapper와 ORM이다.
Spring JDBC (Jdbc Template)
Spring JDBC와 MyBtis는 SQLMapper라는 공통점이 있다. 그 중 Spring JDBC는 Connection에 대한 Configuration을 JdbcTemplate 클래스에 담아 Spring을 통해 주입받는 형식의 Mapper이다. 추상화가 많이 이루어져 있어 편하게 사용할 수 있다. 따로 XML 파일을 사용하지 않는 점이 프로젝트 제작시에는 편리할 수 있지만 비슷한 SQL문을 일일히 작성해야 한다는 단점이 되기도 한다.
출처 : https://velog.io/@dyunge_100/DB-JDBC%EC%97%90-%EB%8C%80%ED%95%9C-%EC%A0%95%EB%A6%AC
[servlet-context.xml]
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.project.together" />
<!-- 데이터베이스 연결 정보를 설정하는 DriverManagerDataSource 클래스의 bean을 설정한다.-->
<beans:bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<beans:property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<beans:property name="url" value="jdbc:mysql://데이터베이스Url/데이터베이스이름"/>
<beans:property name="username" value="아이디"/>
<beans:property name="password" value="비밀번호"/>
</beans:bean>
<!-- DriverManagerDataSource 클래스의 bean을 참조해서 데이터베이스와 연결하는 JdbcTemplate 클래스의 -->
<!-- bean을 설정한다. -->
<beans:bean name="template" class="org.springframework.jdbc.core.JdbcTemplate">
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
Jdbc Template을 사용할 때 UserVO 클래스는 동일하지만 DAO클래스는 달라지는데 데이터베이스와 연결하는 JdbcTemplate 클래스의 bean을 /src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml 파일에서 설정해야 한다.
[UserDAO.java]
public class UserDAO {
private JdbcTemplate template; // 1
private static final Logger logger = LoggerFactory.getLogger(BoardDAO.class);
public UserDAO() { // 2
this.template = Constant.template;
}
//이메일 리스트를 넘겨 사용자가 입력한 이메일이 이미 존재하는지 확인
public List<String> getEmailList(){
try {
String sql = "select not null userEmail from user";
// 3
List<String> mailList = template.queryForList(sql.toString(), String.class);
return mailList;
}catch(Exception e) {
e.printStackTrace();
}
return null;
}
//회원가입
public int joinAction(UserVO vo) {
String sql = "insert into user(userID, userName, userEmail, userBirth, userPhone, userPassword, userAvailable, userEmailHash, userEmailChecked)"
+ " values(?, ?, ?, ?, ?, ?, 1, null, 0)";
template.update(sql, vo.getUserID(), vo.getUserName(), vo.getUserEmail(), vo.getUserBirth(),vo.getUserPhone(), vo.getUserPassword());
return 0;
}
//아이디 중복검사
public int joinCheckID(String userID) {
try {
String sql = "select userID from user where userID = ?";
String exist = (String)template.queryForObject(sql, new BeanPropertyRowMapper<String>(String.class), userID);
if(exist != null) {
return 1;
}
}catch(Exception e) {
e.printStackTrace();
}
return 0;
}
//로그인 실행
public int loginAction(String userID, String userPassword) {
try {
String sql = "select * from user where userID = ? and userPassword = ?";
String exist = (String)template.queryForObject(sql, new BeanPropertyRowMapper<String>(String.class), userID, userPassword);
if(exist != null) {
return 1; //회원 정보가 존재하면 return 1;
}
}catch(Exception e) {
e.printStackTrace();
}
return 0; //존재하지 않으면 return 0;
}
//회원 정보 가져오기
public UserVO getUserVO(String userID) {
try {
String sql = "select * from user where userID = ? and userAvailable = 1";
UserVO vo = template.queryForObject(sql, new BeanPropertyRowMapper<UserVO>(UserVO.class), userID);
System.out.println("userDAO 회원 정보: " +vo);
return vo;
}catch(Exception e) {
e.printStackTrace();
}
return null;
}
1) bean 설정 후 JdbcTemplate 클래스 타입의 객체를 생성한다.
2) UserDAO 클래스 안에 UserDAO()메서드는 DAO클래스의 객체(bean)가 생성되는 순간 servlet-context.xml 파일에서 생성되어 컨트롤러가 전달받아 Constant클래스의 JdbcTemplate 클래스 타입의 객체에 저장된 bean으로 초기화 시키는 역할을 한다.
3) 생성된 template의 template.query , template.queryForList, template.queryForObject, template.update 를 사용해 sql문을 인자로 전달해 DB 정보를 가져오거나 업데이트 할 수 있다.
MyBatis
MyBatis도 SQL Mapper 중 하나지만 Spring JDBC와 차이점이 있다면 위에서 말했던 것 처럼 Spring JDBC는 SQL문을 DAO 클래스에서 함께 작성하는 특징이 간단한 프로젝트에서는 장점이 될 수 있지만, 프로젝트의 크기가 커지고 코드가 길어질수록 SQL문을 메서드마다 작성해야하고 유지보수가 편리하지 않다는 단점이 있는데 그런 점을 보완할 수 있는 것이 MyBatis이다. MyBatis는 SQL문 자체를 XML 파일로 분리시키고 Java 메서드와 매핑해 반복되는 코드를 줄이기 때문에 유지보수가 편리하다는 장점이 있다.
[JdbcTemplate > servlet-context.xml ]
<beans:bean name="template" class="org.springframework.jdbc.core.JdbcTemplate">
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
[MyBatis > servlet-context.xml ]
// 1 -------------------------------------------------------------------------------
<!-- 데이터베이스 연결 정보와 데이터베이스에 연결한 후 실행할 sql 명령이 저장된 xml 파일의 경로를 -->
<!-- 기억하는 bean을 설정한다. -->
<beans:bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 데이터베이스 연결 정보를 참조시킨다. -->
<beans:property name="dataSource" ref="dataSource"></beans:property>
<!-- 실행할 sql 명령이 저장된 xml 파일의 경로를 설정한다. -->
<!-- value 속성에 xml 파일이 위치한 패키지 이름과 파일 이름을 입력한다. -->
<!-- value 속성에 지정한 패키지에 xml 파일이 없으면 fileNotFoundException이 발생된다. -->
<beans:property name="mapperLocations" value="classpath:com/project/dao/*.xml"></beans:property>
<!-- 필요하다면 typeAliases 설정 정보가 저장된 xml 파일의 경로를 설정한다. -->
<beans:property name="configLocation" value="classpath:mybatis-config.xml"></beans:property>
</beans:bean>
// 2 -------------------------------------------------------------------------------
<!-- 데이터베이스 연결 정보, 실행할 sql 명령이 저장된 xml 파일의 경로를 참조하여 mybatis mapper로 사용할 -->
<!-- bean을 설정한다. -->
<beans:bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<beans:constructor-arg index="0" ref="sqlSessionFactory"></beans:constructor-arg>
</beans:bean>
1) JdbcTemplate의 dataSource bean과 동일하게 사용하지만 사용할 xml 파일들이 위치한 경로와 VO객체를 사용하기 위한 typeAliases 설정 정보가 저장된 xml 파일의 경로를 value에 넣어 bean을 설정한다.
2) 모든 정보를 참조해 MyBatis Mapper로 사용할 bean을 설정한다.
[user.xml]
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
// !!!! 위 인터페이스의 경로를 작성한다
<mapper namespace="com.project.dao.UserMapper">
//id는 인터페이스에 선언된 메서드 이름과 동일해야한다.
<select id="getEmailList" parameterType="java.util.List" resultType="String">
select userEmail from user where userEmail is NOT NULL and userEmail != '' and userAvailable = 1
</select>
<!-- 같은 아이디가 존재하는지 검사 -->
<select id="getUserID" parameterType="String" resultType="String">
select userID = #{userID} from user where userID = #{userID} and userAvailable = 1
</select>
<!-- 회원가입 -->
<insert id="join" parameterType="userVO">
insert into user (userID, userName, userEmail, userBirth, userPhone, userPassword, userAvailable, userEmailHash, userEmailChecked)
values (#{userID}, #{userName}, #{userEmail}, #{userBirth}, #{userPhone}, #{userPassword}, 1, null, 0)
</insert>
<!-- 로그인 회원 정보 확인 -->
<select id="login" parameterType="userVO" resultType="String">
select userName from user where userID = #{userID} and userPassword = #{userPassword} and userAvailable = 1
</select>
</mapper>
[UserMapper.java]
public interface UserMapper {
// mapper로 사용되는 interface의 추상 메소드 형식은 resultType id(parameterType)와 같은 형식으로 만든다.
// UserDAO 인터페이스의 추상 메소드 이름이 xml 파일의 sql 명령을 식별하는 id로 사용되고 추상 메소드의 인수로
// 지정된 데이터가 xml 파일의 sql 명령으로 전달된다.
// sql 명령을 실행하는 xml 파일의 parameterType 속성에는 1개의 자료형만 쓸 수 있는데 아래와 같이 여러개의
// 데이터를 넘겨야 할 경우 인수로 넘어가는 데이터를 모두 멤버 변수로 가지고 있는 클래스를 사용하면 된다.
List<String> getEmailList();
int joinAction(UserVO vo);
int joinCheckID(String userID);
int loginAction(String userID, String userPassword);
}
위 예시는 UserMapper 인터페이스가 Mapper이기 때문에 namespace에 UserMapper를 작성했지만 각자 프로젝트에서 Mapper이름을 적으면 된다.
이제 컨트롤러에서 사용해보자 간단하게 list와 join sql 예시로 작성했다. 이 글에서는 DB와 연결할 때의 차이점을 보기 위해 파일 구조를 [HomeController - UserMapper - user.xml] 과 같이 간단하게 작성했지만 대부분의 MyBatis를 사용한 스프링 프로젝트는 [Controller - Servicle - ServiceImpl - DAO - Mapper - xml ] 과 같은 구조로 이루어진다.
@Controller
public class HomeController {
// servlet-context.xml 파일에서 생성한 mybatis bean(sqlSession)을 사용하기 위해서 SqlSession 인터페이스 타입의
// 객체를 선언한다.
// servlet-context.xml 파일에서 생성된 mybatis bean을 자동으로 읽어와서 SqlSession 인터페이스 타입의 객체에
// 넣어주도록 하기 위해서 @Autowired 어노테이션을 붙여준다.
@Autowired
private SqlSession sqlSession;
@RequestMapping("/")
public String home(Locale locale, Model model) {
System.out.println("컨트롤러의 home() 메소드");
return "home";
}
@RequestMapping("/list")
public String getList(HttpServletRequest request, Model model) {
System.out.println("컨트롤러의 getList() 메소드");
// !!!! mapper로 사용할 interface 객체에 userMapper를 넣어준다. !!!!
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<String> emailList = mapper.getEmailList(); //데이터베이스 실행 결과를 List에 저장한다.
model.addAttribute("list", emailList); //return 할 페이지에 결과를 넣어준다.
return "list";
}
//회원가입 페이지
@RequestMapping("/join")
public String join(HttpServletRequest request, Model model) {
System.out.println("컨트롤러의 join() 메소드");
return "join";
}
//회원가입 실행
@RequestMapping("/join/action")
public String joinAction(HttpServletRequest request, Model model) {
System.out.println("컨트롤러의 joinAction() 메소드");
// !!!! mapper로 사용할 interface 객체에 userMapper를 넣어준다. !!!!
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//HttpServletRequest 인터페이스 객체로 넘어온 join.jsp에서 사용자가 입력한 데이터를 받는다.
String userID = request.getParameter("userID");
String userName = request.getParameter("userName");
String userEmail = request.getParameter("userEmail");
String userBirth = request.getParameter("userBirth");
String userPhone = request.getParameter("userPhone");
String userPassword = request.getParameter("userPassword");
//UserVO 객체로 생성한다.
UserVO vo = new UserVO(userID, userName, userEmail, userBirth, userPhone, userPassword);
//회원정보를 저장하는 join sql을 실행한다.
mapper.join(vo);
//정상적으로 완료되면 메인페이지로 이동한다.
return "main";
}
}
JPA
JPA는 ORM(Object Relational Mapping) 기술로 대표적인 오픈 소스로는 Hibernate가 있고 CRUD 메서드를 기본적으로 제공한다. MyBatis와 같이 쿼리를 직접 만들지 않아도 되며 1차 캐싱, 쓰기지연, 변경감지, 지연로딩을 제공한다.
MyBatis는 쿼리가 수정되어 데이터 정보가 변경되면 사용 되고 있던 코드들을 함께 수정해줘야 하는 반면 JPA는 객체만 변경하면 된다. 즉 객체 중심으로 개발할 수 있지만 복잡한 쿼리는 해결하기 어렵다는 단점이 있다.
Intellij Community버전으로 Spring boot 프로젝트를 생성하려면 spring initializr를 사용해야한다.
Gradle-Groovy , Java를 선택해 프로젝트를 생성하고 intellij에서 프로젝트를 연 후 버전 2에서만 jdk1.8을 사용할 수 있기 때문에 build.gradle 파일의 pluin version을 2로 수정하고 java sourceCompatibility = 8로 수정했다.
그리고 jpa를 추가하지 않았다면 dependency를 다음과 같이 수정한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.7.5'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}
[ UserRepo .java]
테이블과 VO 클래스는 이미 생성되었다고 가정하고 JpaRepository를 상속받는 UserRepo 인터페이스를 생성한다.
package com.toogether.repo;
import com.toogether.vo.UserVO;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepo extends JpaRepository <UserVO, String> {
}
상속할 때 <엔티티 타입, 엔티티 PK 속성> 을 지정해야 한다.
[UserController.java]
User관련 요청들을 처리하는 UserController를 다음과 같이 만든다.
package com.project.controller;
import com.project.repo.UserRepo;
import com.project.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserRepo userRepo;
@PostMapping("/insert")
public UserVO create(@RequestBody UserVO user){
return userRepo.save(user);
}
@GetMapping("/{id}")
public String getUserVO(@PathVariable String id){
UserVO user = userRepo.getById(id);
//userVO.ifPresent(System.out::println);
return user.getUserName();
}
}
기존 @Controller는 페이지를 return하지만 @RestController는 데이터를 return한다.
Chrome의 Talend API Tester로 DB와 잘 연결이 됐는지 확인해보자
/user/insert 가 요청되면 create메서드가 실행되어 user 데이터값을 임의로 입력했을 때 정상적으로 데이터베이스에 저장되는지 테스트 해 보자.
이렇게 데이터베이스에 정상적으로 저장된 것을 확인할 수 있다.
/user/{id}가 요청되면 getUserVO 메서드가 실행되어 전달한 userID값과 일치하는 객체를 가져와 해당 데이터의
userName값을 반환하는지 테스트 해 보자.
위에서 저장된 데이터의 userID를 전달하면 'JSON테스트'가 반환되는 것을 확인할 수 있다.
JDBC , Spring JDBC, MyBatis, JPA 모두 사용 해봤을 때, JPA가 압도적으로 간편하고 SQL 쿼리문을 작성하지 않아서 복잡한 쿼리문이 필요한 경우를 제외하면 JPA를 사용하는게 생산성을 높일 수 있을 것 같다고 느껴졌다.
하지만 JPA를 사용하기 전에 JDBC와 MyBatis 등 데이터 매핑 방식에 대한 기본적인 지식이 있어야 JPA를 사용할 수 있지 않을까 싶다 어떤 사람이 무언갈 쉽게 한다면 그 사람은 장인이다 라는 말이 여기서도 적용되는 것 같다. 한마디로 MyBatis를 이미 사용할 줄 아는 사람이 더 간편하고 쉽게 개발하기 위해서 JPA를 사용하는 것은 좋은 방법이지만 입문자나 초보자는 JDBC와 MyBatis를 이해한 뒤에 JPA를 사용해보는게 좋을 것 같다는 개인적인 생각이다.
현재는 Spring Boot와 JPA를 공부하기 시작한지 얼마 되지 않아서 데이터베이스와 연결하는 방법의 차이점을 정리하기위해 간단하게 작성해본 코드라 알맞지 않은 부분이 많은 것 같다.
앞으로 @Autowired 의 정확한 사용법과 Spring MVC 구조에 대해 더 자세하게 공부할 예정이다. 끝 !