http://www.hibernate.org/43.html

Open Session in View#

이 패턴을 이해하기 위해서는 Sessions and transactions을 꼭 읽어야 한다. 이 페이지는 Hibernate 3.1.x에서 언급되고 있으며 여기의 코드는 이전버전에서는 작동하지 않을것이다.

  • 문제점[1]
  • 인터셉터 사용하기[2]
  • 3-티어 환경은 어떤가.?[3]
  • 오랜 통신(Conversation)을 위한 확장된 Session패턴은 어떤가.?[4]
  • 나는 서블릿 필터를 작성하길 원하지 않는다, 그것들은 너무 보수적(old school)!! [5]
  • 예외는 어떻게 다루는가.?[6]
  • view를 표현하기 전에 트랜잭션을 커밋할수 있는가.?[7]
  • 하나의 Session에서 두개의 트랜잭션을 사용할수 있는가.?[8]
  • Hibernate는 요청시 왜 객체를 로드할수 없는가.?[9]
  • 이것은 매우 어렵다, 이것을 좀더 쉽게 할수 없는가.?[10]

The problem[#1]#

전형적인 (웹-)애플리케이션에서의 공통적인 이슈는 중요한 로직이 모두 완료되고 Hibernate Session은 닫히고 데이터베이스 트랜잭션이 종료된후 view를 표현하는 것이다. 만약 JSP(또는 다른 view표현 기법)내 Session에 로드된 분리된 객체에 접근한다면, 당신은 로드되지 않은 collection이나 초기화되지 않은 프록시에 접근할것이다. 이 때 당신이 경험하게 되는 예외는 LazyInitializationException: Session has been closed (또는 유사한 메시지)이다. 물론 이것은 당신이 작업을 단위작어을 종료한뒤에야 보게될것이다.

첫번째 해결법은 view를 표현하기 위한 다른 작업단위를 여는것이다. 이것은 언제나 올바른 접근법이 되는것은 아니지만 수행하기가 쉽다. 완료된 액션을 위한 view를 표현하는 것은 분리된 단위작업이 아닌 첫번째 단위작업내에서 추측된다. 액션수행(action execution), Session을 통한 데이터 접근, 그리고 같은 가상머신내 모든 view를 표현하는 투티어 시스템내에서 이 해결법은 view가 표현될때까지 Session이 열린 상태로 유지된다.

interceptor 사용하기[#2]#

자동으로 Session컨텍스트 관리를 하기 위한 Hibernate내장 지원으로 당신의 Session핸들링을 구현한다면, Sessions and transactions를 보라. 당신은 지금 view가 표현되고 데이터베이스를 커밋하며, 나아가 Session을 닫은 후 실행하는 몇가지 interceptor가 필요하다. 반면에, 대부분의 애플케이션에서 당신은 다음과 같은 것들이 필요하다. HTTP요청이 다루어질때, 새로운 Session과 데이터베이스 트랜잭션이 시작된다. 응답이 클라이언트로 보내어지기전과 모든 작업이 수행된후, 트랜잭션은 커밋되고 Session은 닫힐것이다.

서블릿 컨테이너내 가장 표준적인 interceptor는 ServletFilter이다. CaveatEmptor로 부터 얻어진 모든 요청을 수행되고 모든 응답전에 사용자정의 필터로 몇라인을 두는것은 당연하다.

public class HibernateThreadFilter implements Filter {

    private static Log log = LogFactory.getLog(HibernateThreadFilter.class);

    private SessionFactory sf;

    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {

        try {
            log.debug("Starting a database transaction");
            sf.getCurrentSession().beginTransaction();

            // Call the next filter (continue request processing)
            chain.doFilter(request, response);

            // Commit and cleanup
            log.debug("Committing the database transaction");
            sf.getCurrentSession().getTransaction().commit();

        catch (StaleObjectStateException staleEx) {
            log.error("This interceptor does not implement optimistic concurrency control!");
            log.error("Your application will not work until you add compensation actions!");
            // Rollback, close everything, possibly compensate for any permanent changes
            // during the conversation, and finally restart business conversation. Maybe
            // give the user of the application a chance to merge some of his work with
            // fresh data... what you do here depends on your applications design.
            throw staleEx;
        catch (Throwable ex) {
            // Rollback only
            ex.printStackTrace();
            try {
                if (sf.getCurrentSession().getTransaction().isActive()) {
                    log.debug("Trying to rollback database transaction after exception");
                    sf.getCurrentSession().getTransaction().rollback();
                }
            catch (Throwable rbEx) {
                log.error("Could not rollback transaction after exception!", rbEx);
            }

            // Let others handle it... maybe another interceptor for exceptions?
            throw new ServletException(ex);
        }
    }

    public void init(FilterConfig filterConfigthrows ServletException {
        log.debug("Initializing filter, obtaining Hibernate SessionFactory from HibernateUtil");
        sf = HibernateUtil.getSessionFactory();
    }

    public void destroy() {}

}

만약 당신이 이 필터를 자동적인 Session컨텍스트 지원과 조합한다면, 작성한 DAO는 다음과 같을것이다.

public class ItemDAO {

    Session currentSession;

    public ItemDAO(Session) {
        currentSession = session;
    }

    public Item getItemById(Long itemId) {
        return (ItemcurrentSession.load(Item.class, itemId);
    }
}

DAO가 생성되었을때, currentSession이 셋팅된다. 예를 들면, factory에서

public static void DAOFactory {

    public static getItemDAO() {
        return new ItemDAOHibernateUtil.getSessionFactory().getCurrentSession() );
    }
}

DAO에 관련된 좀더 많은 정보가 필요하다면, Generic Data Access Objects를 보라.

지금 당신의 애플리케이션 컨트롤러는 DAO를 사용할수 있고 세션이나 트랜잭션을 전혀 고민하지 않게 한다. 이를테면, 당신의 서블릿은

public String execute(HttpRequest request) {

    Long itemId = request.getParameter(ITEM_ID);

    ItemDAO dao = DAOFactory.getItemDAO();

    request.setAttributeRESULT, dao.getItemById(itemId) );
  
    return "success";
}

모든 Http요청을 위해 수행하는 필터를 가능하게 하기 위해서, 당신의 web.xml설정파일에 이것을 추가하라.

    <filter>
        <filter-name>HibernateFilter</filter-name>
        <filter-class>my.package.HibernateThreadFilter</filter-class>
    </filter>

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

주의 : view가 표현된후 Session은 없어지기 때문에, 데이터베이스 예외는 성공적인 출력이 생성된 후에 발생한다. 만약 일반적인 JSP와 서블릿을 사용한다면, 페이지 출력은 대개 8K인 버퍼로 표현된다. 만약 버퍼가 꽉 찬다면, 이것은 클라이언트 브라우저로 내보내진다. 그래서 사용자가 성공적인 페이지(200K)를 보게된다. 하지만 사실 예외는 발생한다. 이것을 피하기 위해, 서블릿 엔진 버퍼로 표현하지 말거나 버퍼크기를 안정적인 크기로 증가시켜라. 대부분의 웹 애플리케이션은 표준적인 서블릿 엔진 버퍼로 표현하지 않아서 이 문제에 걸리지 않는다.

3-티어 환경은 어떤가.?[#3]#

이 패턴은 view를 표현할때 당신이 local Session을 사용할수 있을때만 발생한다는 것이 명백하다. 3-티어환경에서, view는 비지니스와 데이터 접근 레이어를 가진 service가상머신이 아닌 presentation가상머신에서 표현될것이다. 그러므로, Session과 트랜잭션을 열어둔체로 유지하는 것은 선택사항이 아니다. 이 경우, 당신은 presentation레이어로 많은 양의 데이터를 보내야만 하기 때문에 view는 이미 닫혀진 Session으로 만들어질수 있다. 이것은 당신이 독립된(Detached) 객체, 데이터 전송객체(DTO-Data Transfer Object) 또는 Command패턴을 사용하는 두가지의 혼합을 사용한다면 당신의 아키텍처에 의존한다.

오랜 통신(Conversation)을 위한 확장된 Session패턴은 어떤가.?[#4]#

여러개의 데이터베이스 트랜잭션을 위한 하나의 Session을 사용하고자 한다면, 당신은 스스로 이것을 완벽하게 구현하거나 내장된 Session컨텍스트 관리를 확장해야 한다.

당신이 EJB/CMT와 JTA범위의 Session을 사용하지 않는다면, 하나의 접근법은 내장된 ThreadLocalSessionContext를 확장하고 트랜잭션 끝에서 Session을 닫는것을 막는것이다.

public class ExtendedThreadLocalSessionContext extends ThreadLocalSessionContext {

    private static final Log log = LogFactory.getLogExtendedThreadLocalSessionContext.class );

    public ExtendedThreadLocalSessionContext(SessionFactoryImplementor factory) {
        super(factory);
    }

    // Always set FlushMode.NEVER on any Session
    protected Session buildOrObtainSession() {
        log.debug("Opening a new Session");
        Session s = super.buildOrObtainSession();

        log.debug("Disabling automatic flushing of the Session");
        s.setFlushMode(FlushMode.NEVER);

        return s;
    }

    // No automatic flushing of the Session at transaction commit, client calls flush()
    protected boolean isAutoFlushEnabled() { return false}

    // No automatic closing of the Session at transaction commit, client calls close()
    protected boolean isAutoCloseEnabled() { return false}

    // Don't unbind after transaction completion, we expect the client to do this
    // so it can flush() and close() if needed (or continue the conversation).
    protected CleanupSynch buildCleanupSynch() {
        return new NoCleanupSynch(factory);
    }
    private static class NoCleanupSynch extends ThreadLocalSessionContext.CleanupSynch {
        public NoCleanupSynch(SessionFactory factory) { super(factory)}
        public void beforeCompletion() {}
        public void afterCompletion(int i) {}
    }
}

Hibernate셋팅에서 hibernate.current_session_context_class와 같이 확장을 명명하는 것을 잊지말라. EJB/CMT를 위해, 당신은 JTASessionContext의 확장을 작성해야만 한다. 지금 우리는 통신이 종료될때까지 데이터베이스 트랜잭션 사이에서 Session을 저장하기 위한 HttpSession을 사용한다.

public class HibernateThreadExtendedFilter implements Filter {

    private static Log log = LogFactory.getLog(HibernateThreadExtendedFilter.class);

    private SessionFactory sf;
    
    public static final String HIBERNATE_SESSION_KEY    = "hibernate_session";
    public static final String END_OF_CONVERSATION_FLAG = "end_of_conversation";

    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {


        // Try to get a Hibernate Session from the HttpSession
        HttpSession httpSession =
                ((HttpServletRequestrequest).getSession();
        Session hibernateSession =
                (SessionhttpSession.getAttribute(HIBERNATE_SESSION_KEY);

        try {

            if (hibernateSession != null) {
                log.debug("< Continuing conversation");
                ExtendedThreadLocalSessionContext.bind(hibernateSession);
            else {
                log.debug(">>> New conversation");
            }

            log.debug("Starting a database transaction");
            sf.getCurrentSession().beginTransaction();

            // Do the work...
            chain.doFilter(request, response);

            // End or continue the long-running conversation?
            if (request.getAttribute(END_OF_CONVERSATION_FLAG!= null) {

                log.debug("Flushing Session");
                sf.getCurrentSession().flush();

                log.debug("Committing the database transaction");
                sf.getCurrentSession().getTransaction().commit();

                log.debug("Closing and unbinding Session from thread");
                sf.getCurrentSession().close()// Unbind is automatic here

                log.debug("Removing Session from HttpSession");
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);

                log.debug("<<< End of conversation");

            else {

                log.debug("Committing database transaction");
                sf.getCurrentSession().getTransaction().commit();

                log.debug("Unbinding Session from thread");
                hibernateSession = ExtendedThreadLocalSessionContext.unbind(sf);

                log.debug("Storing Session in the HttpSession");
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, hibernateSession);

                log.debug("> Returning to user in conversation");
            }

        catch (StaleObjectStateException staleEx) {
            log.error("This interceptor does not implement optimistic concurrency control!");
            log.error("Your application will not work until you add compensation actions!");
            // Rollback, close everything, possibly compensate for any permanent changes
            // during the conversation, and finally restart business conversation. Maybe
            // give the user of the application a chance to merge some of his work with
            // fresh data... what you do here depends on your applications design.
            throw staleEx;
        catch (Throwable ex) {
            // Rollback only
            try {
                if (sf.getCurrentSession().getTransaction().isActive()) {
                    log.debug("Trying to rollback database transaction after exception");
                    sf.getCurrentSession().getTransaction().rollback();
                }
            catch (Throwable rbEx) {
                log.error("Could not rollback transaction after exception!", rbEx);
            finally {
                log.error("Cleanup after exception!");

                // Cleanup
                log.debug("Closing and unbinding Session from thread");
                sf.getCurrentSession().close()// Unbind is automatic here

                log.debug("Removing Session from HttpSession");
                httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);

            }

            // Let others handle it... maybe another interceptor for exceptions?
            throw new ServletException(ex);
        }

    }

    public void init(FilterConfig filterConfigthrows ServletException {
        log.debug("Initializing filter, obtaining Hibernate SessionFactory from HibernateUtil");
        sf = HibernateUtil.getSessionFactory();
    }

    public void destroy() {}

}

이 필터는 통신이 끝나는 시점에 Session이 결국 닫힌다는 것을 제외하고 당신의 나머지 애플리케이션을 위해 명백하다. 위 예제는 요청범위내 드러나는 특수한 표시자 플래그를 사용한다. 당신은 요청을 처리하는 동안 몇가지 임의의 기준에 기초하여 이 플래그를 셋팅할수 있다. 당신은 통신 경계를 위한 다른 인터셉터 레이어나 워크프롤우 엔진을 사용하겠는가.?

나는 서블릿 필터를 작성하길 원하지 않는다, 그것들은 너무 보수적(old school)!![#5]#

당신 생각이 맞다. 서블릿 필터는 정말 최신 기술이 아니다. 어쨌든, 그것들은 웹 애플리케이션에서 전체를 감싸는 인터셉터처럼 매우 잘 작동한다. 그러므로 서블릿 필터를 사용해서 나쁜것 아무것도 없다. 이미 언급한것처럼, 당신이 자체적인 인터셉터를 가지는 웹 프레임워크를 사용한다면, 당신은 아마도 그것들이 좀더 유연하다는 것을 알게될것이다. 많은 컨테이너가 요즘 사용자 정의 인터셉터를 제공한다. 마지막으로, 매우 유연한 접근법은 인터셉터가 자바 애플리케이션내 어디에도 적용될수 있는 AOP다. 예제를 원한다면 CaveatEmptor 데모 애플리케이션 을 보라.

예외는 어떻게 다루는가.?[#6]#

명백하게도, 예외가 발생할때마다 데이터베이스 트랜잭션은 롤백된다. Hibernate내에서의 또다른 규칙은 현재 Session이 닫히고 즉시 버려져도, 이것은 재사용될수 없다는 것이다. 나아가 이것은 당신이 Hibernate관점에서 예외를 다루기 위해 해야만 하는 것이다. 물론 당신은 이것이 실패하거나 사용자정의 에러를 보여주길 원한다면, 몇가지 작업단위를 다시 시도하길 원할것이다. Hibernate의 범위 밖에 두거나 당신이 좋아하는 방법으로 구현될수 있다.

view를 표현하기 전에 트랜잭션을 커밋할수 있는가.?[#7]#

외관상으로, Hibernate문서내에서 장려되지 않음에도 불구하고, 몇몇 개발자는 view가 표현될때까지 Session을 유지하지만 view를 표현하기 전에 데이터베이스 트랜잭션을 커밋하는 번형된 형태를 사용한다. 결과는 Session을 열어둔체로 유지하지 않는다면(늦은(lazy) 로딩은 데이터베이스 트랜잭션 외부에서 작동하지 않는다) 같다. 이것은 Session을 열어둔체로는 문제가 도지 않는다. 하지만 connection을 가지지 않는다. anti-pattern이 작동할때 많은 혼동이 발생한다. 하지만 자동-커밋이 실제로 사용중이기 때문에 간단하다. 트랜잭션 처리 기본정보와 자동커밋 모델이 적절하지 않은 이유에 대한 좀더 많은 정보는 Sessions and transactions에서 보라.

하나의 Session에서 두개의 트랜잭션을 사용할수 있는가.?[#8]#

맞다. 이것은 이 패턴의 더 나은 구현물이다. 하나의 데이터베이스 트랜잭션은 요청 이벤트를 처리하는 동안 데이터를 읽고 쓰기 위해 사용된다. 두번째 데이터베이스 트랜잭션은 view를 표현하는 동안 데이터를 읽기 위해 사용된다. 객체의 변경은 이 시점에서 이루어지지 않는다. 데이터베이스 락은 첫번째 트랜잭션내에서 일찍 풀리고 좀더 측정가능하도록 허용된다. 두번째 트랜잭션은 가능한한 최적화(이를 테면, 몇몇 데이터베이스는 트랜잭션 커밋후 가장 좋은 cleanup을 위해 읽기전용 트랜잭션 셋팅을 요구한다.)될수 있다. 두개의 트랜잭션을 사용하기 위해 당신은 간단한 서블릿 필터보다 더 강력한 인터셉터가 필요하다. 여기서 AOP가 가장 좋은 선택일수 있다. JBoss Seam 프레임워크는 이 모델을 사용한다.

Hibernate는 요청시 왜 객체를 로드할수 없는가.?[#9]#

매달 몇몇의 사람들은 Hibernate가 LazyInitializationException를 던지는것 대신에 데이터베이스에 대해 새로운 connection을 열고(새로운 Session을 시작하는) collection을 로드하거나 프록시를 초기화하는 작업이 가능하길 바라는 생각을 가진다. 물론 이 생각이 처음에는 훌륭했다. 당신이 임시적인(ad-hoc) 트랜잭션 접근의 중요성에 대해 생각하기 시작한다면 다양한 결점이 나타날것이다.

If Hibernate would, hidden from the developer and outside of any transaction demarcation, start random database connections and transactions, why have transaction demarcation at all? What happens when Hibernate opens a new database connection to load a collection, but the owning entity has been deleted meanwhile? (Note that this problem does not appear with the two-transaction strategy as described above - the single Session provides repeatable reads for entities.) Why even have a service layer when every object can be retrieved by simply navigating to it? How much memory should be consumed by this and which objects should be evicted first? All of this leads to no solution, because Hibernate is a service for online transaction processing (and certain kinds of batch operations) and not a "streaming objects from some persistent data store in undefined units of work"-service. Also, in addition to the n+1 selects problem, do we really need an n+1 transaction and connection problem?

The solution for this issue is of course proper unit of work demarcation and design, supported by possibly an interception technique as shown in the pattern here, and/or the correct fetch technique so that all required information for a particular unit of work can be retrieved with minimum impact, best performance, and scalability.

이것은 매우 어렵다, 이것을 좀더 쉽게 할수 없는가.?[#10]#

Hibernate는 퍼시스턴스 서비스만 할수 있다. 여기서 언급되는 문제점은 애플리케이션 내부구조나 프레임워크의 임무이다. EJB3 프로그래밍 모델은 트랜잭션과 퍼시스턴스 컨텍스트 관리를 쉽게 만들고 API를 사용하기 위해 Hibernate EntityManager 를 사용한다. 완전한 J2EE애플리케이션 서버나 가볍게 내장가능한 EJB3컨테이너내에서 당신의 EJB들을 수행하라. JBoss Seam프레임워크는 자동 컨텍스트 관리를 위한 내장된 지원을 가진다.

노트 : 몇몇의 사람들은 아직도 이 패턴이 퍼시스턴스 레이어와 Hibernate사이에 의존성을 생성한다고 믿는다. 이것은 그렇지 않다. 포럼에서 관련 쓰레드를 보라.

노트 : 이 페이지의 대부분의 덧글은 진부하다.

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-7) was last changed on 06-Apr-2006 09:45 by 이동국