티스토리 뷰

이 글에서는 백엔드 시스템에서 데이터를 영속성으로 관리하기 위해 사용하는 다양한 기술을 소개하고자 한다. JDBC API, JdbcTemplate, MyBatis, Hibernate, JPA + Hibernate, Spring Data JPA 까지 총 6개의 방식으로 Task(할 일)를 CRUD 하는 과정을 비교하면서 각 기술의 차이점을 살펴보자.

아래에서 예제 코드를 확인할 수 있습니다.
https://github.com/kimjunyoung90/persistence-examples

기본적으로 데이터베이스 연동을 위해 Apache Commons DBCP를 사용하여 Connection Pool을 구성된다.

영속성이란?
영원히 계속되는 성질이나 능력. Persistence.
애플리케이션이 종료되거나 시스템이 재시작되어도 데이터가 사라지지 않고 지속적으로 보존되는 특성

공통 구성

기본적으로 HTTP 통신을 하는 백엔드 시스템을 구성하며 필요한 모듈은 아래와 같다.

- spring-mvc

- spring-jdbc

- spring-tx

- javax.servlet-api

- commons-dbcp2

- mysql-connector-java

<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-webmvc</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-jdbc</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-tx</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>  
<dependency>  
    <groupId>javax.servlet</groupId>  
    <artifactId>javax.servlet-api</artifactId>  
    <version>3.0.1</version>  
    <scope>provided</scope>  
</dependency>  
<dependency>  
    <groupId>org.apache.commons</groupId>  
    <artifactId>commons-dbcp2</artifactId>  
    <version>2.0</version>  
</dependency>  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.33</version>  
</dependency>

공통 설정

HTTP 통신을 하는 애플리케이션을 구성하기 위해 공통적으로 아래와 같이 Dispatcher Servlet을 등록하고 Servlet을 위한 Spring 설정을 진행한다.

web.xml 설정

web 폴더 하위에 web.xml과 dispatcher-servlet.xml 설정파일을 생성하고 설정을 아래와 같이 진행한다.

<?xml version="1.0" encoding="UTF-8"?>  
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"  
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"  
         version="4.0">  

    <listener>        
        <listener-class>
            org.springframework.web.context.ContextLoaderListener  
        </listener-class>  
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>  
        <param-value>classpath:applicationContext.xml</param-value>  
    </context-param>  

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>  
            <param-value>
                /WEB-INF/dispatcher-servlet.xml  
            </param-value>  
        </init-param>
    </servlet>  

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>  
        <url-pattern>/</url-pattern>  
    </servlet-mapping>  

    <filter>
        <filter-name>encodingFilter</filter-name>  
        <filter-class>
            org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>  
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>  
        </init-param>
        <init-param>
            <!-- 요청 뿐만 아니라 응답에도 인코딩 지정-->
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>  
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

DispatcherServlet Spring 설정

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
       xmlns:mvc="http://www.springframework.org/schema/mvc"  
       xmlns:context="http://www.springframework.org/schema/context"  
       xsi:schemaLocation="  
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd           http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">  

    <!-- @Controller가 붙은 클래스를 controller로 사용하도록 설정   -->  
    <mvc:annotation-driven/>  

    <!-- 컴포넌트 자동 빈 등록 -->  
    <context:component-scan base-package="com.myapp.eisenhower.controller"/>  
</beans>

container 관련 Spring 설정

resources 폴더 하위에 applicationContext.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"  
       xmlns:tx="http://www.springframework.org/schema/tx"  
       xmlns:context="http://www.springframework.org/schema/context"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">  

    <context:component-scan base-package="com.myapp.eisenhower"/>  

    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">  
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>  
        <property name="url" value="jdbc:mysql://localhost:3306/eisenhower?characterEncoding=euckr"/>  
        <property name="username" value="root"/>  
        <property name="password" value="Rlawnsdud1@"/>  
    </bean>  
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource"/>  
    </bean>  
    <tx:annotation-driven transaction-manager="transactionManager"/>  

</beans>

위 설정들을 통해 기본적인 설정은 완료되었고 이제부터 영속성을 위해 사용하는 다양한 기술들을 살펴본다.

1. JDBC API

순수 JDBC API를 사용하여 데이터베이스 관련 코드를 작성하는 예제이다. JDBC API는 JDK에 포함되어 있으며 java.sql에서 제공하는 API를 확인할 수 있다.

의존성 추가

JDBC API는 JDK에 기본적으로 포함되어 있기 때문에추가적으로 필요한 모듈은 없다.

코드 구현

JDBC API 를 사용하여 단건 조회, 다건조회, 삽입, 수정, 삭제 총 5개의 데이터 접근 로직을 작성한 예제이다.

@Repository  
public class TaskDaoImpl implements TaskDao {  

    @Autowired  
    private DataSource dataSource;  

    public Task findById(Long id) {  
        String sql = "SELECT * FROM tasks WHERE id = ?";  
        try (Connection conn = dataSource.getConnection();  
             PreparedStatement ps = conn.prepareStatement(sql)) {  

            ps.setLong(1, id);  
            try (ResultSet rs = ps.executeQuery()) {  
                if (rs.next()) {  
                    return mapRowToTask(rs);  
                }  
            }  

        } catch (SQLException e) {  
            throw new RuntimeException("Find failed", e);  
        }  
        return null;  
    }  

    public List<Task> findAllByUser(Long userId) {  
        List<Task> list = new ArrayList<>();  
        String sql = "SELECT * FROM tasks WHERE user_id = ? ORDER BY due_date";  

        try (Connection conn = dataSource.getConnection();  
             PreparedStatement ps = conn.prepareStatement(sql)) {  

            ps.setLong(1, userId);  
            try (ResultSet rs = ps.executeQuery()) {  
                while (rs.next()) {  
                    list.add(mapRowToTask(rs));  
                }  
            }  

        } catch (SQLException e) {  
            throw new RuntimeException("Find by userId failed", e);  
        }  
        return list;  
    }  

    public void insert(Task task) {  
        String sql = "INSERT INTO tasks (user_id, title, description, is_urgent, is_important, due_date, completed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())";  

        try (Connection conn = dataSource.getConnection();  
             PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {  

            ps.setLong(1, task.getUserId());  
            ps.setString(2, task.getTitle());  
            ps.setString(3, task.getDescription());  
            ps.setBoolean(4, task.isUrgent());  
            ps.setBoolean(5, task.isImportant());  
            ps.setTimestamp(6, new Timestamp(task.getDueDate().getTime()));  
            ps.setBoolean(7, task.isCompleted());  

            ps.executeUpdate();  

            try (ResultSet rs = ps.getGeneratedKeys()) {  
                if (rs.next()) {  
                    task.setId(rs.getLong(1));  
                }  
            }  

        } catch (SQLException e) {  
            throw new RuntimeException("Insert failed", e);  
        }  
    }  

    public void update(Task task) {  
        String sql = "UPDATE tasks  
            SET title = ?, description = ?, is_urgent = ?, is_important = ?, due_date = ?, completed = ?, updated_at = NOW() WHERE id = ?";  

        try (Connection conn = dataSource.getConnection();  
             PreparedStatement ps = conn.prepareStatement(sql)) {  

            ps.setString(1, task.getTitle());  
            ps.setString(2, task.getDescription());  
            ps.setBoolean(3, task.isUrgent());  
            ps.setBoolean(4, task.isImportant());  
            ps.setTimestamp(5, new Timestamp(task.getDueDate().getTime()));  
            ps.setBoolean(6, task.isCompleted());  
            ps.setLong(7, task.getId());  

            ps.executeUpdate();  

        } catch (SQLException e) {  
            throw new RuntimeException("Update failed", e);  
        }  
    }  

    public void delete(Long id) {  
        String sql = "DELETE FROM tasks WHERE id = ?";  
        try (Connection conn = dataSource.getConnection();  
             PreparedStatement ps = conn.prepareStatement(sql)) {  

            ps.setLong(1, id);  
            ps.executeUpdate();  

        } catch (SQLException e) {  
            throw new RuntimeException("Delete failed", e);  
        }  
    }  

    private Task mapRowToTask(ResultSet rs) throws SQLException {  
        Task task = new Task();  
        task.setId(rs.getLong("id"));  
        task.setUserId(rs.getLong("user_id"));  
        task.setTitle(rs.getString("title"));  
        task.setDescription(rs.getString("description"));  
        task.setUrgent(rs.getBoolean("is_urgent"));  
        task.setImportant(rs.getBoolean("is_important"));  
        task.setDueDate(rs.getTimestamp("due_date"));  
        task.setCompleted(rs.getBoolean("completed"));  
        task.setCreatedAt(rs.getTimestamp("created_at"));  
        task.setUpdatedAt(rs.getTimestamp("updated_at"));  
        return task;  
    }  
}

2. JdbcTemplate

spring-jdbc에서 제공하는 JdbcTemplate을 사용하는 예제이다. 순수 JDBC API를 사용하여 데이터 관련 코드를 작성하는 경우 반복됐던 코드들이 상당히 줄어들게 된다.

의존성 추가

spring-jdbc 모듈을 추가한다. 현재 구성으로는 트랜잭션 처리 때문에 spring-jdbc 모듈이 추가된 상태이다.

<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-jdbc</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>

관련 설정

JdbcTemplate을 사용하려면 Spring Container에 JdbcTemplate을 Bean으로 등록하고 dataSource를 속성으로 추가해야 한다. applicationContext.xml 에 설정하면 된다.

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource">
</bean>

코드 구현

JdbcTemplate을 사용하여 데이터 관련 코드를 작성한 예제이다. JDBC API를 직접 사용했을 때보다 반복적인 코드가 줄어들어 상당히 간결해 진것을 확인할 수 있다.

@Repository  
public class TaskDaoImpl implements TaskDao {  

    @Autowired  
    private JdbcTemplate jdbcTemplate;  

    @Override  
    public Task findById(Long id) {  
        String sql = "SELECT * FROM tasks WHERE id = ?";  
        return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Task.class), id);  
    }  

    @Override  
    public List<Task> findAllByUser(Long userId) {  
        String sql = "SELECT * FROM tasks WHERE user_id = ? ORDER BY due_date ASC";  
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Task.class), userId);  
    }  

    @Override  
    public void insert(Task task) {  
        String sql = "INSERT INTO tasks (user_id, title, description, is_urgent, is_important, due_date, completed) VALUES (?, ?, ?, ?, ?, ?, ?)";  
        jdbcTemplate.update(sql,  
                task.getUserId(),  
                task.getTitle(),  
                task.getDescription(),  
                task.isUrgent(),  
                task.isImportant(),  
                task.getDueDate(),  
                task.isCompleted()  
        );  
    }  

    @Override  
    public void update(Task task) {  
        String sql = "UPDATE tasks SET title = ?, description = ?, is_urgent = ?, is_important = ?, due_date = ?, completed = ? WHERE id = ?";  
        jdbcTemplate.update(sql,  
                task.getTitle(),  
                task.getDescription(),  
                task.isUrgent(),  
                task.isImportant(),  
                task.getDueDate(),  
                task.isCompleted(),  
                task.getId()  
        );  
    }  

    @Override  
    public void delete(Long id) {
        jdbcTemplate.update("DELETE FROM tasks WHERE id = ?", id);  
    }  
}

3. SQL Mapper 사용(MyBatis)

사실 실무에서 JDBC API 또는 JdbcTemplate을 사용하기 보다 영속성 프레임워크를 사용하여 데이터 관련 코드를 작성한다. 3번째로는 영속성 프레임워크 중 하나인 SQL Mapper MyBatis를 사용하여 데이터 관련 코드를 작성하는 예제이다.

의존성 추가

mybatis와 spring과 mybatis를 연결하는 mybatis-spring 모듈을 추가한다.

<dependency>  
    <groupId>org.mybatis</groupId>  
    <artifactId>mybatis</artifactId>  
    <version>3.2.3</version>  
</dependency>  
<dependency>  
    <groupId>org.mybatis</groupId>  
    <artifactId>mybatis-spring</artifactId>  
    <version>1.2.2</version>  
</dependency>

관련 설정

MyBatis를 사용하기 위해서는 SqlSessionFactoryBean을 bean으로 등록해야 한다. SqlSessionFactoryBean은 MyBatis 관련 코드를 실행하기 위한 핵심인 SqlSession 객체를 생성하여 반환해 주는 bean 이다. SqlSessionFactoryBean의 프로퍼티로 dataSource, xml Mapper 등을 등록한다.

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
    <property name="dataSource" ref="dataSource"/>  
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>  
    <property name="typeAliasesPackage" value="com.myapp.eisenhower.model"/>  
</bean>

MyBatis는 데이터베이스에서 실행할 sql을 xml 파일에 작성하여 관리한다. sql이 작성되어 있는 파일의 위치를 myBatis 설정 시 제공해야 한다.

아래는 SQL이 작성된 xml 파일과 연결된 Mapper Interface를 자동 등록하는 설정이다. 기본적으로 백엔드 시스템의 흐름이 Service -> DAO 인데, SQL Mapper를 사용하는 경우 Service -> Mapper -> sql.xml(xml Mapper)로 약간 달라지게 된다.

<!-- MyBatis Mapper XML 위치 등록 -->  
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
    <property name="basePackage" value="com.myapp.eisenhower.mapper"/>  
</bean>

코드 구현

1. Mapper Interface 생성

sql 이 작성된 xml과 연결이 되는 Mapper Interface를 생성한다. 기존에는 DAO에 SQL이 직접 작성되어 있는데 반해, SQL Mapper는 DAO 역할을 하는 Mapper Interface와 실제 실행되는 SQL이 분리되어 있어 가독성이 좋다.

public interface TaskMapper {  
    Task findById(Long id);  
    List<Task> findByUserId(Long userId);  
    void insert(Task task);  
    void update(Task task);  
    void delete(Long id);  
}

SQL이 분리되어 있어 JdbcTemplate보다 인터페이스 부분이 한결 간결해 보인다. 위 인터페이스의 메서드와 일치하는 id를 가진 SQL을 xml 파일에서 찾아 실행해준다.

<mapper namespace="com.myapp.eisenhower.mapper.TaskMapper">  

    <resultMap id="taskResultMap" type="com.myapp.eisenhower.model.Task">  
        <id property="id" column="id"/>  
        <result property="userId" column="user_id"/>  
        <result property="title" column="title"/>  
        <result property="description" column="description"/>  
        <result property="isUrgent" column="is_urgent"/>  
        <result property="isImportant" column="is_important"/>  
        <result property="dueDate" column="due_date"/>  
        <result property="completed" column="completed"/>  
        <result property="createdAt" column="created_at"/>  
        <result property="updatedAt" column="updated_at"/>  
    </resultMap> 

    <select id="findById" parameterType="long" resultMap="taskResultMap">  
        SELECT * FROM tasks WHERE id = #{id}  
    </select>  

    <select id="findByUserId" parameterType="long" resultMap="taskResultMap">  
        SELECT * FROM tasks WHERE user_id = #{userId} ORDER BY due_date ASC  
    </select>  

    <insert id="insert" parameterType="Task" useGeneratedKeys="true" keyProperty="id">  
        INSERT INTO tasks (user_id, title, description...)  
        VALUES (#{userId}, #{title}, #{description}...)    
    </insert>  

    <update id="update" parameterType="Task">  
        UPDATE tasks  
        SET title = #{title}, description = #{description} ...
        WHERE id = #{id}    
    </update>  

    <delete id="delete" parameterType="long">  
        DELETE FROM tasks WHERE id = #{id}  
    </delete>  

</mapper>

4. Hibernate

영속성 프레임워크 중 ORM 프레임워크를 사용하여 구현한 예제이다. ORM 프레임워크는 데이터 접근 영역을 Repository라고 부른다. DDD의 영향을 받아 DAO에서 이름이 변경되었다. Hibernate는 ORM 프레임워크의 표준 인터페이스인 JPA의 구현체이다. JPA 프로바이더라고 한다. JPA 인터페이스를 구현한 프로바이더는 OpenJPA, Hibernate 등 다양하게 존재하지만 가장 많이 사용하는 Hibernate를 살펴보고자 한다. 프로바이더는 프로바이더 단독으로 사용할 수도 있고 JPA + 프로바이더 조합으로 사용할 수 있다.

의존성 추가

Hibernate를 사용하기 위해서는 hibernate-core, spring-orm 모듈이 필요하다.

<dependency>  
    <groupId>org.hibernate</groupId>  
    <artifactId>hibernate-core</artifactId>  
    <version>4.2.12.Final</version>  
</dependency>  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-orm</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>

관련 설정

Hibernate를 사용하기 위해서는 LocalSessionFactoryBean을 container에 등록해야 한다. packagesToScan 프로퍼티에는 데이터베이스 테이블을 의미하는 엔티티 객체를 넣어준다.

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">  
    <property name="dataSource" ref="dataSource"/>  

    <!-- 매핑 리소스 파일 설정 -->  
    <property name="packagesToScan" value="com.myapp.eisenhower.domain"/>  

    <!-- Hibernate 속성 설정 -->  
    <property name="hibernateProperties">  
        <props>            
            <prop key="hibernate.dialect">
                org.hibernate.dialect.MySQL5InnoDBDialect
            </prop>
            <prop key="hibernate.show_sql">true</prop>  
            <prop key="hibernate.format_sql">true</prop>  
            <prop key="hibernate.hbm2ddl.auto">update</prop>  
        </props>
    </property>
</bean>

트랜잭션을 처리하는 TransactionManager도 달라진다. HibernateTransactionManager를 transactionManager로 등록한다.

<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">  
    <property name="sessionFactory" ref="sessionFactory"/>  
</bean>

코드 구현

ORM 프레임워크를 사용하면서 부터는 Model 대신에 Entity(데이터베이스 테이블과 매핑되는 객체) 클래스를 구현하게 된다. 해당 Entity 클래스는 데이터베이스의 테이블이며 도메인 모델이라고 한다.

@Entity  
@Table(name = "tasks")  
public class Task {  

    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
    private Long userId;  
    private String title;  
    private String description;  
    private boolean isUrgent;  
    private boolean isImportant;  
    private Date dueDate;  
    private boolean completed;  
    private Date createdAt;  
    private Date updatedAt;  

    // Getters & Setters 생략
}

데이터 접근 코드는 DAO가 아닌 Repository가 담당하게 된다. DAO는 단순히 데이터베이스 접근 역할을 하는 객체를 형상화했다면, Repository는 도메인 객체들을 관리하는 인터페이스이다. 데이터베이스 영역을 완전히 숨기고 객체 지향적인 방식을 좀 더 강조한 것으로 보여진다. ORM 프레임워크에서는 기본적으로 SQL을 직접 작성하지 않는다.(내부적으로 SQL이 생성되지만..) 물론 복잡한 SQL의 경우 직접 작성하기도 한다.

@Repository
@Transactional
public class TaskRepository {  
    @Autowired  
    private SessionFactory sessionFactory;  

    public Task findById(Long id) {  
        return (Task) sessionFactory.getCurrentSession().get(Task.class, id);  
    }  

    public List<Task> findAll() {  
        return sessionFactory.getCurrentSession().createQuery("from Task").list();  
    }  

    public void save(Task task) {  
        sessionFactory.getCurrentSession().save(task);  
    }  

    public void update(Task task) {  
        sessionFactory.getCurrentSession().update(task);  
    }  

    public void delete(Long id) {  
        Task task = findById(id);  
        if (task != null) {  
            sessionFactory.getCurrentSession().delete(task);  
        }  
    }  
}

JPA + Hibernate

JPA + Hibernate는 JPA 인터페이스를 사용하여 데이터베이스 관련 코드를 작성하지만 내부적으로는 프로바이더가 동작하는 방식이다. 인터페이스는 껍데기이기 때문에 구현체 없이 동작할 수 없다.

의존성 추가

hibernate-entitymanager 모듈이 필요하게 된다.

<dependency>  
    <groupId>org.hibernate</groupId>  
    <artifactId>hibernate-entitymanager</artifactId>  
    <version>4.3.4.Final</version>  
</dependency>  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-orm</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>

관련 설정

JPA 인터페이스를 사용하는 경우 LocalContainerEntityManagerFactoryBean을 container에 등록해야 한다. 다른 설정들은 hibernate와 거의 동일하다.

<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">  
    <property name="database" value="MYSQL"/>  
</bean>  

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">  
    <property name="dataSource" ref="dataSource"/>  
    <property name="packagesToScan" value="com.myapp.eisenhower.domain"/>  
    <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>  
    <property name="jpaProperties">  
        <props>            
            <prop key="hibernate.dialect">
                org.hibernate.dialect.MySQL5InnoDBDialect
            </prop>
            <prop key="hibernate.show_sql">true</prop>  
            <prop key="hibernate.format_sql">true</prop>  
            <prop key="hibernate.hbm2ddl.auto">update</prop>  
        </props>    
    </property>
</bean>  

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">  
    <property name="entityManagerFactory" ref="entityManagerFactory"/>  
</bean>

코드 구현

Entity 클래스는 Hibernate와 동일하고 Repository 부분만 조금 다르다. EntityManager가 CRUD를 위해 실행하는 메서드 명칭이 프로바이더와는 조금 다르다.

@Repository  
@Transactional  
public class TaskRepository {  
    @PersistenceContext  
    private EntityManager entityManager;  

    public Task findById(Long id) {  
        return entityManager.find(Task.class, id);  
    }  

    public List<Task> findAll() {  
        return entityManager.createQuery("SELECT t FROM Task t", Task.class).getResultList();  
    } 

    public void save(Task task) {  
        entityManager.persist(task);  
    }   

    public void update(Task task) {  
        entityManager.merge(task);  
    }  

    public void delete(Long id) {  
        Task task = findById(id);  
        if (task != null) {  
            entityManager.remove(task);  
        }  
    }  
}

6. Spring Data JPA

마지막으로 Spring Data JPA를 사용한 예제이다. Spring Data JPA는 JPA가 더 추상화된 버전이라 매우 간결하다.

의존성 추가

추가로 spring-data-jpa 모듈이 필요하다.

<dependency>  
    <groupId>org.springframework.data</groupId>  
    <artifactId>spring-data-jpa</artifactId>  
    <version>1.6.0.RELEASE</version>  
</dependency>  
<dependency>  
    <groupId>org.hibernate</groupId>  
    <artifactId>hibernate-entitymanager</artifactId>  
    <version>4.3.4.Final</version>  
</dependency>  
<dependency>  
    <groupId>org.springframework</groupId>  
    <artifactId>spring-orm</artifactId>  
    <version>4.0.4.RELEASE</version>  
</dependency>

관련 설정

설정은 JPA + Hibernate와 동일하나 아래 설정만 추가해주면 된다.
Spring Data JPA에서 Repository 인터페이스들을 자동으로 스캔하고 bean으로 등록하는 역할을 하는 설정이라고 한다.

<!-- Enable Spring Data JPA Repositories -->  
<jpa:repositories base-package="com.myapp.eisenhower.repository"/>

Repository 부분도 상당히 간결하다. JpaRepository를 상속받은 인터페이스만 선언하면 된다. 기본적으로 CRUD기능이 제공된다.

public interface TaskRepository extends JpaRepository<Task, Long> {  
    //기본 CRUD 기능 자동 제공  
}

Service Layer에서 위 TaskRepository 인터페이스를 사용한 예제이다. 메서드의 의미가 굉장히 전달이 잘된다.

@Service  
public class TaskServiceImpl implements TaskService {  
    @Autowired  
    private TaskRepository taskRepository;  

    @Override  
    public Task getTask(Long id) {  
        return taskRepository.findById(id);  
    }  

    @Override  
    public List<Task> getTasks() {  
        return taskRepository.findAll();  
    }  

    @Override  
    public void saveTask(Task task) {  
        taskRepository.save(task);  
    }  

    @Override  
    public void updateTask(Task task) {  
        taskRepository.update(task);  
    }  

    @Override  
    public void deleteTask(Long id) {  
        taskRepository.delete(id);  
    }  
}

결론

Spring 기반의 백엔드 시스템에서 다양한 방식으로 데이터 영속성을 위한 코드를 작성해봤다. 기존에는 다양한 영속성 기술의 차이점을 명확히 이해하기 어려웠지만, 이번 비교를 통해 각 기술의 코드 간결성, 의미 전달성이 어떻게 발전했는지 명확히 확인할 수 있었다. 다양한 기술이 존재하는 만큼, 각 기술의 장단점을 비교하여 상황에 맞게 선택하는것이 중요하다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함