[자바 ORM 표준 JPA] JPA 소개
[자바 ORM 표준 JPA] JPA 소개
JPA
애플리케이션#
지금 시대는 객체를 관계형 DB에 관리
객체 지향 언어를 사용한 프로젝트라고 하여도 결국엔 SQL이 제일 중요할 수 밖에 없다.
관계형 DB가 알아 들을 수 있는 것은 SQL이니까
SQL 중심적인 개발의 문제점#
- 무한 반복, 지루한 코드
CRUD#
- INSERT
- UPDATE
- SELECT
- DELETE
- 자바 객체를 SQL로 ..
- SQL을 자바 객체로 ..
객체 CRUD#
간단히 Member라는 객체를 생성하고, sql로 CRUD를 개발하였다고 생각하자.
public class Member {
private Long memberId;
private String name;
...
}
INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES // 인서트 쿼리 변경
SELECT MEMBER_ID, NAME FROM MEMBER M // 셀렉트 쿼리 변경
UPDATE MEMBER SET … // 업데이트 쿼리 변경
객체 CRUD - 필드추가#
개발이 끝났을때, 기획에서 연락처를 추가해 달라는 요청이 왔다
public class Member {
private Long memberId;
private String name;
/* 신규추가 tel */
private String tel;
...
}
/*
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES -- 신규추가 tel
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M -- 신규추가 tel
UPDATE MEMBER SET … , TEL = ? -- 신규추가 tel
*/
SQL에 의존적인 개발을 피하기 어렵다.#
관계형 DB를 사용하는 이상 SQL에 의존적이며, DB의 테이블 또는 컬럼에 따라 SQL을 계속 수정하고 작성하게 되어있다.
패러다임의 불일치#
관계형 데이터베이스의 사상과 객체 지향의 사상이 매우 다름
객체 VS 관계형 데이터베이스#
관계형 데이터베이스#
데이터를 잘 정규화 해서 보관
객체#
속성과 기능을 묶어서 캡슐화 하여 객체간 유기적으로 사용
객체를 영구 보관하는 다양한 저장소#
- RDB
- NoSql
- File
- etc
현실적 대안은 RDB
객체를 관계형 데이터베이스에 저장#
객체 -> SQL 변환 -> RDB
이렇게 객체를 SQL로 변환하여 주는 업무를 개발자가 한다.
출근해서 SQL을 하루종일 작성하고 있는것이.. SQL 매퍼의 일을 개발자가 하고 있다.
객체와 관계형 데이터베이스의 차이#
- 상속 객체의 상속관계 같은 것은 없고 유사한 것은 있지만, 없다고 본다.
- 연관관계 객체 참조와 RDB의 PK, FK를 이용해 조인하여 연관관계를 찾을 수 있음
- 데이터 타입
- 데이터 식별 방법
상속#
Album 저장#
- 객체 분해
- INSERT INTO ITEM …
- INSERT INTO ALBUM ..
Album 조회#
- 각각의 테이블에 따른 조인 SQL 작성
- 상속 관계에 따라 각각 객체 생성
- SQL 조회결과에 따라 각각 객체에 데이터 세팅.. 등 복잡
- 그래서 DB에 저장할 객체에는 상속 관계 안쓴다
자바 컬렉션에 저장하면?#
객체를 담을 수 있는 특별한 컬렉션이 있다고 가정하면 !
list.add(album);
자바 컬렉션에서 조회하려면?#
Album album = list.get(albumId);
//보모 타입으로 조회 후 다형성 활용
Item item = list.get(albumId);
컬렉션에 넣고 빼는 것은 심플하지만, 관계형 DB에 넣고 빼는 순간 중간에서 SQL 매핑작업을 개발자가 손수 작업을 해줘야 한다.
연관관계#
- 객체는 참조를 사용 : member.getTeam()
- 테이블은 외래 키를 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID
객체를 테이블에 맞추어 모델링#
class Member {
Long id; // MEMBER_ID 컬럼 사용
Long teamId; // TEAM_ID FK 컬럼 사용
String username; // USERNAME 컬럼 사용
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
}
테이블에 맞춘 객체 저장#
객체다운 모델링#
class Member {
Long id; // MEMBER_ID 컬럼 사용
Team team; // 참조로 연관관계를 맺는다.
String username; // USERNAME 컬럼 사용
Team getTeam(){
return team;
}
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
}
객체 모델링 저장#
객체 모델링 조회#
--select.member.info
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
public Member find(Long memberId){
// SQL 실행
Map result = sql.executeQuery("select.member.info");
// 데이터베이스에서 조회한 회원 관련 정보 세팅
Meber member = new Member();
member.set("id", result.get("id"));
...
...
// 데이터베이스에서 조회한 팀관련 정보를 모두 입력
Team team = new Team();
team.set("id", result.get("teamId"));
...
...
// 회원과 팀 관계 설정
member.setTeam(team);
// 회원 객체 반환
return member;
}
이러한 번잡함을 해결하기 위해 Meber와 Team을 모두 포괄하는 SuperDto(MemberTeamDto) 등 으로 한번에 받음
객체 모델링, 자바 컬렉션에 관리#
list.add(member);
Member member = list.get(memberId);
Team team = member.getTeam();
자바 컬렉션에 넣는다고 생각하면 이러한 형태가 객체지향적으로 설계하기 괜찮음
객체 그래프 탐색#
객체는 자유롭게 객체 그래프를 탐색 할 수 있어야 한다.
처음 실행하는 SQL에 따라 탐색 범위 결정#
--select.member.info
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
member.getTema() //OK
member.getOrder() // null
조회한 sql에 따라 order 테이블엔 값이 있어도 sql에서 order를 조회 하지 않았기 때문에 getOrder()에서는 null이 발생
엔티티 신뢰 문제#
class meberService {
...
public void process(){
Member member = memberDAO.find(memberId);
member.getTeam(); // ???
member.getOrder().getDelivery(); // ???
}
}
누군가 개발해둔 소스를 보고 Meamber를 조회하고, getTeam과 getOrder로 Team과 Order를 사용하면 되겠다 생각이 들겠지만,
실제로 memberDAO.find()를 열어서 실제 조회하는 sql을 확인하고 내부동작이 어떻게 되어있는지 확인 하지 않는 이상 어떤 결과가 있을지 모른다.
이러한 계층구조 객체를 레이어드 아키텍처라 하는데, 레이어드 아키텍처는 다음 레이어에서 신뢰하고 사용을 할 수 있어야 한다. 하지만 이경우 엔티티 신뢰 문제가 발생.
모든 객체를 미리 로딩할 수는 없다.#
상황에 따라 동일한 회원 조회 메서드를 여러벌 생성
memberDAO.getMember(); // Member만 조회
memberDAO.getMemberWithTeam(); // Member와 Team 조회
// Member, Team, Delivery 조회
memberDAO.getMemberWithTeamWithDelivery();
계층형 아키텍처#
진정한 의미의 계층 분할이 어렵다.
비교하기#
비교하기 - 자바 객체로 조회#
Long memberId = 100;
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; //다르다. (getMember에서 sql로 조회된 데이터는 같아도 return되는 객체는 new로 새로 만들기 때문에 다름)
class MemberDAO {
public Member getMember(Long memberId) {
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
...
//JDBC API, SQL 실행
return new Member(...); //객체를 새로만듬
}
}
비교하기 - 자바 컬렉션에서 조회#
이러한 특수한 컬렉션이 있다고 가정
Long memberId = 100;
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);
member1 == member2; //같다. (참조 값이 같다)
sql에서 다룰때랑, 자바 컬렉션에서 다룰때랑, 자바 객체에서 다룰때, 관계형 DB에서 다룰때 중간에 많은 믹스매치가 발생
객체답게 모델링 할수록 매핑 작업만 늘어난다.#
객체지향을 배우고 객체지향을 토대로 설계하고 구현할 경우 번잡한 매핑 작업만 들어난다.
그래서 sql에 맞춰서 데이터 전송하는 객체로 만들 수 밖에 없음. (만들 수 있지만, 개발 퍼포먼스 측면에서 지옥이다.)
객체를 자바 컬렉션에 저장 하듯이 DB에 저장할 수 없을까#
1980 년대 부터 많은 개발자들이 고민을 해왔다고합니다. 자바 진영에서는 그 고민의 결과 !
JPA - Java Persistence API#
JPA란 Java Persistence API로 자바 진영의 ORM 기술 표준
ORM ?#
- Object Relational mapping(객체 관계 매칭)
- 객체는 객체대로 설계
- 관계형 데이터베이스는 관계형 데이터베이스대로 설계
- ORM 프레임 워크가 중간에서 매핑
- 대중적인 언어에는 대부분 ORM 기술이 존재
JPA는 애플리케이션과 JDBC 사이에서 동작#
JPA 동작 - 저장#
JPA에게 Meber객체를 넘기면, JPA가 Member 객체를 분석하고, Insert 쿼리를 생성해서 DB에 전달하고 결과를 받음
JPA 동작 - 조회#
JPA에게 Meber객체를 넘기면, JPA가 Member 객체를 분석하고, Select 쿼리를 생성해서 DB에 전달하고 결과를 받음
중요한것은 패러다임 불일치 해결
JPA 소개#
과거 EJB - 엔티티 빈(자바 표준)에도 JPA와 비슷한 ORM 기술이 있었지만 높은 가격에 기능이 떨어지며, 기능 동작도 안되고 안쓰였다고 합니다.
SI 개발자인 개빈 킹(Gavin King)이 EJB의 기능을 만족하지 못하여 만든 기술이 하이버네이트입니다. 이후 많은 개발자들이 동참하여 오픈소스화 하였습니다. EJB는 서서히 몰락하게 되었습니다.
자바 진영에서 하이버네이트를 만든 개빈킹을 데려다 하이버네이트를 똑닮은 표준을 만들었는데 그것이 JPA입니다.
JPA는 표준 명세#
- JPA는 인터페이스의 모음
- JPA 2.1 표준 명세를 구현한 3가지 구현체
- 하이버네이트, EclipseLink, DataNucleus
JPA 버전#
- JPA 1.0(JSR 220) 2006년 : 초기 버전. 복합 키와 연관관계 기능이 부족
- JPA 2.0(JSR 317) 2009년 : 대부분의 ORM 기능을 포함, JPA Criteria 추가
- JPA 2.1(JSR 338) 2013년 : 스토어드 프로시저 접근, 컨버터(Converter), 엔티 티 그래프 기능이 추가
JPA를 왜 사용해야 하는가?#
- SQL 중심적인 개발에서 객체 중심으로 개발
- 생산성
- 유지보수
- 패러다임의 불일치 해결
- 성능
- 데이터 접근 추상화와 벤더 독립성
- 표준
생산성 - JPA와 CRUD#
- 저장 : jpa.persist(member) //영구 저장하다라는 뜻
- 조회 : Member member = jpa.find(memberId)
- 수정 : member.setName(“변경할 이름”) // 객체의 이름을 set으로 변경하면 update 문이 자동으로 날라감
- 삭제 : jpa.remove(member)
유지보수 - JPA : 필드만 추가하면 됨 SQL은 JPA가 처리#
개발이 끝났을때, 기획에서 연락처를 추가해 달라는 요청이 왔다
public class Member {
private Long memberId;
private String name;
/* 신규추가 tel */
private String tel;
...
}
/*
//더이상 sql들을 수정할 필요 없음
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES -- 신규추가 tel
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M -- 신규추가 tel
UPDATE MEMBER SET … , TEL = ? -- 신규추가 tel
*/
JPA와 패러다임의 불일치 해결#
1.JPA와 상속 2.JPA와 연관관계 3.JPA와 객체 그래프 탐색 4.JPA와 비교하기
JPA와 상속#
JPA와 상속 - 저장#
ALBUM 객체를 DB에 저장하고 싶을때, jpa.persist()에 album 객체를 넘기면 JPA가 Insert 쿼리를 나누어서 작성
// 개발자가 할일
jpa.persist(album);
// 나머진 JPA가 처리
Insert Into ITEM ...
Insert Into ALBUM ...
JPA와 상속 - 조회#
Album 객체를 조회하고 싶을때 jpa.find()에 Album의 클래스, PK 값을 넘기면 JPA가 자동으로 ITEM과 ALBUM을 조인하여 데이터를 퍼올려줍니다.
// 개발자가 할일
Album album = jpa.find(Album.class, albumId);
// 나머진 JPA가 처리
Select I.*
, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID
JPA와 연관관계, 객체 그래프 탐색#
member에 팀을 세팅하고 member를 저장하면 team을 포함한 member가 저장됨.
// 연관관계 저장
member.setTeam(team);
jpa.persist(member);
저장된 team을 가져올때도, 자바 컬렉션에서 데이터를 가져오듯이 member.getTeam()을 이용해 team을 가져올수 있음.
// 객체 그래프 탐색
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
신뢰 할 수 있는 엔티티, 계층#
JPA를 이용하면, find()를 이용해 객체 그래프 탐색을 통해서 getTeam()나 getOrder().getDelivery()를 사용하여, 각각의 객체를 자유롭게 조회할 수 있습니다. 또한 Lazy Loading 이라는 기능을 이용하여, 실제 코드를 수행할 당시에 데이터를 불러와 member객체에 하위 계층 객체를 세팅해줍니다. JPA를 이용하면, DB에 데이터가 없지 않는 이상 신뢰 하고 하위 계층을 사용하면 됩니다.
class MemberService{
...
public void process(){
Member member = memberDAO.find(memberId);
member.getTeam();
member.getOrder().getDelivery();
}
}
JPA와 객체 비교하기#
동일한 트랜잭션에서는 조회한 엔티티는 같음 보장
Long memberId = 100;
Member member1 = jpa.find(member.class, memberId);
Member member2 = jpa.find(member.class, memberId);
member1 == member2; // 같다
jpa의 성능 최적화 기능#
JPA를 사용하면 성능이 저하되는 것이 아닌가 고민을 할 수 있을수도 있지만, JPA는 성능을 최적화 하기 위해 여러가지 기능을 제공합니다.
- 1차 캐시와 동일성(identity) 보장
- 트랜잭샨을 지원하는 쓰기 지연(transational write-behind)
- 지연 로딩(Laze Loading)g
1차 캐시와 동일성 보장#
- 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상 ``` Long memberId = 100; Memeber member1 = jpa.find(Member.class, memberId); // SQL 조회 Memeber member2 = jpa.find(Member.class, memberId); // 캐시
member1 == member2 // 같다
```
SQL 1번만 실행
- DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장
트랜잭션을 지원하는 쓰기 지연 - Insert#
- 트랜잭션을 커밋할 때까지 Insert SQL을 모음
- JDBC Batch SQL 기능을 사용해서 한번에 SQL을 전송
transction.begin(); // 트랜잭션 시작
jpa.persist(memberA) // 메모리에 적재
jpa.persist(memberB) // 메모리에 적재
jpa.persist(memberC) // 메모리에 적재
// 여기까지 Insert SQL을 DB에 보내지 않는다.
// 커밋하는 순간 DB에 SQL을 모아서 보낸다.
transction.commit(); // 트랜잭션 커밋
JPA 이전에도 JDBC Batch라는 기능이 있었고 그것을 쓰면 되지만, 코드가 정말 지저분해집니다. JPA가 하나의 트랜잭션에서 memberA, B, C를 메모리에 쌓았다가 Commit 시점에 동일한것을 모아 한번에 JDBC Batch 기능을 이용하여 네트워크를 통해 DB로 데이터를 전송합니다. JPA 옵션으로 설정해 두면 개발자가 신경 쓰지 않아도 이러한 기능을 지원해 줍니다.
지연 로딩과 즉시 로딩#
- 지연 로딩 : 객체가 실제 사용될 때 로딩
- 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
// 지연 로딩
Member member = memberDAO.find(memberId); // SELECT * FROM MEMBER
Team team = member.getTeam(); // SELECT * FROM TEAM
String teamName = team.getName();
JPA를 통해 객체를 조회할때, team 객체를 사용하는 시점에 TEAM 조회 SQL을 통하여 member객체의 team 객체에 데이터를 세팅
member를 조회할 때, 항상 team을 사용한다면 JPA 옵션을 통해서 member를 조회할 때 team을 같이 가져오게 설정을 할 수 있습니다. 옵션을 사용하면 쿼리가 MEMBER외 TEAM을 조인한 이래와 같은 쿼리로 변경해서 사용하게 됩니다.
// 즉시 로딩
Member member = memberDAO.find(memberId); // SELECT M.* ,T.* FROM MEMBER M JOIN Team T On ...
Team team = member.getTeam();
String teamName = team.getName();
JPA를 사용하지 않은 상태에서 MEMBER와 TEAM의 각각의 쿼리를 조인된 하나의 쿼리로 변경하거나, 조인된 각각의 쿼리를 개별의 쿼리로 나누려 작업한다면 진짜 많은 쿼리를 수정해야 될텐데 JPA를 이용하면 어마 어마한 것들을 간단히 사용할 수 있습니다.
ORM은 객체와 RDB의 두 기둥위에 있는 기술#
어느하나 소홀해서는 안되며 균형있게 두가지 지식이 쌓여 있어야 안전안 애플리케이션을 설계할 수 있습니다.