JPA 기초
1. SQL 중심적인 개발의 문제점 🤦♂️
JPA를 사용하기 전, 데이터 등록 조회 삭제 등의 상황마다 INSERT, UPDATE, SELECT 등 CRUD의 무한 반복!
만약 위와 같은 Member 클래스를 생성했다면 DB에 아래와 같은 SQL문을 입력해주어야함.
이후 Member 클래스에 tel이라는 맴버 변수가 추가되면 UPDATE 쿼리로 DB 테이블 구조를 변경해주어야함.
😢 하지만 SQL에 의존적인 개발을 피하기는 어렵다.
개발자가 객체를 SQL로 변환하여 관계형 데이터 베이스에 넣는 과정이다.
이 과정은 자바를 DB 연동하여 함께 사용할 때, 어쩔 수 없이 거쳐야하는 과정이다.
그림에 있는 "객체 -> SQL 사이의 SQL 변환 과정" 에서 개발자가 SQL 매퍼의 역할을 하고 있었다..!🤦♂️
그렇다면 RDB에서 자바의 상속, 연관 관계, 데이터 타입, 데이터 식별을 어떻게 표현했을까? 🤔
상속의 표현은 SQL의 슈퍼타입/서브타입 관계로 표현할 수 있다.
Item이라는 클래스를 상속한 Book, Movie, Album을 테이블에선 슈퍼/서브 타입으로 표현하는 것이다.
만약 이렇게 설계된 테이블에 값을 Insert하게 된다면 어떻게 될까?🤔
바로 자바의 객체를 분리하여 Item 테이블과 Album 테이블에 각각 Insert문을 날려주면 된다.
이것도 귀찮은 과정이지만 조회를 하게 된다면 더욱 복잡해진다..
sql 작성, 객체 생성 등 잡다한 과정을 모두 거쳐야한다. 이걸 모두 개발자가 해야한다.
때문에 상속관계는 DB 저장 시에 사용하지 않는다.
😐 객체답게 모델링 할수록 매핑 작업만 늘어난다..
객체를 자바 컬렉션에 저장 하듯이 DB에 데이터를 저장할 수는 없을까?
이를 해결하는 것이 Java Persistence Api, 바로 JPA이다.
2. JPA란
JPA는 자바 진영의 ORM 기술 표준이다.
- ORM(Object Relational Mapping) : 객체 관계 매핑
- 객체는 객체대로 설계
- 관계형 데이터베이스는 관계형 데이터베이스대로 설계
- ORM 프레임워크가 중간에서 매핑
- 대중적인 언어는 대부분 ORM 기술이 존재
JPA는 애플리케이션과 JDBC 사이에서 동작
원래는 개발자가 직접 JDBC API를 사용했다면, 이를 JPA가 대체하는 방식
JPA의 동작 - 저장
만약 Member객체를 RDB에 저장한다고 가정하자.
우선 Member객체를 MemberDAO에 넘기고 DAO에서 JPA에 Member객체(Jpa에선 Entity라고 부른다)를 보내주기만 하면,
JPA가 Enitity 분석, Insert SQL 생성, JDBC API 사용, 패러다임 불일치 해결을 모두 해준다.
JPA의 동작 - 조회
조회하는 경우도 간단하다.
JPA에게 찾고있는 데이터의 id(Primary Key)를 넘겨주기만 하면 SQL 생성, 데이터-객체 매핑, 패러다임 불일치를 해결하여 Entity를 넘겨준다.
🤗 패러다임 불일치란?
데이터베이스는 데이터 중심으로 구조화되어있다. 객체의 상속, 다형성 같은 개념이없다. 그렇다보니 객체와 데이터베이스가 지향하는 점이 다르다. 이것을 객체와 데이터베이스의 패러다임 불일치라고 한다.
그렇다면 이렇게 편리한 JPA는 누가 어떤 계기로 만들었을까?
과거에도 EJB의 엔티티 빈이라는 자바 ORM 표준 기술이 존재했다.
하지만 이 엔티티 빈이라는 기술은 사용이 너무 어렵고 복잡해서 표준임에도 불구하고 대부분의 개발자들이 사용하지 않았다.
때문에 불편함을 느낀 어떤 개발자가 하이버네이트라는 이름의 ORM 기술을 개발하였고, 이를 오픈 소스로 올렸더니 자바 진영 개발자들이 열광하여 이를 발전시켜나갔다.
반면 자바 표준임에도 외면 받고 있는 EJB를 통해 반성한 자바 진영에서 Hibernate를 만든 개발자를 섭외하여 하이버네이트를 좀 더 명확하게 만든 JPA를 탄생시켰다.
JPA는 오픈 소스 Hibernate 로 시작된 표준이기 때문에 굉장히 실용적이기 때문에 오랜 기간 동안 사랑 받고 있다.
JPA는 표준 명세
JPA는 자바 ORM기술에 대한 API 표준 명세이다.
쉽게 말해 JPA는 인터페이스의 모음이라는 것이다.
따라서 JPA를 사용하려면 JPA Interface를 implements 한 ORM프레임워크를 통해야한다. 하이버네이트가 JPA의 시작에 가장 큰 영향을 주었기 때문에 가장 대중적으로 사용한다.
🛠 생산성
- 저장 : jpa.persist(member);
- 조회 : Member member = jpa.find(memberId);
- 수정 : member.setName(”변경할 이름”);
- 삭제 : jpa.remove(member);
과거 직접 SQL을 수행하던 방식과 비교하면 개발자는 JPA에게 매핑을 어떻게 할 지만 알려주면 된다는 점이 너무나 편리하지 않은가?
👬 JPA와 패러다임의 불일치 해결
- JPA와 상속
- JPA와 연관관계
- JPA와 객체그래프 탐색
- JPA와 비교하기
1. JPA와 상속
해당 그림 기준으로 Album 객체를 DB에 저장하게 된다면, DB에 INSERT 쿼리를 2번 요청해야한다.
:: 상속 - Insert
이러한 상황에서 개발자는 jpa의 persist(album); 명령만 사용하면 나머지 Insert 구문은 JPA가 알아서 분석하여 처리한다.
엔티티만 설계하면 JPA가 알아서 SQL 관련 로직을 처리해준다는 것이다. (개꿀🤞)
2~3. 연관관계 저장, 객체 그래프 탐색
Member 엔티티의 멤버 변수에 연관 관계로 Team 엔티티를 추가한 상황에서도
연관 관계를 저장하는 것 또한 변수의 값만 Team 엔티티로 초기화 해준 후 persist해주면 자동으로 연관 관계에 대한 설정이 처리되어 저장된다.
탐색 시에도 memberId를 통해 Member 객체를 가져오면 Team 엔티티 또한 JPA가 알아서 함께 가져온다.
"때문에 JPA를 사용할 시에 엔티티의 신뢰도가 높아진다."
3. JPA와 비교하기
jpa를 통해 find한 객체는 같은 객체로 인식할까?
정답은 “그럴 수도 있고 아닐 수도 있다”이다.
동일한 트랜잭션 내에서 조회한 엔티티는 같음을 보장받지만, 서로 다른 트랜잭션에서 조회한 엔티티는 같음을 보장받기 어렵다. 이는 1차 캐시와 관련이 있다.
동일한 트랜잭션 내에서 Id를 사용하여 find한 엔티티는 1차 캐시에 저장되는데, 또 다시 같은 Id(Primary Key)로 호출할 시에 다시 DB에서 select하여 가져오는 것이 아닌 1차 캐시에 저장되어있는 데이터를 가져와 탐색 비용을 낮춘다.
📈 JPA의 성능 최적화 기능
- 1차 캐시와 동일성(Identity) 보장
- 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
- 지연 로딩(Lazy Loading)
1. 1차 캐시와 동일성 보장
이러한 기술이 존재하는 이유는 같은 트랜잭션 안에서는 같은 엔티티를 반환하여 조회 성능을 향상시키기 위해서이다.
2. 트랜잭션을 지원하는 쓰기 지원 - INSERT
약간의 설정을 거쳐 사용할 수 있는 기술로, 트랜잭션을 커밋할 떄까지 INSERT SQL을 모았다가 JDBC BATCH SQL 기능을 사용하여 트랜잭션 커밋 시에 한번에 SQL을 전송한다.
이와 같은 BATCH 기술 사용을 통하여, 다수의 insert sql을 처리해야 하는 상황에선 성능을 끌어올릴 수 있을 것이다.
3. 지연 로딩과 즉시 로딩
- 지연 로딩: 객체가 실제 사용될 때 로딩
- 즉시 로딩: JOIN SQL로 한번에 연관된 객체까지 미리 조회
만약 Team 엔티티와 연관 관계인 Member 객체를 find한다고 가정했을 때, find 명령어 사용 시점에서
- Member 엔티티 뿐만 아니라 JOIN SQL을 사용하여 Team 엔티티에 대한 데이터까지 미리 조회하는 방식이 “즉시 로딩”
- 우선 Member 엔티티만 가져오고, 이후에 Team의 데이터를 사용하는 메서드가 호출 되었을 때 Team 엔티티에 대한 데이터를 조회하는 방식이 “지연 로딩”
개발 상황에서, 특히 대규모의 서비스 프로젝트에선 엔티티의 연관 관계가 얼마나 복잡하게 연결되어 있을 지 알 수 없다. 때문에 웬만하면 “지연 로딩” 사용을 권장한다.