지금까지 JPA를 어찌보면 막무가내로 써왔다.
기본에서 벗어나질 않았다고 할까,,
사용하면서 가끔식 개념을 마주칠때마다 늘 헷갈리던게 있다.
우연히 책에서 그 개념을 정리할 수 있게 되어 적어보고자 한다.
바로 객체지향 모델링이다.
JPA를 사용한다고 해서 테이블을 어떻게 객체지향과 연결시키지?
이런 하찮은 질문을 가슴속에 품고 코딩하면서도 당장의 업무에 치여서 알아볼 생각을 못했다.
드디어 시간을 내서 책을 읽었다!
먼저 알아야할 것은 이렇다.
예를 들어서 이런 모양의 테이블이 있다고 하자.
표현의 제약으로 인해 맵처럼 나타내겠다.
Member { id, teamId }
Team { id }
여기서의 id는 곧 pk다.
일반적으로 테이블이라면 위처럼 멤버테이블에서 팀테이블의 키값을 외래키로 잡아서 오고가고 한다.
그러나 객체지향이라면 엔티티를 이렇게 설계해서는 안된다.
객체지향으로 모델링한다면 이런 모양이 되어야 맞다.
Member { id, team }
Team { id }
클래스처럼 생각하면 된다.
멤버 클래스 안에 팀 객체가 있어야 되는 것처럼 ORM을 활용해 데이터 접근시에 객체지향으로 설계한다면 위처럼 되어야하는 것이다.
결론적으로 멤버 객체는 멤버.팀 필드에 팀 객체의 참조를 보관해서 팀 객체와 관계를 맺어야 한다.
ORM을 제대로 사용하기 위해 객체지향 설계를 했는데, 이건 참조라서 양방향이 아니다.
즉, 외래키를 쓴 테이블은 멤버 테이블에서 팀으로, 팀에서 멤버로 모두 타고 들어갈 수 있는 반면.....
객체지향 설계로 인해 참조를 하게되니까 멤버에서 팀으로는 갈 수 있어도 팀에서 멤버로는 올 수 없는 것이다.
결국 이를 위해서 객체와 테이블 사이에 개발자가 무언가 변환을 해줘야하는 것이다.
결론만 말하면 JPA가 이걸 해결한다. 자동으로.
이게 어떻게 되지 했던 것들이 알고보면 JPA가 자동으로 하는 거였다.
(사실 몇가지 더 필요한 동작들이 있지만, 이후에 포스팅하기로 하고 자연스럽게 영속성 컨텍스트로 넘어가본다..^^)
그래서 자세한 과정을 알아보기 위해선 JPA에 대해 더 개념적으로 알아야한다.
부분적으로는 JPA의 모든것 이라고도 할 수 있는 것이 바로 영속성 컨텍스트라는 개념이다.
이라고 책에서는 표현하고 있다.
나 또한 일종의 가상 데이터베이스라고 본다.
객체를 기준으로 가상의 디비를 만들어 둔게 곧 영속성 컨텍스트이고 실제 데이터에 접근할 때는 이런 가상디비가 실제디비와 데이터가 일치하는지 여부를 맞춰가면서 CRUD를 하고 있다고 생각한다.
이와 관련해서 알아둬야할 용어들이 있다.
이정도...?!
이후에 2차 캐시도 있고 더 많은 개념이 나오지만 우선 영속성 컨텍스트라는 범주 안에서는 이 정도가 대표적인 개념이자 용어이다.
각각을 간단하게 알아보자.
지연로딩은 실제 JPA를 활용해서 쿼리를 던져보면 알 수 있다.
쿼리를 던지면 원래 내가 예상하기에 그 순간에 쿼리가 딱 던져져야 한다.
그런데 실제 로그를 찍어보면 그렇지가 않았다.
그때 그때 필요한 때에 맞춰서 쿼리를 던지고 있었다.
바로 이것을 지연로딩이라고 한다.
프록시 객체로 가지고 있다가 필요한 순간에 실제 객체로 로딩하는 것이다.
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업이라고 한다.
좀 더 raw하게 보면, 쓰기지연 SQL 저장소에 모인 쿼리를 DB에 보내는 작업이다.
그럼 쓰기 지연은 뭘까..
JPA는 트랜잭션을 커밋할 때 쿼리를 반영한다.
예를 들어서
member.save()
member.delete()
등등..
이런식으로 쿼리문들이 줄지어 있을 경우, 하나씩 곧장 실행시키는 게 아니다.
한 트랜잭션 안에서 이것들을 쓰기지연SQL저장소에 모아두고 트랜잭션이 커밋될때 던지는 것이다.
이 둘은 설명이 겹쳐서 하나로 묶었다.
먼저 1차캐시라는 건 영속성 컨텍스트 내부의 캐시다.
다른 게시글에서 JPA를 도입하면 성능적으로 이득볼 수 있는 점에 대해 언급했는데, 그 중의 하나가 1차 캐시인 것이다.
내부적으로 캐시를 두고 있기 때문에 실제 데이터베이스에 접근할 일이 줄어든다.
그리고 1차 캐시도 마찬가지로 한 트랜잭션을 기반으로 동작한다.
때문에 원래는 동일한 데이터를 조회한 멤버 객체 2개를 만들면, 객체는 서로 다르다고 인식하기 때문에 '==' 조건을 걸었을 때 다르다고 나온다.
하지만 1차 캐시 덕분에 이게 달라진다.
첫번째 객체를 조회해서 만들 때 이걸 곧장 1차 캐시에 저장하기 때문에 두번째 객체 때는 1차 캐시에서 조회하여 '==' 을 물었을 때 트루값을 얻을 수 있게 되는 것이다.
이러한 것을 바로 동일성 보장이라고 한다.
추가적으로 1차 캐시와 관련하여 접근 순서는 다음과 같다.
이런 순서로 움직인다.
즉, 영속성 컨텍스트입장에서는 1차캐시가 곧 자신의 디비인 것이다.
더티채킹은 업데이트 관련한 내용이다.
실제 JPA의 메서드를 보면 update() 메서드가 없다...
그래서 나도 처음엔 전체를 다시 조회하고 set으로 변경해준 뒤에 다시 등록하는건가? 라고 생각했다.
하지만 그렇게 어처구니 없을 리가 없지..
JPA는 사실 스냅샷이라는 걸 찍는다.
실제 디비에서 처음 호출했던 순간이라던지, 아무튼 기존의 데이터를 스냅샷 찍어두고 마지막에 커밋할때 스냅샷과 일치하는 지 확인하게 되는게 이 과정을 변경감지 == 더티채킹이라고 부른다.
아마도 불순한 것(변경)이 있는지 체크하겠다는 의미인 것 같다.
그래서 그냥 set만 해도 알아서 update 쿼리를 던진다.
그런데 한가지 문제가 있다.
실제 쿼리를 보면 그 행의 전체 필드에 대해 업데이트를 한다.
즉, 필드가 6개인 데이터에서 2개만 값을 변경해도 6개의 값을 모두 태워 update 쿼리를 던지는 것이다.
나는 이것이 성능상의 문제를 일으키지 않을까? 생각했지만 결과는 아니었다.
컬럼이 30개이상 되는 것이 아니라면 성능상의 문제가 없다고 하고, 오히려 전체를 태워 보내는 것이 더 안전성에 좋은 방식이라고 한다.
만약 이를 커스텀하여 2개만 변경되도록 하고 싶다면, 하이버네이트의 DynamicUpdate 어노테이션을 활용하면 된다. (엔티티 상단에 붙여서 사용)
추가로, 검색 쿼리에서는 조건이 복잡한 경우가 많아 일반적으로 제공해주는 메서드만으로는 처리하기 어렵다.
따라서 JPQL을 필두로 여러 방법을 제공하는데, 이에 대해서는 포스팅을 이어가며 쓰도록 하겠다!