기본을 다지기 위해 JPA 기본 시리즈를 작성합니다.
김영한님의 ORM 서적을 참고한 글이며 비슷한 글이 다른 블로그에도 많이 있지만 읽는 것만으로는 이해가 가지않아 내용을 정리하면서 이 내용을 포스팅해보도록 하겠습니다.
JPA?
JPA는 ORM 기술(객체지향 모델링)을 자바 진영의 스프링 프레임워크에서 사용하기 위해 고안됐다. Java Persistence Api
Persistence는 앞으로도 많이 쓰이게 될 용어인데 '영속화'라는 뜻이다.
영속화 라니.. 왜 이런 어려운 말을 쓸까? 나름의 이유는 있다.
브라우저에서 웹 서버, DB까지 여러 곳에서 '저장'을 사용한다 캐쉬, 스토리지, 컨텍스트, config, adapter 등록 등등 여러 '저장'의 형태가 있다. 그 중에서도 특히 DB에 저장하는 것을 '영속화'라고 생각하면 된다. DB는 웹 어플리케이션 시스템에서 가장 확실한 저장방법이기 때문이다.
ORM?
ORM은 객체지향과 관계형DB의 패러다임의 격차를 좁히기 위한 방법이다. SQL, 페이징 등등 DB설정은 일단 뒤로 미루더라도(네임스페이스 파티션 등등) 적어도 CRUD는 객체지향적인 처리를 할 수 있다. 객체 지향 개발과 SQL을 직접 사용하는 패러다임 불일치 문제를 해결하자는 것이다.
그래서 실제로도 JPA안에 DB와의 커넥션을 만들어주는 JDBC가 존재하고 CRUD에 관한 SQL을 만들어주는 여러 구현체가 존재가 필요하다. 이를 도와주는 실질적인 구현체가 바로 'JPA 구현체', '하이버네이트'라고 생각하면 될것이다.
SQL을 어떻게 대체하는데?
JPA는 SQL을 사용하지 않고 객체의 필드와 메소드를 이용해 DB의 변화를 만들어야 한다. 이를 위해서 '영속성 컨텍스트'라는 개념이 등장한다. 영속성 컨텍스트는 객체지향에서 DB에 트랜잭션하기 위한 방법이다. 아무리 Entity로 선언됐을지라도 new 만으로 DB에 create or insert되는 것은 말이 안된다. 철저하게 트랜잭션인지 아니면 단순 로직인지 구분할 필요가 있다. 이를 위해서 영속성 컨텍스트라는 논리적인 공간을 만들어서 이 컨텍스트에 등록된 객체(엔티티)만 변화를 만들게 규약해 놓았다.
Persistence Context는 말 그대로 객체의 변화를 저장하는 환경이다. 근데 모든 객체의 변화를 알 필요는 없고 앞서말했듯 사용자가 지정한 DB에 실질적인 CRUD를 만들 객체, 즉 '엔티티(DB 테이블에 매핑되는 객체이다)의 변화를 저장하는 환경'이다. 그래서 CRUD가 필요한 상황에는 이 Persistence Context를 불러와서 CRUD를 수행하면 된다. 그리고 살짝 관점을 바꿔서 말하면 모든 CRUD는 Persistence Context를 통해서 이뤄진다. 라고 할 수 있다.
Persistence Context는 어디까지나 논리적인 개념이다 그래서 실제로 사용할때는 context에 직접 엔티티를 등록하는 방식을 사용하지 않고 entitymanager라는 클래스를 이용해서 CRUD한다.
엔티티 매니저는 엔티티매니저팩토리를 통해서 만들어진다. 엔티티 매니저의 역할은 영속성 컨텍스트를 관리하는데에 있다. 영속성 컨텍스트는 논리적인 개념으로 실질적으로는 1차 캐시, 스냅샷, 쓰기지연 DB 저장소 등이 포함된다. 즉 엔티티 매니저는 1차캐시, 쓰기지연 저장소 등을 관리하는 역할을 수행하는 것이다.
엔티티 매니저는 싱글톤 객체로 서버에 올라가기 때문에 stateless해야 한다. 즉 별도의 data공간이 있으면 안된다는 것, 그렇기 때문에 영속성 컨텍스트가 분리되는 것이라고 봐도 무방하다. 각각의 쓰레드마다 트랜잭션이 발생하면 JPA는 1차캐시, 쓰기지연 저장소 등을 만들어 놓기 때문에 이를 하나의 쓰레드에서만 사용할 수 있게 되는 것이다. 즉 쓰레드 세이프티가 확보된다.
영속성 컨텍스트는 객체를 세가지로 분류한다.
영속성 컨텍스트에서 다루는 '영속'의 뜻은 DB 영속이 아니다 차라리 영속성 컨텍스트에 저장한다는 개념으로 알아두어야 한다.
1. 영속 2. 비영속 3. 준영속
영속 : 영속성 컨텍스트가 관리 중인 상태
'영속', 굉장히 헷갈리는 단어다.. '영속'은 DB에 저장하는 행동이라고 하지않았나? 하지만 '영속'상태는 '영속'상태가 아니다. 영속성 컨텍스트에 저장됐을 뿐이다. 그리고 우리는 영속성 컨텍스트에 저장하기 위한 방법으로 엔티티 매니저를 사용한다.
Member findMember = em.find(Member.class, "member1")
// "member1" 을 ID로 가지고 있는 Member 엔티티를 찾아서 findMember로 반환하는 코드
이 코드를 기준으로 설명하자면,
영속 컨텍스트의 1차 캐시안에 member1을 id로 하는 Member가 있는지 확인한다, 없으면 db에 접근해서 1차 캐시에 등록하고 반환한다. 이 과정에서 member1은 영속성컨텍스트에 '영속'돼있는 것이다. 즉 member1은 영속상태라고 할 수 있다.
만약 이 member1의 값을 다른 코드에서 변경한다면? 어떻게 될까??
MemberService.changeAge("member1", "26") // member1아이디를 가진 객체의 나이를 26살로 바꾼다.
이렇게 되면 영속성 컨텍스트에 접근하게 돼서 먼저 1차 캐쉬를 읽게된다. 그리고 1차캐시에 존재하는 것을 확인한 후 age값을 26으로 바꿔준다(변경 감지 방식 사용). 트랜잭션의 커밋 타이밍에 맞춰서 DB에 flush되면서 반영되게 된다. 즉 member1이라는 ID를 가진 Member객체는 영속상태이다.
member.setUsername("회원명 변경")
이 코드는 직관적으로도, DB에 반영이 되지 않음을 알 수 있다... 즉 이 member는 영속성 컨텍스트에서 관리하는 객체가 아니라 그냥 영속상태에 있던 객체가 떨어져 나온 준영속 상태의 객체이다. 사실상의 비영속 객체이지만 준영속으로 불리는 이유는 이 준영속상태의 객체가 다시 영속상태로 변하기 때문이다.
나는 영속->준영속->영속 이렇게 변하기 때문에 비영속보다는 준영속이라는 이름을 쓴다고 이해했다. 결국 핵심은 detach()이런걸 쓰냐하는게아니라 식별자를 기준으로 영속상태가 되어서 DB에 저장된적이 있는 객체이냐 아니냐이다.
그리고 준영속->영속으로 변화하는 과정에서 필요한 것이 바로 merge와 변경감지이다.
준영속 상태는 더이상 엔티티매니저가 관리하지 않기 때문에, member에서 다른 엔티티 정보를 요청하게 된다면 프록시가 리턴되기 때문에 에러가 발생한다. 이런 문제를 준영속 상태의 지연 로딩 문제라고 하는데 이를 해결할 수 있는 방법은 크게 두가지 이다..
1. 뷰가 필요한 엔티티를 미리 로딩해 두는 방법
2. OSIV 방식을 이용해서 VIEW 단에서도 영속성 컨텍스트를 사용할 수 있게 해주는 방식이다.
1. 미리 로딩해두는 방식으로는 JPQL의 페치 조인 방식을 사용하는 방식과 DTO를 만들어서 그에 맞는 JPQL을 작성함으로써 한번에 로딩하는 방식이 있다.
사실 이 둘은 굉장히 유사하고, 비지니스로직이나 화면구성이나 지연로딩, DTO가 얼마나 복잡한지에 따라 선택해서 사용할 수 있다.
DTO로 안될때는 그냥 프록시 서비스를 하나 두는 방법도 존재한다. 이를 파사드 방식이라고 하는데 프레젠테이션 계층과 서비스 계층을 더욱 확실하게 분리할 수 있다.
2. OSIV
- 클라이언트 요청이 들어오면 서블릿 필터 혹은 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단 이때 트랜잭션은 시작하지 않는다.
- 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다.
- 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다.
- 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지한다.
- 서블릿 필터나 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 단 이때 플러시를 호출하지 않고 바로 종료한다.
즉... 우리가 준영속 상태라고 했던 객체의 값이 변화한 이후 새로운 요청이 들어오지 않은 상태에서 트랜잭션을하게되면 영속성 컨텍스트안의 값이 변경이 됐기 때문에 변경값을 인식하고 변경이 진행된다. 따라서 트랜잭션을 잘 확인하며 사용해야 한다.
OSIV 정리
스프링 OSIV의 특징
- 스프링 OSIV는 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때까지 같은 영속성 컨텍스트를 유지한다. 따라서 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다.
- 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다. 트랜잭션이 없는 프리젠테이션 계층은 지연 로딩을 포함해서 조회만 할 수 있다.
스프링 OSIV의 단점
- OSIV를 사용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다는 점을 주의해야 한다. 특히 트랜잭션 롤백 시 주의해야 하는데, 이건 이후 다른 포스팅에서 자세히 알아보도록 하겠다.
- 프리젠테이션 계층에서 엔티티를 수정하고 난 후 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다.
- 프리젠테이션 계층에서 지연 로딩에 의한 SQL이 실행된다. 따라서 성능 튜닝시에 확인해야 할 부분이 넓어진다.
- 너무 오랜시간 동안 데이터베이스 커넥션 리소스를 사용한다. 따라서 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이것을 결국 장애로 이어진다.
'스프링 > JPA' 카테고리의 다른 글
JPA - 양방향 연관관계에서 '연관관계의 주인' Mappedby (0) | 2021.12.27 |
---|