하이버 3.0부터는 lazy=true"가 기본값이라고 나와있습니다. (여기에.. Association fetching strategies 참조)

콜렉션이든 단일 클래스든 모두 Lazy Loading.

하지만 이 말은 XML 설정에 국한 된 말입니다. 애노테이션 설정에서는 그렇치 않습니다.
여기에 2.2.5.5. Association fetching 이 부분을 참조하면 다음과 같이 나와있습니다.

@OneToMany와 @ManyToMany 의 FetchMode는 Lazy
@OneToOne과 @ManyToOne의 FetchMode는 Eager

따라서, XML 설정으로 lazy="true"를 생각하고 코딩한 것을 애노테이션으로 옮길 때는 모두 명시적으로 fetch=FetchMode.LAZY로 설정해주어야 예상치 못한 쿼리가 발생하는 것을 방지할 수 있습니다.

예상치 못한 쿼리는 다음과 같이 발생합니다.
사용자 삽입 이미지
이런 상황(상속 구조는 Table Per Subclass로 맵핑)에서.. User가 가지고 있는 DefaultBillingDetails를 가져온다고 가정합니다. User와 DefaultBillingDetails는 @OneToObe 이나 @ManyToOne으로 맵핑할 수 있습니다. 그러면 default가 EAGER죠.

        User loadedUser = (User) session.get(User.class, 1l);
        assertNotNull(loadedUser);
        BillingDetails billingDetails = loadedUser.getDefaultBillingDetails();
        System.out.println("읽어올 때 타입을 알 수 있나? " + new Boolean(billingDetails instanceof CreditCard).toString());

- default 상황에서 발생하는 쿼리

    select
        user0_.id as id3_2_,
        user0_.DEFAULT_BILLING_DETAILS_ID as DEFAULT3_3_2_,
        user0_.name as name3_2_,
        billingdet1_.BILLING_DETAILS_ID as BILLING1_0_0_,
        billingdet1_.USER_ID as USER2_0_0_,
        billingdet1_1_.NUMBER as NUMBER1_0_,
        billingdet1_2_.ACCOUNT as ACCOUNT2_0_,
        case
            when billingdet1_1_.BILLING_DETAILS_ID is not null then 1
            when billingdet1_2_.BILLING_DETAILS_ID is not null then 2
            when billingdet1_.BILLING_DETAILS_ID is not null then 0
        end as clazz_0_,
        user2_.id as id3_1_,
        user2_.DEFAULT_BILLING_DETAILS_ID as DEFAULT3_3_1_,
        user2_.name as name3_1_
    from
        User user0_
    left outer join
        BILLING_DETAILS billingdet1_
            on user0_.DEFAULT_BILLING_DETAILS_ID=billingdet1_.BILLING_DETAILS_ID
    left outer join
        CREDIT_CARD billingdet1_1_
            on billingdet1_.BILLING_DETAILS_ID=billingdet1_1_.BILLING_DETAILS_ID
    left outer join
        BANK_ACCOUNT billingdet1_2_
            on billingdet1_.BILLING_DETAILS_ID=billingdet1_2_.BILLING_DETAILS_ID
    left outer join
        User user2_
            on billingdet1_.USER_ID=user2_.id
    where
        user0_.id=?

User와 User가 가지고 있는 BillingDetails를 left outter join으로 가져옵니다.
BillingDetails를 Table Per Subclass로 맵핑했기 떄문에 CREDIT_CARD와 BANK_ACCOUNT에 left ountter join을 사용하여 모든 레코드를 가져옵니다.
마지막으로 BillingDetails가 ManyToOne으로 참조하는 User를 가져옵니다.(이건 User의 DefaultBillingDetail과 연관을 맺고 있는 User가 아닙니다. 그런 속성은 없습니다. 이 속성은 User가 가지고 있는 BillingDetail 콜렉션과 양방향으로 연관을 맺고 있는 속성입니다.)

굉장한 쿼리가 만들어졌습니다. User가 defaultBillingDetail을 가져와서 아직 사용하지도 않았는데 말이죠.

- @OneToOne 또는 @ManyToOne에서 fetch=FetchMode.LAZY 로 설정한 경우

    select
        user0_.id as id3_0_,
        user0_.DEFAULT_BILLING_DETAILS_ID as DEFAULT3_3_0_,
        user0_.name as name3_0_
    from
        User user0_
    where
        user0_.id=?

딱 User의 정보만 가져왔습니다. 분명 가져오라고 했는데... 쿼리를 날리지 않았습니다. 그러면 이 상태에서 BillingDetail에 있는 pay()라는 메소드를 실행하면 NullPointerException이 떨어질까요?

        User loadedUser = (User) session.get(User.class, 1l);
        assertNotNull(loadedUser);
        BillingDetails billingDetails = loadedUser.getDefaultBillingDetails();
        System.out.println("읽어올 때 타입을 알 수 있나? " + new Boolean(billingDetails instanceof CreditCard).toString());
        billingDetails.pay();
        System.out.println("이제는 읽어올 때 타입을 알 수 있을까? " + new Boolean(billingDetails instanceof CreditCard).toString());

이렇게 해봤습니다.

Hibernate:
    select
        user0_.id as id3_0_,
        user0_.DEFAULT_BILLING_DETAILS_ID as DEFAULT3_3_0_,
        user0_.name as name3_0_
    from
        User user0_
    where
        user0_.id=?
읽어올 때 타입을 알 수 있나? false
Hibernate:
    select
        billingdet0_.BILLING_DETAILS_ID as BILLING1_0_1_,
        billingdet0_.USER_ID as USER2_0_1_,
        billingdet0_1_.NUMBER as NUMBER1_1_,
        billingdet0_2_.ACCOUNT as ACCOUNT2_1_,
        case
            when billingdet0_1_.BILLING_DETAILS_ID is not null then 1
            when billingdet0_2_.BILLING_DETAILS_ID is not null then 2
            when billingdet0_.BILLING_DETAILS_ID is not null then 0
        end as clazz_1_,
        user1_.id as id3_0_,
        user1_.DEFAULT_BILLING_DETAILS_ID as DEFAULT3_3_0_,
        user1_.name as name3_0_
    from
        BILLING_DETAILS billingdet0_
    left outer join
        CREDIT_CARD billingdet0_1_
            on billingdet0_.BILLING_DETAILS_ID=billingdet0_1_.BILLING_DETAILS_ID
    left outer join
        BANK_ACCOUNT billingdet0_2_
            on billingdet0_.BILLING_DETAILS_ID=billingdet0_2_.BILLING_DETAILS_ID
    left outer join
        User user1_
            on billingdet0_.USER_ID=user1_.id
    where
        billingdet0_.BILLING_DETAILS_ID=?

pay() 메소드를 호출하는 순간, BillingDetails 하나를 가져오고 해당 메소드를 호출해 줍니다.