ORM(Object-Relational Mapping)
ORM이란 말그대로 객체와 관계형 데이터베이스를 직접 매핑해 사용하는 전략을 말한다. Spring을 이용하면 @Entity를 이용해 객체와 실제 RDB의 테이블을 매핑해 개발하는데 이러한 방식이 ORM 전략을 따르는 것이다. 자바에서는 이러한 ORM의 표준 명세로 JPA(Java Persistance API)를 사용하고 있다.
JPA(Java Persistance API)
JPA는 위에서 설명한바와 같이 자바에서 제공하는 인터페이스로 어떤 기능을 제공하는 라이브러리가 아니다. JPA는 ORM 전략을 따르는 자바 어플리케이션에서 관계형 데이터베이스를 어떻게 사용해야 하는지에 대해 정의한 기술 명세서이다.
JPA와 같이 Hibernate에 대해서도 많이 들어보았을텐데 JPA라는 인터페이스를 구현한 오픈소스가 Hibernate이다. Spring 프로젝트를 처음 생성하고 Gradle이 가져온 라이브러리를 보면 Hibernate가 포함되어 있다. 그렇지만, Spring에서 기본적으로 Hibernate를 JPA의 구현체로 사용하고 있는 것이지 JPA의 구현체로는 DataNucleus, EclipseLink 등 여러가지가 있다고 한다.
예를 들면, JPA를 공부하기 위해 EntityManager를 이용하는데 보통 @Autowired로 EntityManager를 주입받아 사용한다. Spring에서 기본적으로 Hibernate를 이용하기 때문에 Hibernate가 구현한 EntityManager가 주입될 것이다. EntityManager에 다른 JPA 구현체를 이용한다면 아래와 같은 개념이지 않을까 싶다.
//보통 사용하는 방식
@Autowired EntityManager em;
//이런식의 개념이지 않을까 싶습니다.
EntityManager em = Hibernate.EntityManager();
EntityManager em = EclipseLink.EntityManager();
EntityManager em = DataNucleus.EntityManager();
마지막으로, Spring에서 이러한 JPA의 구현체들을 사용하기 쉽게 모듈화 해놓은 것이 바로 Spring Data JPA이다. 간략하게 ORM과 JPA에 대해 정리하였으니 이제 본격적으로 ORM 개념이 탄생하게된 배경에 대해 쭉 알아보자.
JDBC(Java Database Connectivity)
JDBC는 세상에 데이터베이스 기술이 탄생한 후 자바에서 자바 어플리케이션과 데이터베이스를 연결하기위해 만든 초창기 기술이다. 1997년에 자바SE에 올라왔고 JPA의 근간이 되는, 아직까지도 사용하고 있는 기술이다. 자바에서는 JDBC의 사용을 위해 MySQL, OracleDB 등 데이터베이스와 연동하기 위한 Driver들을 만들어뒀다.
JDBC DriverManager는 크게 Connection, Statement, ResultSet(테이블 형태의 데이터 객체) 3가지 객체를 이용해 동작한다. Connection 객체를 만들어 DB에 연결해 쿼리를 요청할 수 있는 상태(Statement)를 만들고 Statement 객체를 이용해 쿼리를 요청하고, ResultSet 객체를 이용해 쿼리의 결과를 받아온다. 기본적인 흐름은 아래 그림과 같다.
아래는 JDBC DriverManager를 이용해 DB에 SELECT 쿼리에 대한 응답을 요청하는 코드이다. 개발하면서 쿼리를 짜는게 힘들다고 생각했는데, 모든 요청에 대해 이렇게 연결을 만들고 쿼리를 하나씩 짜는 방식에 비하면 정말 행복한 세상에 살고 있다는 것을 느꼈다.
//setting dependency on build.gradle
implementation 'mysql:mysql-connector-java:8.0.28'
//url example for Connection
"jdbc:{database}://{host}:{port}/schema"
-> "jdbc:mysql://localhost:3306/thinkingpotato"
// JDBC 관련 변수 선언
String url = "jdbc:mysql://localhost:3306/thinkingpotato";
String username = {username};
String password = {password};
// 1.DriverManager를 이용해 Connection 가져오기
// - username과 password를 설정하지 않았다면 url만 매개변수로 전달
Connection connection = DriverManager.getConnection(url, username, password);
// 2.Query 생성
String query = "select * from {table}";
// 3.Connection과 query 변수를 이용해 Statement객체 만들고 쿼리 요청
// - statement 요청을 보낼때 execute(), executeQuery(), executeUpdate() 등 상황에 따라 사용할 수 있는 메서드가 많다. select할때는 executeQuery() 이용하는듯.
PrepareStatement statement = connection.prepareStatement(query);
ResultSet result = statement.executeQuery();
// 4.ResultSet 결과값 출력
// - 아래와 같이 값을 읽어오면 된다. result.next()로 row를 한개씩 불러온다.
while(result.next()) {
System.out.println(result.getString("username"));
}
// 4.JDBC 연결 해제
statement.close();
connection.close();
정리하면서 한가지 의문이 들었던게 DriverManager에게 어떤 DB를 사용할 것인지 알려주지도 않았고 Driver를 정의해두지도 않았는데 어떻게 사용하는 데이터베이스를 인식하고 동작하는지 궁금했다. 정답은 url을 이용하는 것이었다. 아래 사진은 DriverManager의 getConnection() 메서드인데, 미리 정의해둔 registerdDrivers List를 가져와 url의 앞부분에 명시된 데이터베이스 유형을 보고 Driver를 선택하는 방식으로 동작한다.
SQLMapper - JDBCTemplate과 MyBatis
SQLMapper는 SQL문과 객체를 매핑하여 데이터를 객체화한다는 개념이다. SQLMapper가 탄생한 이유는 기존 JDBC로 직접 SQL을 작성했을때의 문제점을 보완하기 위해서이다. 문제점으로는 쿼리를 요청할때마다 중복 코드를 발생시키고 Connection, Statement 등 자원의 관리를 따로 해줘야하는 불편함이 있다. SQLMapper는 ORM에 포함되는 개념은 아니지만, ORM의 출발점이지 않을까 싶다.
JDBCTemplate
자바에서는 SQLMapper로 JDBCTemplate와 MyBatis가 있고 JDBCTemplate가 먼저 탄생했다. JDBCTemplate은 쿼리의 결과와 객체의 필드를 매핑하고, RowMapper라는 인터페이스를 이용해 결과를 받아온다. 또한, 전부는 아니지만 기존 JDBC방식의 Connection, Statement, ResultSet의 관리와 반복적인 처리를 대신해준다. 결과값을 객체에 매핑하는데에 여전히 많은 코드가 필요하긴 하지만 귀찮은 과정을 JDBCTemplate가 대신 처리해준다는 점에서 이점이 있는 것으로 보인다. 기본적인 흐름은 아래와 같다.
아래는 JDBCTemplate를 이용해 DB에 SELECT 쿼리에 대한 응답을 요청하는 코드이다. JDBCTemplate을 이용하기 위해 Gradle에 의존성을 추가해주고 앞에서 언급한 RowMapper의 구현체를 선언해 응답을 받아온다. JDBCTemplate 코드에서는 DB url이 없어졌는데 .properties에 DB연결에 대한 정보를 저장하고 사용하면 된다.
//Setting dependency on build.gradle
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
public class PotatoRowMapper implements RowMapper<List<PotatoVO>> {
@Override
public List<PotatoVO> mapRow(ResultSet rs, int rowNum) throws SQLException {
List<PotatoVO> potatoes = new ArrayList<>();
while(rs.next()) {
PotatoVO potato = new PotatoVO();
potato.setId(...);
...
potatoes.add(potato);
}
return potatoes;
}
}
// 1. Autowired를 이용해 DataSource 주입
@Autowired
private JdbcTemplate;
// 2. 조회 Query 생성
String query = "select * from {table}";
// 3. jdbcTemplate를 이용해 조회 요청 - RowMapper를 이용해 반환값을 받아온다.
List<PotatoVO> potatoes = jdbcTemplate.queryForObject(query, new PotatoRowMapper());
// 4. RowMapper를 이용해 받아온 List형태의 결과값 출력
for(PotatoVO potato : potatoes) {
System.out.println(potato.getId());
}
RowMapper를 하나씩 선언해야한다는 부분에서 결국 반복적으로 코드를 짜야하고 JDBC를 직접 이용하는 방법과 코드의 양은 크게 달라진게 없는 것 같지만, 코드가 훨씬 깔끔해졌고 JDBCTemplate를 이용함으로써 어느정도 개발 비용이 줄었을 것으로 예상된다.
MyBatis
JDBCTemplate 이후의 SQLMapper로 MyBatis가 등장했다. MyBatis 또한 반복적인 코드를 단순화 시키는 것이 목표였고 문자열로 직접 선언하던 쿼리를 XML파일과 어노테이션을 이용해 코드와 분리하였다. MyBatis를 사용하는 방법은 두가지가 있는데 XML파일에 쿼리를 작성해놓고 Mapper 어노테이션을 활용한 인터페이스를 이용해 XML파일의 쿼리를 호출하는 방법과 Insert, Select 등의 어노테이션을 이용해 @Mapper가 적용된 인터페이스에서 쿼리를 직접 전달하는 방식이 있다. 우선, XML을 이용하는 방법부터 코드로 알아보자.
먼저 MyBatis를 이용하기 위한 Gradle에 의존성을 추가한다. 다음으로 XML파일을 생성하고 아래와 같이 이용할 쿼리문을 정의하는 방식으로 이용한다. 아래 XML파일은 resoureces 경로에 mappings 디렉토리를 만들어 선언하였다.
//Setting dependency on build.gradle
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.0'
<?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">
<!-- MyBatis기반 Query저장을 위한 XML파일 -->
<mapper namespace="org.example.thinkingpotato.PotatoMapper">
//쿼리를 이용할 함수이름과 응답-객체 매핑
<select id="selectPotato" resultType="org.example.thinkingpotato.vo.PotatoVO">
SELECT *
FROM POTATO
WHERE id = #{id};
<!-- id값은 Mapper 인터페이스의 @Param을 이용해 전달한다-->
</select>
//쿼리를 이용할 함수이름과 요청-객체 매핑
<insert id="insertPotato" parameterType="org.example.thinkingpotato.vo.PotatoVO">
INSERT INTO POTATO(name)
VALUES (#{name});
</insert>
</mapper>
다음으로는 MyBatis를 이용하기 위해서는 DBConfiguration을 등록해줘야한다. 아래와 같이 @Bean을 등록해서 사용하는데 자세하게 뜯어보기에는 MyBatis를 제대로 배우기 위해 작성하는 글이니 생략하자. 이부분을 보고 느낀게 당연한 것이지만 초기 기술의 문제를 해결해 나갈수록 결국 기술의 복잡도도 커진다는 생각을 했다. 기술의 보이는 부분만 보는 것이아니라 내가 사용하는 기술을 제대로 알고 사용하기 위해서는 기능의 발전 과정과 복잡한 매커니즘을 이해하는 것이 필요하다는 생각이 든다.
@Configuration
@MapperScan(basePackages = "org.example.thinkingpotato.mapper.*")
@EnableTransactionManagement
public class DBConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
final SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapping xml 파일 위치를 bean mapper location 에 등록
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath:mappings/*.xml"));
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
XML파일을 이용해 쿼리를 작성했으면 @Mapper를 이용한 인터페이스를 선언해 사용할 수 있다. XML파일에 정의한 id값과 메서드 이름을 맞춰주고 결과, 요청타입을 선언해 이용한다. 아래 코드를 보면 Mapper를 주입받아 사용하는 것을 볼 수 있는데 코드가 매우 간결해졌다. MyBatis를 이용하기 위해 많은 코드를 작성해야하는 것은 맞지만, 서비스 레벨에서 함수 하나로 쿼리를 요청할 수 있게된 것이다. 코드를 자세히 보면 JPARepository와 비슷한 부분을 볼 수 있는데 앞에서 SQLMapper의 개념은 ORM은 아니지만 ORM의 출발점이라고 말한 이유가 여기에 있다.
//XML을 이용한 ByBatis
@Mapper
public interface PotatoMapper {
//AccountMapper.xml의 id=selectPotato인 쿼리를 호출한다.
PotatoVO selectPotato(@Param("id") int id);
//AccountMapper.xml의 id=insertPotato인 쿼리를 호출한다.
void insertPotato(PotatoVO vo);
}
@Autowired
PotatoMapper potatoMapper;
//삽입
potatoMapper.insertPotato(new PotatoVO("potatoname"));
//조회
var potato = potatoMapper.selectPotato(1);
여기까지가 ORM의 탄생 전 과정이다. SQLMapper의 DB의존성 및 중복 쿼리 문제로 ORM이 탄생했다고 한다. ORM이 등장하면서 데이터베이스의 주도권을 뺏어왔다고해도 과언이 아니라고하며, 앞서 살펴본 기술들은 직접 쿼리를 요청하거나 Mapper를 이용해 조작하는 방식이었다면 ORM부터는 테이블을 하나의 객체를 매핑시키는 것으로 완전히 새로운 패러다임을 제시했다.
JDBC -> JDBCTemplate -> MyBatis 순서로 자바의 ORM 표준인 JPA가 등장하기 전 데이터베이스 이용 기술 발전에 대해서 다뤄보았다. 글이 길어져서 뒤로 갈수록 힘이 좀 빠졌는데 그래도 만족스럽게 잘 정리한 것 같다. 다음 글로는 ORM, JPA의 자세한 개념과 SpringDataJPA에 대한 내용을 정리해보자.
[참고자료]
https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/