Object-Relational Mapping
객체 지향 애플리케이션과 관계형 DB의 충돌
객체에 필드가 변경되었을 때, SQL문에 변경사항이 너무 많은 문제점이 있다.
또한 객체를 관계형 디비에 저장하려 하면 복잡한 과정이 필요하다는 문제점도 있다.
그래서 지금까지는 객체를 테이블에 맞추어 모델링할 수 밖에 없었다고 한다.
이 방법은 너무 불편해서 객체를 자바 컬렉션 저장하듯 DB에 저장할 수는 없을까? 라는 고민 끝에 등장한 것이 ORM이다.
JPA는 자바 표준 ORM으로써, 객체는 객체대로 설계하고 관계형 디비는 관계형 디비대로 설계한 후 ORM 프레임워크가 중간에서 매핑하도록 한다.
SQL 중심적인 개발에서 객체 중심으로 개발을 하게 될 것이다.
SQL문은 JPA가 알아서 처리해 줄 것이다.!
이로 인해 생산성, 유지보수, 성능 문제가 해결되고, 패러다임 불일치 문제 또한 해결되며, JPA는 각각의 디비에 해당하는 방언을 알아서 처리해주기 때문에 특정 데이터베이스에 종속적으로 작성하지 않아도 된다.
하이버네이트는 40가지 이상의 방언을 지원한다고 한다.
영속성 컨텍스트란 엔티티를 영구 저장하는 환경이다. 이는 논리적인 개념으로, 눈에 보이지는 않는다.
엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있다.
스프링 프레임워크 같은 컨테이너 환경(J2EE)에서는 엔티티 매니저와 영속성 컨텍스트가 N:1의 관계를 가진다.
변경 감지를 제외한 나머지 이점들은 위에서 설명했다.
1차 캐시와 디비의 동일성을 보장하는 장면이다.
member2를 처음 조회했을 때 1차 캐시에 없다면, 1차 캐시에 저장 후 member2를 반환한다.
이후 다시 member2를 조회했을 때는 1차 캐시를 조회하여 조회 성능을 약간 높일 수 있다.
persist 메서드를 이용해 memberA를 저장할 때는 insert문 생성과 동시에 1차 캐시에 저장하게 된다.
추가로 memberB 또한 영속성 컨텍스트에 persist 하게 되면 총 2개의 insert SQL문이 쓰기 지연 SQL 저장소에 저장되고, 후에 트랜잭션이 커밋되는 시점에 SQL문이 DB로 날아갈 것이다.
commit 시점에 flush(플러쉬)가 우선 발생한다.
flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
flush가 실행되면...
1. 변경 감지 (dirty checking)가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해서 수정된 엔티티를 찾는다.
2. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 지연 저장소에 등록한다.
3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
데이터베이스에 변경내용을 SQL로 전달하지 않고, 트랜잭션만 커밋하면 어떤 데이터도 데이터베이스에 반영되지 않기 때문에 트랜잭션을 커밋하기 전에 반드시 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 한다.
이러한 이유로 영속성 컨텍스트에는 등록과 조회, 삭제 기능만 있지, 수정 기능이 따로 있지 않다.
EntityManager.persity(엔티티), EntityManager.find(엔티티), EntityManager.remove(엔티티)
만약 memberA 객체(엔티티)에 대한 내용 중 수정하고 싶은 것이 있다면, 다음과 같이 하면 된다.
memberA.setUsername("hi");
memberA.setAge(10);
// em.update(memberA) -> 이런 기능은 존재하지 않는다.
transaction.commit();
memberA는 영속성 컨텍스트에서 관리되고 있기 때문에, 엔티티의 데이터를 수정하면 커밋 시점에 변경 감지가 동작해서 update 쿼리가 자동으로 생성되는 것이다.
아래는 변경감지를 나타낸 그림이다.
즉, flusth는 영속성 컨텍스트의 변경 내용을 디비에 반영하는 작업이다. (영속성 컨텍스트의 변경내용을 디비에 동기화)
변경 감지 -> 수정된 엔티티에 대한 update 쿼리 생성 후 쓰기 지연 SQL 저장소에 등록
-> 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
영속성 컨텍스트를 flush 하는 경우는 다음과 같다.
1. em.flush
2. 트랜잭션 커밋
3. JPQL 쿼리 실행
영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태이므로, 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.
준영속 상태로 만드는 방법은 다음과 같다.
1. em.detach(엔티티)
2. em.clear()
3. em.close()
데이터베이스 방언에 맞게 DDL을 자동으로 생성한다.
이 때 옵션으로 create, create-drop, update, validate, none이 있는데 개발 상황에 알맞게 맞춰 사용하면 된다.
운영 장비에는 절대 create, create-drop, update를 사용하면 안 된다.
회원과 팀의 관계를 예시로 사용하려 한다.
회원과 팀은 서로 N:1의 관게이다. (다대일 관계)
이를 ERD로 표현하면 아래와 같다.
TEAM_ID 라는 외래키를 통해 조인해서 연관된 테이블을 양방향으로 찾을 수 있다.
반면 이를 팀과 회원 객체 사이의 연관관계로 표현하면 아래와 같다.
Member 객체는 소속 Team을 알 수 있으나 Team 객체는 소속된 Member에 대해 알 수가 없다.
객체는 참조를 통해 연관된 객체의 값을 가져오기 때문에, Member 객체가 Team 객체를 참조할 수는 있지만 Team 객체는 Member 객체를 참조할 수가 없다.
따라서 이는 현재 단방향 관계이다.
이를 양방향으로 매핑하고자 하면 다음과 같이 표현할 수 있다.
Team 클래스에 members에 대한 리스트를 갖고 있으면 Team -> Member 단방향 관계가 만들어져, Team과 Member 양방향 객체 연관관계가 만들어진다.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Colomn(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID") // 외래 키 지정 (디비 컬럼명을 적어주어야 함.)
private Team team; // 참조 객체
...
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 참조된 객체 (연관관계를 맺을 객체 지정)
List<Member> members = new ArrayList<>();
...
테이블 간의 관계에서는 외래키 하나로 두 테이블 양쪽에 대한 연관관계를 가질 수 있다.
하지만 객체 간의 관계에서는 단방향 관계가 두 개 모여 양방향 관계처럼 보이는 것 뿐이다.
두 객체를 다대일 관계로 엮게 되면, 관계의 주인이 필요하다.
연관관계의 주인만이 외래키를 관리하고, 주인이 아닌 쪽은 읽기만 가능하다.
이처럼 주인이 아닌 객체에서는 값이 등록되지 않기 때문에, 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 연관관계 메서드가 필요하다.
테이블 상 외래키가 있는 객체를 주인으로 지정한다. (N쪽에 항상 외래키가 위치하므로 이쪽이 주인)
사실 단방향 매핑만으로도 연관관계 매핑은 완료가 된 것이다.
하지만 반대 방향으로의 단방향 매핑을 생성하여 양방향 연관관계를 맺는 것은 역방향으로 조회할 일이 많음을 예상하여 해당 기능을 추가하는 것이다.
멤버 혹은 팀을 조회할 때, 그와 연관된 객체를 함께 조회해야할까?
지연 로딩을 사용하면 프록시를 통해 조회를 하게 된다.
다음과 같은 코드로 예시를 보자.
Team team = member.getTeam() // 프록시 조회,
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회)
만약 연관된 두 객체를 함께 조회해야 할 일이 많다면, 즉시로딩을 사용해서 함께 조회하면 된다.
하지만 그런 상황이 아니라면, 가급적 지연로딩만 사용하는 것이 좋다.
즉시 로딩은 JPQL에서 상상도 못한 쿼리가 나갈 수 있어, 1+N 문제를 일으키기도 한다.
@ManyToOne(fetch=FetchType.XXXX) <- LAZY or EAGER
JPA를 사용하면 테이블이 아닌 엔티티 객체를 중심으로 개발을 하게 된다.
그래서 검색을 위한 SQL 문도 테이블이 아닌 엔티티 객체를 대상으로 하게 된다.
JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공하는데, 이는 기존의 SQL 문법과 굉장히 유사하다.
차이점은 SQL은 데이터베이스 테이블을 대상으로 쿼리하는 데에 반해, JPQL은 엔티티 객체를 대상으로 쿼리한다는 것이다.
JPQL은 SQL을 추상화했기 때문에, 특정 데이터베이스 SQL에 의존적이지 않다.
List<Member> result = em.createQuery("select m from Member m where m.age > 18", Member.class)
.getResultList();
위 소스 코드의 실행 결과는 다음과 같다.
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age>18
쿼리문을 직접 작성하지 않고, 자바 코드로 JPQL을 작성할 수 있는 QueryDSL이 있는데 이는 단순하고 쉽다. 또한 동적쿼리를 작성하기에도 편리하고.
하지만 이번 포스팅에서는 QueryDSL에 대해 다루지 않겠다.
GROUP BY, HAVING, ORDER BY
select
m,
COUNT(m),
SUM(m.age),
AVG(m.age),
MAX(m.age),
MIN(m.age),
from Member as m // as는 생략 가능
String jpql = "select m from Member m order by m.name desc";
List<Member> result = em.createQuery(jpql, Member.class)
.setFirstResult(10) // 조회 시작 위치, 0부터 시작
.setMaxResults(20) // 조회할 데이터 수
.getResultList();
내부조인, 외부조인(left join), 세타 조인
ON 절을 활용해 조인할 수 있다.
페치조인은 실무에서 매우 종요하다고 한다.
이는 SQL의 조인 종류가 아니라, JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능으로, join fetch 명령어를 사용한다.
JPQL: select m from Member m join fetch m.team
SQL: select m.*, t.* from Member m inner join Team t on m.TEAM_ID = T.id
아래는 페치 조인을 사용한 소스코드와 그 실행 결과이다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
for (Member member: members) {
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().name());
}
username = 회원1, teamName = 팀A
username = 회원2, teamName = 팀A
username = 회원3, teamName = 팀B
... 뭐 더 있고 하지만 fetch join은 조금 어려우니 나중에 추가로 포스팅 하겠다!
인프런 김영한 강사님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고하여 포스팅하였습니다.