SQL 중심적인 개발의 문제점
만약 다음과 같은 class가 있고, 이와 같은 모양에 테이블이 있다고 가정해보자.
public class Member {
private String memberId; //회원 ID
private String name; //이름
private String birthdate; //생년월일
}
그렇다면 해당 테이블에 대한 CRUD문은 다음과 유형을 가질 것이다.
INSERT INTO MEMBER(MEMBER_ID, NAME, BIRTHDATE) VALUES ...
SELECT NAME, BIRTHDATE FROM MEMBER M ...
UPDATE MEMBER SET ...
DELETE MEMBER WHERE ...
그런데 만약 여기서 tel이라는 이름으로 연락처를 추가해야 한다면 어떻게 될까?
class에 tel을 추가하는 건 기본이며, 해당 테이블과 관련된 CRUD 쿼리를 사용하는 모든 곳들을 수정해줘야 한다.
현실적인 대안은 관계형 데이터베이스
NoSQL이 분산 환경에서 단순 검색 및 추가 작업을 위한 키 값을 최적화할 수 있고, 지연과 처리율이 우수하다는 등 NoSQL 분명히 많은 장점이 있지만
현실에 있는 데이터들은 각각의 연관 관계로 이루어져있기 때문에 결국 현실적인 대안은 관계형 데이터베이스를 써야 한다.
객체와 관계형 데이터베이스의 차이
- 상속
- 객체 : 부모가 가지고 있는 속성 및 기능들을 물려받는다.
- 데이터베이스 : 실질적인 상속은 없지만 각 테이블의 공통되는 속성을 별개의 테이블로 만들고,
해당 테이블의 키 값을 외래 키로 갖는다. (슈퍼 타입 ←→ 서브 타입)
- 연관 관계
- 객체 : 참조를 사용한다. (예시 : member.getTeam())
- 테이블 : 외래키를 사용한다. (예시 : JOIN ON M.TEAM_ID = T.TEAM_ID)
- 데이터 타입
- 데이터 식별 방법
JPA
- JPA : Java Persistence API
- Java 진영의 ORM 기술 표준
ORM
- ORM : Object Relational Mapping (객체 관계 매핑)
- 객체는 객체대로 설계
- 관계형 데이터베이스는 관계형 데이터베이스대로 설계
- ORM 프레임워크가 중간에서 매핑
- 대중적인 언어에는 대부분 ORM 기술이 존재
JPA의 특징
- JPA는 애플리케이션과 JDBC 사이에서 동작한다.
- JPA는 인터페이스의 모음이다. (Hibernate, EclipseLink, DataNucleus)
JPA를 왜 사용해야 하는가?
- SQL 중심적인 개발에서 객체 중심으로 개발
- SQL 작성시 코드 반복의 감소
- 모델링 문제 해결
- 생산성
- 저장 : jpa.persist(member)
- 조회 : Member member = jpa.find(memberId)
- 수정 : member.setName(“변경할 이름”)
- 삭제 : jpa.remove(member)
- 유지보수
- 필요한 항목이 있을 때 필드 하나만 추가해주면 SQL은 JPA가 해결해 준다.
- 패러다임의 불일치 해결
- JPA와 상속
- JPA와 연관관계
- JPA와 객체 그래프 탐색
- JPA와 비교하기 (동일한 트랜잭션 안에서 조회한 엔티티는 서로 같음을 JPA가 보장한다.)
- 성능 최적화
- 캐싱 기능 : 1차 캐시와 동일성 보장
- 버퍼링 기능 : 트랜잭션을 지원하는 쓰기 지연
- 지연 로딩 : 객체가 실제로 사용될 때 로딩하는 전략
- 즉시 로딩 : Join SQL로 한 번에 연관된 객체까지 미리 조회하는 전략
- 데이터 접근 추상화와 벤더 독립성
- 표준
JPA 동작 과정
- 기본 구조
- 애플리케이션과 JDBC 사이에서 동작
- 개발자가 JPA 사용시 JPA 내부에서 JDBC API를 통해서 SQL을 호출하여 DataBase와 통신
- 저장 과정
- 개발자가 저장을 원하는 객체를 JPA에 전달
- JPA가 Entity를 분석
- JPA가 Insert SQL 생성
- JPA가 JDBC API를 사용하여 SQL을 DB에 전송
- 조회 과정
- 개발자가 조회를 원하는 객체의 PK 값을 JPA에 전달
- JPA가 Entity의 매핑 정보를 바탕으로 적절한 Selete SQL 생성
- JPA가 JDBC API를 사용하여 SQL을 DataBase에 전달
- DataBase가 JPA에게 결과를 전달
- JPA가 DataBase한테 전달받은 결과를 객체에 매핑하여 전달
1차 캐시와 동일성 보장
- 같은 트랜잭션 안에서는 동일한 class를 사용하는 엔티티는 여러 개가 있어도 모두 동일한 것으로 취급한다.
- DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read를 보장한다.
String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시
println(m1 == m2); //출력 : true
//※ 전제 : 같은 데이터베이스의 트랜잭션 안에서만 성립한다.
//1. SQL 쿼리를 통해 DB에서 조회한다.
//2. JPA가 해당 결과물을 갖고 있는다.
//3. 같은 memberId로 jpa.find를 하면 jpa는 쿼리를 실행하지 않고, jpa가 들고 있는 메모리 상에서 반환한다.
쓰기 지연 (INSERT)
- 트랜잭션을 커밋할 때까지 INSERT SQL을 모은다.
- JDBC BATCH SQL 기능을 사용해서 한번에 SQL을 전송한다.
transaction.begin(); //트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC); //여기까지 INSERT SQL을 DB에 보내지 않는다.
transaction.commit(); //트랜잭션 커밋, 커밋하는 순간 DB에 INSERT SQL을 모아서 보낸다.
쓰기 지연 (UPDATE/DELETE)
- UPDATE, DELETE로 인한 ROW LOCK 시간 최소화
- 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고 바로 커밋
transaction.begin(); //트랜잭션 시작
changeMember(memberA);
deleteMember(memberB);
비즈니스_로직_수행(); //비즈니스 로직 수행 동안 DB ROW LOCK이 걸리지 않는다.
transaction.commit(); //트랜잭션 커밋
지연 로딩과 즉시 로딩
- 지연로딩 : 객체가 실제 사용될 때 로딩
Member member = memberDAO.find(memberId); //SELECT * FROM MEMBER ... 실행
Team team = member.getTeam();
String teamName = team.getName(); //SELECT * FROM TEAM ... 실행
- 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
Member member = memberDAO.find(memberId); //SELECT M.*, T.* FROM MEMBER JOIN TEAM ... 실행
Team team = member.getTeam();
String teamName = team.getName();