Spring과 함께 Database를 더욱 효율적으로 다루기 위한 JPA에 대한 내용이다.
프로젝트를 진행할 때, 객체 지향 언어와 관계형 데이터베이스를 사용하게 되는 경우가 많다. 이 둘을 함께 사용한다는 것은 객체를 RDB에 저장한다는 것이고, 그렇게 하기 위해서는 INSERT INTO.. 등의 수많은 객체에 대한 SQL문을 작성해야 한다.
Public class Member {
Private String memberId;
Private String name;
Private String tel;
…
}
이러한 객체와
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
UPDATE MEMBER SET … TEL = ?
…
SQL문을 한땀한땀 작성해야 한다.
이러한 SQL 중심적인 개발은 변화가 일어날 때마다 모든 SQL을 작성해야 한다.
이는 위의 코드와 같이 객체에 대한 CRUD를 계속하여 만들어야 함을 의미하며,
이후에도 변화가 생기면, 하나하나 직접 쿼리문을 고쳐야 하므로 같은 작업을 계속 반복하여 하게 된다.
프로그램을 만드는 데 있어서 반복적인 작업을 계속하여 수행하게 되는 것이다...
객체와 관계형 데이터베이스의 차이
상속
데이터베이스 테이블에는 객체에서 생각하는 상속이 존재하지 않는다.
대신, 슈퍼타입, 서브타입 관계를 설정하여 필요한 경우 JOIN을 통해 사용할 수 있다.
하지만 자바 컬렉션에서 사용한다면...
list.add(album);
과 같이 매우 간단하게 표현할 수 있는 데에 반해 복잡하게 표현될 것이다.
연관관계
객체는 참조를 사용하며,
member.getTeam();
테이블은 외래 키를 사용하여 JOIN을 한다
JOIN ON M.TEAM_ID = T.TEAM_ID
그렇기 때문에 객체를 테이블에 맞추어 저장하고, 모델링하게 된다.
--> 이는 객체다운 모델링이 아니다.
객체는 자유롭게 객체 그래프를 탐색할 수 있어야 하는데, 데이터베이스에 객체를 저장할면 처음에 어떤 SQL을 실행하여 생성했는지에 따라 탐색 범위가 결정되기 때문이다.
이러한 상황에서 객체답게 모델링을 할수록 매핑 작업만 늘어나게 된다.
객체를 자바 컬렉션에 저장하듯이 간단하게 DB에 저장할 수 있는 방법을 원한다면..
JPA를 알아보자..
JPA : Java Persistence API
자바 진영의 ORM 기술 표준.
ORM
Objec-relational mapping (객체 관계 매핑)
객체는 객체대로 설계, 관계형 데이터베이스는 관계형 데이터베이스대로 설계
ORM 프레임워크가 중간에서 매핑 -> 패러다임 불일치 문제를 해결
대중적인 언어에는 대부분 ORM 기술이 존재
JPA는 애플리케이션과 JDBC 사이에서 동작
원래는 개발자가 직접 JDBC API를 썼다면, 그거를 JPA가 대신 써준다고 생각하면 된다.
// 저장
jpa.persist(member)
// 조회
Member member = jpa.find(memberId)
// 수정
member.setName("name)
// 삭제
jpa.remove(member)
JPA와 상속
조회 ex)
// 개발자
Album album = jpa.find(Album.class, albumId);
// 나머지는 JPA가 처리
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITM_ID = A.ITEM_ID
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; // true
“==” 비교하면 같다는 것을 알 수 있다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않음
// 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // 트랜잭션 커밋
지연 로딩
실제로 사용되는 객체를 로딩한다
즉시 로딩
JOIN SQL로 한 번에 연관된 객체까지 미리 조회할 수 있도록 한다.
이는 예를 들자면 member를 사용할 때, team이 자주 사용되는 경우에 설정을 통해 member를 사용할 때 team도 불러오도록 설정할 수 있음을 말한다
ORM(Object-Relational Mapping)은 이름 그대로 객체(Object)와 관계형 데이터(Relational data)를 매핑하기 위한 기술이다. 매핑이 필요한 이유는 객체 지향 언어과 관계형 데이터베이스 사이에서 패러다임 불일치 문제가 발생하기 때문이다. 이 둘 사이에서 발생하는 문제로 인해 개발자는 더 많은 코드를 작성해야 하며, 이는 반복적이고 점점 힘들어지는 작업을 하게 된다. ORM은 이러한 문제를 줄이고, 객체지향적인 설계에 집중할 수 있도록 도와준다.
객체와 관계형 데이터베이스 간 발생하는 패러다임 불일치는 데이터 표현 방식의 다름에서 나타난다. 객체와 관계형 데이터베이스의 목표와 동작 방식이 서로 다르기 때문에 이러한 불일치 문제가 발생한다.
setting
Persistence.xml을 만들어서 넣어야 JPA를 실행시킬 수 있다
위치가 중요한데,
/META-INF/persistence.xml 위치에 생성한다.
<persistence-unit name="hello">
Persistence-unit name으로 이름 지정
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
Javax.persistence로 시작: JPA 표준 속성
Hibernate로 시작: 하이버네이트 전용 속성
Javax로 시작, hibernate로 시작하는 것으로 구분할 수 있다.
Javax는 표준을 지키는 것으로, hibernate를 사용하지 않아도 적용이 된다.
Hibernate는 hibernate 전용 옵션인 것이다.
//쿼리가 프린트되어 보이도록
<property name="hibernate.show_sql" value="true"/>
//Sql 포맷팅
<property name="hibernate.format_sql" value="true"/>
//주석 부분. 쿼리가 왜 나오게 되었는지에 대한 주석으로 설명이 제공됨
<property name="hibernate.use_sql_comments" value="true"/>
Dialect
각각의 데이터베이스는 사용 방식이 조금씩 다른데, 이를 데이터베이스 방언이라고 한다. JPA에게 어떤 데이터베이스 방언을 사용하는지 알려주어야 하는데, 이 때 사용하는 것이 Dialect이다. h2dialect를 붙이면, JPA에게 h2라는 데이터베이스의 방언을 사용한다고 알려주는 것이다.
JPA는 EntityManagerFactory와 EntityManager를 사용한다.
Persistence.createEntityManagerFactory("hello")
Hello -> persistence unit name -> 아까 xml에서 설정한 이름!
EntityManagerFactory는 로딩 시점에 딱 하나만 만들어야 한다.
emf.createEntityManager();
그리고, 실제로 db에 저장하는 등의 트랜잭션 단위를 할 때마다(고객이 어떤 행동을 할 때마다) EntitiyManager를 만들어줘야 한다.
JPA에서는 트랜잭션 단위가 매우 중요하다. JPA에서 모든 데이터를 변경하는 작업은 꼭 트랜잭션 안에서 작업해야 한다.
Member 클래스 생성
@Entity
public class Member {
}
@Entity를 반드시 넣어줘야 한다. 그래야 JPA가 처음 로딩될 때, JPA를 사용하는 녀석인지 알 수 있기 때문이다.
@Entity
@Table(name = "USER")
public class Member {
@Id
private Long id;
@Column(name = "username")
private String name;
public Long getId() {
return id;
}
@Table, @Column 어노테이션을 사용하여 매핑을 해줄 수도 있다.
Member member = new Member();
member.setId(1L);
member.setName("HelloA");
em.persist(member); // member 저장
tx.commit(); // 트랜잭션 커밋
em.close();
emf.close();
이러한 기존 코드는 문제가 발생했을 때, close가 호출이 안되기 때문에, 예외처리를 해주는 것이 좋다
try{
// 회원 등록
Member member = new Member();
member.setId(2L);
member.setName("HelloB");
em.persist(member); // member 저장
tx.commit(); // 트랜잭션 커밋
}catch(Exception e){
tx.rollback();
}finally{
em.close();
}
emf.close();
정상적일 때는 commit을 해주고,
문제가 발생했을 때는 rollback을 해준다.
마지막에 EntityManager를 닫아주는 것이 중요한데, em은 결국 내부적으로 데이터베이스 커넥션을 물고 동작하기 때문에, 사용 후에는 .close()를 통해 닫아주어야 한다.
그리고 전체 애플리케이션이 끝나면 emf까지 닫아준다.
회원 조회, 삭제, 수정
// 회원 조회
Member findMember = em.find(Member.class, 1L);
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());
// 회원 삭제
em.remove(findMember); // 찾은 아이를 remove에 넣어주면 된다. / 그러면 delete 쿼리가 실행되어 삭제됨
// 회원 수정
findMember.setName("HelloJPA");
회원 수정 후 Em.persist를 통해 저장해야하는가 싶을 수 있지만, 안해도 된다.
JPA는 자바 컬렉션을 다루듯이 다루도록 설계되었기 때문이다.
Member findMember = em.find(Member.class, 1L);
JPA를 통해서 엔티티를 가져오게 되면 JPA가 관리를 한다.
JPA는 변경이 되었는지 안되었는지 트랜잭션 시점에 체크하고,
바뀌었다면 업데이트 쿼리를 날린 후에 커밋을 진행한다.
// JPQL
List<Member> result = em.createQuery("select m from Member as m", Member.class)
.setFirstResult(5)
.setMaxResults(8)
.getResultList();
JPQL은 이런 식으로 페이징을 하는 데에 유용하게 쓰일 수 있다
JPQL은 객체를 대상으로 하는 객체지향 쿼리라고 보면 된다
데이터베이스 방언에 맞춰서 알맞게 번역을 하여 사용된다
JPQL
JPA를 사용하면 엔티티 중심으로 개발하게 된다.
문제는 검색 쿼리인데, 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색한다.
하지만, 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다.
따라서, 애플리케이션이 필요한 데이터만 db에서 불러오려면 결국 검색 조건이 포함된 sql이 필요하다.
SQL은 데이터베이스 테이블을 대상으로 쿼리를 날리는 반면,
JPQL은 엔티티 객체를 대상으로 쿼리를 날리므로 방언을 바꿔도, 코드를 고칠 필요가 없다.
정리