주요글: 도커 시작하기
반응형

스프링 MockMv의 단독 모드를 사용해서 한 개 컨트롤러에 대한 MockMvc를 생성하고, 테스트 코드를 작성했다.


mockMvc = MockMvcBuilders.standaloneSetup(SomeController).build();


여기서 문제는 익셉션 상황을 테스트하는 코드에서 발생했다. 이 프로젝트에서는 컨트롤러에서 발생한 익셉션의 처리를 표준화하기 위해 @ControllerAdvice를 사용했는데, 위와 같이 컨트롤러 단독 모드로 생성한 MockMvc의 경우 @ControllerAdvice를 이용한 익셉션 처리가 되지 않는 것이었다.


다행히 단독 모드로 생성한 StandaloneMockMvcBuilder는 다음의 메서드를 제공하고 있었다.


setHandlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers)


그래서, @ControllerAdvice를 지정한 클래스를 익셉션 핸들러로 사용하는 ExceptionHandlerExceptionResolver의 구현 객체를 만들어서 사용하기로 했다. 그 구현 객체를 생성하는 코드는 다음과 같다.


public static ExceptionHandlerExceptionResolver createExceptionResolver() {

    ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {

        @Override

        protected ServletInvocableHandlerMethod getExceptionHandlerMethod(

                HandlerMethod handlerMethod, Exception exception) {

            // 익셉션을 CommonExceptionHandler가 처리하도록 설정

            Method method = new ExceptionHandlerMethodResolver(

                    CommonExceptionHandler.class).resolveMethod(exception);

            return new ServletInvocableHandlerMethod(new CommonExceptionHandler(), method);

        }

    };

    exceptionResolver.getMessageConverters().add(new MappingJackson2HttpMessageConverter());

    exceptionResolver.afterPropertiesSet();

    return exceptionResolver;

}


위 코드에서 눈여겨 볼 점은 생성한 exceptionResolver에 알맞은 MessageConverter를 등록했다는 점이다. 새로 등록한 ExceptionResolver는 독립 모드로 생성한 MokcMvc가 사용하는 MessageConverter를 사용하지 않기 때문에, 위 코드처럼 필요한 MessageConverter를 ExceptionResolver에 직접 등록해 주었다.


@ControllerAdvice 클래스를 이용해서 익셉션을 처리하는 ExceptionResolver를 생성하는 기능을 만들었으니 이제 이 기능을 사용해서 MockMvc를 생성하면 된다.


mockMvc = MockMvcBuilders.standaloneSetup(someController)

        .setHandlerExceptionResolvers(createExceptionResolver()).build();


이제 someController에서 익셉션이 발생하면, @ControllerAdvice를 적용한 클래스를 이용해서 익셉션을 처리할 수 있게 된다.

반응형

신림 프로그래머 모임에서 했던, 스프링 시큐리티 구조 이해 자료입니다.



반응형

Ant 경로 패턴은 경로를 지정할 때 유용하게 사용할 수 있는 경로 표현 패턴인데, 스프링은 특정 경로가 Ant 경로 패턴 경로와 일치하는지 여부를 확인할 때 사용할 수 있는 org.springframework.util.AntPathMatcher 클래스를 제공하고 있다. 


AntPathMatcher 클래스의 사용방법은 간단하다. boolean match(String pattern, String path) 메서드를 이용해서 path가 Ant 패턴인 pattern에 매칭되는지 확인하면 된다. 매칭될 경우 match() 메서드는 true를 리턴한다.


AntPathMatcher pathMatcher = new AntPathMatcher();

assertThat(pathMatcher.match("/a/**/b", "/a/1/2/3/b"), equalTo(true));

assertThat(pathMatcher.match("/a/**/b", "/a/1/2/3/b/c"), equalTo(false));


AntPathMatcher의 또 다른 기능은 패턴을 기준으로 매칭되는 부분을 추출하는 것이다. 이 기능을 사용하려면 다음과 같이 extractPathWithinPattern(String pattern, String path) 메서드를 사용하면 된다. 이 메서드는 pattern의 '*', '?' 등을 기준으로 경로에서 패턴에 매칭되는 부분을 추출해준다. 다음은 이 메서드의 실행 결과를 보여주고 있다.


assertThat(pathMatcher.extractPathWithinPattern("/a/*", "/a/1"), equalTo("1"));

assertThat(pathMatcher.extractPathWithinPattern("/a/*", "/a/1/b"), equalTo("1/b"));

assertThat(pathMatcher.extractPathWithinPattern("/a/b/*", "/a/b/c"), equalTo("c"));

assertThat(pathMatcher.extractPathWithinPattern("/a/b/*.xml", "/a/b/c.xml"), equalTo("c.xml"));


assertThat(pathMatcher.extractPathWithinPattern("/*", "/a/b/c.xml"), equalTo("a/b/c.xml"));

assertThat(pathMatcher.extractPathWithinPattern("/*.xml", "/a/b/c.xml"), equalTo("a/b/c.xml"));


assertThat(pathMatcher.extractPathWithinPattern("/a/*", "/a/1/b"), equalTo("1/b"));

assertThat(pathMatcher.extractPathWithinPattern("/a/**", "/a/1/b"), equalTo("1/b"));


assertThat(pathMatcher.extractPathWithinPattern("/a/*", "/a/1/b.xml"), equalTo("1/b.xml"));

assertThat(pathMatcher.extractPathWithinPattern("/a/**", "/a/1/b.xml"), equalTo("1/b.xml"));


assertThat(pathMatcher.extractPathWithinPattern("/a/*/b", "/a/1/b"), equalTo("1"));

assertThat(pathMatcher.extractPathWithinPattern("/a/**/b", "/a/1/b"), equalTo("1"));


assertThat(pathMatcher.extractPathWithinPattern("/a/**/b.xml", "/a/1/b.xml"), equalTo("1"));

assertThat(pathMatcher.extractPathWithinPattern("/a/**/*.xml", "/a/1/b.xml"), equalTo("1/b.xml"));


위 코드에서 equalTo() 부분의 결과가 extractPathWithinPattern() 메서드의 실행 결과인데, 결과를 보면 '*'와 '**'가 차이가 없다는 것을 알 수 있다.


다음 extractPathWithinPattern() 실행 결과는 어떻게 될까?


pathMatcher.extractPathWithinPattern("/a/**/b", "/a/1/2/3/b") ????


실행 결과로 "1/2/3" 이라고 결과를 예상했겠지만, 실제 결과는 "1/3/b" 다. 상상에서 완전히 벗어난다. 다음은 몇 가지 상상을 벗어나느 결과를 만들어내는 몇 개 예제를 만들어 본 것이다.


assertThat(pathMatcher.extractPathWithinPattern("/a/**/b", "/a/1/2/3/b"), equalTo("1/3/b"));

assertThat(pathMatcher.extractPathWithinPattern("/a/**/b", "/a/1/2/3/b"), equalTo("1/3/b"));

assertThat(pathMatcher.extractPathWithinPattern("/a/**/b", "/a/1/2/3/4/b"), equalTo("1/3/4/b"));

assertThat(pathMatcher.extractPathWithinPattern("/a/a1/**/b", "/a/1/2/3/4/b"), equalTo("2/4/b"));


위 결과를 보면 알 수 있겠지만, match() 메서드를 기준으로 '*'나 '**'에 매칭되는 경로가 두 개 이상이 되면 extractPathWithinPattern() 메서드는 상상한 것과 완전히 다른 결과를 돌려준다. AntPathMatcher를 사용하려면 이 점에 주의하기 바란다.


반응형

ConversionService와 관련해서 레퍼런스 문서만 보면 전체 구조를 정리하기가 어려워서 ConversionService와 관련된 주요 타입을 클래스 다이어그램으로 정리했다. (이미지를 클릭하면 원본 이미지 조회)




반응형

최근 수행중인 프로젝트에서 JPA의 구현체로 하이버네이트를 사용하고 있다. 매핑 설정에 따라 다르겠지만, 이런 JPA 구현체들은 Lazy 로딩을 구현하기 위해 프록시 객체를 사용하고 있는데, 이와 관련해서 한 가지 주의해야 할 것이 있다. 그것은 바로 this를 리턴하는 메서드와 관련된 것이다.


현재 수행중인 프로젝트에는 상속을 매핑한 코드가 있다. 예를 들면 다음과 같은 구조이다.


public class Content { ... 최상위 }

public class VideoContent extends Content { .... }

public class MovieContent extends VideoContent { ... }

public class BookContent extends Content { ... }


그리고, 특정 클래스과 다음과 같이 Content를 *-to-One의 형식으로 레퍼런스하고 있다.


public class Order {

    private Content content;

    public Content getContent() {

        return content;

    }

}


Order 객체를 구한 뒤에 Content 객체에 접근해야 하는 경우가 있으며, 특히 Content 타입이 아닌 실제 타입에 접근해야 할 때가 있다. 이런 경우에 아주 단순하게 생각하면 다음과 같이 Content에 자기 자신을 리턴하는 코드를 넣는 방법을 생각해 볼 수 있을 것이다.


public Content {

    public Content self() {

        return this; // 자기 자신을 리턴

    }

}


Order o = getOrder(xxx);

Content proxy = o.getContent(); // Content는 프록시

Content real = proxy.self(); // self() 메서드는 자기 자신을 리턴하므로 프록시가 아닌 실제 대상 객체???

if (real instanceof MovieContent) { // 하지만, real은 프록시 객체이므로 항상 false

    ...

}


하지만, 위와 같은 방법은 통하지 않는다. 그 이유는 프록시 객체는 대상 객체가 대상 객체 자신을 리턴하는 경우 프록시 객체를 리턴하도록 만들어지기 때문이다. 

  • proxy.self() 메서드는 대상 객체의 self() 메서드를 호출한다.
  • 대상 객체의 self() 메서드는 자기 자신을 리턴한다.
  • proxy.self() 메서드는 대상 객체가 리턴한 객체가 대상 객체와 동일한지 확인한다.
  • 동일하다면 프록시 객체 자신을 리턴한다.
그렇다면, 왜 프록시 객체가 대상 객체를 리턴하지 않고 자기 자신을 리턴하는 것일까? 그 이유는 간단하다. 대상 객체에 직접 접근하게 될 경우 ORM이 제공하는 Dirty Checking이나 Lazy Loading 등이 적용되지 않기 때문이다.

이런 이유로 실제 대상 객체에 접근해서 뭔가를 하고 싶다면, 대상 객체가 자기 자신을 리턴하도록 하지 말고, Double dispatch를 사용해야 한다. Double dispatch란 다음과 같은 것이다.
  • A 객체의 a() 메서드는 B 인터페이스를 파라미터로 갖는다.
  • B 인터페이스의 b() 메서드는 A를 파라미터로 갖는다.
  • A 객체의 a() 메서드는 파라미터로 전달받은 B 인스턴스의 b() 메서드를 호출할 때 자기 자신을 전달한다.
위 내용을 코드로 다시 살펴보면 다음과 같다.
public interface B {
    public void b(A a);
}

public class A {
    public void a(B b) {
        b.b(this);
    }
}

A aobj = new A();
B bobj = new BImpl();
aobj.a(bobj); // 내부적으로 bobj.b(a)가 호출됨

위 코드를 보면 aobj.a() 메서드를 호출하면, a() 메서드는 내부적으로 다시 파라미터로 전달받은 bobj의 b() 메서드를 호출한다. 이렇게 두 객체간의 호출이 두 번 이루어지기 때문에 이를 Double Dispatch라고 표현한다.

그럼, 요놈을 앞서 Content에 적용해 보자. 다음과 같이 변경해 주면 된다.

public interface Accessor {
    public void access(Content c);
}

public class Content {
    public void access(Accessor access) {
        accessor.access(this);
    }
}

위 코드에서 Accessor의 구현체는 Content의 프록시 객체가 아닌 실제 객체에 접근할 수 있게 된다.

Order o = getOrder(xxx);

Content proxy = o.getContent(); // Content는 프록시

proxy.access(new Accessor() {
    public void access(Content c) {
        // c는 proxy의 대상 객체인 실제 객체
        if (c instanceof MovieContent) {
            ...
        }
    }
});

Content 및 Content의 자식 클래스들은 상속 관계에 있으므로 사용하므로 Visitor 패턴을 사용하면 instanceof 연산자를 사용하지 않고 실제 타입으로 바로 접근할 수도 있어 더 보기 좋은 코드를 만들어 낼 수 있을 것이다.



  1. 권남 2012.08.29 23:10

    저도 비슷한 상황에서 Visitor 패턴으로 해결한 적이 있습니다.
    하지만 이런일 자체가 안일어나게 만드는게 제일 좋을 것 같습니다.
    예전에 Visitor 패턴으로 베베꽈가면서 만든 기능을 이번 프로젝트에서는 각 엔티티가 특정 interface를 구현하게 해서 간단히 해결했던게 생각나네요.
    즉, 예제에서 보여주신 Accessor가 할 일을 각 엔티티가 인터페이스를 구현해서 알아서 하게 만든거죠.
    상속구조니까 Accessor가 할일을 인터페이스 메소드로 빼고 @Override를 계속해가면서 어쩌면 쉽게 해결 될수도 있어보입니다. 하지만 프로javascript:;젝트 실제 요구사항이 어떤지를 모르니 속단은 금물이겠죠.

    • 최범균 madvirus 2012.08.30 09:22 신고

      상속 구조에서 하위 타입이 특정 인터페이스를 상속받는다 하더라도, 최상위 타입에 대한 프록시 객체를 사용하기 때문에, 해당 인터페이스를 사용할 수 없는 상황이었습니다.
      그렇다고 해당 인터페이스를 최상위 타입이 상속받도록 할 수도 없었습니다. 왜냐면, 최상위 타입에는 해당 인터페이스에 대한 역할을 필요가 없었기 때문이다.
      또한, 각 엔티티의 역할이 아니면서 엔티티의 타입에 따라서 다르게 동작해야 하는 것들이 있었습니다.
      그래서 위와 같이 double dispatch되는 방식의 코드를 사용하게 되었습니다.

반응형

JPA를 이용해서 구현하는 프로젝트에서 두 개의 프로퍼티를 묶어서 식별자로 사용해야 하는 경우가 생겼다. 그래서, @EmbeddedId를 이용해서 ID 필드를 지정하였다. 이 프로젝트는 Spring Data JPA를 이용해서 Repository를 자동생성하고 있는데, @EmbeddedId 부분에서 문제가 발생했다. 아래 코드는 문제를 발생시킨 리포지토리의 인터페이스이다.


public interface FollowRepository extends Repository<Follow, FollowId> {


    Page<Follow> findAll(Specification<Follow> spec, Pageable pageable);

...

}


Follow의 식별자의 타입은 FollowId 클래스이고, FollowId 클래스는 두 개의 프로퍼티를 갖고 있다. 그런데, 위 코드에서 findAll()을 실행하는 순간에 다음과 같은 쿼리가 실행되면서 문제가 발생했다.


select count((follow0_.FOLLOWING_ID, follow0_.USER_ID)) as col_0_0_ 

from FOLLOW follow0_ 

where ....


DBMS로 오라클을 사용하고 있는데, 위 count() 부분에서 쿼리 오류가 발생한 것이다. 처음엔 Spring Data JPA 문제일까 해서 커스텀 구현을 넣어 보았다.


public class FollowRepositoryImpl implements FollowRepositoryCustom {


    @PersistenceContext

    private EntityManager entityManager;


    @Override

    public Page<Follow> findAll(Specification<Follow> spec, Pageable pageable) {

        List<Follow> result = getResultList(spec, pageable);

        long total = count(spec);

        return new PageImpl<Follow>(result, pageable, total);

    }


    private List<Follow> getResultList(Specification<Follow> spec,

            Pageable pageable) {

        ...

        return query.getResultList();

    }


    private long count(Specification<Follow> spec) {

        CriteriaBuilder cb = entityManager.getCriteriaBuilder();

        CriteriaQuery<Long> c = cb.createQuery(Long.class);

        Root<Follow> root = c.from(Follow.class);

        c.select(cb.count(root));

        Predicate predicate = spec.toPredicate(root, c, cb);

        c.where(predicate);


        TypedQuery<Long> query = entityManager.createQuery(c);

        return query.getSingleResult();

    }


}


하지만, 결과는 동일했다. JPA API를 직접 사용해도 변화가 없는 걸 봐서는 Spring Data JPA의 문제는 아니였다. Spring Data JPA 1.1 버전을 사용하고 있었는데, 이 버전은 하이버네이트 3.6.9를 기준으로 하고 있어서 해당 버전의 하이버네이트를 사용했었다.


하이버네이트의 버전 문제가 아닐까해서 범위를 좀 더 좁혀서 구글링을 해 봤더니 하이버네이트에서 이런 문제가 발생하고 있는 듯 했다. (관련 문의가 포럼에 있었다.) 좀 더 파 볼까 하다가, 하이버네이트 버전을 올려보기로 결심했다. 하이버네이트 버전을 3.6.9에서 4.1.4로 올렸다.


결과는? 야호! 테스트케이스의 녹색바! 에러가 없어졌다. 처음부터 하이버네이트 버전 업부터 할 걸, 괜히 코드 작성했다. 오픈소스에서 뭔가 이상한 문제가 있으면 먼저 버전을 올려보라는 걸 다시 한 번 확인했다.


  1. 권남 2012.07.12 17:37

    @IdClass를 사용할때도 마찬가지 현상이 발생합니다.
    @IdClass/MySQL

  2. PKH갈휘 2012.07.16 22:22

    버전업후에 말끔해졌네요, 감사합니다.

반응형

ORM!! 오늘 한번 더 ORM을 추종할 수 밖에 없는 일이 벌어졌다. 경험한 내용이 흥분도 되고 ORM의 좋음을 공유하고자 이렇게 글을 남긴다.


최초의 설계: 별도 클래스, 별도 테이블


현재 진행하는 프로젝트에서 컨텐츠에 대한 모델을 설계할 때 컨텐츠 종류마다 별도 테이블과 별도 클래스로 구성하도록 설계를 진행했었다. 각 컨텐츠들이 비슷한 데이터를 갖고 있었지만, 절반은 다른 데이터를 갖고 있었다. 또한, 컨텐츠 타입은 두 개였고 컨텐츠마다 사용되는 영역이 달랐기 때문에, 상위 클래스에 공통 정보를 두지 않고 서로 계층 관계에 묶이지 않는 별도 클래스로 구성하였다. 물론, 테이블도 별도로 구성하였다.




변화의 압박: 서로 다른 컨텐츠 종류에 공통으로 적용되는 기능들


최초 개발하는 동안에는 크게 문제될 것이 없었다. 그런데, PoC 프로젝트를 거의 마무리하는 과정에서 1.0 버전의 기획으로 다음의 기능들이 추가되었다.

  • 모든 컨텐츠에 대해 댓글 달기
  • 모든 컨텐츠에 대해 좋아요 하기
  • 모든 컨텐츠에 대해 즐겨찾기 하기
  • 모든 컨텐츠에 연관 정보 넣기
  • 새로운 종류의 컨텐츠 타입 추가 및 새로운 타입 컨텐츠에도 댓글/좋아요/연관 정보 넣기
고민에 휩싸이기 시작했다. 예를 들어, 댓글 구현은 다음과 같이 두 가지 중 하나로 할 수 있다.
  • 각 컨텐츠 타입마다 댓글을 위한 테이블을 구분해서 만들고, 각 컨텐츠 타입별로 댓글 관련 인터페이스 묶음 구현하기.
  • 댓글 테이블에 컨텐츠 타입 보관 위한 컬럼을 추가하고, 한 묶음의 인터페이스로 각 컨텐츠 타입을 위한 댓글 기능 구현하기.

구현이야 할 수 있겠지만, 둘 다 딱히 마음에 안 들었는다. 첫 번째 방법은 완전히 동일한 데이터 구조, 구현 코드, 테이블이 중복된다는 점이 불만이었다.



두 번째 방법은 하나의 신규 테이블에서 서로 다른 테이블에 대한 참조를 가져야 할 수도 있다는 것이 거슬렸다. 예를 들어, 두 컨텐츠에 동일하게 추가되는 정보인 경우, 두 컨텐츠에 대한 외부키를 각각 갖고 있어야 했다.



또한, 두 방법 모두 2.0 버전에 새로운 컨텐츠 타입이 추가되거나 각 컨텐츠 타입에 공통으로 적용되는 기능이 추가되면 같은 짓을 반복해야 하는 동일한 문제점을 갖고 있었다.


그래서, 뭐가 문제일까 하고 잠시 고민해 본 결과, 모델링을 잘못 했다는 결론에 다다랐다. PoC 시점에서는 그리 잘못 되지 않았지만, 몇 달 후에 신규 기능들이 추가되면서  최초에 만들었던 모델로는 깔끔하게 처리할 수 없게 된 것이다. 이 모든 문제를 깔끔하게 해결하는 방법은 모델을 다시 정리하는 것 뿐이었다.


상속으로 풀기로 결심!


이 모든 문제의 근원은 추상화의 변경에서 비롯되었다. 최초에는 두 개의 컨텐츠를 논리적인 하나의 컨텐츠로 바라보지 않았다. 그런데, 댓글, 연관 정보, 좋아요 기능이 생기면서 개별 컨텐츠 타입이 아닌 '컨텐츠'라는 개념이 도출된 것이다. 따라서, 컨텐츠라는 새로운 상위 개념을 도출했고, 이 상위 개념에 대해서 '좋아요', '댓글' 등의 기능을 적용하기로 하였다. 새로운 모델은 다음과 같이 변경되었다.



컨텐츠라는 단일 개념을 표현하기 위해 Content를 출현시켰고, 댓글과 좋아요 등의 기능은 새롭게 추가한 Content에 대고 구현을 하도록 했다. 각 개별 타입들의 변이는 Content의 하위 타입으로 처리하였다. 그리고, 각 컨텐츠 타입마다 별도의 테이블을 가졌던 것에서 모든 컨텐츠 타입을 한 개의 테이블에 저장하기로 결정했다.


변경의 여파는?


모델을 새롭게 정의했으니, 이제 기존 코드에 변경할 차례이다. 시간은 대략 얼마나 걸렸을까? 주요 기능의 정상적인 동작을 확인하는 데 까지 40분 정도 (밖에 안) 걸렸다. 물론, 개발 과정이기 때문에 기존 데이터의 마이그레이션이나 변경 등의 이슈는 없었고, 단지, 테스트를 위해 마련해 둔 데이터를 변경하는 정도의 데이터 수정 작업이 있었다. 하지만, 그렇다고 하더라도 모델을 변경하고 테이블을 합친 것에 비하면 이는 정말 짧은 시간이다.


그렇다면 어떻게 이것이 가능했을까? 이는 ORM을 사용했기 때문이다. 실제로 모델을 변경하기 위해 한 작업은 다음과 같다.

  • 상위 Content 클래스를 추가하고 각 하위 타입에 대해 공통인 필드와 관련 메서드를 추가
  • 각 하위 Content 타입 클래스에서 상위 클래스로 옮겨간 필드 및 관련 메서드 제거
  • Content 및 하위 클래스에 대한 ORM 설정
  • 테스트 데이터 구성
  • 기능 테스트 (약 40% 진행)
위에서 가장 시간이 오래 걸린 작업은 테스트 데이터 구성과 기능을 테스트 한 시간이다. Content 및 하위 클래스 구성, 그리고 ORM 관련 설정에는 불과 10분 정도 밖에 소요되지 않았다. 테스트 데이터를 구성하는데 10분 정도가 소요되었고, 기능 테스트에 20분 정도가 소요되었다. 물론, 전체 기능 중에서 조회 위주로만 기능을 테스트 했지만, 놀라운 점은 조회 기능 테스트하는 과정에서 오류가 발생하지 않았다는 점이다. 게다가 위의 작업 내역을 보면 DB 연동을 처리하는 리포지토리는 변경도 하지 않았다. 오!! 놀랍지 않은가?

ORM이 아니였다면?


어떻게 이런 과감한 변경을 하면서도 짧은 시간에 변경이 가능했을까? 정답은 바로 ORM 때문이다.

만약 ORM이 아니였다면 관련된 쿼리들을 쫓아다니면서 테이블을 변경해주고, 쿼리를 변경해주는 등의 쌩노가나를 해야만 했을 것이다. 물론, 이 과정에서 짜증 유발과 함께 오타도 발생했을 거고, 누가 오타를 덜 내면서 쿼리를 변경해 내느냐가 그 사람의 (노가다) 능력으로 인정받았을 것이다.

하지만, 이 프로젝트에서는 ORM을 사용했기에, 매우 적은 스트레스를 받으며, 약간의 노가다 만으로, 그야말로 스마트하게 변경 작업을 진행할 수 있었다. 프로젝트를 진행하다보면, SQL로 했으면 거부감이 확 들었을 이런 종류의 변경에서부터 작게는 사소한 컬럼 추가까지 다양한 변경이 발생하게 되는데, 이만하면 ORM을 도입하기 위한 약간의 학습 비용은 프로젝트 전체로 봤을 때 그리고 향후 유지보수를 생각해 봤을 때 절대로 큰 비용이 아닐 것이다. 아니 오히려 매우 작은 비용일 것이다.

반응형

Confluence Wiki를 설치해 본 사람은 설치 과정이 참 편하다는 것을 알 수 있다. 홈디렉토리 경로만 지정해주면 주요 설정은 웹 어플리케이션을 처음 구동할 때 설정하게 된다. 예를 들어, JDBC 연결 정보를 웹 브라우저를 이용해서 입력하고, DB를 생성하는 작업을 설치 마법사와 같은 방식으로 진행하게 된다.


많은 웹 어플리케이션들이 스프링을 이용해서 기능을 구현하는 경우가 많은데, 스프링 기반의 웹 어플리케이션을 사용하는 경우 어떻게 설치 위자드를 제공할 수 있을까? 요즘 이런 고민을 해 봤는데 답은 아주 간단하다. 스프링의 리로드 기능을 사용하는 것이다.


예를 들기 위해 다음과 같은 아주 간단한 웹 어플리케이션을 하나 만들어 보자.

  • WAR 파일로 배포되는 웹 어플리케이션
  • 최초 실행 시 설정 과정
    • 설치 과정1: 설치 안내
    • 설치 과정2: DB 정보 입력
    • 설치 과정3: 설정 완료
      • 지정 디렉토리에 설정 파일 저장
    • 설정 완료 후, 서비스 제공
  • 설정 완료 후, 이후 WAS를 재시작할 경우 설정 없이 서비스 제공
    • 설정 여부는 설정 파일 존재 여부로 확인
간단함을 위해 다음과 같이 코드를 구성했다.
  • 설정 과정 위한 파일
    • SetupController 클래스: 설정 과정을 처리하는 컨트롤러
    • ContextReloader 클래스: 설정 완료된 경우 또는 이미 설정된 경우 서비스 제공하도록 스프링 컨테이너 리로딩
    • setup-config.xml: 설정 과정에서 사용되는 스프링 설정 파일
  • 실 서비스 위한 파일
    • HomeController: 서비스의 첫 화면을 위한 컨트롤러
    • application-config.xml: 실제 서비스 용 스프링 설정 파일
web.xml 설정은 설정 위한 설정으로

web.xml 파일은 설정 과정을 위한 정보를 담는다.

<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:/setup-config.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

setup-config.xml 파일은 다음과 같이 사용자의 설정 과정을 처리하기 위한 SetupController와 설정이 완료되면 스프링 컨테이너를 리로딩해주는 ContextReloader로 구성된다.


<?xml version="1.0" encoding="UTF-8"?>

<beans ...>

<bean id="contextReloader" class="org.chimi.rr4s.setup.ContextReloader">

</bean>


<bean class="org.chimi.rr4s.setup.SetupController">

<property name="contextReloader" ref="contextReloader" />

</bean>

         .... 스프링 MVC 관련 나머지 설정 들

</beans>


SetupController는 설정 과정을 처리하고 설정이 완료되면 설정 정보를 저장한 뒤에 ContextReloader를 이용해서 컨테이너를 리로딩한다. 아래는 SetupController의 예시 코드이다.


public class SetupController {


private ContextReloader contextReloader;


@RequestMapping("/home")

public String home() {

                // 설정 과정에서의 첫 화면은 설정 관련 안내 페이지

return "setup/home";

}


@RequestMapping("/setup/step1")

public String step1() {

                // 설정 폼을 보여주기 위한 처리

return "setup/step1";

}


@RequestMapping("/setup/save")

public String step2(...) {

saveConfig(); // 설정 저장

contextReloader.reloadContext(); // 리로딩

return "setup/completed";

}


private void saveConfig() {

FileOutputStream fos = new FileOutputStream(new File("./target/conf.properties"));

// 설정 파일을 지정한 경로에 저장

}


public void setContextReloader(ContextReloader contextReolader) {

this.contextReloader = contextReolader;

}

}


위 코드에서 핵심은 설정을 입력하면 설정 정보를 지정한 위치의 파일에 저장하고, ContextReloader.reloadContext()를 호출한다는 점이다. (실제로는 필요한 DB를 생성하는 과정 등이 포함되겠지만 여기서는 설명에 필요한 것만 보여주었다.)


ContextReloader는 다음과 같다.


public class ContextReloader implements ApplicationContextAware,

ApplicationListener<ContextRefreshedEvent> {


private ApplicationContext applicationContext;


public void reloadContext() {

if (applicationContext instanceof ConfigurableWebApplicationContext) {

ConfigurableWebApplicationContext configurableCtx = 

(ConfigurableWebApplicationContext) applicationContext;

String[] configLocations = { "classpath:/application-config.xml" };

configurableCtx.setConfigLocations(configLocations);

configurableCtx.refresh();

}

}


@Override

public void onApplicationEvent(ContextRefreshedEvent event) {

// 설정 확인

if (new File("./target/conf.properties").exists()) {

// 설정 존재하면 reloadContext();

reloadContext();

}

}


@Override

public void setApplicationContext(ApplicationContext context)

throws BeansException {

this.applicationContext = context;

}


}


ContextReloader는 스프링 컨테이너를 리로딩해야 하기 때문에 ApplicationContextAware를 implement해서 ApplicationContext를 전달받을 수 있도록 했다. 또한, 컨테이너가 초기화가 완료되는 이벤트를 받을 수 있도록 ApplicationListener를 implement하였다. 이 이벤트를 받는 이유는 뒤에서 다시 설명한다.


reloadContext() 메서드는 현재 스프링 컨테이너가 사용할 설정 파일을 application-config.xml로 변경한 뒤에 refresh()를 수행한다. 즉, reloadContext()를 호출하면 application-config.xml에 설정된 내용을 이용해서 스프링 컨테이너가 초기화된다. 따라서, 앞서 SetupController 클래스에서 설정을 완료한 뒤에 reloadContext()를 호출하게 되면, 실제 서비스를 위한 스프링 설정이 사용되는 것이다.


onApplicationEvent() 메서드는 ContextRefreshedEvent를 처리하는데, 이 이벤트를 처리하는 이유는 최초에 한 번만 설정 과정을 거치고 이후에는 거치지 않기 위함이다. 예를 들어, WAS를 재구동해야 하는 경우 기존에 이미 설정을 완료했다면 설정 화면이 아닌 서비스 화면이 나와야 할 것이다. 그런데, web.xml 파일은 설정 과정을 위한 setup-config.xml 파일을 사용하므로, 다시 설정 화면이 나올 것이다. 이를 방지하기 위해 컨테이너 초기화가 완료될 때 기존 설정 정보가 존재하는 지 확인하는 과정을 구현할 필요가 있는데, 컨테이너 초기화 시에 발생하는 이벤트가 ContextRefreshedEvent 이다. 따라서, ContextRefreshedEvent 이벤트를 수신하는 onApplicationEvent() 메서드에서 설정 파일이 존재하는 지 확인하고, 만약 존재한다면 서비스용 스프링 설정 파일로 컨테이너를 리로딩함으로써 서비스를 제공할 수 있게 된다.


반응형
JPA를 사용할 때 입력 값에 따라 동적으로 검색 조건을 변경하려면 Criteria API를 사용하면 되지만, 이게 참 이쁘지가 않고 코드를 만지다 보면 짜증이 나기도 한다. 최근에 하는 프로젝트에서 Spring Data JPA를 선택했는데, Spring Data JPA 덕분에 검색 조건을 조합할 때 발생하는 짜증을 많이 감소시킬 수 있게 되었다. Spring Data JPA가 Repository 또는 Dao의 인터페이스만 정의하면 알아서 구현체를 만들어준다는 장점이 정말 좋은데, 그에 못지 않게 검색 조건을 추상화한 Specification을 지원한다는 것 역시 아주 마음에 든다.

Specification을 사용하는 방법은 아주 간단한데, 그 방법은 다음과 같다.
  1. Specification을 입력 받도록 Repository 인터페이스를 정의하기
  2. 검색 조건을 모아 놓은 클래스 만들기
  3. 검색 조건을 조합한 Specification 인스턴스를 이용해서 검색하기
하나씩 만들어보자.

1. Specification을 입력 받는 Repository 메서드 정의
Spring Data에서 Specification은 검색 조건을 추상화하기 위해 사용되는데, 이 추상화된 검색 조건을 이용해서 데이터를 검색하도록 구현하려면 다음과 같이 Specification을 검색 메서드의 파라미터로 추가해주기만 하면 된다.

public interface ContentRepository extends Repository<Content, Long> {

Page<Content> findAll(Specification<Content> spec, Pageable pageable);

}


2. 검색 조건 모아 놓은 클래스 만들기
다음에 할 일은 검색 조건을 모아 놓은 클래스를 하나 만드는 것이다. 이 클래스를 구현할 때 JPA의 Criteria API를 사용하게 된다. 아래는 구현 예이다.

public class ContentSpecs {


    public static Specification<Content> titleLike(final String keyword) {

        return new Specification<Content>() {

            @Override

            public Predicate toPredicate(Root<Content> root,

                    CriteriaQuery<?> query, CriteriaBuilder cb) {

                return cb.like(root.get(Content_.title), "%" + keyword + "%");

            }

        };

    }


    public static Specification<Content> category(final Category category) {

        return new Specification<Content>() {

            @Override

            public Predicate toPredicate(Root<Content> root,

                    CriteriaQuery<?> query, CriteriaBuilder cb) {

                return cb.equal(root.get(Content_.category), category);

            }

        };

    }

    ... // 기타 검색 조건 생성 클래스

}


위에서 눈여겨 볼 점은 Criteria를 이용해서 검색 조건을 지정하는 코드가 category(), titleLike()와 같은 메서드로 추상화된다는 점이다.

3. 검색 조건 조합해서 검색하기
이제 할 일은 앞서 만든 검색 조건을 모아 놓은 클래스를 이용해서 검색 조건을 조합하고 Repository를 이용해서 결과를 가져오는 것이다. 아래는 예시 코드이다.

Specifications<Content> spec = Specifications.where(ContentSpecs.titleLike(query));
if (category != null) {
     spec = spec.and(ContentSpecs.category(category));
}
Page<Content> p = contentRepository.findAll(spec); // david hong 님의 지적으로 수정합니다.


Spring Data JPA가 제공하는 Specifications를 사용하면 두 개 이상의 Specification을 AND나 OR 등으로 조합할 수 있다.


왜 좋지?
이 방식이 마음에 드는 이유는 검색 조건을 생성할 때 도메인의 용어를 사용할 수 있게 된다는 점이다. 앞서 만들었던 ContentSpecs.titleLike()만 보더라도 이 검색 조건이 제목을 LIKE 검색하기 위한 조건이라는 것을 알 수 있다.

마음에 드는 또 다른 이유는 검색 조건을 조합하는 코드에서 Criteria와 같은 실제 구현 기술의 타입을 사용하지 않는다는 점이다. 사용자는 ContentSpecs.category() 메서드를 사용해서 검색 조건을 추상화한 Specification 객체를 구하기만 하면 된다. 내부적으로 JPA의 Criteria를 사용하는지의 여부는 전혀 알 필요가 없다.

마지막으로 이 방식은 누구나 쉽게 만들 수 있을 만큼 쉽다. 처음 이 방식을 접하는 경력자 뿐만 아니라 이제 3개월 된 신입들도 별다른 어려움 없이 위 방식에 적응했다.

불필요한 코드 작성을 줄이고 그러면서도 이전보다 쉽게 DB 연동 부분을 처리하고 싶은 개발자분들에게 JPA + Spring Data JPA/Specification의 조합을 아주 강력하게 권장한다. 


  1. david hong 2013.03.15 16:53

    spec 설정과 repository 의 findAll 호출간에 relation 이 없는데 findAll(spec,pageRequest) 와 같은 형태로 전달해야 하지 않나요?

    • 최범균 madvirus 2013.03.17 20:38 신고

      글에 오류가 있었네요. findAll(pageRequest)가 아니라 findAll(spec) 이었습니다. 지적하신 부분 수정해 놓았습니다. 잘못된 부분 알려주셔거 고맙습니다.

  2. ncrash 2013.12.06 17:10

    좋은 잘 보고 있습니다 ^^

    저의 개발 환경에서는 Hibernate Metamodel generator를 사용하지 않고 개발했던 터라
    Content_ 처럼 자동생성 되는 클래스 없어서 해매다 답은 찾아서 적용하고 있습니다

    http://stackoverflow.com/questions/8634241/how-to-make-a-custom-search-by-spring-data-jpa-framework/8634316#8634316


    헌데 Specifications를 통해 적용하는데 한가지 문제가 있어 질문 드립니다.
    where에 대한 조건은 적용이 잘 됩니다.

    헌데 and조건 부터는 먹지를 않네요.. 하이버네이트 버전 문제인가 싶어 버전을 바꿔봐도 동일하구요.

    Specifications<TransactionLog> spec = Specifications.where(TransactionLogSpecs.startedDateBetween(dt.minusDays(1).toDate(), dt.toDate()));
    spec.and(TransactionLogSpecs.AppIdLike("asdf"));

    • 최범균 madvirus 2013.12.06 18:16 신고

      and()를 호출해줄 때 마다 새로운 객체가 생성됩니다. (기존 정보가 변경되는 게 아니죠.) 그래서 아래와 같이 and() 호출해 줄 때 마다 할당을 새로 해 주어야 합니다.

      spec = spec.and(....);
      spec = spec.and(....);

    • 일용직노동자 2020.02.17 15:04

      저 또한 메타모델 제너레이터를 사용하지않고있어서 헤매이고 있었는데 매우 도움되었습니다! 감사합니다!

  3. ncrash 2013.12.08 11:15

    답변을 엄청 빨리 달아주셨는데 이제서야 확인했네요
    답변 감사 드립니다 ^^

    전 StringBuilder.appned() 메소드와 동일한 형태로 처리되는 줄 알았는데 헛다리를 ㅠ.ㅠ

    그렇다면 위 포스팅 내용도 아래와 같이 변경되야 하는거 아닌지 궁금하네요

    if (category != null) {
    spec = spec.and(ContentSpecs.category(category));
    }

    • 최범균 madvirus 2013.12.10 09:11 신고

      그러고 보니 비슷한 말을 동료한테도 들었던 기억이 나네요. 고쳐나야겠어요.

  4. 자림이 2020.09.23 10:29 신고

    참고하고 갑니다~

  5. lu 2021.07.01 18:16

    Hibernate Metamodel generator를 kotlin dsl gradle 에서 사용할땐

    plugins {
    kotlin("kapt") version "1.5.0"
    }
    dependencies {
    kapt("org.hibernate:hibernate-jpamodelgen:5.4.12.Final")
    }

    를 추가해주면 되네요.

반응형
Spring MVC를 이용하여 MVC 기반의 웹 어플리케이션을 개발하는 방법을 살펴본다.

Spring MVC

비록, Spring이 스트러츠를 비롯하여 다양한 웹 프레임워크와 비교적 잘 연동되는 편이긴 하지만, 서로 다른 두 프레임워크를 연동하기 위해서는 설정의 중복 등 개발 과정에서 불편함이 존재한다. 서로 잘 맞지 않는 단추를 억지로 끼워 맞추는 것 같다.

Spring 자체적으로 제공하는 MVC 프레임워크를 사용하면, Spring이 제공하는 AOP, 트랜잭션 처리, DI 등의 기능을 그대로 사용하면서 MVC 패턴에 기반하여 웹 어플리케이션을 개발할 수 있다. 또한, 스트러츠와 Spring을 연동하기 위해 설정의 중복과 같은 개발 과정상의 불편을 해소할 수도 있다.

본 글에서는 Spring MVC의 구성에 대해서 살펴보고, 실제로 Spring MVC를 사용하여 웹 어플리케이션을 개발하는 기본적인 방법을 살펴보도록 하겠다. 그외 Spring MVC를 이용한 웹 어플리케이션에 대해 좀더 자세한 내용이 알고 싶다면 http://www.springframework.org/documentation 사이트를 참고하기 바란다.

Spring MVC의 구성 및 실행 흐름

다른 MVC 기반의 프레임워크와 마찬가지로 Spring MVC도 컨트롤러를 사용하여 클라이언트의 요청을 처리하게 된다. 이 컨트롤러의 역할을 하는 것이 DispatcherServlet인데, DispatcherServlet을 비롯하여 Spring MVC의 주요 구성 요소는 표 1과 같다.

구성 요소 설명
DispatcherServlet 클라이언트의 요청을 전달받는다. Controller에게 클라이언트의 요청을 전달하고, Controller가 리턴한 결과값을 View에 전달하여 알맞은 응답을 생성하도록 한다.
HandlerMapping 클라이언트의 요청 URL을 어떤 Controller가 처리할지를 결정한다.
Controller 클라이언트의 요청을 처리한 뒤, 그 결과를 DispatcherServlet에 알려준다. 스트러츠의 Action과 동일한 역할을 수행한다.
ViewResolver Commander의 처리 결과를 보여줄 View를 결정한다.
View Commander의 처리 결과를 보여줄 응답을 생성한다.

이들 주요 구성 요소간의 메시지 흐름은 그림 1과 같다.


각 흐름을 좀더 자세하게 설명하면 다음과 같다.

  1. 클라이언트의 요청이 DispatcherServlet에 전달된다.
  2. DispatcherServlet은 HandlerMapping을 사용하여 클라이언트의 요청이 전달될 Controller 객체를 구한다.
  3. DispatcherServlet은 Controller 객체의 handleRequest() 메소드를 호출하여 클라이언트의 요청을 처리한다.
  4. Controller.handleRequest() 메소드는 처리 결과 정보를 담은 ModelAndView 객체를 리턴한다.
  5. DispatcherServlet은 ViewResolver로부터 처리 결과를 보여줄 View를 구한다
  6. View는 클라이언트에 전송할 응답을 생성한다.
여기서 개발자가 직접 개발해주어야 하는 부분은 클라이언트의 요청을 처리할 Commander 클래스와 클라이언트에 응답 결과 화면을 전송할 JSP나 Velocity 템플릿 등의 View 코드이다. 나머지, DispatcherServlet이나 HandlerMapping, ViewResolver 등은 Spring이 제공하는 기본 구현체를 사용하면 된다.

Spring MVC를 이용한 웹 어플리케이션 개발

Spring MVC를 이용하여 웹 어플리케이션을 개발하는 과정은 다음과 같다.

  1. 클라이언트의 요청을 받을 DispatcherServlet을 web.xml 파일에 설정한다.
  2. 요청 URL과 Controller의 매핑 방식을 설정한다.
  3. Controller의 처리 결과를 어떤 View로 보여줄 지의 여부를 결정하는 ViewResolver를 설정한다.
  4. Controller를 작성한다.
  5. 뷰 영역의 코드를 작성한다.
DispatcherServlet 설정 및 Spring 콘텍스트 설정

Spring MVC를 사용하기 위해서는 가장 먼저 web.xml 파일에 DispatcherServlet 설정을 추가해주어야 한다. DispatcherServlet은 서블릿 클래스로서 다음과 같이 서블릿 설정 및 서블릿 매핑 설정을 web.xml 파일에 추가해주면 된다.

<?xml version="1.0" ?>

<web-app version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
                        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

위 설정은 모든 *.do로 들어오는 요청을 DispatcherServlet이 처리하도록 하고 있다.

DispatcherServlet은 설정 파일에서 지정한 서블릿 이름을 사용하여 Spring 설정 파일을 로딩한다. 예를 들어, 위 코드의 경우 DispatcherServlet의 이름이 "example"인데, 이 경우 사용되는 Spring 설정 파일은 'WEB-INF/example-servlet.xml' 이다.

만약 기본적으로 사용되는 Spring 콘텍스트 설정 파일이 아닌 다른 이름의 파일을 사용하고 싶다면 다음과 같이 contextConfigLocation 초기화 파라미터에 사용할 설정 파일의 목록을 지정해주면 된다.

    <servlet>
        <servlet-name>multipleConfig</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                /WEB-INF/mvc1.xml,
                /WEB-INF/mvc2.xml
            </param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>

이때 각 설정 파일은 콤마를 사용하여 구분한다. 만약 각 설정 파일에서 동일한 이름의 빈을 지정했다면 더 뒤에 위치한 설정 파일에 명시된 빈이 우선순위를 갖는다.

HandlerMapping 설정

클라이언트의 요청을 Spring의 DispatcherServlet이 처리하도록 설정했다면, 다음으로 해야 할 작업은 어떤 HandlerMapping을 사용할지의 여부를 지정하는 것이다. HandlerMapping은 클라이언트의 요청을 어떤 Commender가 수행할 지의 여부를 결정해주는데, 표 2와 같이 두 개의 구현체가 주로 사용된다.

구현체 설명
BeanNameUrlHandlerMapping 요청 URI와 동일한 이름을 가진 Controller 빈을 매핑한다.
SimpleUrlHandlerMapping Ant 스타일의 경로 매핑 방식을 사용하여 URI와 Controller 빈을 매핑한다.

BeanNameUrlHandlerMapping

BeanNameUrlHandlerMapping은 요청 URI와 동일한 이름을 갖는 Controller 빈으로 하여금 클라이언트의 요청을 처리하도록 한다. 예를 들어, http://some.com/hello.do 와 같은 요청 URL에 대해 "/hello.do" 라는 이름을 가진 Controller 빈이 해당 요청을 처리하도록 한다. 아래 코드는 BeanNameUrlHandlerMapping를 사용하는 경우의 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"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanNameUrlMapping" 
        class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
    
    <bean name="/hello.do"
        class="net.daum.ts.techReport.report2.springmvc.HelloController" />
    
    <bean name="/login.do"
        class="net.daum.ts.techReport.report2.springmvc.LoginController" />
    
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

만약 요청 URL이 http://some.com/login.do 였다면, 이름이 "/login.do"인 LoginController가 해당 요청을 처리하게 된다. 비슷한 이유로, http://some.com/hello.do 요청은 HelloController가 처리하게 된다.

HandlerMapping 빈을 따로 생성하지 않은 경우 DispatcherServlet은 기본적으로 HandlerMapping의 구현체로 BeanNameUrlHandlerMapping을 사용한다. 따라서, 위 코드에서 HandlerMapping과 관련된 설정은 생략할 수 있다.

SimpleUrlHandlerMapping

SimpleUrlHandlerMapping은 Ant 스타일의 매핑 방식을 사용하여 Controller를 매칭한다. 아래 코드는 SimpelUrlHandlerMapping을 사용하여 클라이언트의 요청 URL과 매핑될 Controller를 지정하는 예를 보여주고 있다.

<?xml version="1.0" encoding="UTF-8" ?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanNameUrlMapping" 
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /content/*.html=contentController
                /**/help.html=helpController
            </value>
        </property>
    </bean>
    
    <bean name="contentController" 
        class="net.daum.ts.techReport.report2.springmvc.ContentController" />
    
    <bean name="helpController" 
        class="net.daum.ts.techReport.report2.springmvc.HelpController" />
    
    <bean id="viewResolver" 
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

SimpleUrlHandlerMapping은 mappings 프로퍼티를 통해서 매핑 설정 목록을 입력받는다. 이때 설정의 이름은 Ant 스타일의 URL 경로를 의미하며, 값은 매핑될 Controller 빈의 이름을 의미한다. 예를 들어, 위 예제의 경우 http://some.com/content/1.html 이나 http://some.com/content/mvc.html 요청이 들어오면 contentController가 처리하도록 매핑하였다.

SimpleUrlHandlerMapping에서 사용되는 규칙은 다음과 같다.

  • ? - 한 글자에 매칭된다.
  • * - 0 개 이상의 글자에 매칭된다.
  • ** - 0개 이상의 디렉토리에 매칭된다.
아래는 org.springframework.util.AntPathMatcher API 문서에 예시로 나온 몇 가지 매핑 설정의 예이다.

  • com/t?st.jsp - com/test.jsp, com/tast.jsp 또는 com/txst.jsp 등에 매칭된다.
  • com/*.jsp - com 디렉토리에 있는 모든 .jsp에 매칭된다.
  • com/**/test.jsp - com 경로 아래에 위치한 모든 test.jsp와 매칭된다.
  • org/springframework/**/*.jsp - org/springframework 경로 아래에 위치한 모든 jsp와 매칭된다.
두 HandlerMapping 구현체의 공통 프로퍼티

앞서 살펴본 두 HandlerMapping 구현 클래스는 AbstractUrlHandlerMapping 클래스를 상속받고 있으며, AbstractUrlHandlerMapping 클래스는 AbstractHandlerMapping 클래스를 상속받고 있다. 이 두 클래스는 공통으로 사용되는 몇 가지 프로퍼티를 제공하고 있는데, 먼저 AbstractHandlerMapping 클래스는 다음과 같은 프로퍼티를 제공하고 있다.

프로퍼티 설명
interceptors 사용할 인터셉터의 목록
defaultHandler 매칭되는 핸들러를 찾지 못할 때 사용할 기본 핸들러
order 하나의 서블릿 콘텍스트에서 다수의 HandlerMapping을 사용할 수 있는데, 이 경우 HandlerMapping 간의 우선순위를 지정한다.

AbstractUrlHandlerMapping 클래스가 제공하는 프로퍼티 목록은 표 4와 같다.

프로퍼티 설명
alwaysUseFullPath 웹 어플리케이션 콘텍스트의 경로를 포함할 지의 여부를 결정한다. 예를 들어, 콘텍스트 경로가 /main 일때, http://some.com/main/a.html 에 대해, 이 값이 true면 /main/a.html 이 사용된다. 이 값이 false이면 /a.html이 사용된다. 기본값은 false이다.
urlDecode HandlerMapping에 요청 URL을 전달하기 전에 특정 인코딩을 사용해서 디코딩할지의 여부를 결정한다. 이 값을 true로 지정하면 요청에 명시된 인코딩이나 ISO-8859-1을 사용하여 디코딩한다. 기본값은 false이다.
lazyInitHandlers 싱글톤 타입의 핸들러에 대해 lazy initialization을 적용할지의 여부를 지정한다. 기본 값은 false이다.

ViewResolver 설정

HandlerMapping을 설정했다면, 이제 ViewResolver를 설정할 차례이다. ViewResolver는 Controller의 실행 결과를 어떤 뷰를 보여줄지의 여부를 결정하는 기능을 제공한다. 주로 사용되는 ViewResolver에는 다음의 두 가지가 존재한다.

  • InternalResourceViewResolver - JSP를 사용하여 뷰를 생성한다.
  • VelocityViewResolver - Velocity 템플릿 엔진을 사용하여 뷰를 생성한다.
InternalResourceViewResolver

InternalResourceViewResolver는 JSP를 사용하여 Commander의 결과 뷰를 생성할 때 사용된다. 아래 코드는 InternalResourceViewResolver의 설정 예를 보여주고 있다.

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="beanNameUrlMapping"
        class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
    
    <bean name="/hello.do"
        class="net.daum.ts.techReport.report2.springmvc.HelloController" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

InternalResourceViewResolver는 다음과 같이 두 개의 프로퍼티를 입력받는다.

  • prefix - Controller가 리턴한 뷰 이름 앞에 붙을 접두어
  • suffix - Controller가 리턴한 뷰 이름 뒤에 붙을 확장자
예를 들어, HelloController가 처리 결과를 보여줄 뷰의 이름으로 "hello"를 리턴했다고 하자. 이 경우 InternalResourceViewResolver에 의해 사용되는 뷰는 "/view/hello.jsp"가 된다. 따라서, /view/hello.jsp가 생성한 응답 결과 화면이 클라이언트에 전달된다.

VelocityViewResolver

VelocityViewResolver는 Velocity 템플릿을 사용하여 뷰를 생성할 때 사용된다. 아래 코드는 VelocityViewResolver의 설정 예를 보여주고 있다.

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean name="/hello.mul"
        class="net.daum.ts.techReport.report2.springmvc.HelloController" />
    
    <bean id="velocityConfig" 
        class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/view_vm/" />
        <property name="velocityProperties">
            <value>
                input.encoding=UTF-8
                output.encoding=UTF-8
            </value>
        </property>
    </bean>
    
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html; charset=UTF-8" />
        <property name="cache" value="true" />
        <property name="prefix" value="" />
        <property name="suffix" value=".vm" />
    </bean>
</beans>

VelocityViewResolver를 사용하기 위해서는 먼저 Velocity의 설정 정보를 관리해주는 VelocityConfigurer를 생성해주어야 한다. VelocityConfigurer에서 사용가능한 주요 프로퍼티는 다음과 같다.

  • resourceLoaderPath - Velocity 템플릿이 위치하는 경로 (웹 콘텍스트 루트 기준)
  • velocityProperties - Velocity 설정 프로퍼티
예를 들어, 위 코드의 경우는 웹 어플리케이션 루트에 위치한 /view_vm/ 디렉토리에 Velocity 템플릿 파일이 위치한다는 것을 의미한다.

VelocityViewResolver를 설정할 때에는 다음의 프로퍼티를 알맞게 지정해준다.

  • contentType - 응답의 컨텐트 타입을 지정한다.
  • prefix - InternalResourceViewResolver와 동일
  • suffix - InternalResourceViewResolver와 동일
위 설정의 경우, Controller가 선택한 뷰가 "hello"라고 한다면 /view_vm/hello.vm 파일이 템플릿으로 선택된다. 또한 생성되는 뷰의 컨텐트 타입은 "text/html" 이고 캐릭터셋은 UTF-8이 된다.

Controller 구현

지금까지는 환경 설정과 관련된 부분에 대해서 설명했는데, 이제 본격적으로 클라이언트의 요청을 처리하는 데 사용되는 Controller의 구현에 대해서 살펴보도록 하자. Controller를 구현하는 가장 간단한 방법은 Controller 인터페이스를 implements 하는 것이다. 하지만, Controller 인터페이스를 직접적 implements 하기 보다는, Controller 인터페이스를 implements 하고 몇 가지 추가적인 기능을 구현하고 있는 클래스들을 상속받아 Controller를 구현하는 것이 일반적이다.

Controller 인터페이스

org.springframework.web.servlet.mvc.Controller 인터페이스는 다음과 같이 정의되어 있다.

public interface Controller {
    
    ModelAndView handleRequest(HttpServletRequest request,
                               HttpServletResponse response)
                               throws Exception

}

Controller를 구현하는 가장 단순한 방법은 Controller 인터페이스를 구현한 뒤, handleRequest() 메소드를 알맞게 구현하는 것이다. 아래 코드는 구현 예이다.

package net.daum.ts.techReport.report2.springmvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class SimpleController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        // request를 사용하여 클라이언트 요청 분석 후
        // 클라이언트 요청에 따른 알맞은 처리
        ModelAndView mav = new ModelAndView("hello");
        mav.addObject("greeting", "안녕하세요");
        return mav;
    }

}

handleRequest() 메소드는 파라미터로 전달받은 request 객체와 response 객체를 사용하여 클라이언트가 요청한 기능을 알맞게 구현하면 된다.

handleRequest() 메소드는 ModelAndView 객체를 리턴한다. ModelAndView 객체는 클라이언트의 요청을 처리한 결과를 보여줄 뷰 페이지와 관련해서 다음과 같은 정보를 제공한다.

  • 결과를 보여줄 뷰 이름 지정 : 위 코드의 경우 "hello"를 뷰로 선택
  • 뷰에서 사용할 모델 데이터 : 위 코드의 경우 addObject() 메소드로 추가한 객체, 뷰에서는 "greeting" 이라는 이름을 사용하여 데이터에 접근할 수 있다.
예를 들어, ViewResolver를 다음과 같이 설정했다고 해 보자.

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/view/" />
        <property name="suffix" value=".jsp" />
    </bean>

위 viewResolver는 "/view/" + 뷰이름 + ".jsp"를 뷰로 사용하게 된다. SimpleController는 뷰 이름으로 "hello"를 리턴하므로, "/view/hello.jsp"가 뷰로 사용된다. 또한, 이 뷰에서는 다음과 같이 ModelAndView에 저장된 객체를 사용할 수 있게 된다.

<%@ page contentType="text/html; charset=UTF-8" %>

<strong>${greeting}</strong> ..

AbstractController를 이용한 구현

Controller를 구현할 때에는 Controller 인터페이스를 직접 구현하기보다는, AbstractController 추상 클래스를 상속 받아 구현하는 경우가 더 많다. AbstractController 클래스는 Controller 인터페이스를 implements 하여 추가적인 기능을 제공하고 있다.

AbstractController 클래스는 표 5와 같은 프로퍼티를 제공하고 있으며, 이 프로퍼티를 사용하여 HTTP Session, 캐시 등의 설정을 제어할 수 있다.

프로퍼티 설명
supportedMethods Controller가 처리할 수 있는 메소드를 지정한다. GET과 POST 등 처리할 수 있는 메소드를 지정할 수 있다. 만약 지원되지 않는 메소드를 사용하여 요청이 들어오면 ServletException 예외를 발생시킨다.
requiresSession Controller가 HTTP Session을 필요로 하는 지의 여부를 지정한다. 이 값이 true인 경우, 클라이언트와 관련된 세션이 존재하지 않으면 ServletException 예외를 발생시킨다.
synchronizeSession HTTP Session을 사용하여 Controller에 대한 처리를 동기화 할지의 여부를 지정한다.
cacheSeconds HTTP 응답에 캐시와 관련된 디렉티브를 생성할지의 여부를 지정한다. 기본 값은 -1이며, 이 경우 캐시 디렉티브가 응답 결과에 포함되지 않는다.
useExpiresHeader HTTP 1.0에 호환되는 "Expires" 헤더의 사용 여부를 지정한다. 기본값은 true 이다.
useCacheHeader HTTP 1.1에 호환되는 "Cache-Control' 헤더의 사용 여부를 지정한다. 기본값은 true 이다.

AbstractController 클래스를 상속받아 Controller를 구현하는 클래스는 아래 코드와 같이 handleRequestInternal() 메소드를 구현해주면 된다.

package net.daum.ts.techReport.report2.springmvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

public class HelloController extends AbstractController {

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        ModelAndView view = new ModelAndView("hello");
        view.addObject("greeting", "안녕하세요");
        return view;
    }

}

AbstractController 클래스의 handleRequest() 메소드는 내부적으로 필요한 작업을 수행한 뒤, handleRequestInternal() 메소드를 호출한다. 따라서, AbstractController 클래스를 상속받는 경우에는 handleRequest() 메소드가 아닌 handlerRequestInternal() 메소드를 구현해주어야 한다.

AbstractCommandController를 이용한 파라미터 값 전달

요청 파라미터의 값을 특정한 객체에 저장하고 싶다면 AbstractCommandController 클래스를 사용하면 된다. AbstractCommandController 클래스를 상속받은 클래스는 다음과 같이 handle() 메소드를 알맞게 구현해주면 된다.

package net.daum.ts.techReport.report2.springmvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractCommandController;

public class LoginController extends AbstractCommandController {

    @Override
    protected ModelAndView handle(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException errors)
            throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        authService.authenticate(loginCommand.getId(), loginCommand.getPassword());
        return new ModelAndView("loginSuccess", "loginCommand", loginCommand);
    }

}

handle() 메소드의 세 번째 파라미터는 파라미터의 값을 저장한 command 객체이다. 이 command 객체의 타입을 지정하기 위해서는 다음과 같이 Spring 설정 파일에서 commandClass 프로퍼티의 값으로 클래스의 이름을 지정해주면 된다.

    <bean name="/login.do"
        class="net.daum.ts.techReport.report2.springmvc.LoginController">
        <property name="commandClass" 
            value="net.daum.ts.techReport.report2.springmvc.LoginCommand" />
    </bean>

handle() 메소드에 전달되는 command 파라미터는 commandClass 프로퍼티에 지정한 타입의 객체가 된다. 이때 파라미터와 생성할 command 객체의 프로퍼티 사이의 매핑은 자바빈 스타일을 따른다. 예를 들어, 파라미터 이름이 id 인 경우 setId() 메소드를 통해 파라미터 값이 전달되며, 파라미터 이름이 address 인 경우 setAddress() 메소드를 통해 파라미터 값이 전달된다.

SimpleFormController를 이용한 폼 전송 처리

SimpleFormController는 AbstractCommandController와 마찬가지로 파라미터의 값을 객체에 저장할 때 사용된다. 차이점이 있다면, SimpleFormController는 폼 전송 개념이 적용되어 있다는 것이다. SimpleFormController는 POST 방식으로 요청이 들어올 경우 doSubmitAction() 메소드를 통해 요청을 처리하는 반면에, GET 방식으로 요청이 들어오면 "formView" 프로퍼티로 지정한 뷰를 출력한다.

SimpleFormController는 클라이언트가 POST로 전송한 데이터는 commandClass 프로퍼티로 지정한 타입의 객체에 저장되어 doSubmitAction() 메소드에 전달된다. 아래 코드는 SimpleFormController의 구현 예를 보여주고 있다.

package net.daum.ts.techReport.report2.springmvc;

import org.springframework.web.servlet.mvc.SimpleFormController;

public class LoginFormController extends SimpleFormController {

    @Override
    protected void doSubmitAction(Object command) throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        if (!loginCommand.getId().equals("madvirus")) {
            throw new LoginFailException("invalid id: "+loginCommand.getId());
        }
    }

}

클라이언트가 요청한 작업을 성공적으로 수행하지 못한 경우, doSubmitAction() 메소드는 예외를 발생시켜서 올바르게 처리하지 못했음을 알리게 된다. 이 경우, 예외 타입에 따라 알맞게 예외 처리를 해 주어야 한다.

GET 방식으로 데이터를 전송하는 경우에는 doSubmitAction() 메소드가 호출되지 않는다. 대신, formView 프로퍼티로 지정한 뷰를 보여준다. 아래 코드는 SimpleFormController 클래스를 상속받은 클래스의 구현 예를 보여주고 있다.

    <bean name="/loginUsingForm.do"
        class="net.daum.ts.techReport.report2.springmvc.LoginFormController">
        <property name="commandClass" 
            value="net.daum.ts.techReport.report2.springmvc.LoginCommand" />
        <property name="formView" value="loginForm" />
        <property name="successView" value="loginSuccess" />
    </bean>

뷰 영역에 데이터를 전달하고 싶다면 다음과 같이 onSubmit() 메소드를 추가적으로 구현해주면 된다.

package net.daum.ts.techReport.report2.springmvc;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;

public class LoginFormController extends SimpleFormController {

    @Override
    protected void doSubmitAction(Object command) throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        if (!loginCommand.getId().equals("madvirus")) {
            throw new LoginFailException("invalid id: "+loginCommand.getId());
        }
    }
    
    @Override
    protected ModelAndView onSubmit(Object command, BindException errors) 
    throws Exception {
        try {
            ModelAndView mav = super.onSubmit(command, errors);
            mav.addObject("loginCommand", command);
            return mav;
        } catch(Exception e) {
            throw e;
        }
    }
}

위 코드에서 super.doSubmit() 메소드를 호출하면 doSubmitAction() 메소드가 호출된다. 따라서, doSubmitAction() 메소드가 성공적으로 수행되면 ModelAndView.addObject() 메소드를 사용하여 뷰에 데이터를 전달할 수 있게 된다.

뷰의 구현

뷰에서는 ModelAndView 객체에 저장한 객체를 사용할 수 있다. 예를 들어, Controller에서 다음과 같이 ModelAndView에 데이터를 저장했다고 해 보자.

    protected ModelAndView handle(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException errors)
            throws Exception {
        LoginCommand loginCommand = (LoginCommand) command;
        ModelAndView mav = new ModelAndView("loginSuccess");
        mav.addObject("loginCommand", loginCommand);
        return mav;
    }

이 경우 "loginSuccess" 뷰에 해당되는 JSP는 다음과 같이 EL(Expression Language)이나 request.getAttribute() 등의 메소드를 사용하여 ModelAndView 객체에 저장된 값을 사용할 수 있다.

로그인 정보: ${loginCommand.id}
<%
    LoginCommand cmd = (LoginCommand) request.getAttribute("loginCommand");
%>

Velocity 템플릿에서도 다음과 같이 Velocity의 템플릿 언어를 사용하여 ModelAndView 객체에 저장된 값을 사용할 수 있다.

<body>
인사말: ${greeting}
</body>

  1. 샤르비엘★ 2010.12.02 23:29 신고

    좋은정보 감사드립니다. 글 퍼가도 되나요?

  2. 오동통통! 2011.07.26 23:33

    감사합니다~! 출처와 함께 글퍼갈께요!

  3. 2011.08.03 11:48

    감사합니다..

  4. minitialk 2012.07.19 13:20 신고

    감사합니다.
    퍼가서 열심히 공부할게요~~

  5. CruzerDK 2012.09.27 00:28

    좋은 정보 감사합니다
    출처와 함께 퍼갈게여 ^^

  6. vinshan 2013.05.07 19:06

    좋은글 감사합니다 저도 출처와 함께 퍼가서 열심히 공부할께요

  7. 이온 2013.07.09 13:55

    좋은정보 감사합니다. 출처와 함께 퍼길게요~

  8. 김군 2013.11.11 09:24

    좋은 글 감사합니다. 출처와 함께 퍼갈게요^^

  9. 쫑맹 2014.04.29 08:27

    좋고 쉬운 설명 감사합니다~~

  10. 불사영 2014.05.08 15:31

    좋은정보 감사합니다. 출처와 함께 퍼갈께요

  11. 호시코 2015.01.08 13:27

    좋은 정보 감사합니다^^ 출처와 함께 퍼갈게요^^

  12. enter 2016.08.04 13:58

    좋은정보 감사합니다. 출처와 함께 문서작업 자료에 부분 발췌 하겠습니다.
    감사합니다.

  13. 다크써클남 2016.08.10 17:40

    현재 저의는 bean을 xml 아닌 bean.java 파일 만들어 @data 이런 방식으로 사용합니다.
    위 방식과 저의 프로젝트에서 사용하는 방식은 버전차이 혹은 기술차이가 있나요?
    spring STS 3.6.4를 사용하고 있습니다.

    • 최범균 madvirus 2016.08.12 08:31 신고

      @Data 애노테이션은 스프링과 상관없이 lombok 이라는 기술에서 사용하는 애노테이션으로 알고 있습니다.

  14. 기리 2016.10.01 20:46

    감사합니다 도움 많이 되네요

반응형
Spring 2.0에 새롭게 추가된 AOP 관련 태그와 새로운 트랜잭션 설정 관련 태그에 대해서 살펴본다.

사용하기 쉬워진 AOP 적용

Spring 2.0은 많은 면에서 발전을 이루었는데 그 중 한가지가 AOP를 좀 더 명확하게 적용할 수 있게 되었다는 점이다. Spring 2.0은 기존의 <bean> 태그 이외에 콘텍스트에 알맞은 태그를 확장할 수 있도록 설계되었으며, AOP를 위한 태그도 새롭게 추가되었다. 본 글에서는 Spring 2.0에 새롭게 추가된 태그를 사용하여 AOP를 적용하는 방법에 대해서 살펴보고, 추가적으로 새로운 트랜잭션 설정 방법에 대해서도 살펴볼 것이다.

본 글을 읽기 전에, 아직까지 AOP와 관련된 용어에 익숙하지 않은 개발자들을 위해 AOP와 관련된 몇 가지 용어를 간단하게 정리해보았다.

  • Pointcut : pointcut은 매칭 규칙으로서, 프로그램이 실행될 때 aspect를 적용할 지점(point)의 집합을 정의한다. aspect가 적용되는 지점을 joinpoint 라고 부른다. 예를 들어, 메소드 실행, 처리해야 할 예외와 같은 것이 joinpoint가 될 수 있다. Spring AOP의 경우 public 메소드에 대해서만 joinpoint를 지정할 수 있다.
  • Advice : joinpoint에 aspect를 언제 적용할지를 나타낸다. Spring AOP의 경우 Before advice, After returning advice, After throwing advice, After advice, Around advice의 5가지의 advice가 존재한다.
  • Aspect : 다양한 객체에서 요구하는 소프트웨어의 기능을 구현한 모듈. 로깅, 트랜잭션 처리 등이 aspect의 좋은 예이다. Spring AOP는 Aspect를 일반적인 자바 클래스로 구현한다.
Spring 2.0은 하위 호환을 위해 기존 Spring 1.x 버전이 제공하던 AOP 기능을 유지하면서, "aop" 네임스페이스 태그를 사용한 AOP 설정 방식을 추가하였다. 새롭게 추가된 태그를 사용하여 보다 쉽게 Aspect를 적용할 수 있게 되었는데 본 절에서는 기존 1.x 방식의 단점에 대해서 살펴본 뒤, 새로운 AOP 설정 방식을 살펴볼 것이다.

Spring 1.x의 AOP 설정

Spring 1.x 버전의 경우 Aspect를 적용하기 위해서 다음과 같은 형태의 코드를 사용했었다. 이 코드를 보면 AOP를 적용하기 위해 프록시 빈을 정의하고(ProxyFactoryBean), Advice와 Aspect가 혼용된 WelcomeAdvice를 정의한 것을 알 수 있다.

    <bean id="kwikEMartTarget"
        class="com.springinaction.chapter03.store.AquKwikEMart" />
    
    <bean id="welcomeAdvice"
        class="com.springinaction.chapter03.store.WelcomeAdvice" />
    
    <bean id="kwikEMart"
        class="com.springframework.aop.framework.ProxyFactoryBean">
        <property name="proxyInterfaces">
            <value>com.springinaction.chapter03.store.KwikEMart</value>
        </property>
        <property name="interceptorNames">
            <list>
                <value>welcomeAdvice</value>
            </list>
        </property>
        <property name="target">
            <ref bean="kwikEMartTarget" />
        </property>
    </bean>

외부에서 Aspect가 적용될 객체에 접근할 때 사용되는 빈의 이름은 프록시 객체의 이름이어야 한다. (위 코드의 경우 AquKwikEMart 객체에 Aspect를 적용하기 위해서는 "kwikEMartTarget"이 아닌 "kwikEMart" 이름으로 접근해야만 AOP가 적용된다.)

Aspect를 적용하고 싶지 않다면 프록시 빈의 설정을 변경하거나 Advice를 목록에서 제거하는 등의 방법을 사용해야 하는 불편함이 있다. 위와 같은 코드의 가장 큰 문제점은 AOP를 위한 설정 코드가 꽤나 복잡하다는 점이다. 또 한, 설정 파일만으로는 WelcomeAdvice의 타입을 알 수 없다. WelcomeAdvice가 Before 타입인지 After 타입인지 또는 Around 타입인지의 여부는 WelcomeAdvice의 소스 코드나 API 문서를 봐야만 확인이 가능하다.

Spring 2.0의 AOP 설정

Spring 2.0에는 AOP 설정하기 위한 "aop" 네임스페이스가 추가되었으며, 이 네임스페이스와 관련된 XML 스키마도 함께 추가하였다. "aop" 네임스페이스와 관련 XML 스키마는 다음과 같이 <beans> 태그에 명시할 수 있다.

<?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:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans   
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
    ...
</beans>

위와 같이 "aop" 네임스페이스와 관련된 XML 스키마를 지정한 뒤에, 다음과 같이 <aop:config> 태그를 사용하여 AOP 관련 정보를 설정할 수 있다.

    <bean id="logging"
        class="net.daum.cto.ts.techReport.report0.LoggingAspect">
    </bean>

    <aop:config>
        <aop:aspect id="loggingAspect" ref="logging">
            <aop:pointcut id="loggingPointCut"
                expression="execution(* net.daum.cto.ts.techReport.*.*Service*.*(..))" />
            <aop:around pointcut-ref="loggingPointCut" method="log" />
        </aop:aspect>
    </aop:config>
    <bean id="reservationService"
        class="net.daum.cto.ts.techReport.report0.ReservationServiceImpl">
    </bean>

위 코드에서 <aop:*>에 해당하는 태그는 다음과 같은 정보를 설정한다.

  • <aop:config> - AOP 설정 정보임을 나타낸다.
  • <aop:aspect> - Aspect를 설정한다.
  • <aop:pointcut> - Pointcut을 설정한다.
  • <aop:around> - Around Advice를 설정한다. 이 외에도 다양한 Advice를 설정할 수 있다.
위 코드에서 <aop:aspect> 태그의 ref 속성은 Aspect로서 기능을 제공할 빈을 설정할 때 사용되며, 위 코드의 경우 "logging" 빈이 Aspect 기능을 제공한다고 명시하고 있다.

위 코드에서 주목할 점은 Aspect를 적용하기 위해 더이상 프록시 객체를 사용할 필요가 없다는 점이다. 앞서 Spring 1.x 버전의 경우 Aspect를 적용하기 위해 프록시 객체를 사용했던 반면에, Spring 2.0에서는 <aop:pointcut>의 expression 속성에 Aspect가 적용될 클래스 또는 메소드에 대한 정보를 지정하기만 하면 자동으로 Aspect가 해당 메소드에 적용된다.

위 코드를 보면 "loggingAspect"가 ReservationServiceImpl의 모든 메소드에 Around Advice로 적용되며, 이때 Aspect의 구현 클래스인 LoggingAspect의 log() 메소드가 호출된다는 것을 쉽게 유추해낼 수 있는데, Spring 2.0의 AOP 설정은 이렇게 설정 파일만 보더라도 어렵지 않게 어떤 코드에 어떤 Aspect가 어떤 타입의 Advice로 적용되는지를 파악해낼 수 있다.

Spring 2.0의 AOP 설정하기

Aspect 설정

Srping 2.0에서 Aspect를 선언하려면 다음 코드처럼 <aop:aspect> 태그를 사용하면 된다.

<aop:config>
    <aop:aspect id="someConcernAspect" ref="someConcern">
        ...
    </aop:aspect>
</aop:config>

<bean id="someConcern" class="...SomeConcernImpl">
   ...
</bean>

<aop:aspect> 태그의 ref 속성은 Aspect의 구현을 제공하는 빈을 지정한다. 위 코드의 경우 "someConcern" 빈이 "someConcernAspect"의 구현을 제공하게 된다.

Spring 2.0에서 <aop:> 태그는 내부적으로 AspectJ에서 제공하는 기능을 사용한다. 따라서, <aop:> 태그를 사용하려면 AspectJ와 관련된 jar 파일을 클래스패스에 추가해주어야 한다. Maven을 사용할 경우 다음과 같은 <dependency> 태그를 추가해주면 된다.

    <dependency>
        <groupId>aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.5.3</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.5.3</version>
        <scope>runtime</scope>
    </dependency>

Spring 2.0의 Aspect 구현하기

Spring 1.x 버전의 경우 Aspect로서 동작하기 위해서는 MethodBeforeAdvice 인터페이스와 같은 AOP를 위한 인터페이스를 구현했어야 하는 반면에, Spring 2.0에서 Aspect로서 사용될 클래스는 특정한 인터페이스를 구현하지 않아도 된다. 즉, 일반 자바 클래스를 Aspect로 사용할 수 있는 것이다. 예를 들어, 아래 클래스는 Spring 2.0에서 Around Advice로 적용 가능한 Aspect 클래스의 예이다.

package net.daum.cto.ts.techReport.report0;

import org.aspectj.lang.ProceedingJoinPoint;

public class LoggingAspect {
    public Object log(ProceedingJoinPoint call) throws Throwable {
        System.out.println("from LoggingAspect : entering method : "
                + call.toShortString());
        Object point = call.proceed();
        System.out.println("from LoggingAspect : exiting method : "
                + call.toShortString());
        return point;
    }
}

위 코드를 보면 Aspect로 동작하기 위해 특정한 인터페이스를 필요로 하지 않는 것을 알 수 있다. 단지, Advice 타입에 따라 Aspect가 적용될 때 호출될 메소드의 파라미터를 알맞게 설정해주면 된다.

Pointcut 설정하기

ointcut을 설정할 때에는 다음과 같이 <aop:pointcut> 태그를 사용하면 된다.

<aop:aspect id="loggingAspect" ref="logging">
    <aop:pointcut id="loggingAspect"
        expression="execution(* net.daum.cto.ts.techReport.*.*Service*.*(..))" />
    ...
</aop:aspect>

<aop:pointcut>의 expression 속성은 pointcut에 포함될 joinpoint를 표현한다. expression 속성은 AspectJ의 문법을 따르고 있다.

<aop:pointcut>의 expression

<aop:pointcut> 태그의 expression은 pointcut을 정의할 때 사용되며 execution과 같은 designator를 사용하여 pointcut을 명시하게 된다. execution 외에도 within, this, target 등의 designator가 존재한다.

  • execution : 메소드 실행 joinpoint와 매칭할 때 사용된다.
  • within : 특정 패키지에 있는 모든 타입의 joinpoint와 매칭할 때 사용된다.
  • this : 특정 타입을 구현하고 있는 프록시의 joinpoint와 매칭할 때 사용된다.
  • target : 특정 타입의 인스턴스의 joinpoint와 매칭할 때 사용된다.
  • args : 메소드의 인자 타입이 일치하는 joinpoint와 매칭할 때 사용된다.
이 외에도 @target, @args 등의 designator가 존재하는데, 이들 designator는 Annotation을 사용할 때 적용할 수 있는 것으로서, 본 글에서는 이에 대해서 설명하지 않겠다. 이에 대한 자세한 내용이 알고 싶다면 Spring 레퍼런스 문서를 참고하기 바란다.

위에서 설명한 designator 중에서 가장 많이 사용되는 것은 execution 이다. execution 은 다음과 같은 형식을 사용하여 pointcut을 정의할 수 있다.

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? 
      name-pattern(param-pattern) throws-pattern?)

execution의 몇 가지 예를 들면 다음과 같다.

  • 모든 public 메소드의 실행: execution(public * *(..))
  • set으로 시작하는 모든 메소드의 실행: execution(* set*(..))
  • AccountService 인터페이스에 정의된 모든 메소드의 실행: execution(* com.xyz.service.AccountService.*(..))
execution 외에 나머지 designator의 설정 예는 다음과 같다.

  • com.xyz.service 패키지에 있는 모든 joinpoint: within(com.xyz.service.*)
  • com.xyz.service 패키지 및 그 하위 패키지에 있는 모든 joinpoint: within(com.xyz.service..*)
  • AccountService 인터페이스를 구현한 프록시의 모든 joinpoint: this(com.xyz.service.AccountService)
  • AccountService 인터페이스를 구현한 객체의 joinpoint: target(com.xyz.service.AccountService)
  • args(java.io.Serializable): 한 개의 파라미터를 갖고 그 파라미터의 타입이 Serializable인 메소드 호출
Advice 선언하기

Aspect와 Pointcut을 설정했다면 다음으로 할 작업은 Advice를 설정하는 것이다. Spring 2.0은 다음과 같이 5개의 Advice를 제공하고 있다.

  • Before Advice - 메소드가 호출되기 이전에 실행된다.
  • After Returning Advice - 메소드가 정상적으로 리턴된 다음에 실행된다.
  • After Throwing Advice - 메소드가 예외를 발생할 경우 실행된다.
  • After Advice - 메소드가 정상적으로 실행되었는지의 여부에 상관없이 메소드의 실행이 종료된 다음에 실행된다. finally와 동일한 기능을 제공한다.
  • Around Advice - 메소드의 호출 이전과 이후에 Aspect를 적용할 수 있다.
Before Advice

Before Advice를 설정할 때에는 <aop:before> 태그를 사용하면 된다.

<bean id="securityChecker"
    class="net.daum.cto.ts.techReport.report0.SecurityChecker">
</bean>

<aop:aspect id="securityCheckAspect" ref="securityChecker">
    <aop:pointcut id="needAuthMethod" ... />
    <aop:before 
         pointcut-ref="needAuthMethod"
         method="checkAuth" />
</aop:aspect>

<aop:before>에 적용될 메소드는 다음과 같이 두 가지 형태로 구현할 수 있다.

public void checkAuth(ProceedingJoinPoint call) {
    ...
}

public void checkAuth() {
    ...
}

첫번째 메소드는 ProceedingJoinPoint 클래스를 파라미터로 전달받는데, 이 객체에는 호출되는 메소드의 정보가 저장되어 있다. 따라서 호출되는 메소드에 대한 정보가 필요한 경우 첫번째 형태를 사용하면 된다. 호출되는 메소드에 대한 정보가 필요없다면 두번째와 같이 파라미터가 없는 메소드를 사용한다.

After Returning Advice

After Returning Advice는 다음과 같이 <aop:after-returning> 태그를 사용하여 정의한다.

<bean id="returnValueChecker"
    class="net.daum.cto.ts.techReport.report0.ReturnValueChecker">
</bean>
<aop:aspect id="returnValueCheckAspect" ref="returnValueChecker">
    <aop:pointcut id="needCheckRetValMethod" 
        expression="execution(* net.daum.cto.ts.techReport.*.*Service*.check*(..))" />
    <aop:after-returning pointcut-ref="needCheckRetValMethod"
        returning="returnVal"
        method="checkReturnValue" />
</aop:aspect>

returning 속성은 리턴값을 저장할 파라미터의 이름을 나타낸다. 예를 들어, 위 설정에서 ReturnValueChecker.checkReturnValue() 메소드는 다음과 같이 returnValue 파라미터를 값으로 갖게 된다.

public class ReturnValueChecker {
    public void checkReturnValue(Object returnVal) throws Exception {
        ...
    }
}

만약 리턴값을 사용할 필요가 없다면 returning 속성을 지정하지 않아도 되며, 이 경우 Aspect로 사용될 메소드는 다음과 같이 파라미터를 가져서는 안 된다.

public class ReturnValueChecker {
    public void checkReturnValue() throws Exception {
        ...
    }
}

Aspect를 구현한 클래스의 메소드에서 리턴 타입을 전달받기 위해서는 asm 라이브러리가 필요하다. Maven을 사용할 경우 다음과 같이 asm 라이브러리에 대한 의존관계를 추가해주어야 한다.

    <dependency>
        <groupId>asm</groupId>
        <artifactId>asm-all</artifactId>
        <version>2.2</version>
        <scope>runtime</scope>
    </dependency>

After Throwing Advice

After Throwing Advice는 다음과 같이 <aop:after-throwing> 태그를 사용하여 지정할 수 있다.

<bean id="loggingException"
    class="net.daum.cto.ts.techReport.report0.LoggingException">
</bean>
<aop:aspect id="loggingExceptionAspect" ref="loggingException">
    <aop:pointcut ... />
    <aop:after-throwing
        pointcut-ref="servicePointcut2"
        throwing="ex"
        method="doLogException" />
</aop:aspect>

위 코드에서 throwing 속성은 doLogException 메소드의 파라미터 이름을 의미한다. 이때 파라미터의 타입은 발생한 예외 클래스 또는 그 상위 타입이어야 한다. 아래 코드는, doLogException() 메소드의 작성 예를 보여주고 있다.

public void doLogException(Throwable ex) {
    System.out.println("---- 예외 발생했음!!!!:"+ex.getClass().getName());
}

After Returning Advice와 마찬가지로 발생한 예외에 대한 정보가 필요없다면 throwing 속성을 지정할 필요가 없으며, 이 경우 메소드는 파라미터를 가져서는 안 된다.

앞서 리턴값을 전달받는 경우와 마찬가지로, Aspect를 구현한 클래스의 메소드에서 예외 객체를 전달받기 위해서는 asm 라이브러리가 필요하다.

After Advice

After Advice는 finally의 성격을 갖는 Advice로서 <aop:after> 태그를 사용하여 지정한다.

<aop:aspect id="releaseResourceAspect" ref="releaseResource">
    <aop:pointcut ... />
    <aop:after pointcut-ref="usingResourceMethod"
        method="release" />
</aop:aspect>

Around Advice

Around Advice는 메소드의 호출을 완벽하게 제어할 수 있는 Advice로서 메소드 호출 이전 및 이후, 그리고 심지어 메소드 호출가 예외 처리까지도 제어할 수 있다. Around Advice는 다음과 같이 <aop:around> 태그를 사용하여 설정할 수 있다.

<aop:aspect id="loggingAspect" ref="logging">
    <aop:pointcut id="servicePointcut1"
        expression="execution(* net.daum.cto.ts.techReport.report0.*.*(..))" />
    <aop:around pointcut-ref="servicePointcut1" method="log" />
</aop:aspect>

Around Advice가 적용될 때 호출될 메소드는 다음과 같이 첫번째 파라미터로 ProceedingJoinPoint 타입을 지정해주어야 한다.

public Object log(ProceedingJoinPoint call) throws Throwable {
    // 메소드가 호출되기 전에 전처리
    
    Object retVal = call.proceed();
    
    // 메소드가 호출된 이후 후처리
    
    return retVal;
}

메소드 몸체에서는 ProceedingJoinPoint.proceed() 메소드를 호출하여 원래 호출하려던 메소드를 실행할 수 있다. ProceedingJoinPoint.proceed() 메소드는 Object 타입을 리턴하는데, 이때 리턴받은 값을 리턴하거나 또는 새로운 값을 리턴할 수도 있다.

AOP를 이용한 트랜잭션 처리

Spring 2.0에는 트랜잭션을 위한 태그도 새롭게 추가되었다. 트랜잭션을 위한 태그는 "tx" 네임스페이스를 가지며, 다음과 같이 트랜잭션 관련 태그와 AOP 관련 태그를 사용하여 트랜잭션을 원하는 pointcut에 적용할 수 있다.

<?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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    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-2.0.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
    
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" ...>
        ...
    </bean>
    
    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
        
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true" />
            <tx:method name="somethingSpecial*" propagation="REQUIRES_NEW"/>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>
    
    <aop:config>
        <aop:pointcut id="fooServiceOperation"
            expression="execution(* x.y.service.FooService.*(..))" />
        <aop:advisor advice-ref="txAdvice"
            pointcut-ref="fooServiceOperation" />
    </aop:config>
    
    <bean id="fooService" class="x.y.service.DefaultFooService" />
</beans>

기존에 프록시 객체를 사용하여 트랜잭션을 적용했던 반면에, 위 코드의 경우 서비스를 정의한 빈은 변경하지 않고 <tx:advice> 태그와 <aop:advisor> 태그를 사용하여 서비스 빈에 트랜잭션을 적용한 것을 알 수 있다.

관련링크:
반응형
데이터를 조회하는 두 가지 방법(HQL, Criteria)에 대해서 살펴본다.

HQL을 사용한 데이터 조회

Hibernate는 HQL 실행 방식, Criteria 실행 방식 그리고 SQL 직접 실행 방식의 세 가지 데이터 조회 방식을 제공한다. 이들 세 가지 데이터 조회 방식 중 HQL을 사용하는 방법에 대해서 살펴보도록 하자.

HQL은 SQL과 비슷한 형태의 구문을 갖는 Hibernate의 쿼리 언어로서 OR 매핑 파일에서 설정한 객체를 쿼리문구에서 사용할 수 있다는 특징이 있다. 또한, SQL 만큼 쉽기 때문에 다양한 형태로 데이터를 조회하고자 할 때 유용하게 사용될 수 있다.

HQL의 기본

HQL을 이용하는 가장 기본적인 코드는 다음과 같다.

    Query query = session.createQuery("from Item");
    List list = query.list();
    
    // 또는 간단히 한줄로
    List list = session.createQuery("from Item").list();
    
    for (int i = 0 ; i < list.size() ; i++) {
        Item item = (Item)list.get(i);
    }

Hibernate Session은 createQuery(String hql) 메소드는 전달받은 HQL을 실제 쿼리로 변환하여 수행하는 net.sf.hibernate.Query 객체를 리턴한다. Query는 쿼리 실행과 관련된 인터페이스로서 다양한 조건을 사용해서 데이터를 조회할 수 있는 메소드를 제공하며, list() 메소드를 통해서 쿼리 실행 결과를 java.util.List로 생성해준다.

createQuery() 메소드가 전달받는 HQL의 가장 기본적인 형태는 다음과 같다.

    from 클래스이름

위 쿼리는 '클래스이름'과 관련된 테이블로부터 모든 행 데이터를 읽어온다. 클래스이름은 매핑 설정 파일에 명시한 클래스의 이름으로서 패키지를 포함한 완전한 클래스 이름을 사용해도 되고 패키지 부분을 제외한 클래스 이름만 사용해도 된다. 예를 들면 아래의 두 경우 모두 올바르게 동작한다.

    from javacan.hibernate.test.Item
    
    from Item

where 절이나 기타 다른 절에서 관련 객체의 프로퍼티값을 참조하기 위해 다음과 같이 별칭을 부여할 수 있다.

    from Member as member
    
    from Member member

'as' 뒤에 별칭을 입력하는데 'as'는 생략할 수 있다.

where 절과 파라미터 값 매핑

where 절은 SQL과 동일한 방법으로 입력한다. 차이점이 있다면, 테이블 컬럼명 대신에 퍼시스턴트 객체의 프로퍼티명을 사용한다는 점이다. 예를 들어, 특정 Member 객체 중에서 name 프로퍼티의 값이 '최범'으로 시작하는 것의 목록을 추출하고 싶다면 다음과 같이 쿼리를 작성하면 된다.

    from Member as member where member.name like '최범%'

물론, and, or, not 등을 사용하여 검색 조건을 조합할 수 있다. 예를 들면 아래와 같이 사용할 수 있다.

    from Member as member where member.id like '%mad%' or member.name like '최%'

JDBC의 PreparedStatement와 마찬가지로 HQL도 파라미터를 지정하는 형태로 비교값을 명시할 수 있다. PreparedStatement가 '?'를 사용하여 값을 지정하는 방식만 제공하는 반면에 HQL은 이름을 사용하는 방식과 '?'를 사용하는 방식의 두가지를 제공하고 있다. 먼저 다음은 이름을 사용하는 방식은 다음과 같이 값을 삽입할 부분에 :이름 의 형태를 삽입한다.

    Apartment apt = (Apartment)session.get(Apartment.class, "01135001");
    
    Query query = session.
        createQuery("from Member as member "+
                    "where member.id = :id or member.apartment = :apt");
    query.setParameter("id", "era13");
    query.setParameter("apt"apt);

Query.setParameter() 메소드를 사용해서 이름 부분에 삽입될 값을 지정한다. 이름을 사용하기 때문에 값의 의미가 명확한 장점이 있다.

'?'를 사용하는 방식은 PreparedStatement를 사용하는 경우와 동일한 방식으로 쿼리를 작성하면 된다. 위 코드를 '?'를 사용하여 작성하면 아래와 같이 바뀐다.

    Apartment apt = (Apartment)session.get(Apartment.class, "01135001");
    
    Query query = session.
        createQuery("from Member as member where member.id = ? or member.apartment = ?");
    query.setParameter(0, "era13");
    query.setParameter(1, apt);

위 코드에서 주의할 점은 JDBC의 PreparedStatement는 '?'의 인덱스가 1부터 시작하는 반면에, HQL의 '?'는 0부터 시작한다는 것이다.

setParameter() 메소드를 사용할 때 할당될 값의 타입에 대해 명확하게 정의하고 싶다면 타입을 세번째 인자로 받는 setParameter() 메소드를 사용하면 된다. 또는 setString(), setDouble(), setEntity()와 같은 메소드를 사용해서 파라미터 값을 지정해주어도 된다.

    query.setParameter("id", id, Hibernate.STRING);
    query.setInteger("no", 10);
    query.setEntity("apt", apartment);

대부분의 경우는 하이버네이트가 자동으로 값에 대한 매핑을 처리해주기 때문에 setParameter() 메소드만으로도 충분한다.

비교 연산자와 수식

HQL은 SQL과 같은 기본 연산자를 제공하며, 이들은 다음과 같다.

    =   <>   <   >   >=   <=    between    in    like

between이나 in, like 등은 not 연산자와 함께 사용될 수 있다. 예를 들어, 특정 목록에 속해 있지 않은 값만 읽어오고 싶다면 다음과 같이 not in 연산자를 사용할 수 있을 것이다.

    from Member as mem where mem.apartment not in (?, ?, ?);

HQL은 또한 사칙 연산을 수행하는 +, -, *, / 연산자를 제공한다. 아래 코드는 비교 연산자와 수식 연산자를 함께 사용한 예이다.

    from Member mem where (mem.height - 110) * 0.1 < mem.weight - (mem.height - 110)

select 절을 이용한 개별 프로퍼티 읽기

HQL은 SQL과 달리 select 절이 필요없으며 from 절만 입력해도 쿼리가 실행된다. 이렇게 from 절만 필수인 이유는 HQL이 조회한 결과가 퍼시스턴트 객체로 추출되기 때문이다. 하지만, 경우에 따라 특정 프로퍼티만을 추출하고 싶은 경우가 있으며, 이럴 때에는 다음과 같이 select 절을 사용해야 한다.

    Query query = session.
        createQuery("select member.id, member.name from Member as member "+
                    "where member.id = ? or member.apartment = ?");
    query.setParameter(0, "era13");
    query.setParameter(1, apt);
    
    List list = query.list();
    
    for (int i = 0 ; i < list.size() ; i++) {
        Object[] row = (Object[])list.get(i);
        // row[0] - member.id
        // row[1] - member.name
    }

Query를 실해한 결과 List는 select 절에 표시된 값들을 저장한 객체 배열(Object[])을 갖고 있다. 이 객체 배열은 하나의 행과 관련된 데이터를 저장하고 있다. 배열에 저장되는 순서는 select 절에 나열한 순서와 동일하다. 즉, 위의 코드의 경우 배열의 0번 인덱스에는 member.id가, 1번 인덱스에는 member.name이 저장된다.

함수 사용

select 절이나 where 절에서는 SQL Dialect에 따라 관련 DBMS가 제공하는 함수를 사용할 수 있다. ('Hibernate를 이용한 ORM 2 - 세션(커넥션) 및 트랜잭션 프로퍼티 설정'에서 SQL Dialect를 설정하는 방법을 살펴봤었다.) 예를 들어, 오라클을 사용하는 경우 다음과 같이 오라클이 제공하는 to_date() 함수와 substr() 함수를 사용할 수 있다.

    select to_date(member.regdate, 'YYYY/MM/DD'), substr(member.name, 1, 2)
    from Member as member where member.id = ? or member.apartment = ?

DBMS에 특정한 함수 뿐만 아니라 집합 관련 표준 SQL 함수인 count, avg, sum, max, min 함수도 사용할 수 있다.

    select max(member.mark), min(member.mark), avg(mark) from Member member

Hibernate가 제공하는 HQL 함수/프로퍼티

Hibernate는 HQL에서 콜렉션 프로퍼티와 관련된 쿼리를 쉽게 할 수 있도록 하기 위한 특별한 함수와 프로퍼티를 제공하는데 이들 함수는 다음과 같다.

Hibernate가 제공하는 콜렉션 관련 함수/프로퍼티
함수 관련 프로퍼티 설명
size() size 콜렉션의 크기를 구한다.
minIndex() minIndex 인덱스의 최소값을 구한다.
maxIndex() maxIndex 인덱스의 최대값을 구한다.
minElement() minElement 콜렉션의 요소 중 최소값을 구한다.
maxElement() maxElement 콜렉션의 요수 중 최대값을 구한다.

위 함수들은 다음과 같이 where 절에서 사용될 수 있다.

    from Item item where maxElement(item.bids) > 1000
    
    from Item item where item.bids.maxIndex > 5

any, all, in 함수를 조건에서 사용할 수도 있다. 예를 들어, Item과 관련된 모든 Bid의 amount 값이 100보다 큰 Item을 구하고 싶다면 다음과 같은 쿼리를 사용하면 된다.

    from Item item where 100 < all (select b.amount from item.bids b)

group by 절과 having 절

group by 절을 사용해서 특정 컬럼 값으로 그룹핑 할 수 있으며, having 절을 사용해서 그룹핑에 대해 조건을 줄 수도 있다. group by 절이나 having 절의 사용법은 SQL에서의 사용방법과 비슷하다. 예를 들어, 관련 Bid의 개수가 4개 이상인 Item의 개수를 구하고 싶은 경우 다음과 같은 쿼리를 사용하면 된다.

    from Item item group by item.id having size(item.bids) > 4

결과 순서 처리 및 페이징

HQL에서 조회한 결과를 정렬하고 싶은 경우에는 order by 절을 사용하면 된다. order by 절의 사용법은 SQL과 비슷하다. 예를 들어, Member 객체의 name 프로퍼티 값의 내림차순으로 정렬하고 싶다면 다음과 같은 HQL을 사용하면 된다.

    from Member member order by member.name desc

SQL과 마찬가지로 여러 프로퍼티에 대해서 정렬 방식을 명시할 수 있다.

    from Member member order by member.name desc, member.apartment.name asc

Query 인터페이스는 페이징 처리를 위한 메소드를 제공하고 있는데, 이 두 메소드는 아래와 같이 사용한다.

    Query query = session.createQuery("from Member member order by member.name asc");
    query.setFirstResult(30);
    query.setMaxResults(15);

Query.setFirstResult() 메소드는 Query의 결과 목록 중에서 읽어올 첫번째 항목의 인덱스값을 입력한다. 이때 인덱스 값은 0부터 시작한다. 따라서, 위 코드는 31번째 항목부터 읽어오게 된다. setMaxResults() 메소드는 읽어올 최대 개수를 나타낸다. 따라서, 위 코드는 Query 결과의 31번째 항목부터 최대 15개를 읽어오게 된다. 이 두 메소드를 사용하면 페이징 처리를 쉽게 할 수 있을 것이다.

연관의 조인(join) 처리

Hibernate의 연관은 결국 SQL 조인을 통해서 처리된다. HQL에서 연관을 위한 조인은 다음과 같이 네 가지 방식으로 지원된다.

  • from 절에서 join과 fetch 사용하기
  • 자동으로 조인 처리하기
  • where 절과 함께 카타시안 조인 사용하기
from 절에서 join 사용하기

HQL은 SQL이 제공하는 내부 조인과 외부 조인을 모두 사용할 수 있다. 예를 들어, 일반적인 내부 조인은 다음과 같이 사용할 수 있다.

    Query query = session.
        createQuery("from Item item inner join item.bids");
    
    List list = query.list();
    for (int i = 0 ; i < list.size() ; i++) {
        Object[] object = (Object[])list.get(i);
        Item item = (Item)object[0];
        Bid bid = (Bid)object[1];
        System.out.print(item.getId());
        System.out.print("-");
        System.out.println(bid.getId());
    }

위 코드에서 눈여겨 볼 부분은 Query.list()가 리턴한 List가 담고 있는 값이 객체 배열(Object[])이라는 것이다. 내부 조인을 했기 때문에 객체 배열의 첫번째 요소에는 Item 객체가 저장되고, 배열의 두번째 요소에는 내부 조인으로 읽어온 Bid 객체가 저장된다.

다음과 같이 외부 조인을 사용할 수도 있다.

    from Item item left outer join item.bids

외부 조인을 할 때에도 내부 조인을 할 때와 마찬가지로 from 절에 있는 조인된 객체들을 객체 배열에 담아 리턴한다.

내부 조인이든 외부 조인이든, 실행한 결과는 (왼쪽이나 오른쪽의) 조인된 객체에 대해서 중복된 값을 갖게 된다. 예를 들어, 다음의 쿼리를 생각해보자.

    from Item item inner join item.bids where item.id = 1

id가 1인 Item과 연관된 Bid 객체의 id 값이 1, 2, 3 이라고 해 보자. 이때 Query.list() 메소드는 다음과 같은 객체 배열을 담은 List를 리턴하게 된다.

Item Bid
Item (id = 1) Bid (id = 1)
Item (id = 1) Bid (id = 2)
Item (id = 1) Bid (id = 3)

즉, (내부 또는 외부) 조인을 하면 하나의 Item 객체만 리턴하는 것이 아니라 같은 값을 갖는 Item 객체를 중복해서 리턴하게 되는 것이다. 조인을 사용할 때에는 이 점에 유의해야 한다.

조인되는 객체에 alias를 부여할 수도 있다. 예를 들어, Item과 조인되는 Bid 중에서 amount 프로퍼티의 값이 100 이상인 것만 조인하고 싶다면 다음과 같은 쿼리를 사용할 수 있다.

    from Item item join item.bids bid where bid.amount >= 100

위와 같이 조인되는 객체에 alias를 부여해서 where 절의 쿼리를 좀더 쉽게 작성할 수 있게 된다.

from 절에서 fetch를 사용한 쿼리 조회

앞서 살펴봤던 'from Item item inner join item.bids where item.id = 1' HQL 쿼리는 다음과 같이 두개의 쿼리를 사용한다.

    select item.ITEM_ID, ..., bid.BID_ID 
    from ITEM item inner join BID bid on item.ITEM_ID = bid.ITEM_ID
    where item.ITEM_ID = 1
    
    select bid.BID_ID, ... from BID bid where bid.ITEM_ID = 1

위 SQL에서 첫번째 쿼리는 Item 객체를 읽어오기 위한 쿼리이며, 두번째 쿼리는 Item 객체를 읽어온 뒤 관련 Bid 객체를 읽어오기 위한 쿼리이다.

fetch 키워드를 사용하면 위와 같이 두 번의 쿼리를 수행하지 않고 한번의 쿼리만으로 조인되는 객체를 읽어올 수 있다. 예를 들어, fetch 키워드를 사용해서 다음과 같은 HQL 쿼리를 실행할 수가 있다.

    Query query = session.
        createQuery("from Item item left join fetch item.bids "+
                    "where item.id = 1");
    List list = query.list();
    for (int i = 0 ; i < list.size() ; i++) {
        Item item = (Item)list.get(i);
        System.out.println(item.getId());
    }

fetch 키워드를 join 뒤에 붙이면 다음과 같이 한번의 SQL 쿼리로 Item과 연관된 Bid 객체를 읽어오게 된다.

    select item.ITEM_ID, ..., bid.BID_ID ... 
    from ITEM item left outer join BID bid on item.ITEM_ID = bid.ITEM_ID
    where item.ITEM_ID = 1

따라서 한번의 SQL 쿼리로 읽어오고 싶다면 fetch 키워드를 사용해서 쿼리 성능을 높일 수 있다.

fetch 모드를 사용할 때 주의할 점은 Query.list()가 리턴한 List가 배열 객체가 아닌 from 절의 퍼시스턴트 객체를 포함한다는 점이다. 앞서 코드를 보면 List에서 객체를 읽어올 때 객체 배열(Object[])이 아닌 Item 으로 형변환하는 것을 알 수 있다.

자동으로 조인 처리하기

Hibernate는 HQL의 where 절이나 order by, group by 등에 연관된 객체의 프로퍼티가 사용되면 자동으로 조인을 수행한다. 예를 들어 다음의 HQL을 보자.

    from Bid bid where bid.item.minPrice >= 1000

위 HQL 쿼리는 다음과 같은 카타시안 조인을 사용해서 처리된다.

    select bid.BID_ID, .., bid.ITEM_ID 
    from BID bid, ITEM item 
    where item.MINPRICE>=1000 and bid.ITEM_ID = item.ITEM_ID

Hibernate는 where 절에서 사용된 값을 추출하기 위해 조인이 필요하다고 판단될 경우 위와 같이 조인을 수행하게 된다.

하지만, 자동으로 조인을 처리할 때 주의할 점은 HQL의 from 절에 명시된 객체만 결과 목록에 포함된다는 것이다. 실제로 사용되는 SQL에는 Item 객체와 매핑되는 ITEM 클래스가 from 절에 포함되지만, HQL에는 from 절에 Bid 객체만 포함되어 있기 때문에 Query.list() 메소드가 리턴하는 List에는 Bid만 포함된다.

where 절과 함께 카타시안 조인 사용하기

카타시안 조인을 직접 수행할 수도 있다. 예를 들어, 다음의 HQL 쿼리를 보자.

    from Member member, Item item
    where member.id = 'someman' and item.member = member

위 쿼리를 실행하면 [Member, Item] 배열을 저장한 List가 리턴된다.

조회환 결과를 임의의 클래스 객체에 저장하기

매핑 파일에서 작성한 클래스가 아닌 임의의 클래스에 조회 결과를 저장하고 싶다면 다음과 같이 HQL을 작성하면 된다.

    select new MemberRow(member.id, member.name, member.securityNo)
    from Member member
    where ...

위 코드에서 MemberRow 클래스는 인자를 세개 받는 생성자가 존재해야 한다.

위와 같이 임의의 클래스를 명시했다면 데이터를 조회할 때에도 지정한 클래스를 사용해야 한다.

    Query query = session.createQuery(
        "select new MemberRow(member.id, member.name, member.securityNo) "+
        "from Member member where ... ");
    list = query.list();
    for (int i = 0 ; i < list.size() ; i++) {
        MemberRow memberRow = (MemberRow)list.get(i);
    }

Criteria API 사용한 데이터 조회

HQL 뿐만 아니라 Criteria API를 사용해서 데이터를 조회할 수 있다. HQL이 SQL에 가까운 쿼리언어를 사용해서 데이터 조회와 관련된 조건을 입력했면, Criteria API는 Criteria가 제공하는 메소드를 사용해서 데이터 조회와 관련된 조건을 입력한다. 즉, Criteria API는 좀더 객체 지향적인 형태로 검색 조건을 명시할 수 있도록 해 준다.

Criteria 기본 사용 형태

Criteria API는 net.sf.hibernate.Criteria 인터페이스를 사용해서 검색 조건을 생성하는데, 다음과 같이 Session.createcriteria() 메소드를 사용해서 기본 검색 조건을 갖는 Criteria 인스턴스를 생성할 수 있다.

    Criteria crit = session.createCriteria(Member.class);
    List list = crit.list();

위 코드에서 눈여겨 볼 부분은 createCriteria() 메소드의 인자로 매핑 설정 파일에서 명시한 자바 클래스가 온다는 것이다. 위 코드는 다음과 같은 HQL을 이용한 코드와 동일한 결과를 리턴하게 된다.

    Query query = session.createQuery("from Member");
    List list = query.list();

조건 넣기

HQL과 마찬가지로 검색 조건을 지정하지 않으면 전체 행을 읽어오는데, 대부분의 경우는 검색 조건을 넣기를 원할 것이다. 검색 조건을 추가할 때에는 net.sf.hibernate.expression.Expression 클래스와 net.sf.hibernate.Criterion 인터페이스를 사용한다.

Criteria.add(Criterion crit) 메소드는 Criterion이 저장하고 있는 내용을 검색 조건에 추가하는데, Expression 클래스가 제공하는 메소드를 사용해서 손쉽게 Criterion 인스턴스를 생성할 수 있다. 예를 들어, id가 10 인 Item을 검색하고 싶다면 다음과 같이 Expression.eq() 메소드를 사용해서 Criterion을 추가할 수 있다.

    Criteria crit = session.createCriteria(Item.class);
    crit.add( Expression.eq("id", new Integer(10) );
    List list = crit.list();

Expression 클래스는 다양한 조건을 처리할 수 있는 메소드를 제공하고 있으며, 이들 메소드 중에서 가장 기본적인 메소드는 다음과 같다.

Expression의 기본 조건 관련 Criterion 생성 메소드
메소드 설명
SimpleExpression eq(String propertyName, Object value) 프로퍼티의 값이 value와 같은지 검사하는 조건을 생성한다.
Criterion eqProperty(String propertyName, String otherPropertyName) 두 프로퍼티의 값이 같은지 검사하는 조건을 생성한다.
Criterion between(String propertyName, Object lo, Object hi) 프로퍼티가 두 값 사이에 존재하는 지 검사하는 조건을 생성한다.
SimpleExpression ge(String propertyName, Object value) 프로퍼티의 값이 value보다 크거나 같은 지 검사하는 조건을 생성한다. ( >= 연산)
SimpleExpression gt(String propertyName, Object value) 프로퍼티의 값이 value보다 큰지 검사하는 조건을 생성한다.
SimpleExpression lt(String propertyName, Object value) 프로퍼티의 값이 value보다 작은지 검사하는 조건을 생성한다.
Criterion ltProperty(String propertyName, String otherPropertyName) 프로퍼티 값이 다른 프로퍼티 값보다 작은 지 검사하는 조건을 생성한다.
SimpleExpression le(String propertyName, Object value) 프로퍼티 값이 value보다 작거나 같은 지 검사하는 조건을 생성한다.
Criterion leProperty(String propertyName, String otherPropertyName) 프로퍼티 값이 다른 프로퍼티 값보다 작거나 같은 지 검사하는 조건을 생성한다.
Criterion in(String propertyName, Collection values) 프로퍼티 값이 values 목록에 포함되는 지 검사하는 조건을 생성한다.
Criterion in(String propertyName, Object[] values) 프로퍼티 값이 values 목록에 포함되는 지 검사하는 조건을 생성한다.
SimpleExpression like(String propertyName, Object value) like 검색 조건을 생성한다.
SimpleExpression like(String propertyName, String value, MatchMode matchMode) like 검색 조건을 생성한다.
Criterion ilike(String propertyName, Object value) 대소문자를 구분하지 않는 (Postgres의 ilike와 같은) 검색 조건을 생성한다.
Criterion ilike(String propertyName, String value, MatchMode matchMode) 대소문자를 구분하지 않는 (Postgres의 ilike와 같은) 검색 조건을 생성한다.
Criterion isNotNull(String propertyName) 프로퍼티 값이 null이 아닌지 검사하는 조건을 생성한다.
Criterion isNull(String propertyName) 프로퍼티 값이 null인지 검사하는 조건을 생성한다.
Criterion allEq(Map propertyNameValues) map의 키를 프로퍼티 이름으로 값을 비교할 값으로 사용해서, map에 저장된 모든 프로퍼티의 값이 관련 키의 값과 같은 지 검사하는 조건을 생성한다.

여러 개의 검색 조건을 추가할 때에는 다음과 같이 두 가지 방법중의 한가지를 사용하면 된다.

    Criteria crit = session.createCriteria(Item.class);
    crit.add( Expression.gt("minValue", new Integer(5000) );
    crit.add( Expression.eq("user", someUser) );
    
    // 또는 다음과 같이 연속된 방법을 조건 생성 가능
    Criteria crit = session.createCriteria(Item.class)
                           .add( Expression.gt("minValue", new Integer(5000) )
                           .add( Expression.eq("user", someUser) );

StringBuffer.append() 메소드와 마찬가지로 Criteria.add() 메소드는 Criteria 자기 자신을 리턴하기 때문에 위 코드의 두번째 방법과 같이 코드를 생성할 수 있다. (SimpleExpressoin 타입은 Criteria 인터페이스를 구현하고 있으므로, SimpleExpressoin 타입 역시 연속해서 add() 메소드를 사용할 수 있다.)

like() 메소드와 ilike() 메소드를 보면 net.sf.hibernate.expression.MatchMode 클래스를 사용하는 것을 알 수 있는데, MatchMode 클래스는 like 검색을 어떻게 할지를 결정한다. MatchMode는 다음과 같이 네 개의 상수값을 제공하고 있으며, 이들 네 가지 상수값을 통해서 like 검색의 비교 방법을 결정하게 된다.

  • MatchMode.ANYWHERE - like '%값%'의 형태로 검색한다.
  • MatchMode.START - like '값%'의 형태로 검색한다.
  • MatchMode.END - like '%값'의 형태로 검색한다.
  • MatchMode.MATCH - like '값'의 형태로 검색한다.
기본적으로 Criteria.add() 메소드는 추가된 조건들을 AND 로 연결한다. 하지만, OR 연산이 필요한 경우도 있고, AND와 OR를 함께 사용해야 하는 경우도 있다. 이런 경우에는 아래의 메소드를 사용하면 된다.

Expression의 기본 조건 관련 Criterion 생성 메소드
메소드 설명
Criterion or(Criterion lhs, Criterion rhs) 두 조건을 OR로 처리한다.
Criterion and(Criterion lhs, Criterion rhs) 두 조건을 AND로 처리한다.
Conjunction conjunction() conjunction() 메소드가 생성한 Conjunction의 add() 메소드로 추가한 조건을 AND로 연결한다.
Disjunction disjunction() disjunction() 메소드가 생성한 Disjunction의 add() 메소드로 추가한 조건을 OR로 연결한다.
Criterion not(Criterion expression) 관련 검색 조건(expression)의 not에 해당하는 조건을 생성한다.

위 메소드들의 사용예를 들기 위해 다음과 같은 검색 조건이 있다고 해 보자.

    where (age > 10 and sex = 'F') or (sex = 'M' or age <= 10 or bisexual = 'T')

위와 같은 조건은 아래의 코드를 사용해서 생성할 수 있다.

    Criteria crit = session.createCriteria(Member.class);
    crit.add(
        Expression.or(    /* 중간의 or */
            Expression.and(    /* 좌측의 (age > 10 and sex = 'F') */
                Expression.gt("age", new Integer(10)), Expression.eq("sex", "F")    
            )
            ,
            Expression.disjunction()  /* 우측의 (sex = 'M' or age <= 10 or bisexual = 'T') */
                .add(Expression.eq("sex", "M") )
                .add(Expression.le("age", new Integer(10)) )
                .add(Expression.eq("bisexual", "T"))
        )
    )

결과 순서 처리 및 페이징

Criteria의 addOrder() 메소드를 사용하면 결과의 조회순서를 명시할 수 있다. 예를 들어, name 프로퍼티의 오름차순으로, age의 내림차순으로 정렬하고 싶다면 다음과 같은 코드를 사용하면 된다.

    Criteria crit = session.createCriteria(Member.class);
    crit.addOrder(Order.asc("name"));
    crit.addOrder(Order.desc("age"));

net.sf.hibernate.expression.Order 클래스의 asc() 메소드는 오름차순을, desc() 메소드는 내림차순을 의미한다.

페이징 처리를 위해 사용되는 메소드는 Query 인터페이스와 마찬가지로 setFirstResult() 메소드와 setMaxResults() 메소드를 사용하면 된다.

    Criteria crit = session.createCriteria(Member.class);
    crit.addOrder( Order.asc("name") );
    crit.setFirstResult(30);
    crit.setMaxResults(15);

Criteria의 연관 처리

Criteria.setFetchMode() 메소드를 이용한 연관 객체 조회 모드 지정

연관된 객체를 한번의 쿼리로 읽어오고 싶은 경우에는 setFetchMode() 메소드를 사용할 수 있다. Criteria.setFetchMode() 메소드는 다음과 같이 연관된 프로퍼티를 어떻게 읽어올지를 지정할 수 있도록 해 준다.

    Criteria crit = session.createCriteria(Item.class);
    crit.setFetchMode("bids", FetchMode.EAGER)
        .add( Expression.gt("minValue", new Integer(100) );

net.sf.hibernate.FetchMode 클래스는 setFetcMode() 메소드에 지정할 int 상수값을 정의하고 있으며, 각 상수값은 아래와 같다.

  • FetchMode.DEFAULT - 매핑 설정 파일에 있는 값을 그대로 사용한다.
  • FetchMode.EAGER - 외부 조인을 사용해서 한번의 쿼리로 연관된 객체를 읽어온다. (매핑에서 outer-join="true"로 지정한 것과 같다.)
  • FetchMode.LAZY - lazy 모드로 읽어온다. (매핑에서 outer-join="false"로 지정한 것과 같다.)
중첩된 Criteria.createCriteria() 도는 alias를 이용한 연관 조인 처리

Criteria.createCriteria() 메소드를 사용해서 연관된 객체에 대한 조인 처리를 수행할 수 있다. 다음의 코드를 살펴보자.

    Criteria itemCrit = session.createCriteria(Item.class);
    itemCrit.add( Expression.ge("minValue", new Integer(100)) );
    
    Criteria bidCrit = itemCrit.createCriteria("bids"); // 연관된 객체에 대한 검색 조건 생성
    bidCrit.add( Expression.gt("amount", new Integer(100)) );
    
    List list = itemCrit.list();

alias를 사용해서 조인을 처리할 수도 있는데, alias는 Criteria.createAlias() 메소드를 사용하여 지정한다. 다음은 위 코드를 alias를 사용해서 다시 작성해본 것이다.

    Criteria itemCrit = session.createCriteria(Item.class);
    itemCrit.createAlias("bids", "bid");
    itemCrit.add( Expression.ge("minValue", new Integer(100)) );
    itemCrit.add( Expression.gt("bid.amount", new Integer(100));

Criteria의 데이터 조회 방식

앞서 HQL에서는 조인해서 객체를 가져오는 경우 배열에 저장해서 리턴한 것을 기억할 것이다. 아래 코드는 앞서 살펴봤었던 HQL을 이용한 조인 처리 코드이다.

    Query query = session.
        createQuery("from Item item inner join item.bids");
    
    List list = query.list();
    for (int i = 0 ; i < list.size() ; i++) {
        Object[] object = (Object[])list.get(i);
        Item item = (Item)object[0];
        Bid bid = (Bid)object[1];
        ...
    }

하지만, Criteria는 조인 처리를 하더라도 리턴되는 값을 최초에 Session.createCriteria() 메소드를 사용해서 명시한 객체이다. 예를 들어, 위와 같은 조인을 처리하는 Criteria 코드는 아래와 같다.

    Criteria itemCrit = session.createCriteria(Item.class);
    Criteria bidCrit = itemCrit.createCriteria("bids");
    
    List list = itemCrit.list();
    for (int i = 0 ; i < list.size() ; i++) {
        Item item = (Item)list.get(i);
        ...
    }

만약 연관된 객체를 동시에 읽고 싶다면, 다음과 같이 Criteria.returnMaps() 메소드를 호출해서 한 행과 관련된 Item과 Bid 객체를 Map에 저장할 수 있다.

    Criteria itemCrit = session.createCriteria(Item.class);
    itemCrit.createAlias("bids", "bid");
    itemCrit.add( Expression.gt("bid.amount", new Integer(100)) );
    
    List list = itemCrit.returnMap().list();
    for (int i = 0 ; i < list.size() ; i++) {
        Map map = (Map)list.get(i);
        Item item = (Item)map.get("this");
        Bid bid = (Bid)map.get("bid");
        ...
    }

관련링크:
반응형
Hibernate에서 콜렉션 프로퍼티와 many-to-many 관계를 매핑하는 방법을 살펴본다.

콜렉션 매핑 처리

앞선 글들에서 one-to-many의 관계를 설명할 때 set 태그를 사용했던 것을 기억할 것이다. 이 set 태그는 자바의 콜렉션 타입인 java.util.Set을 사용할 때 이용되는데 Hibernate는 Set 말고도 List, Map 그리고 자바 콜렉션에 포함되어 있지는 않은 Bag을 지원하고 있다.

Set, (ID) Bag, List, Map을 어떻게 매핑 처리하는 지 살펴보고, 복합 타입을 콜렉션 타입으로 처리하는 방법도 살펴본다.

Set을 이용한 저장

먼저 Set을 이용하는 방법을 살펴보자. Set을 매핑하는 방법을 살펴보기 전에 예제로 사용될 자바 코드를 살펴보자. 아래 자바 클래스의 images 프로퍼티가 Set 이다.

    public class Content {
        private Set images = new java.util.HashSet();
        
        public Set getImages() {
            return images;
        }
        public void setImages(Set images) {
            this.images = images;
        }
        ...
    }

위 코드에서 Set 타입 프로퍼티인 images는 이미지의 이름을 나타내는 String의 집합을 저장하기 위해서 사용된다. 이 정보를 저장하려면 다음과 같이 두 개의 테이블이 필요할 것이다.


이 관계를 Hibernate의 매핑 설정 파일에서 표현하려면 다음과 같이 set 태그를 사용해주면 된다.

    <class name="Content" table="CONTENT">
        ...
        <set name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <element type="string" column="FILENAME" not-null="true" />
        </set>
    </class>

위 매핑에서 사용된 태그는 set, key, element이며 각 태그의 의미는 아래와 같다.

  • set - 프로퍼티의 타입이 Set임을 나타낸다. table 속성은 Set에 저장될 요소의 데이터를 저장하고 있는 테이블을 나타낸다.
  • key - set에서 명시한 테이블에서 부모 테이블에 대한 외부키값을 저장하는 컬럼을 명시한다. 예제의 경우 CONTENT_IMAGE 테이블의 CONTENT_ID 컬럼이 CONTENT 테이블을 참조하는 외부키에 해당한다.
  • element - Set에 삽입될 요소에 대한 정보를 지정한다. type 속성은 Set에 저장될 요소의 타입을, column은 Set에 저장될 요소의 값을 읽어올 컬럼명을 나타낸다.
Set에서 중요한 점은 중복된 값을 저장할 수 없다는 점이다. 따라서, Set에 해당하는 데이터를 저장하는 테이블인 CONTENT_IMAGE는 CONTENT_ID, FILENAME 컬럼을 주요키로 사용해야 한다.

Set에 데이터를 저장하거나 Set에서 데이터를 삭제할 때에는 Set 프로퍼티를 직접 변경하면 된다. 예를 들어, Set에 새로운 데이터를 저장할 때에는 다음과 같은 코드를 사용하면 된다.

    Session session = ...;
    Transaction tx = session.beginTransaction();
    
    Content content = new Content();
    Set images = content.getImages();
    images.add("img1.gif");
    images.add("img2.gif");
    
    tx.commit();
    session.close();

Bag을 이용한 저장

Set과 달리 Bag은 중복된 값을 저장할 수 있는 콜렉션이다. 자바의 콜렉션은 Bag을 제공하지 않지만, 프로퍼티 타입을 List로 지정해서 Bag을 구현할 수 있다. java.util.ArrayList와 같은 클래스를 Bag의 구현 클래스로 사용하면 된다. 예를 들면 아래와 같다.

    public class Content {
        ...
        private List images = new ArrayList();
        
        public List getImages() {
            return images;
        }
        public void setImages(List images) {
            this.images = images;
        }
    }

Bag을 사용할 경우 중복된 값을 저장할 수 있기 때문에, 아래 그림과 같이 Bag의 요소를 저장하는 CONTENT_IMAGE 테이블이 외부키만 가질 뿐 주요키는 존재하지 않게 된다.


설정 파일에 관계를 표시하는 방법은 Set과 완전히 동일하며 차이점이라면 set 태그 대신에 bag 태그를 사용하는 것 뿐이다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <bag name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <element type="string" column="FILENAME" not-null="true" />
        </bag>
    </class>

순서가 있는 Bag을 이용한 저장

앞서 Bag은 순서가 없었는데, idbag 태그를 사용하여 순서가 있는 Bag을 사용할 수 있다. 순서가 있는 Bag을 사용하려면 먼저 Bag의 요소를 저장할 테이블에 순서값을 갖는 주요키를 추가해주어야 한다. 예를 들어, CONTENT_IMAGE 테이블에 다음과 같이 주요키 컬럼을 추가할 수 있다.


위와 같이 주요키를 사용하면, 주요키값을 사용해서 순서를 갖는 Bag을 정의할 수 있게 된다. 순서가 있는 Bag은 idbag 태그를 사용해서 정의하면 아래와 같이 collection-id 태그를 사용해서 주요키 컬럼에 대한 정의를 추가하는 것만 제외하면 bag을 정의할 때와 동일한 코드를 사용한다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <idbag name="images" table="CONTENT_IMAGE" lazy="true">
            <collection-id type="int" column="CONTENT_IMAGE_ID">
                <generator class="increment" />
            </collection-id>
            <key column="CONTENT_ID" />
            <element type="string" column="FILENAME" not-null="true" />
        </idbag>
    </class>

List를 이용한 저장

List는 인덱스라는 개념을 갖고 있다. List는 0번째 인덱스에 저장되는 요소, 5번째 인덱스에 저장되는 요소와 같이 요소의 저장 위치 정보를 필요로 하는 것이다. 따라서 List를 저장하기 위해서는 요소의 위치를 나타내는 인덱스값을 저장할 컬럼을 필요로 한다. 예를 들어, 아래 그림에서 CONTENT_IMAGE 테이블의 POSITION 컬럼과 같이 인덱스를 저장할 컬럼을 필요로 하는 것이다.


위와 같이 List의 인덱스 값을 저장할 컬럼을 지정했다면, 다음과 같이 list 태그를 사용해서 List 타입의 프로퍼티를 설정할 수 있다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <list name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <index column="POSITION" />
            <element type="string" column="FILENAME" not-null="true" />
        </list>
    </class>

위 코드에서 중요한 건 index 태그인데, 이 index 태그는 List의 인덱스 값을 저장할 컬럼을 지정해준다.

Map을 이용한 저장

프로퍼티에 Map을 사용해야 하는 경우도 있다. 예를 들어, 회원 가입시 회원이 답한 질문에 대해서만 값을 기록하고 싶다고 해 보자. 이 경우 다음과 같이 테이블이 구성될 것이다. ANSWER 테이블의 MEMBER_ID 컬럼은 외부키이며, QUESTION_NO 컬럼은 Map의 키를 저장한다. Map의 키는 고유해야 하므로 테이블의 주요키는 <외부키(MEMBER_ID), Map의 키(QUESTION_NO)>의 복합키로 구성된다.


이때, MEMBER 테이블과 매핑되는 자바 코드를 아래와 같이 작성할 수 있다.

    public class Member {
        ...
        
        private Map answers = new java.util.HashMap();
        
        public Map getAnswers() {
            return answers;
        }
        public void setAnswers(Map answers) {
            this.answers = answers;
        }
    }

위와 같은 자바 코드가 있을 때, Map 프로퍼티에 대한 매핑 설정은 아래와 같이 지정할 수 있다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" lazy="true">
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <element type="string" column="ANSWER" not-null="true" />
        </map>
    </class>

Map의 키를 저장하는 컬럼을 index 태그로 지정하며, Map의 값을 저장하는 컬럼을 element 태그로 명시한다. Map의 키는 외부키마다 고유하면 되므로, Map은 중복된 값을 가질 수 있다. (즉, ANSWER 컬럼에 중복된 값을 가질 수 있다.)

복합 프로퍼티의 집합 처리

앞서 살펴봤던 Content에서 이미지의 크기 정보까지 저장해야 한다고 해 보자. 이 경우, 다음과 같이 테이블 관계는 다음과 같이 변경될 것이다.


위 관계를 나타내기 위해서는 다음과 같이 Content 클래스 뿐만 아니라 CONTENT_IMAGE 테이블의 정보를 담을 클래스가 필요하다. 즉, 아래와 같이 두 개의 클래스를 필요로 한다.

    public class Content {
        ...
        private List images = new ArrayList();
        
        public List getImages() {
            return images;
        }
        public void setImages(List images) {
            this.images = images;
        }
    }
    
    public class Image {
        private String filename;
        private int width;
        private int height;
        
        public String getFilename() {
            return filename;
        }
        public void setFilename(String filename) {
            this.filename = filename;
        }
        public int getHeight() {
            return height;
        }
        public void setHeight(int height) {
            this.height = height;
        }
        public int getWidth() {
            return width;
        }
        public void setWidth(int width) {
            this.width = width;
        }
    }

Content의 images 프로퍼티에 저장되는 값은 Image가 될 것이다. 이렇게 집합에 들어갈 요소가 여러 프로퍼티로 구성된 경우 element 태그 대신에 composite-element 태그를 사용하면 된다. composite-element 태그는 다음과 같이 사용된다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <list name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <index column="POSITION" />
            <composite-element class="javacan.hibernate.test.Image">
                <property name="filename" column="FILENAME" />
                <property name="width" column="WIDTH" />
                <property name="height" column="HEIGHT" />
            </composite-element>
        </list>
    </class>

composite-element 사용시 부모/자식 간에 양방향 연결 매핑

composite-element 태그를 사용할 때, 위와 같이 설정할 경우 Content(부모)에서 Image(자식)로만 접근이 가능하며 반대로 Image(자식)에서 Content(부모)로는 접근할 수가 없다. (예를 들어, Image.getContent()와 같은 메소드가 없다.) 양방향으로 접근할 수 있도록 하려면 Image 클래스에서 다음의 두 메소드를 추가하고,

    public class Image {
        ...
        private Content content;
        
        public void setContent(Content content) {
            this.content = content;
        }
        public Content getContent() {
            return content;
        }
    }

그런 뒤, 매핑 설정 파일에서 다음과 같이 composite-element 태그에 parent 태그를 추가해주면 된다. parent 태그의 name 속성은 부모를 참조하는 프로퍼티의 이름을 나타낸다.

    <class name="javacan.hibernate.test.Content" table="CONTENT">
        ...
        <list name="images" table="CONTENT_IMAGE" lazy="true">
            <key column="CONTENT_ID" />
            <index column="POSITION" />
            <composite-element class="javacan.hibernate.test.Image">
                <parent name="content" />
                <property name="filename" column="FILENAME" />
                <property name="width" column="WIDTH" />
                <property name="height" column="HEIGHT" />
            </composite-element>
        </list>
    </class>

sort와 order-by를 사용한 정렬

Hibernate는 콜렉션의 정렬과 관련해서 두 가지 속성 sort와 order-by를 제공한다. sort는 데이터베이스에서 데이터를 읽어온 뒤 메모리 상에서 정렬을 수행하는 방식이며, order-by는 데이터베이스에서 정렬된 값을 읽어오는 방식이다.

sort 속성을 사용한 Map과 Set의 정렬

먼저 sort 방식부터 살펴보도록 하자. Map과 Set은 sort 속성을 사용해서 데이터를 정렬할 수 있다. 예를 들어, Map에 대한 데이터를 메모리에서 정렬하고 싶다면 다음과 같이 sort 속성을 지정하면 된다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" sort="natural" lazy="true">
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <element type="string" column="ANSWER" not-null="true" />
        </map>
    </class>

Hibernate는 map 태그의 sort 속성을 "natural"로 지정하면 SortedMap을 사용하여 관련 값을 저장한다. 정렬 대상은 Map의 키에 해당하는 필드이며, Object.compareTo() 메소드를 사용하여 키를 비교하게 된다.

만약 composite-element와 같이 복합 요소를 값으로 사용하는 경우에는 다음과 같이 sort 속성의 값에 Comparator 클래스를 명시해주면 된다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" lazy="true"
                sort="javacan.hibernate.test.ImageComparator" >
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <composite-element class="javacan.hibernate.test.Image">
                ...
            </composite-element>
        </map>
    </class>

sort 속성에 명시한 Comparator는 composite-element에서 명시한 클래스의 알맞은 속성값을 사용하여 값을 비교하면 된다.

set 태그에 sort 속성을 추가하면 TreeSet과 같은 방식으로 동작하며, map의 경우와 마찬가지로 "natural"을 값으로 갖거나 또는 Comparator를 직접 명시할 수도 있다.

Bag과 List는 sort를 사용하여 정렬할 수 없다. (Bag의 경우는 마땅한 구현체가 존재하지 않으며, List는 인덱스값을 사용하여 정렬하기 때문이다.)

order-by 속성을 사용한 Map, Set, Bag의 정렬

order-by 속성은 SQL의 ORDER BY 절을 통해 데이터베이스의 정렬 기능을 사용하여 목록을 읽어오는 방식이다. 이 속성을 통해 테이블의 임의의 컬럼을 기준으로 데이터를 읽어올 수 있게 된다. Map과 Set 그리고 Bag에 대해서 이 속성을 사용할 수 있으며, 다음과 같이 SQL 쿼리의 ORDER BY 부분에 들어갈 쿼리를 입력해주면 된다.

    <class name="javacan.hibernate.test.Member" table="MEMBER">
        ...
        <map name="answers" table="ANSWER" lazy="true"
                  order-by="QUESTION_NO asc">
            <key column="MEMBER_ID" />
            <index column="QUESTION_NO" type="int" />
            <element type="string" column="ANSWER" not-null="true" />
        </map>
    </class>

order-by 속성을 사용할 때 주의할 점은 Set과 Map의 경우 JDK 1.4 또는 그 이상의 버전에서만 지원하는 LinkedHashSet과 LinkedHashMap을 사용해서 매핑을 처리한다는 점이다. 따라서, JDK 1.3 을 사용하는 경우에는 Map과 Set에서 order-by 속성을 사용할 수 없다.

many-to-many 관계의 매핑 처리

때에 따라 many-to-many 관계를 표시해주어야 하는 경우도 있다. 보통 many-to-many 관계는 카테고리와 항목의 관계를 표시할 때 사용된다. 예를 들어, 쇼핑몰의 카테고리와 제품의 관계를 생각해보자. 카테고리에는 여러 제품이 포함될 수 있으며, 반대로 제품은 여러 카테고리에 포함될 수 있다.

이런 many-to-many의 관계를 표현하기 위해서는 다음과 같이 중간에 다리 역할을 하는 테이블을 필요로 한다.


테이블의 관계에서는 위와 같이 중간에 다리 역할을 하는 테이블이 존재하지만, 자바에서는 중간 객체를 필요로 하기 보다는 아래와 같이 콜렉션을 통해서 many-to-many 관계를 표시하게 될 것이다.

    public class Category {
        private Integer id;
        private Set items = new HashSet(); // Item에 대한 매핑 저장
        
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public Set getItems() {
            return items;
        }
        public void setItems(Set items) {
            this.items = items;
        }
        ...
    }

    public class Item {
        private Integer id;
        private Set categories = new HashSet(); // Category에 대한 매핑 저장
        
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public Set getCategories() {
            return categories;
        }
        public void setCategories(Set categories) {
            this.categories = categories;
        }
    }

이 관계를 표시하는 방법은 콜렉션과 one-to-many를 사용하던 것과 동일한데, one-to-many 대신에 다음과 같이 many-to-many 태그를 사용하여 표현하면 된다.

    <class name="javacan.hibernate.test.Category" table="CATEGORY" lazy="true">
        <id name="id" type="int" column="CATEGORY_CODE" unsaved-value="null">
            <generator class="increment" />
        </id>
        
        <set name="items" table="CATEGORY_ITEM" lazy="true" cascade="save-update">
            <key column="CATEGORY_CODE" />
            <many-to-many class="javacan.hibernate.test.Item" column="ITEM_ID" />
        </set>
    </class>
    
    <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
        <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
            <generator class="increment" />
        </id>

        <set name="categories" table="CATEGORY_ITEM" lazy="true" cascade="save-update"
             inverse="true">
            <key column="ITEM_ID" />
            <many-to-many class="javacan.hibernate.test.Category" column="CATEGORY_CODE" />
        </set>
    </class>

위 코드를 보면 many-to-many 관계를 표현하기 위해 콜렉션 태그인 set 태그에 many-to-many 태그를 중첩한 것을 알 수 있는데, 설정 정보에서 사용된 요소를 설명하면 아래와 같다.

  • set 태그의 table 속성 - 두 테이블을 연결해서 many-to-many 관계를 만들어주는 연관 테이블을 명시한다.
  • key 태그의 column 속성 - 연관 테이블에서 현재 클래스에 대한 외부키 저장 필드를 명시한다. 예를 들어, Category 클래스 설정 부분에서 key 태그의 column 속성은 CATEGORY_ITEM(연관 테이블)가 CATEGORY 테이블을 참조하는 컬럼인 CATEGORY_ID를 값으로 갖는다.
  • many-to-many 태그의 class 속성 - Set의 요소로 저장될 클래스. Category의 Set에는 Item이 저장되므로, Category에서는 이 속성의 값에 "Item"을 명시한다.
  • many-to-many 태그의 column 속성 - 연관 테이블에서 Set에 저장할 요소를 참조할 때 사용될 컬럼. 예를 들어, Category 클래스의 Set에는 Item이 포함되는데, CATEGORY_ITEM(연관 테이블)이 ITEM 테이블을 참조하는 컬럼은 ITEM_ID 이므로, Category에서는 이 속성의 값에 "ITEM_ID"를 명시한다.
Item 클래스와 관련된 set 태그를 보면 inverse 속성의 값이 true인 것을 알 수 있다. 양방향으로 연결하고 싶을 때에 inverse 속성의 값을 true로 지정한다고 했었는데, 실제로 이 inverse 속성의 값이 의미하는 바는, '연관의 반대편에서만 연관의 변경작업을 할 수 있다'는 의미이다. 예를 들어, Category-Item 관계에서 Item에 inverse 속성값이 true인데, 이는 Item에서 연관된 카테고리를 수정하더라도 그 변경 내역이 적용되지 않는다는 것을 의미한다. 다음의 코드를 보자.

    Transaction tx = session.beginTransaction();
    Item item = (Item)session.get(Item.class, id);
    Set categories = item.getCategories();
    categories.clear();
    tx.commit();

위 코드를 실행하면 예상은 Item이 관련 카테고리에 제거되는 것이겠지만, inverse 속성이 true이기 때문에 Item에서 변경한 Category와의 연관은 적용되지 않는다. Item-Category 관계에서 연관을 변경하고 싶다면, 다음과 같이 Category에서 처리해야 한다.

    Transaction tx = session.beginTransaction();
    Item item = (Item)session.get(Item.class, id);
    Iterator iter = item.getCategories().iterator();
    while (iter.hasNext()) {
        Category category = (Category)iter.next();
        category.getItems().remove(item);
    }
    tx.commit();

inverse 속성을 true로 지정할 관계를 어느 객체에서 처리해주어야 하는 지 기억해두기 바란다. 그래야 버그없는 어플리케이션을 개발할 수 있을 것이다. 관련링크:
반응형
Hibernate에서 퍼스시턴트 객체를 이용한 CRUD 처리 및 퍼시스턴트 생명주기, 객체로딩전략, 영속성 전이에 대해서 살펴본다.

퍼시스턴트 객체의 영속성 생명주기

Hibernate의 퍼시스턴트 객체는 영속성과 관련해서 아래 그림과 같은 세 가지 상태를 갖는다.


위 그림에서 모서리가 둥근 건 퍼시스턴트 객체의 상태를 나타내며, 메소드는 Hibernate Session이 제공하는 메소드를 의미한다. 각 상태는 퍼시스턴트가 객체가 데이터베이스 테이블과 연결되었느냐에 따라 달라진다.

  • 비영속 상태 - 퍼시스턴트 객체를 처음 만들었을 때의 상태. 데이터베이스 테이블에 관련 데이터가 없으며, 연관된 Session이 없다.
  • 영속 상태 - 현재 활성화된 Session과 연결된 퍼시스턴트 객체. 이 상태의 퍼시스턴트 객체는 고유성을 가지며, 프로퍼티 값의 변경이 Session을 통해 자동으로 데이터베이스에 반영된다.
  • 준영속 상태 - 영속 상태의 퍼시스턴트 객체가 Session과 연결이 끊기면 준영속 상태가 된다. Hibernate의 관리를 받지는 않지만, 영속 데이터를 갖고 있다.
위 상태를 코드에서 표시하면 아래와 같다.


퍼시스턴트 객체가 고유성이 없는 경우(즉, 데이터베이스에 1대 1로 매핑되는 데이터가 존재하지 않는 경우) 비영속 상태에 해당한다. 예를 들어, 위 코드처럼 퍼시스턴트 객체를 새로 생성한 경우에 비영속 상태가 된다.

비영속 상태의 객체를 Session을 통해 저장하면(save() 또는 saveOrUpdate() 등을 사용) 퍼시스턴트 객체는 영속 상태가 된다. 영속 상태의 퍼시스턴트 객체는 활성화된 Session과 관련을 맺는다. 보통 영속 상태가 된다 하더라도 Session의 트랜잭션이 커밋되지 않으면 데이터베이스에 실제로 저장되지는 않는다.

일단, 퍼시스턴트 객체가 영속 상태가 되면 퍼시스턴트 객체의 값 변화는 자동으로 데이터베이스에서 반영된다. (이를 automatic dirty checking)이라 한다. Hibernate Session은 트랜잭션이 끝날 때 Session과 관련 있는 영속 상태 퍼시스턴트 객체의 프로퍼티가 변경되었는 지 확인한 후, 변경된 내용을 데이터베이스와 동기화한다.

Session이 종료되면 Session과 관련된 모든 영속 상태의 퍼스시턴트 객체는 준영속 상태가 된다. 준영속 상태의 퍼시스턴트 객체는 데이터베이스와 연결되어 있지 않기 때문에 프로퍼티 값을 변경하더라도 그 내역이 데이터베이스에 반영되지는 않는다. 준영속 상태의 퍼시스턴트 객체는 나중에 다시 다른 Session과 연결될 수 있으며, Session과 연결되면 다시 영속 상태로 바뀌게 된다. 다음의 코드는 준영속 상태로 된 객체가 다시 영속 상태로 변경되는 예를 보여주고 있다.

    Session session1 = sessions.openSession();
    ...
    Book book = (Book)session1.get(Book.class, new Integer(1)); // 영속 상태
    ...
    session1.close(); // 이 시점에서 book은 준영속 상태로 변경
    
    ...
    
    Session session2 = sessions.openSession();
    ...
    session2.update(book); // 이 시점에서 다시 영속 상태로 변경
    ...
    session2.close();

준영속 객체의 활용

준영속 객체는 Session과 연결되어 있지는 않지만 테이블의 값을 저장하고 있기 때문에, 데이터를 보여줄 때 유용하게 사용할 수 있다. 보통 MVC 패턴에 기반한 프로젝트인 경우 다음과 같은 방식으로 준영속 객체를 사용한다.

  • 컨트롤러는 Hibernate를 사용하여 데이터베이스로부터 데이터를 읽어온다.
  • 컨트롤러는 읽어온 퍼시스턴트 객체를 준영속 객체로 변환한다. (보통, Session을 종료)
  • 컨트롤러는 준영속 객체를 뷰에 전달한다.
  • 뷰는 준영속 객체를 사용해서 데이터를 출력한다.
뷰에서는 일반 자바빈 객체를 사용해서 데이터를 출력하듯이 준영속 객체를 사용해서 데이터를 출력하면 된다.

또한 준영속 객체는 다시 영속 객체로 전환될 수 있기 때문에, 여러 단계에 걸쳐서 데이터를 수정한 후 DB에 반영하는 방식에서도 잘 적용될 수 있다. 예를 들어, 웹 어플리케이션인 경우 세션에 준영속 객체를 저장한 뒤 여러번에 걸쳐서 객체의 데이터를 수정한다. 그리고 마지막 과정에서 세션에 저장했던 준영속 객체를 읽어와 영속 객체로 전환한 뒤 변경 결과를 반영하면 된다.

Session이 제공하는 CRUD 처리 메소드

Hibernate를 사용하는 가장 큰 이유는 SQL 쿼리를 작성하지 않고도 Hibernate가 제공하는 Session의 메소드를 사용해서 간단한 코드로 CRUD 작업을 처리할 수 있기 때문이다. Session은 CRUD 작업을 처리할 수 있는 메소드를 제공하는데, 이들 메소드는 다음과 같다.

  • save(Object obj) - 새로운 객체를 (테이블에) 저장한다.
  • save(Object obj, Serializable id) - 새로운 객체를 (테이블에) 저장한다. 이때 식별값으로 두번째 인자로 전달받은 객체를 사용한다.
  • Object get(Class type, Serializable id) - 지정한 타입의 객체를 읽어온다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • Object load(Class type, Serializable id) - 지정한 타입의 객체를 읽어온다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • load(Object obj, Serializable id) - obj의 타입과 관련된 데이터를 읽어와 obj에 저장한다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • update(Object obj) - 객체의 값을 변경한다.
  • update(Object obj, Serializable id) - 지정한 식별자에 해당하는 테이블 행을 객체가 저장한 값으로 변경한다.
  • saveOrUpdate(Object obj) - 새로운 객체인 경우 (테이블에) 저장하고, 이미 존재하는 객체인 경우 (테이블의) 값을 새로운 값으로 변경한다.
  • delete(Object obj) - 객체를 (테이블에서) 삭제한다.
객체를 읽어오는 메소드가 get과 load 두가지가 있는데, 이 두 메소드는 다음과 같은 차이가 있다.

  • get : 읽어올 객체가 없는 경우 null을 리턴하며, 프록시 객체를 리턴하지 않는다.
  • load : 읽어올 객체가 없는 경우 예외를 발생하며, 프록시 객체를 리턴할 수 있다.
load() 메소드는 읽어올 객체가 존재하지 않는 경우 예외를 발생하기 때문에, 존재해야 하는 데이터가 없는 경우를 예외상황으로 처리하고 싶을 때 load() 메소드를 사용하면 된다. 그렇지 않고 단지 객체가 존재하는지의 여부를 판단할 필요만 있는 거라면 get() 메소드를 사용해서 리턴된 값이 null인지의 여부를 검사하는 편이 낫다.

객체를 변경하는 방법은 크게 두가지가 있는데 하나는 Session의 트랜잭션 범위에서 영속 상태의 객체의 프로퍼티를 변경하는 것이며, 두번째 방법은 준영속 상태의 개체를 변경한 뒤 Session과 연관시키는 것이다. 다음은 두 방식의 차이점을 코드로 보여준다.

    Session session1 = sessions.openSession();
    Transaction tx1 = session1.beginTransaction();
    
    Item item = (Item)session1.get(Item.class, new Integer(6));
    item.setTitle(newTitle); // 영속 상태의 객체 프로퍼티 값 변경    
    tx1.commit(); // update() 메소드를 호출하지 않아도 영속 상태 객체의 변경 내용이 적용
    session1.close();
    
    item.setLowPrice(1000); // 준영속 상태의 객체 프로퍼티 값을 변경
    
    Session session2 = sessions.openSession();
    Transaction tx2 = session1.beginTransaction();
    
    session2.update(item); // 준영속 상태를 영속 상태로 변경
    
    tx2.commit();
    session2.close();

객체를 삭제할 때에는 다음과 같이 get()이나 load()로 읽어온 뒤 delete() 메소드를 호출하면 된다.

    Session session = sessions.openSession();
    Transaction tx = session1.beginTransaction();
    
    Item item = (Item)session.get(Item.class, new Integer(6));
    session.delete(item);
    
    tx.commit();
    session.close();

이외에 CRUD를 처리하는 방법으로는 iterate(), update(), delete() 메소드에 쿼리를 전달하는 방식도 있고, Criteria를 사용해서 조건에 따라 조회하는 방법도 있는데 이에 대한 내용은 본 글에서는 다루지 않으며, 시리즈가 진행되는 동안에 살펴볼 것이다.

객체 로딩 전략, 프록시와 외부조인

도메인 영역의 객체는 서로 연관되어 있으며, Hibernate는 이런 연관 관계를 알맞게 처리해준다. 예를 들어, 지난 번 글에서 예제로 사용했었던 Item과 Bid의 관계를 다시 살펴보자.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true" cascade="all">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

      <class name="Bid" table="BID">
            ...
            
            <many-to-one
                         name="item"
                         column="ITEM_ID"
                         class="Item"
                         not-null="true" />
            ...
      </class>

위 매핑 설정에서 알 수 있듯이 하나의 Item은 여러개의 Bid를 갖고 있으며 별도의 설정을 하지 않는 이상 Item을 읽어오면 Item과 관련된 모든 Bid 객체를 읽어오게 된다. 하지만, 경우에 따라서는 Item 객체만 필요하고 Item과 관련된 Bid 객체는 필요하지 않은 경우가 있으며, 이 경우 아래와 같은 코드를 실행할 때 Bid는 읽어오지 않길 원할 것이다.

    Session session = ...;
    ..
    Item item = (Item)session.get(Item.class, id); // 관련 Bid는 읽어오지 않길 원할 때가 있다
    ..
    session.close();

반대로 Bid 객체만 필요하고 Bid와 관련된 Item 객체는 필요하지 않은 경우가 있을 수 있다. 이렇게 연관된 객체를 읽어오지 않고 싶은 경우가 있을 수 있고, 반대로 늘 관련 객체를 함께 읽어오길 원하는 경우도 있을 수 있다. 이렇게 상황에 따라 데이터를 조회하는 네 가지 방식을 사용할 수 있는데, 이 네 가지 방식은 다음과 같다.

  • 즉시 읽어오기 방식 - 연관된 객체를 모두 읽어온다. 만약 one-to-one으로 연관되었을 경우, 연관된 객체를 읽어오기 위해 데이터베이스 조회가 발생한다.
  • 실제 사용할 때 읽어오기(lazy) 방식 - 실제로 연관된 객체가 사용될 때, 데이터베이스 조회가 발생한다. 예를 들어, Bid 객체를 읽어왔을 때, Bid.getItem() 메소드가 호출될 때 Item 객체를 읽어오기 위해 데이터베이스 조회가 발생한다.
  • 조인 읽어오기(outer-join) 방식 - 외부 조인을 수행하는 SQL 쿼리를 사용하여 연관된 객체를 모두 읽어온다. 즉시 읽어오기 방식이 연관 객체를 읽어오기 위해 여러번의 데이터베이스 조회가 발생하는 것과 달리 한번의 조회만 발생한다.
Hibernate는 위의 접근 방식을 매핑 파일에 속성을 사용해서 설정할 수 있도록 하고 있는데, 본 글에서는 실제 사용할 때 읽어오는 lazy 방식과 조인해서 읽어오는 outer-join 방식을 살펴보겠다. (참고로, lazy 방식과 outer-join 방식 중 아무것도 사용하지 않으면 즉시 읽어오는 방식을 사용한다.)

lazy 방식(프록시)

실제로 사용할 때 연관 객체를 읽어오는 걸, lazy 방식이라고 한다. Hibernate는 lazy 방식으로 연관 객체를 읽어올 때 다음과 같이 프록시 객체를 사용한다.


lazy 방식으로 지정된 경우, Hibernate는 위 그림에서 볼 수 있듯이 연관된 객체를 곧바로 읽어오지 않는다. 대신 연관된 객체와 연결될 수 있는 프록시 객체를 생성한다. 연관된 객체가 필요할 때 실제 연관될 객체가 생성되서 프록시와 연결된다.

many-to-one 관계에서 lazy 방식을 적용하기 위해서는 다음과 같이 연관될 클래스에 대한 정보를 담고 있는 class 태그의 lazy 속성값을 true로 지정해주어야 한다. (Bid-Item의 관계가 many-to-one)

      <class name="Item" table="ITEM" lazy="true">
            ...
      </class>

      <class name="Bid" table="BID">
            ...
            
            <many-to-one
                         name="item"
                         column="ITEM_ID"
                         class="Item"
                         not-null="true" />
            ...
      </class>

위와 같이 lazy 속성을 true로 지정하게 되면 Hibernate는 Item 클래스를 위한 프록시 클래스를 자동으로 생성해서 프록시로 사용한다.

one-to-one 관계에서도 마찬가지로 lazy 속성을 사용해서 lazy 방식을 적용할 수 있다. 예를 들어, 아래와 같이 one-to-one 관계를 맺을 때, 양쪽 모두 lazy 방식으로 읽어오도록 설정할 수 있다.

      <class name="Item" table="ITEM" lazy="true">
            ...
            <one-to-one name="detail"
                        class="ItemDetail" 
                        cascade="save-update" />            
            ...
            
      </class>

      <class name="ItemDetail" table="ITEM_DETAIL" lazy="true">
            ...
            <one-to-one name="item"
                        class="Item"
                        constrained="true" />
      </class>

콜렉션을 읽어올 때에도 lazy 방식을 사용할 수 있다. 예를 들어, set 태그에 다음과 같이 lazy 속성을 추가함으로써 lazy 방식으로 콜렉션을 읽어오도록 명시할 수 있다.

    <class name="javacan.hibernate.test.Item" table="ITEM" .. >
        ...

        <set name="bids" inverse="true" cascade="all-delete-orphan" lazy="true">
            <key column="ITEM_ID" />
            <one-to-many class="javacan.hibernate.test.Bid" />
        </set>
    </class>

lazy 방식을 사용할 때 주의할 점은 객체가 영속 상태일 때에만, 즉 Session과 연결되어 있는 경우에만 lazy 방식으로 데이터를 읽어올 수 있다는 것이다. 예를 들어, 아래의 코드처럼 lazy 방식으로 읽어온 프로퍼티를 준영속 상태에서 읽어올 수 없으며, 이런 경우 예외가 발생하게 된다.

      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      // Item의 bids 프로퍼티와 관련 객체를 lazy 방식으로 로딩
      Item item = (Item)session.get(Item.class, someId); 
      
      tx.commit();
      session.close();
      
      // 준영속 상태에서 lazy 방식의 프로퍼티에 접근 -> 예외 발생
      Iterator iter = item.getBids().iterator();

따라서, lazy 방식으로 읽어온 프로퍼티를 준영속 상태에서 사용해야 하는 경우에는 사전에 미리 lazy 방식으로 읽어온 프로퍼티를 초기화해야 한다. 이럴 때 사용할 수 있는 메소드가 Hibernate.initialize()이다. 위와 같은 경우 다음과 같이 Session을 종료하기 전에 Hibernate.initialize() 메소드를 호출해주면 된다.

      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      // Item의 bids 프로퍼티와 관련 객체를 lazy 방식으로 로딩
      Item item = (Item)session.get(Item.class, someId); 
      Hibernate.initialize(item.getBids());      
      tx.commit();
      session.close();
      
      // 사전에 초기화 되었으므로 예외 발생안함
      Iterator iter = item.getBids().iter();

outer-join 속성을 사용한 연관 객체 로딩

lazy 방식은 연관된 객체가 실제로 사용되기 전까지 최대한 늦게 객체를 로딩하는 반면에 outer-join 방식은 최대한 빨리 연관된 객체를 로딩한다. outer-join 방식은 외부 조인을 사용해서 한번의 SQL 쿼리로 연관 객체와 관련된 모든 데이터를 읽어오므로 데이터베이스 조회 회수를 줄이면서 동시에 연관 객체를 읽어오게 된다.

outer-join 속성값을 사용해서 outer-join 방식을 설정할 수 있는데, outer-join 속성에 사용할 수 있는 값을 다음과 같이 세가지가 존재한다.

  • auto - 기본값으로서 프록시가 가능한 경우 lazy 방식으로 읽어오고, 그렇지 않은 경우 outer-join 방식으로 읽어온다.
  • true - 프록시가 가능한 경우에도, 항상 outer-join 방식으로 읽어온다.
  • false - 프로시를 사용하지 않더라도, 항상 outer-join 방식을 사용하지 않는다.
예를 들어, Bid 객체를 읽어올 때 many-to-one의 관계에 있는 Item을 항상 outer-join 방식으로 읽어오고 싶다면 다음과 같이 하면 된다.

      <many-to-one name="item" class="Item" outer-join="true">

영속성 전이

이제 영속성 전이에 대해서 살펴보자. 영속성 전이는 영속 상태의 객체와 연관된 비영속 및 준영속 상태의 객체에 자동으로 영속성이 전파되는 특징을 말한다. 예를 들어, 다음의 코드를 보자.

      Bid bid = new Bid();
      ... // bid 프로퍼티들의 값 설정
      
      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      Item item = (Item)session.get(Item.class, id);
      item.addBid(bid); // 영속 객체(item)에 비영속 객체(bid)를 연관
      
      tx.commit();
      session.close();

위 코드는 영속 객체인 item에 비영속 객체인 bid를 연관시키고 있다. 이때 Hibernate는 자동으로 비영속 상태인 bid 객체를 영속 상태로 전환하게 된다. (영속 상태로 전환된다는 것은 데이터베이스에 저장된다는 것을 의미한다.) 이렇게 영속 상태의 객체와 연관된 비영속/준영속 객체들이 자동으로 영속 상태가 되는 것을 영속성 전이라고 표현한다.

영속성 전이는 저장 뿐만 아니라 삭제인 경우에도 발생할 수 있다. 예를 들어, 하나의 경매 항목이 삭제되면 그와 관련된 입찰 정보도 삭제될 것이다. 이렇게 영속 상태의 객체가 비영속 상태로 변경될 때 연관된 영속 객체들도 함께 비영속 상태로 변경되는 것 역시 영속성 전이에 해당한다.

Hibernate는 영속성 전이를 연관 객체에 대해 어느 정도까지 적용할 지를 설정하기 위해 cascade 속성을 사용하며, cascade 속성은 다음과 같은 값들을 가질 수 있다.

  • none - 연관을 무시한다.
  • save-update - 트랜잭션이 커밋될 때, save()나 update() 메소드가 호출될 때, 새롭게 생성한 비영속 객체를 영속 객체에 연관할 때, 준영속 객체를 영속 상태로 변경할 때, 영속성 전이를 수행한다.
  • delete - 객체가 delete()에 전달될 때 영속성 전이를 수행해서 연관되어 있는 객체를 삭제한다.
  • all - save-update와 delete 옵션을 함께 수행한다. 또한, evict() 메소드나 lock() 메소드가 실행될 때에도 영속성 전이를 수행한다.
  • all-delete-orphan - all과 같은 기능을 수행한다. 더불어, 연관에서 제거된 퍼시스턴트 객체를 삭제한다. 콜렉션과 관련된 영속성 전이에 사용된다.
예를 들기 위해 먼저 다음의 설정을 보자.

      <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
            ...
            <set name="bids" inverse="true" cascade="save-update" >
                  <key column="ITEM_ID" />
                  <one-to-many class="javacan.hibernate.test.Bid" />
            </set>
      </class>
      
      <class name="javacan.hibernate.test.Bid" table="BID">
            
            <many-to-one name="item"
                         column="ITEM_ID"
                         class="javacan.hibernate.test.Item"
                         not-null="false"  />
      </class>
      

위 설정 코드는 Item-Bid를 one-to-many 관계로 하고 있고, Item->Bid로의 영속성 전이를 save-update로 설정하였다. save-update는 영속 객체와 연관된 객체에 영속성이 전이되므로 아래 코드와 같이 새로운 Bid 객체를 생성해서 연관한 경우 새롭게 생성한 Bid 객체가 DB에 저장된다.

      Session session = ...;
      Transaction tx = session.beginTransaction();
      
      Bid bid = new Bid();
      Item item = (Item)session.get(Item.class, id);
      item.addBid(bid);
      
      tx.commit();
      session.close();

하지만, 설정 파일의 set 태그에서 cascade 속성을 none으로 하게 되면 영속성 전이가 수행되지 않으므로, 위 코드에서 새롭게 생성한 Bid 객체는 데이터베이스에 저장되지 않는다.

cascade 속성값이 delete인 경우는 연관된 객체만 삭제한다. 예를 들어, 아래의 코드를 살펴보자.

      Session session1 = session.openSession();
      ...
      Item item1 = (Item)session1.get(Item.class, id);
      session1.delete(item1); // item1과 연관된 ItemDetail도 삭제
      ...
      session1.close();
      
      Session session2 = session.openSession();
      ...
      Item item2 = (Item)session2.get(Item.class, id);
      item2.setDetail(null); // item1과 연관됐었던 ItemDetail을 연관시키지 않음
      session2.delete(item2); // item1만 삭제되고, 연관됐던 ItemDetail은 삭제되지 않음
      ...
      session2.close();

cascade 속성값이 delete인 경우에는 첫번째와 같이 연관되어 있는 객체는 함께 삭제되나, 두번째와 같이 연관됐었다가 연관되지 않게된 객체의 경우는 삭제되지 않는다.

delete와 달리 all-delete-orphan은 연관됐었던 객체를 연관에서 해제하더라도 삭제된다. 예를 들어, Item과 Bid의 관계를 아래와 같이 설정했다고 해 보자. 아래 코드는 Item-Bid의 one-to-many 관계에서 cascade 속성을 all-delete-orphan으로 설정하고 있다.

      <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
            ...
            <set name="bids" inverse="true" cascade="all-delete-orphan" >
                  <key column="ITEM_ID" />
                  <one-to-many class="javacan.hibernate.test.Bid" />
            </set>
      </class>
      
      <class name="javacan.hibernate.test.Bid" table="BID">
            
            <many-to-one name="item"
                         column="ITEM_ID"
                         class="javacan.hibernate.test.Item"
                         not-null="false"  />
      </class>
      

이 상태에서 다음과 같은 코드를 실행했다고 해 보자.

      Session session = HibernateUtil.currentSession();
      tx = session.beginTransaction();
      
      Item item = (Item)session.get(Item.class, new Integer(2));
      item.getBids().clear(); // Set에 있는 모든 객체를 비움
      
      tx.commit();

위 코드는 Item의 bids 프로퍼티인 Set 객체에 저장되어 있던 객체 자체를 삭제하는 게 아니라 단지 Set 객체를 비운 것에 불과하다. 하지만, cascade 속성 값이 all-delete-orphan 이기 때문에, Set에 저장되어 있는 Bid 객체와 매핑된 테이블 데이터도 함께 제거된다.

다음 글에서는

본 글의 예제 코드에서 set 태그를 사용했었는데, Hibernate는 set 태그 이외에 list, map과 같은 다양한 타입의 콜렉션을 지원하고 있다.다음 글에서는 Hibernate가 제공하는 콜렉션에는 어떤 것들이 있으며 어떻게 사용하는 지에 대해서 살펴볼 것이다. 또한 살펴보지 않았던 many-to-many 관계를 Hibernate에서 어떻게 적용할 수 있는 지에 대해서도 살펴볼 것이다.

관련링크:
  1. darkhorizon 2009.04.14 13:40 신고

    좋은 글이네요.
    좀 담아가도 될까요?

    • 최범균 madvirus 2009.04.14 17:37 신고

      원문 링크와 작성자 이름 '최범균'을 게시글 상단에 표시해 주시기 바랍니다.

  2. darkhorizon 2009.04.15 15:16 신고

    네. 감사합니다

반응형
Hibernate에서 객체 사이의 연관, 클래스 상속 관계, 복합키를 설정하는 방법에 대해서 살펴본다.

클래스 상속과 테이블 매핑

먼저 살펴볼 내용은 클래스 상속 관계를 어떻게 테이블과 매핑할 것인가에 대한 내용이다. 클래스 계층을 ORM으로 처리하는 방식은 크게 다음과 같은 세 가지가 존재한다.

  • 클래스 마다 개별적 테이블 - 가장 단순한 방법으로서, 계층도에 있는 클래스와 완전히 매핑되는 테이블을 사용한다.
  • 클래스 계층 구조에 대해 하나의 테이블 - 계층도에 있는 모든 클래스를 하나의 테이블과 매핑시킨다.
  • 하위 클래스 마다 개별적 테이블 - 클래스 계층도와 매핑되도록 테이블을 설계한다. 테이블의 외부키를 사용해서 클래스의 상속 관계를 표현한다.
이 세가지 방식을 Hibernate에서 어떻게 구현할 수 있는 지 차례대로 살펴보도록 하자.

클래스 마다 개별적 테이블 사용

먼저 클래스 마다 개별적 테이블을 사용하는 방법을 살펴보도록 하자. 이 방식을 그림으로 설명하자면 다음과 같이 표현할 수 있다.


위 그림에서 Work 클래스는 추상 클래스이며, Book 클래스와 Article 클래스는 Work 클래스를 상속받은 하위 클래스이다. 이 클래스 계층 정보를 저장할 테이블을 보면 하위 클래스와 1-1 매핑되어 있는데, 여기서 중요한 건 하위 클래스에 매핑되는 테이블은 하위 클래스의 프로퍼티 뿐만 아니라 상위 클래스의 프로퍼티까지도 매핑한다는 점이다. 예를 들어, BOOK_WORK 테이블은 Book 클래스의 프로퍼티 뿐만 아니라 Work 클래스의 프로퍼티를 저장할 수 있는 컬럼이 정의되어 있으며, ARTICLE_WORK 테이블 역시 Article 클래스와 Work 클래스의 프로퍼티를 저장할 수 있도록 되어 있다.

이 방식은 사실상 다형성을 지원하지 않게 된다. 예를 들어, 위의 매핑을 처리하기 위한 설정 파일을 살펴보자.

      <class name="Book" table="BOOK_WORK">
            ...
      </class>
      
      <class name="Article" table="ARTICLE_WORK">
            ...
      </class>

이 설정 파일의 문제는 Hibernate에서 Work 타입으로 작업을 처리할 수 없다는 점이다. 예를 들어, Work 타입인 데이터를 읽어오려면, Book에 해당하는 데이터 목록을 읽어오고 Article에 해당하는 데이터 목록을 읽어온 뒤 두 목록을 합쳐주어야 한다. 이는 SQL 쿼리를 두번 실행한다는 것을 의미한다.

또, 이 매핑 전략은 개념상의 문제를 야기하는데 서로 다른 테이블이 개념상 같은 컬럼을 공유한다는 점이다. 즉, 위 그림에서 BOOK_WORK 테이블의 BOOK_WORK_ID 컬럼과 ARTICLE_WORK 테이블의 ARTICLE_WORK_ID 컬럼은 모두 Work 객체의 식별자값을 저장하기 위해서 사용된다. 이는 두 테이블의 주요키 컬럼에 들어가는 값이 해당 테이블 내에서 뿐만 아니라 두 테이블에 대해서도 유일해야 한다는 것을 의미하며, 따라서 식별자 값의 생성이 복잡해지게 된다.

클래스 계층 구조에 대해 테이블 사용

두번째 전략은 아래 그림과 같이 테이블 하나에 계층 구조에 있는 모든 클래스의 정보를 저장하는 것이다.


위 그림에서 테이블의 PUBLISHING, PRICE, ISBN 컬럼은 Book 객체에 매핑되며, MAGAZINE 컬럼은 Article 객체에 매핑된다. 어떤 컬럼이 어떤 클래스에 매핑되는 지를 구분하기 위해서는 별도의 컬럼을 필요로 하는데, 위 그림에서 WORK_TYPE 컬럼이 이에 해당한다.

Hibernate에서는 discriminator 태그와 subclass 태그를 사용해서 어떤 컬럼이 어떤 클래스의 어떤 프로퍼티에 매핑되는 지를 표시한다. 예를 들어, 위 그림의 OR 매핑은 다음과 같이 Hibnerate에서 표현할 수 있다.

      <class name="Work"
                table="WORK_ALL"
                discriminator-value="WO">
            
            <id name="id" column="WORK_ID" type="long">
                  <generator class="native" />
            </id>
            
            <discriminator column="WORK_TYPE" type="string" />            
            <property name="author" />
            
            ...
            
            <subclass name="Book" discriminator-value="BO">
                  <property name="publishing" />
                  <property name="price" />
                  <property name="isbn" />
            </subclass>

            <subclass name="Article" discriminator-value="AR">
                  <property name="magazine" />
            </subclass>
            
      </class>

먼저 discriminator 태그에 대해서 살펴보자. discriminator 태그는 테이블에 저장된 레코드가 어떤 클래스의 데이터 인지를 구분하기 위해 사용되는 컬럼을 명시해준다. 위 코드에서는 WORK_TYPE 컬럼을 사용해서 타입을 구분하게 된다.

subclass 태그는 하위 클래스의 프로퍼티 매핑을 처리해준다. subclass 태그는 name 속성을 사용해서 하위 클래스의 이름을 전달받으며, propety 태그를 사용해서 하위 클래스의 프로퍼티가 테이블의 어떤 컬럼과 매핑되는 지를 표시한다.

class 태그와 subclass 태그는 discriminator-value 속성을 지정하고 있는데, 이 속성에서 명시한 값이 discriminator 태그에서 지정한 컬럼에 들어갈 값이 된다. 예를 들어, Book 클래스를 정의하고 있는 subclass 태그의 discriminator-value 속성의 값은 'BO'인데, Book 객체를 생성해서 WORK_ALL 테이블에 데이터를 저장할 경우 WORK_TYPE 컬럼에는 'BO'가 삽입된다. 비슷하게 Article 객체를 저장할 경우에는 'AR'이 WORK_TYPE 컬럼에 저장된다.

subclass 태그는 자식 태그로 subclass를 가질 수 있기 때문에 여러 단계의 계층도 처리할 수 있다.

하위 클래스 마다 개별적 테이블 사용

마지막으로 살펴볼 방법은 테이블의 외부키를 사용한 연관을 이용하는 것이다. 아래 그림은 클래스 계층 구조를 테이블의 외부키 참조 형태로 매핑시키는 방법을 보여주고 있다.


하위 클래스에 해당하는 테이블의 주요키는 상위 클래스에 해당하는 테이블의 주요키를 참조하게 된다. 즉, 하위 클래스에 매핑되는 테이블의 주요키는 동시에 외부키가 된다.

Hibernate는 위 그림과 같은 매핑을 처리할 수 있는 joined-subclass 태그를 제공하고 있다. 앞서 살펴본 subclass 태그와 마찬가지로 joined-subclass 태그도 부모 클래스의 매핑 정보를 담고 있는 class 태그에 포함되며, joined-subclass가 처리할 매핑 정보를 담게 된다. 아래 코드는 위 그림에서 보여준 매핑을 설정한 것이다.

      <class name="Work" table="WORK">
            <id name="id" column="WORK_ID" type="long">
                  <generator class="native" />
            </id>
            
            ...
            
            <joined-subclass name="Book" table="BOOK_WORK">
                  <key column="BOOK_WORK_ID" />                  
                  <property name="publishing" />
                  <property name="price" />
                  <property name="isbn" />
            </joined-subclass>
            
            ...
            
      </class>

joined-subclass 태그는 key 태그를 갖는데, 이 key 태그는 하위 클래스에 매핑되는 테이블의 주요키이면서 외부키인 필드를 정의하게 된다. Hibernate는 이 key 태그에서 졍의한 컬럼과 상위 클래스의 id 태그에 명시한 컬럼을 외부 조인해서 데이터를 읽어오게 된다. 하지만, 외부 조인을 한다는 데서 알 수 있듯이 높은 성능을 보장하지는 못한다.

어떤 방식을 선택할 것인가?

클래스 계층 구조와 테이블 사이의 매핑을 처리하는 세 가지 방법이 있는데 이들은 나름대로의 특징이 있다. 일반적으로는 다음과 같은 규칙을 사용해서 매핑 방식을 선택한다.

  • 클래스 마다 개별적 테이블 사용 - 계층 관계의 다형성 및 다형성을 이용한 쿼리가 필요하지 않은 경우
  • 클래스 계층 구조에 대해 테이블 사용 - 계층 관계의 다형성이 필요하고 하위 클래스가 소수의 프로퍼티만을 가진 경우
  • 하위 클래스 마다 개별적 테이블 사용 - 계층 관계의 다형성이 필요하고 하위 클래스가 다수의 프로퍼티를 가진 경우
클래스 계층 구조에 대해 테이블을 사용하는 것이 다형성을 지원하며 쿼리 성능 또한 좋기 때문에, 기본적으로 두번째 방식을 사용하는 것이 좋다. 하지만, 클래스 계층 구조를 처리한다는 것은 여전히 복잡한 처리를 요구하기 때문에 클래스 상속 방식 대신에 딜리게이션(delegation; 위임) 패턴으로 대체할 수 없는 지에 대해서 고민해볼 필요가 잇다. (딜리게이션 모델의 처리가 덜 복잡하며 더 좋은 성능을 낸다.)

클래스 사이의 연관 처리

객체 지향적으로 비즈니스 도메인 영역을 설계하면 객체들 사이에 다양한 관계가 형성된다. 두 객체가 1 대 1로 연관되는 경우도 있으며, 1 대 n, n 대 1 또는 n 대 m 으로 연관되기도 한다. Hibernate는 이러한 다양한 연관 관계를 처리해줄 수 있는데, 어떻게 이러한 연관을 매핑할 수 있는 지 살펴보도록 하자. (many-to-many 관계에 대해서는 나중에 고급 매핑과 함께 살펴볼 것이다.)

many-to-one과 one-to-many

many-to-one의 관계를 설명하기 위해서 다음과 같은 테이블 구조를 사용할 것이다.


위 그림에서 ITEM 테이블은 경매 제품 항목을, BID 테이블은 입찰 항목을 저장한다고 가정하다. 이 경우, ITEM 항목은 여러 개의 BID 항목을 갖게 될 것이다. (여러 사람이 하나의 제품에 입찰을 여러번 할 수 있다.) 따라서, 하나의 ITEM 항목에 대해 BID가 없을 수도 있고 여러 개 존재할 수도 있다. 즉, BID-ITEM의 관계는 many-to-one의 관계가 되는 것이다.

BID 테이블과 매핑될 Bid 클래스는 다음과 같이 어떤 Item 항목에 입찰했는 지 참조하기 위해 프로퍼티를 가질 수 있을 것이다.

      public class Bid {
            ...
            
            private Item item;
            
            public void setItem(Item item) {
                  this.item = item;
            }
            public Item getItem() {
                  return item;
            }
            ...
      }

BID-ITEM의 관계는 many-to-one 이므로 매핑 설정 파일에서는 다음과 같이 Bid의 매핑 정보를 표시할 수 있다.

      <class name="Bid" table="BID">
            ...
            
            <many-to-one 
                         name="item"
                         column="ITEM_ID"
                         class="Item"
                         not-null="true" />
            ...
      </class>

many-to-one의 관계를 표현하고 싶을 때에는 위 코드에서와 같이 매핑 파일에서 many-to-one 태그를 사용하면 된다. many-to-one 태그에서 사용되는 주요 속성은 위 코드에서 보듯이 세 가지인데, 각 속성은 다음과 같은 의미를 갖는다.

  • name - many-to-one의 관계를 맺는 프로퍼티의 이름(Bid 클래스의 item 프로퍼티)
  • column - many-to-one의 관계에서 one에 해당하는 테이블에 대한 외부키를 저장할 컬럼의 이름. BID 테이블의 경우 ITEM 테이블의 주요키에 대한 외부키가 ITEM_ID 컬럼에 저장된다.
  • class - many-to-one에서 one에 해당하는 클래스의 이름.
위 설정은 단방향 참조만을 처리하고 있다. Bid 객체는 getItem() 메소드를 사용해서 관련 Item 객체에 접근할 수 있는 반면에, Item 객체가 Bid 객체들에 접근하는 것에 대해서는 명시하고 있지 않다.

ITEM-BID의 관계는 BID-ITEM 관계와 정반대이므로 one-to-many의 관계가 된다. 즉, 하나의 ITEM에 대해서 여러개의 BID가 존재할 수 있는 것이다. 이를 자바 코드로 표현하면 다음과 비슷할 것이다.

      public class Item {
            ...
            private Set bids = new HashSet();
            
            public void setBids(Set bids) {
                  this.bids = bids;
            }
            
            public Set getBids() {
                  return bids;
            }
            
            public void addBid(Bid bid) {
                  bid.setItem(this);
                  bids.add(bid);
            }
            ...
      }

위 코드에서는 여러 Bid 객체를 저장하기 위해 Set 타입을 사용했는데 이는 Hibernate가 many-to-one의 관계를 지원해주는 타입 중의 하나로서, 설정 파일에서는 다음과 같이 매핑 정보를 설정할 수 있다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" table="BID">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

위 코드에서 먼저 set 태그는 프로퍼티 타입이 Set일 때 사용된다. set 태그의 주요 속성은 다음과 같다.

  • name - 프로퍼티의 이름
  • table - 프로퍼티의 값과 관련된 테이블의 이름. 생략할 수 있다. 위의 예에서는 Item의 bids 프로퍼티에 저장되는 값이 BID 테이블과 매핑된 Bid 이므로 table 속성의 값으로 "BID"를 사용하였다.
set 태그는 두 개의 자식 태그를 갖는다. 첫번째로 key 태그는 one-to-many 관계에서 many에 해당되는 테이블이 one을 참조할 때 사용하는 외부키 컬럼을 표시한다. 위 코드에서 BID 테이블의 외부키는 ITEM_ID 이므로 "ITEM_ID"를 key 태그의 column 속성값으로 지정하였다.

두번째 자식 태그는 one-to-many 태그로서 class 속성을 사용해서 many 부분에 해당하는 클래스의 타입을 명시한다.

하지만, 위의 설정만으로는 끝나지 않는다. 지금까지 설정한 것은 단지 두 개의 단방향 연관을 맺은 것에 불과하다. 예를 들어, Item 클래스의 addBid() 메소드 코드를 보자.

      bid.setItem(item);
      bids.add(bid);

위 코드는 Item 객체와 Bid 객체를 연결해주는 클래스인데, 데이터베이스 입장에서 보면 BID 테이블의 ITEM_ID 컬럼 값을 변경해주는 기능에 해당한다. 문제는 Hibernate가 위 코드를 처리할 때 두번의 쿼리를 실행한다는 점이다. 즉, Bid 객체에 대해서 한번의 쿼리를 수행하고 또한 Item의 bids 프로퍼티가 변경된 것에 대해서 한번의 쿼리를 수행하게 된다.

이렇게 두번의 쿼리가 발생하게 되는 이유는 Item과 Bid가 각각 단방향 관계로 설정되어 있기 때문이다. 양방향 관계로 연결시켜줌으로써 이 문제를 처리할 수 있는데, 양방향 연결은 다음과 같이 set 태그에 inverse 속성값을 true로 지정해줌으로써 명시할 수 있다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

하지만, 이것만으로는 부족하다. 문제가 되는 부분은 Item을 저장하더라도 Bid가 저장되는 것은 아니라는 것이다. 예를 들어, 다음의 코드를 보자.

      Session session = HibernateUtil.currentSession();
      tx = session.beginTransaction();
      
      Item item = new Item();
      Bid bid = new Bid();
      item.addBid(bid);
      
      session.save(item);
      
      tx.commit();

위 코드를 실행했을 때 원하는 것은 Item이 저장될 때 자동으로 새로 생성한 Bid도 저장되는 것이다. 하지만, Hibernate는 Item 객체만 저장할 뿐 Bid 객체는 저장되지 않는다. Bid 객체도 저장하도록 하려면 다음과 같이 cascade 속성을 set 태그에 명시해주어야 한다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true" cascade="save-update">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

위와 같이 cascade 속성의 값을 "save-update"로 지정하면 Item 객체가 저장될 때 관련 객체인 Bid도 함께 저장된다. 저장될 때 뿐만 아니라 기존에 존재하는 Item 객체에 새로운 Bid를 추가한 후, Item 객체를 update() 할 때에도 Bid가 저장된다.

one-to-many 관계에서 one에 해당하는 객체가 삭제될 때 many에 해당하는 객체들도 함께 삭제되어야 하는 경우도 있을 것이다. 예를 들어, 경매 제품 항목(Item)이 삭제된다면 경매와 관련된 입찰 항목(Bid)은 의미가 없어지며, 함께 삭제되는 것이 올바른 구조일 것이다. 이처럼 one-to-many의 관계가 부모-자식 관계인 경우에는 다음과 같이 cascade 속성의 값을 "all-delete-orphan"으로 지정하면 된다.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true" cascade="all-delete-orphan">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

all-delete-orphan은 save-update도 포함하고 있다. cascade 속성은 퍼시스턴트 객체 사이의 연관 관계에서 중요한 기능을 수행하는데, 이에 대한 자세한 내용은 본 시리즈의 다음 글에서 살펴볼 것이다.

주요키를 이용한 one-to-one 연관

다음과 같이 one-to-one의 관계를 갖는 테이블이 있다고 해 보자.


위 테이블에서 ITEM 테이블과 ITEM_DETAIL은 1대 1의 관계를 갖고 있으며, ITEM_DETAIL의 주요키는 동시에 ITEM의 주요키에 대한 외부키이기도 하다. 이렇게 주요키를 사용하여 1대 1로 연관된 경우에는 양 객체에 대한 매핑에서 one-to-one 태그를 사용해주면 된다.

아마도 ITEM 테이블과 ITEM_DETAIL 테이블에 매핑되는 클래스인 Item과 ItemDetail은 아래와 같은 형태의 코드를 갖게 될 것이다.

      public class Item {
            private Integer id;
            private ItemDetail detail;
            
            ...
            public Integer getId() {
                  return id;
            }
            public void setId(Integer id) {
                  this.id = id;
            }
            public ItemDetail getDetail() {
                  return detail;
            }
            public void setDetail(ItemDetail detail) {
                  this.detail = detail;
            }
            ...
      }
      
      public class ItemDetail {
            private Integer id;
            private Item item;
            ...
            
            public Integer getId() {
                  return id;
            }
            public void setId(Integer id) {
                  this.id = id;
            }
            public Item getItem() {
                  return item;
            }
            public void setItem(Item item) {
                  this.item = item;
            }
            ...
      }

위 코드와 같이 주요키로 연관된 두 객체와 테이블 사이의 매핑은 아래와 같이 one-to-one 태그를 사용해서 처리할 수 있다.

      <class name="Item" table="ITEM">
            <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
                  <generator class="increment" />
            </id>
            
            <one-to-one name="detail"
                        class="ItemDetail" 
                        cascade="save-update" />
            
            ...
            
      </class>

      <class name="ItemDetail" table="ITEM_DETAIL">
            <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
                  <generator class="foreign">
                        <param name="property">item</param>
                  </generator>
            </id>
            
            <one-to-one name="item"
                        class="Item"
                        constrained="true"  />
      </class>

먼저, Item 클래스의 one-to-one 태그를 살펴보자. 이 one-to-one 태그는 detail 프로퍼티의 타입인 ItemDetail 클래스와 one-to-one 관계를 맺는다는 것을 보여주며, Item 객체가 새로 생성되거나 변경될 때 ItemDetail도 저장된다고 설정하였다. (cascade 속성의 값이 save-update)

두번째로, ItemDetail 클래스의 one-to-one 태그를 살펴보자. 이 one-to-one 태그는 item 프로퍼티의 타입이 Item이라는 것을 나타내며, constrained 속성의 값을 true로 지정함으로써 외부키와 연결된 프로퍼티라는 것을 명시한다.

ItemDetail 클래스 매핑의 id 태그를 보면 식별자 생성기로 foreign 을 사용하고 있는데, ItemDetail이 식별자로 사용하는 값을 외부키로부터 가져온다는 것을 의미한다. 이때 외부키를 가져올 프로퍼티는 property 파라미터로 지정한다.

외부키를 이용한 one-to-one 연관

one-to-one 매핑을 처리할 수 있는 또 한가지 방법은 외부키를 사용하는 방법이다. 앞서 살펴봤던 ITEM 테이블과 ITEM_DETAIL 테이블과의 관계를 다음과 같이 변경해보자.


위의 관계에서 ITEM_DETAIL은 자신만의 주요키를 갖고 있으며, ITEM은 외부키 ITEM_DETAIL_ID 컬럼을 사용해서 ITEM_DETAIL과 one-to-one 매핑된다. 이 관계를 객체로 표현하는 두 클래스의 코드는 앞서 살펴봤던 Item 클래스 및 ItemDetail 클래스와 완전히 동일하다. 차이점이라면 ItemDetail의 id 프로퍼티의 값은 Item의 id 프로퍼티의 값과 상관없는 고유의 값이라는 것이다.

외부키를 사용한 one-to-one의 관계에서 외부키를 갖고 있는 부분의 설정방법은 many-to-one의 many 부분의 설정방법과 동일하다. (many-to-one은 참조키를 사용할 때의 관계를 표시한다는 것을 앞서 설명하였다.) 예를 들어, (ITEM_DETAIL 테이블로의 외부키를 갖고 있는) ITEM 테이블과 Item 클래스 사이의 매핑은 다음과 같이 many-to-one 태그를 사용하여 외부키 부분을 설정할 수 있다.

      <class name="javacan.hibernate.test.Item" table="ITEM">
            <id name="id" type="int" column="ITEM_ID" unsaved-value="null">
                  <generator class="increment" />
            </id>

            <many-to-one name="detail"
                         class="javacan.hibernate.test.ItemDetail"
                         column="ITEM_DETAIL_ID"
                         cascade="save-update"
                         unique="true"  />
            ...
      </class>
      

위 코드에서 many-to-one 태그는 Item 클래스의 detail 프로퍼티가 외부키인 ITEM_DETAIL_ID와 매핑된다는 것을 나타낸다. cascade 속성값이 "save-update"인데, 이는 Item이 저장되거나 변경될 때 외부키로 연관된 ItemDetail도 저장되거나 변경된다는 것을 의미한다. 또, 하나의 ITEM은 고유의 ITEM_DETAIL_ID와 연관되므로 unique 속성의 값을 true로 지정하였다.

ItemDetail 입장에서, 하나의 ItemDetail은 하나의 Item과 연관되므로 ItemDetail의 매핑 설정은 다음과 같이 one-to-one 태그를 사용하게 된다.

      <class name="javacan.hibernate.test.ItemDetail" table="ITEM_DETAIL">
            <id name="id" type="int" column="ITEM_DETAIL_ID" unsaved-value="null">
                  <generator class="increment" />
            </id>
            
            <one-to-one name="item"
                        class="javacan.hibernate.test.Item" 
                        property-ref="detail" />
            
      </class>
      

one-to-one 태그는 property-ref 속성을 사용해서 자기 자신을 참조하는 객체가 외부키 연관으로 사용하는 프로퍼티의 이름을 입력받는다. 예를 들어, Item 클래스는 detail 프로퍼티를 사용해서 ItemDetail 클래스로의 외부키 연관을 처리하므로 ItemDetail 클래스 매핑 설정에서 one-to-one 태그의 property-ref 속성의 값을 "detail"로 주었다.

복합키의 처리

대부분의 경우 주요키는 하나의 컬럼을 사용하지만, 다수의 컬럼을 주요키로 사용하는 경우도 있다. 이런 키를 복합키(composite key)라고 부르며, Hibernate는 composite-id 태그를 사용해서 복합키를 표시할 수 있도록 하고 있다. composite-key의 사용방법은 매우 간단한데, 다음과 같이 key-property 태그를 사용해서 복합키와 관련된 프로퍼티와 컬럼을 표시해주면 된다.

      <class name="User" table="USER">
            <composite-id>
                  <key-property name="username" column="USERNAME" />
                  <key-property name="organizationId" column="ORGANIZATION_ID" />
            </composite-id>
            ...
      </class>

복합키를 별도의 클래스 표현할 수도 있을 것이다. 예를 들어, 위에서 설정한 User 클래스의 복합키를 다음과 같은 UserId 클래스로 표현한다고 해보자.

      public class UserId implements Serializable {
            private String username;
            private String organizationId;
            
            // ... get/set 메소드
            
            public boolean equals(Object o) {
                  if (this == o) return true;
                  if (o == null) return false;
                  if (!(o instanceof UserId)) return false;
                  UserId target = (UserId)o;
                  if (!target.getUsername().equals(username)) return false;
                  if (!target.getOrganizationId().equals(organizationId)) return false;
                  return true;
            }
            
            // hashCode() 메소드
      }

위와 같이 복합키를 저장하는 별도의 클래스가 존재할 경우 User 클래스는 다음과 같이 복합키 클래스를 저장할 별도의 프로퍼티를 갖게 될 것이다.

      public class User {
            private UserId id;
            
            public void setId(UserId id) {
                  this.id = id;
            }
            public UserId getId() {
                  return id;
            }
            ...
      }

이 경우 다음가 같이 composite-id 태그를 사용할 수 있다.

      <class name="user" table="USER">
            <composite-id name="id" class="UserId">
                  <key-property name="username" column="USERNAME" />
                  <key-property name="organizationId" column="ORGANIZATION_ID" />
            </composite-id>
      </class>

다음 글에서는

본 글에서는 클래스 사이의 상속 및 one-to-many, many-to-one, one-to-one 연관을 처리하는 방법에 대해서 살펴봤고, 복합키를 설정하는 방법에 대해서도 살펴보았다. 이번 글을 통해 다양한 종류의 매핑을 처리할 수 있게 되었을 것이다. 이렇게 매핑 설정한 뒤 필요한 것은 실제로 퍼시스턴트 객체를 생성하고, 변경하고, 삭제하는 등의 작업을 어플리케이션에서 수행하는 것이다. 다음 글에서는 퍼시스턴트 객체를 어떻게 사용하는 지 그리고 객체의 영속성 상태 및 영속성 전이에 대한 내용을 살펴보기로 하자.

관련링크:

+ Recent posts