Reattaching과 Merging

detached 상태의 객체를 persistent 객체로 전이할 때에는 Reattach가 간편하긴 하지만, 여러 개의 Session에 걸쳐 있을 경우에 예상치 못한 문제가 발생할 수 있습니다.

    @Test
    public void reattaching() {
        Session session1 = sessionFactory.openSession();
        Transaction transaction = session1.beginTransaction();
       
        Member member = new Member();
        member.setName(“toby”);
        session1.save(member);
       
        transaction.commit();
        session1.close();
       
       member.setName(“whiteship”);
       
        Session session2 = sessionFactory.openSession();
        Transaction transaction2 = session2.beginTransaction();
       
      Member member2 = (Member) session2.get(Member.class, member.getId());
      session2.update(member);
       
        assertEquals(“whiteship”, member.getName());
       
        transaction2.commit();
        session2.close();
    }

바로 이런 경우인데요. 위의 코드는 NonUniqueObjectException 예외가 발생합니다. 왜 그런지는 비밀입니다.
암튼 위의 코드야 둘이 붙어있으니까 왜 문제가 발생하는지 잘 보이지만, 여러 세션에 걸쳐서 detacked 객체를 붙였다 땟다 한다고 생각하면, 일일히 추적해서 update()를 사용한 줄을 위쪽으로 올려주는 작업을 해야 합니다. 고통이겠죠.

    @Test
    public void merging() {
        Session session1 = sessionFactory.openSession();
        Transaction transaction = session1.beginTransaction();
       
        Member member = new Member();
        member.setName(“toby”);
        session1.save(member);
       
        transaction.commit();
        session1.close();
       
        member.setName(“whiteship”);
       
        Session session2 = sessionFactory.openSession();
        Transaction transaction2 = session2.beginTransaction();
       
      Member member2 = (Member) session2.get(Member.class, member.getId());
      Member member3 = (Member) session2.merge(member);
       
        assertEquals(“whiteship”, member3.getName());
       
        transaction2.commit();
        session2.close();
    }

이럴 때 사용할 수 있는 것이 merge() 입니다. 이 녀석은 재밌게 동작합니다. 저렇게 하면 아무런 문제도 발생하지 않고 원하는 대로 동작합니다.

대신 주의 해야 할 것은 member2 와 member3가 같은 레퍼런스를 가지고 있기 때문에 아래와 같은 코드가 가능합니다.

        Member member2 = (Member) session2.get(Member.class, member.getId());
        Member member3 = (Member) session2.merge(member);
       
        member2.setName(“whiteship2”);
       
        assertEquals(“whiteship2”, member3.getName());

이런 상황이 용납이 안 되는 것은 아닙니다. 하지만 사방에서 저 객체의 값을 변경하면 좀 곤란하겠죠. member2와 member(detached 상태의 객체)는 merge()의 반환값을 가져온 이상. 사장되었다고 생각하고 작업을 하는 것이 좋겠습니다.

merget()와 Persistent Context의 dirty checking이 맞물리면, 정말 SQL이 재미있게 생성되는 모습을 볼 수 있습니다.

위의 예제에서 발생되는 쿼리는 간단합니다.
– member를 넣는 insert 문
– member2를 가져오는 select문
– member3을 가져올 때(merging time)member2가 참조하던 객체를 변경하는 update문

퀴즈 그렇다면 아래의 코드는 어떤 SQL들을 만들어 낼까요?

        Session session1 = sessionFactory.openSession();
        Transaction transaction = session1.beginTransaction();
       
        Member member = new Member();
        member.setName(“toby”);
        session1.save(member);
       
        transaction.commit();
        session1.close();
       
        member.setName(“whiteship”);
       
        Session session2 = sessionFactory.openSession();
        Transaction transaction2 = session2.beginTransaction();
       
        Member member2 = (Member) session2.get(Member.class, member.getId());
        Member member3 = (Member) session2.merge(member);
       
        member2.setName(“toby”);
       
        assertEquals(“toby”, member3.getName());
       
        transaction2.commit();
        session2.close();

위와 같이 간단하게 발생되는 쿼리 종류를 써주세요. 그리고 왜!! 왜 그런 결과가 생기는지도 설명해주세요~

Persistence context 확장하기

특징

  • 요청을 처리한 다음에 Persistent context를 닫지 않고 DB Connection을 끊은 상태에서
    사용자의 다음 요청을 기다린다. 사용자의 다음 요청이 오면 다시 DB에 연결하고 다음 요청을 처리한다. Conversation이
    끝나면, Persistent context를 DB에 동기화하고, 닫는다. 다음 Conversation을 시작할 때는 새로운
    Persistence context를 시작하고, 이전 Conversation에서 사용했던 entity 객체들을 재사용하지 않는다.
  • Detached 상태의 객체가 없다. 모든 객체는 Transient 상태이거나 Persistent 상태다.
  • 손수 reattachment 하거나 merging할 필요가 없다.
  • 이 책의 나중에 두 가지 Conversation 구현 전략에 대해 자세하게 다룰 것이다.

The identity of detached objects

특징

  • Reference to a detached object: 객체의 레퍼런스가 identity를 보장받는 범위를 벗어나는 것을 말한다.
  • Set Collection에 객체들을 담을 때, 동일한 주키로 읽어들인 Detached 상태의 객체와
    Persistent 상태의 객체는 모두 동일한 레코드를 나타내는 것들이기 때문에, 이전 예제에서 다음의 코드의 결과 Set에는
    하나의 객체만 들어가야 한다.
Set<Member> members = new HashSet<Member>();
members.add(loadedMember1);
members.add(loadedMember2);
members.add(loadedMember3);

assertEquals(1, members.size());
  • 그러나..2개가 들어가있다. loadedMember1과 loadedMember2는 같은 레퍼런스를 가지고
    있으니까, loadedMember2는 추가되지 않고, loadedMember3는 전혀 다른 레퍼런스를 가지고 있기 때문에 추가
    된다.
  • Set에 add()를 할 때, equals()로 비교를 하는데, Object 클래스에서 equals()를 ==으로 구현해 두었기 때문에 그렇게 된다.
  • 따라서 위의 결과가 1이 되게 하려면, equals()와 hashCode()를 재정의 해주어야 한다.

equals()와 hashCode() 이해하기

  • Detached 상태의 객체를 Set에 집어넣는 일이 절대로 없다면, 굳이 equals()와 hashCode()를 구현하지 않아도 된다.
  • Conversation 구현 전략으로 Extended Persistence Context를 선택하고 Detached 상태의 객체들을 애플리케이션에 제거하면 된다.
  • 이렇게 하면 당연히 Persistence Context도 Conversation으로 확장되었으니, Scope of Object identity의 범위도 Conversation으로 확장된다.
  • equlas()를 재정의 하면 반드시 hashCode()도 재정의 해야 한다. 왜? 같은 객체들은 반드시 같은 해시코드를 반환해야 하니까.

Data identity 비교하기

@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (id == null)
return false;
if (!(other instanceof Member))
return false;
final Member that = (Member) other;
return this.id.equals(that.getId());
}

@Override
public int hashCode() {
return id == null ? System.identityHashCode(this) : id.hashCode();
}
  • 비추하는 방법니다.
  • Transient 객체를 Set에 넣은 다음에, Session.save()를 사용해서 Persistent
    상태로 바꾸면 Set에 포함되어 있는 상태에서 hashCode 값이 바뀌게 된다. 이것은 Set의 제약을 위반하게 되는 것이다.
    왜 그럴까? 생각해보자. 0이라는 hashCode를 가진 녀석이 Set에 들어간 다음에 1로 바꼈다. 그러면 나중에 1이라는
    hashCode를 가진 녀석을 Set에 집어 넣으면 어떻게 될까? 처음에 0을 가지고 있다가 1로 바뀐 녀석은 사라지고, 뒤에
    추가된 객체만 남아있을 것이다.(테스트 코드로 확인해 볼 것.)

주키 속성을 제외한 모든 속성을 비교하기

@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (!(other instanceof Member))
return false;
final Member that = (Member) other;
if (!this.getName().equals(that.getName()))
return false;
return true;
}

@Override
public int hashCode() {
int result = 14;
result = 29 * result + getName().hashCode();
return result;
}
  • 콜렉션은 비교하지 않는다. 객체 하나의 동일성을 확인하려고 전체 객체 맵을 확인할 필요는 없으니까.
  • 단점1: 같은 레코드를 표현하는 두 개의 객체가 서로 다른 Session에서 존재하다가 누군가 속성을 바꾸면 그 둘은 다른 객체가 되버린다.
  • 단점2: 서로 다른 DB indentity를 가진 객체들이 같은 객체로 취급될 여지가 있다.

비즈니스 키를 사용해서 구현하기

  • business key: 각 개체들마다 유일한 값을 가지는 속성이나 속성들의 집합. natural primary key와 다른 점은 바뀔 수 있다는 것이다.
  • Entity 클래스에는 반드시 business key가 있어야 한다고 주장한다. 심지어 그 키가 개체의 모든 속성을 포함하는 것일지라도.(보통 immutable 클래스가 그렇다.)
  • surrogate key가 DB와 application이 사용하는 것이라면, business key는 사용자가 단일 레코드를 식별하기 위해 사용하는 것들이다.
@Override
public boolean equals(Object other) {
if (this == other)
return true;
if (!(other instanceof Member))
return false;
final Member that = (Member) other;
if (!this.email.equals(that.getEmail()))
return false;
return true;
}

@Override
public int hashCode() {
return email.hashCode();
}
  • 위에서 언급했던 모든 문제가 해결된다.
  • equals() 메소드에서 other객체의 속성에 접근할 때, .으로 바로 접근할 수 있는데도 굳이 gette를 사용하는 이유? 프록시 일수도 있기 때문에…

참조할 것

The Scope of Object Identity

특징

  • Java identity: a==b
  • Database identity: x.getId().equals(y.getId())
  • Scope of object identity: Java Identity와 Database identity모두 보장되는 상태.

Scope of object identity 종류

  • No identity scope: 하나의 DB 레코드가 같은 자바 객체로 여러번 애플리케이션으로 반환되든지
    말든지 신경 안 쓴다. 문제가 생긴다. (만약에 두 개의 객체가 하나의 레코드를 표현하고 있는데, 그 두 개의 객체의 정보를
    수정하면, DB에는 어떤 객체의 변화를 반영해야 하는가? 어떻게 하든 둘 중 하나는 손실 되겠군.)
  • Persistence context-scoped identity: 단일 Persistence context
    내부에서는 오직 하나의 객체만이 하나의 DB 레코드를 표현할 수 있도록 보장한다. 위에서 언급했던 문제가 없어지고 context
    레벨에서의 cache를 보장할 수 있다.
  • Process-scoped identity: 한 단계 더 나가서, 전체 JVM 프로세스 내부에서 오직 하나의 객체만이 하나의 DB 레코드를 표현할 수 있다.
  • 하이버는 persistence context-scope으로 구현했다.

Persistence context-scope 예제

@Test
public void persistenceContextScope() throws Exception {
Session session = sessionFactory.openSession();
Member member = new Member();
session.save(member);
Long memberId = member.getId();

Member loadedMember1 = (Member) session.get(Member.class, memberId);
Member loadedMember2 = (Member) session.get(Member.class, memberId);

// Java Identity In Persistence Context
assertTrue(loadedMember1 == loadedMember2);
// DB Identity In Persistence Context
assertTrue(loadedMember1.getId().equals(loadedMember2.getId()));
session.flush();
session.close();

Session session2 = sessionFactory.openSession();
Member loadedMember3 = (Member) session2.get(Member.class, memberId);

// Java Identity Out Persistence Context
assertFalse(loadedMember1 == loadedMember3);

// DB Identity Out Persistence Context
assertTrue(loadedMember1.getId().equals(loadedMember3.getId()));

session2.flush();
session2.close();
}
  • Persistent Context 내에서는(Persistent 상태) 같은 주키로 가져온 객체는 Java identity와 DB identity 모두 동일하다.
  • 하지만 Detached 상태의 객체와 Persistence 상태의 객체의 Java Identity는 보장되지 않는다. 물론 이 때도 주키 속성은 가지고 있으니까 DB Identity는 보장된다.
  • Detached 상태의 객체를 가지고 작업하는 것는 위와 같이 Scope of Object identity가 보장되지 않는 상태에서 작업하는 것이다.

Conversation 소개

특징

  • Persistence를 사용하는 여러 화면에 걸쳐 사용자의 요청을 처리해야 하는 경우.
  • 두 가지 구현 방법이 있다.

Detached Object를 이용하는 방법

  • session-per-request-with-detatched-objects
  • Persistent context는 오직 하나의 요청을 처리하는 동안만 지속되고, Detached 상태의
    객체들은 Conversation 내부에서는 계속해서 reattach하거나 merge를 해서 다시 Persistent 상태로
    만들어서 사용한다.

Persistence context를 확장하는 방법

  • session-per-conversation
  • Persistent context를 전체 작업의 단위로 확장시킨다.