Hibernate + Maven + HSQL 설정

  • 먼저 maven으로 simple project를 만든다.
  • pom.xml에 hsqldb와 하이버네이트 dependency를 추가한다.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.tistory.jekalmin</groupId>
    <artifactId>hibernate-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.3.6.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>2.3.2</version>
        </dependency>
    </dependencies>
</project>
  • src/main/resources 밑에 hibernate.cfg.xml을 생성한다.
    • JBoss Tool 이클립스 플러그인안에 Hibernate Tools를 설치했다면 new > Other... > Hibernate Configuration File로 가서 생성할 수 있다.
    • 플러그인 설치를 안하셨다면 그냥 xml 만들어서 붙여넣자.

hibernate.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<!-- 하이버네이트 설정 파일 -->
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="hibernate.connection.url">jdbc:hsqldb:mem:test</property>
        <property name="hibernate.connection.username">sa</property>

        <!-- 하이버네이트 엔티티를 hsql에 맞게 변환해주는 클래스 -->
        <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>


        <!-- 커넥션 풀 개수 -->
        <property name="hibernate.connection.pool_size">1</property>

        <!-- hsql에 있는 마지막 연결이 끊어지면 데이터베이스 shutdown 하는 플래그 -->
        <property name="hibernate.connection.shutdown">true</property>

        <!-- 등록된 엔티티의 테이블이 없을 경우 자동으로 생성해주는 설정 -->
        <property name="hibernate.hbm2ddl.auto">create</property>

        <!-- db에 요청한 sql 출력 -->
        <property name="hibernate.show_sql">true</property>

        <!-- 엔티티 등록 -->
        <mapping class="com.test.Member"/>
        <!-- 엔티티 등록 끝 -->
    </session-factory>
</hibernate-configuration>
  • 이제 Member 클래스를 만든다. 예제에서는 com.test 패키지 밑에 만들었다.

Member.java

package com.test;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table
public class Member {
    @Id
    @GeneratedValue
    private int seq;
    private String name;
    private int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getSeq() {
        return seq;
    }

    @SuppressWarnings("unused")
    private void setSeq(int seq) {
        this.seq = seq;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member [seq=" + seq + ", name=" + name + ", age=" + age + "]";
    }

}
  • 실행할 Test 클래스를 com.test 패키지 밑에 만든다.

Test.java

package com.test;

import java.util.List;

import org.hibernate.FlushMode;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cfg.Configuration;

public class Test {

    static SessionFactory factory;
    static Session session;

    /**
     * hibernate.cfg.xml의 설정을 읽어서
     * db와 연결하는 설정
     */
    static {
        Configuration configuration = new Configuration().configure("hibernate.cfg.xml");
        StandardServiceRegistryBuilder builder = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties());
        factory = configuration.buildSessionFactory(builder.build());
        session = factory.openSession();
    }

    public static void main(String[] args) {

        insert();
        detail();
        update();
        delete();

        session.flush(); // delete나 update 한 내용을 커밋한다.

        list();

        session.close();
        factory.close();
    }

    /**
     * 등록
     * Member의 seq는 @GenerateValue 어노테이션에 의해 자동 생성된다.
     */
    private static void insert(){
        Member member1 = new Member("Min", 26);
        Member member2 = new Member("Park", 27);
        session.save(member1);
        session.save(member2);
    }

    /**
     * 상세
     */
    private static void detail(){
        Member member = (Member)session.get(Member.class, 2);
        System.out.println("detail member : " + member);
    }

    /**
     * 리스트
     */
    private static void list(){
        Query query = session.createQuery("from Member"); // from Member에서 Member는 클래스명과 동일 (대소문자구분)
        List members = query.list();
        for(Object member : members){
            System.out.println("list : " + member);
        }
    }

    /**
     * 수정
     * 조회를 먼저 해서 Member를 가져온 후 수정한다.
     */
    private static void update(){
        Member member = (Member) session.get(Member.class, 2);
        member.setAge(60);
        session.save(member);
    }

    /**
     * 삭제
     * 조회를 먼저 해서 Member를 가져온 후 삭제한다.
     */
    private static void delete(){
        Member member = (Member) session.get(Member.class, 1);
        session.delete(member);
    }




}

CRUD 코드를 최소화 하기 위해 하나의 session에서 모든 작업을 했다. session은 default로 FlushMode가 AUTO인데, flush 하는 타이밍은 http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch11.html#objectstate-flushing 여기서 참고하길 바란다.

ApplicationContext를 써야하는 이유

개요

필자는 스프링을 사용할 때, 웹에 관련된 설정들은 DispatcherServlet에 넣고, 순수 자바 설정들은 ApplicationContext에 넣으라고 배웠다. 그러나 모든 설정을 DispatcherServlet안에 넣어도 잘 작동하는데, 왜 굳이 ApplicationContext를 써야 할까?

예제

스프링 시큐리티 작동 방식을 예로 들어보자. 스프링 시큐리티는 필터를 이용해서 사용자의 인증과 권한 여부를 체크한다. 만약 ApplicationContext를 사용하지 않는다면, 스프링 시큐리티의 설정 파일들을 DispatcherServlet에 정의해야한다. 그렇다면 문제는 무엇일까?

이것은 톰캣 같은 WAS(Web Application Server)가 생성하는 컴포넌트 순서가 중요하다. web.xml에 등록된 컴포넌트가 생성되는 순서는 Listener - Filter - Servlet 순서이다. 심지어 서블릿은 load-on-startup 같은 설정을 해주지 않으면 호출되기 전까지 생성되지도 않는다. 근데 스프링 시큐리티에서 사용할 설정을 서블릿인 DispatcherServlet 안에 넣을 경우, WAS에서 필터를 생성할 때는 아직 DispatcherServlet이 생성되지 않았으므로 시큐리티에 관련된 필터를 생성할 수 없다.

그래서 필터를 생성하기 전에 스프링 관련 설정을 적용하려면 listener에 등록해야 한다.

그렇다면 스프링에서 제공하는 CharacterEncodingFilter는 어떨까? 인코딩 필터는 스프링에서 제공하긴 하지만 encoding 변수만 설정해주면 되기 때문에 web.xml 안에서 설정이 가능하다. 그래서 스프링과는 별도로 작동할 수 있었다.

결론

ApplicationContext를 안썼을 때 문제점이 더 많을 수도 있다. 필자는 이것을 쓰는 것이 싫은 것이 아니다. 과연 어떤 경우에 ApplicationContext가 꼭 필요한가 궁금했었는데, 스프링에서 설정을 기반으로 필터를 등록해야 한다면, ApplicationContext를 Listener로 올리는 것은 선택이 아닌 필수이다.


ApplicationContext vs DispatcherServlet 2 : 컨텍스트 간의 bean 공유?

ApplicationContext와 DispatcherServlet은 부모와 자식 관계로 연결되어 있는데, 왜 component-scan은 양쪽 다 해야할까? 두 컨텍스트는 같은 빈들을 가리키는 걸까? 밑에 예제에서는 ApplicationContextHolder 라는 클래스를 만들어서 두 컨텍스트를 가지고 있다가, 두 컨텍스트가 가지고 있는 빈의 인스턴스가 같은지 비교해보았다.


application-context.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: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/context http://www.springframework.org/schema/context/spring-context.xsd">

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

    

    <bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

<bean class="com.tistory.jekalmin.dao.TestJdbcTemplate"/>

</beans>

TestJdbcTemplate은 applicationContext에만 등록하고, dispatcherServlet에는 등록하지 않기 위해 어노테이션을 사용하지않고 application-context.xml에서 bean으로 등록하였다.


spring-servlet.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:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"

xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd

http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.tistory.jekalmin"/>

<context:annotation-config/>

<bean class="com.tistory.jekalmin.common.ApplicationContextHolder"/>

<bean class="com.tistory.jekalmin.dao.TestJdbcTemplate"/>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">

<property name="prefix" value="/WEB-INF/view/"/>

<property name="suffix" value=".jsp"/>

</bean>

</beans>

application-context.xml과 spring-servlet.xml 양쪽에 component-scan을 하였다.


ApplicationContextHolder.java


package com.tistory.jekalmin.common;


import java.util.ArrayList;

import java.util.List;


import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;


public class ApplicationContextHolder implements ApplicationContextAware{

public static List<ApplicationContext> list = new ArrayList<ApplicationContext>();

@Override

public void setApplicationContext(ApplicationContext ctx)

throws BeansException {

list.add(ctx);

}


}

두 컨텍스트에서 ApplicationContextHolder 빈을 각각 생성할 때, ApplicationContextHolder 클래스에서 컨텍스트에 접근할 수 있도록 컨텍스트를 등록하였다.


TestDao.java


package com.tistory.jekalmin.dao;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

@Repository

public class TestDao {

@Autowired

public TestJdbcTemplate jdbcTemplate;

}

TestDao는 어노테이션을 이용해 등록했으므로 양쪽 컨텍스트에서 component-scan을 이용해 등록될 것이다.


TestJdbcTemplate.java


package com.tistory.jekalmin.dao;

public class TestJdbcTemplate {

}

TestJdbcTemplate은 위에 application-context.xml에서만 등록하였다.


TestController.java


package com.tistory.jekalmin.controller;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.ApplicationContext;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

import com.tistory.jekalmin.common.ApplicationContextHolder;

import com.tistory.jekalmin.dao.TestDao;

@Controller

public class TestController {

@Autowired

private TestDao testDao;

@RequestMapping("/")

public void index(){

for(ApplicationContext ctx : ApplicationContextHolder.list){

System.out.format("[%s] testController hashCode : %d\n", ctx.getId(), ctx.getBean("testController").hashCode());

System.out.format("[%s] testDao hashCode : %d\n", ctx.getId(), ctx.getBean("testDao").hashCode());

System.out.format("[%s] testJdbcTemplate hashCode : %d\n", ctx.getId(), testDao.jdbcTemplate.hashCode());

}

}

}

이제 url을 호출하여 테스트 해보자. 테스트 내용은 기존에 ApplicationContext에서 가져온 빈과 DispatcherServlet에서 가져온 빈의 hashCode를 비교해 인스턴스가 같은지 다른지 확인하였다.

url 호출 결과는 아래와 같다.


[org.springframework.web.context.WebApplicationContext:] testController hashCode : 631271355

[org.springframework.web.context.WebApplicationContext:] testDao hashCode : 1666005388

[org.springframework.web.context.WebApplicationContext:] testJdbcTemplate hashCode : 1060031077

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testController hashCode : 319851274

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testDao hashCode : 896263012

[org.springframework.web.context.WebApplicationContext:/dispatcherServlet] testJdbcTemplate hashCode : 1060031077

결과를 보면 testJdbcTemplate만 hashCode가 같고, testController와 testDao는 다른 것을 확인 할 수 있다.

결론 : ApplicationContext와 DispatcherServlet에서 component-scan을 이용해서 등록된 빈들은 다른 인스턴스이다. 다만 ApplicationContext에만 등록된 빈의 경우 DispatcherServlet에서 검색해보고 없으면 부모인 ApplicationContext에서 가져온다.