김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.
객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공하는데 RDB 와의 차이점은 크게 상속, 연관관계, 데이터 타입, 데이터 식별 방법으로 볼 수 있습니다.
객체의 상속 관계를 DB 에 저장할 수는 없고 슈퍼타입, 서브타입을 사용해서 해결해야 하는데 이를 사용하는 방법은 복잡합니다.
Album 을 저장한다고 했을 때 객체를 분해하여 Item 테이블에 필요한 데이터, Album 테이블에 필요한 데이터를 가져옵니다. 그 다음 Item 테이블과 Album 테이블에 각각 Insert 쿼리를 날려주어야 합니다.
조회할 때는 Item 과 Album 테이블을 조인하는 SQL 을 작성하고, 각각의 객체를 생성해서 데이터를 넣는 등 복잡한 과정을 거치게 됩니다.
만약 DB 가 아닌 자바 컬렉션인 리스트에 저장한다고 가정했을 때 list.add(album)
을 사용하면 됩니다. 조회하는 것도 아래처럼 get()
을 이용해 꺼낼 수 있으며, 다형성을 활용할 수도 있습니다.
Album album = list.get(albumId);
Item item = list.get(albumId);
다른 곳에 있는 데이터가 필요할 때 객체는 참조를 사용하지만 테이블은 참조가 없기 때문에 외래키를 사용해서 조인을 합니다.
그래서 Member 와 Team 객체를 테이블에 저장하기 위해서는 위처럼 객체를 테이블에 맞추어 모델링 해야합니다.
class Member {
String id;
Long teamId; // TEAM_ID FK 컬럼을 사용
String username;
}
class Team {
Long id; // PK : TEAM_ID
String name;
}
객체를 테이블에 맞추어 모델링하면 Member 가 Team 에 대한 참조를 가지는 것이 아닌 Team 의 Id 를 가지고 있게 됩니다. 이렇게 구현해야 아래 그림처럼 SQL 을 작성할 때 필요한 정보들을 꺼내서 사용할 수 있습니다.
이렇게 테이블에 맞추어 생성하면 객체다운 모델링이 이루어지지 않습니다. 왜냐하면 객체는 참조를 통해 연관관계를 맺기 때문입니다. 아래는 참조를 이용하여 객체다운 모델링을 한 예시입니다.
class Member {
String id;
Team team; // 참조로 연관관계를 맺는다
String username;
}
class Team {
Long id; // PK : TEAM_ID
String name;
}
이전과는 다르게 Team 이라는 참조를 가진 것을 확인할 수 있습니다. 그래서 필요할 때 member.team
혹은 member.getTeam()
을 통해 바로 Team 을 꺼낼 수 있게 됩니다.
하지만 이렇게 했을 때 DB 에 Insert 하기 번거로워지는데 Member 가 가진 정보를 저장할 때 객체는 Team 에 대한 FK 를 가지고 있는 것이 아닌 참조를 가지고 있기 때문에 member.getTeam().getId()
를 사용해서 TEAM_ID 를 꺼내야 합니다.
참조를 가진 객체로 모델링 한 상태에서 DB 에서 Member 와 Team 이 연관관계가 있는 상태로 조회하고 싶을 때 아래처럼 해야합니다.
SELECT
M.*, T.*
FROM MEMBER M
JOIN TEAM T
ON M.TEAM_ID = T.TEAM_ID
public Member find(String memberId) {
//SQL 실행 ...
Member member = new Member();
//데이터베이스에서 조회한 회원 관련 정보를 모두 입력
Team team = new Team();
//데이터베이스에서 조회한 팀 관련 정보를 모두 입력
//회원과 팀 관계 설정
member.setTeam(team);
return member;
}
우선 Member 와 Team 을 조인을 통해 둘 다 조회해야 합니다. 그리고 Member 객체와 Team 객체를 만들어 조회된 정보를 입력한 후에 Member 와 Team 의 연관관계를 세팅해야 합니다.
만약 자바 컬렉션에서 관리를 한다고 가정했을 때 list.add(member)
를 하면 Member 를 저장할 수 있습니다.
Member member = list.get(memberId);
Team team = member.getTeam();
Member 가 필요할 때는 list 에서 바로 꺼내서 사용할 수 있고, Team 이 필요한 경우 member 에서 참조를 통해 Team 을 꺼낼 수 있습니다.
위처럼 연관관계가 있다고 했을 때 객체는 참조를 통해 자유롭게 객체 그래프를 탐색할 수 있어야 합니다. Member 에서 Member.getOrder()
, Order.getOrderItem()
처럼 사용해서 계속 따라갈 수 있어야 합니다.
하지만 DB 에 객체를 보관하다보면 내가 처음에 어떤 SQL 을 실행해서 객체를 만들었는지에 따라 탐색 범위가 결정되게 됩니다.
SELECT
M.*, T.*
FROM MEMBER M
JOIN TEAM T
ON M.TEAM_ID = T.TEAM_ID
예를 들어, 위의 SQL 로 조회한다고 했을 때 Member 와 Team 에 대해서 조회할 수 있습니다. 연관관계에 의해 member.getTeam()
을 하면 Member 와 Team 을 둘 다 조회했기 때문에 가능합니다.
하지만 member.getOrder()
를 하면 null 이 됩니다. 왜냐하면 Member 와 Team 에 대한 select 를 했기 때문에 Order 는 DB 에서 조회되지 않았기 때문입니다.
계층형 아키텍처에서는 다음 계층을 믿고 사용할 수 있어야 하는데 이렇게 되면 엔티티에 대한 신뢰 문제가 발생하게 됩니다.
class MemberService {
...
public void process() {
Member member = memberRepository.find(memberId);
member.getTeam(); // ??
member.getOrder().getDelivery(); // ??
}
}
예를 들어, memberRepository 를 통해 memberId 로 Member 를 조회합니다. 이때 여기서 member.getTeam()
이나 member.getOrder().getDelivery()
을 할 수 있을지 확신할 수 없습니다.
왜냐하면 SELECT 를 통해 어떤 데이터를 조회했는지 모르기 때문입니다. 그래서 이를 확신할 수 없어 memberRepository 의 find()
내부를 살펴보아야 합니다.
객체 그래프를 통해 자유롭게 탐색할 수 있게 하려면 모든 객체를 미리 로딩해야 하는데 그렇게 되면 상황에 따라 동일한 조회 메서드를 아래처럼 여러 개 생성해야 합니다.
memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam();//Member와 Team 조회
//Member,Order,Delivery
memberDAO.getMemberWithOrderWithDelivery();
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; //다르다.
class MemberDAO {
public Member getMember(String memberId) {
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
...
//JDBC API, SQL 실행
return new Member(...);
}
}
만약 memberId 가 100 이라고 했을 때 이를 두 번 조회하면 두 개의 Member 객체가 나오게 됩니다. 이를 ==
으로 비교하면 서로 다르다고 출력됩니다.
왜냐하면 Member 를 조회할 때 SQL 문을 작성해 조회하고, 그 결과로 새로운 인스턴스를 반환하기 때문입니다.
String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);
member1 == member2; //같다.
하지만 위처럼 컬렉션을 이용한다면 member 객체를 두 번 조회하고 비교해도 동일한 인스턴스가 나오기 때문에 둘이 동일하다고 판단합니다.
참조와 연관관계를 사용하면서 객체답게 모델링 할 수록 매핑 작업만 늘어나게 됩니다. 지금까지 함께 살펴보았듯이 자바 컬렉션에 저장하면 편하게 할 수 있습니다. 객체를 자바 컬렉션에 저장하듯이 DB 에 저장한다면 굉장히 편리할 것입니다. 이때 사용하는 것이 JPA( Java Persistence API ) 입니다.
JPA 는 Java Persistence API 의 약자로, 자바 진영의 ORM 기술 표준입니다.
ORM 은 Object-relational mapping 의 약자로 객체 관계 매핑을 의미하는데 객체는 객체대로 설계하고, RDB 는 RDB 대로 설계하면 ORM 프레임워크가 중간에서 매핑을 해줍니다.
JAVA 애플리케이션에서 DB 랑 통신을 하려면 JDBC API 를 사용해야 합니다. JPA 는 애플리케이션과 JDBC 사이에서 동작하며, 개발자가 직접 사용해야 했던 JDBC API 를 JPA 가 대신 사용해줍니다.
Member 객체를 저장한다고 가정했을 때 Member 객체를 MemberDAO 에 넘기고, MemberDAO 가 JPA 에게 Member 엔티티를 넘겨주면 JPA 가 Member 엔티티를 분석해서 Insert SQL 을 만들어줍니다. 그리고 JDBC API 를 사용해서 DB 에 Insert 쿼리를 날리게 됩니다.
조회할 때도 JPA 에게 memberId 를 넘겨주면 JPA 가 Member 객체를 분석해서 select 쿼리를 만들고, JDBC API 를 사용해 조회하고, ResultSet 을 매핑까지 해줍니다. 결과적으로 Entity Object 를 만들어서 우리에게 반환해줍니다.
SQL 중심적인 개발에서 객체 중심으로 개발할 수 있으며, CRUD 를 구현할 때 아래처럼 사용하면 됩니다.
저장 : jpa.persist(member)
조회 : Member member = jpa.find(memberId)
수정 : member.setName(“변경할 이름”)
삭제 : jpa.remove(member)
기존에는 필드가 추가되면 관련된 모든 SQL 을 수정했어야 했습니다. 하지만 JPA 를 사용하면 직접 SQL 을 작성하지 않아도 되기 때문에 필드가 추가되어도 개발자가 유지보수 하지 않아도 됩니다.
1.JPA와 상속
2.JPA와 연관관계
3.JPA와 객체 그래프 탐색
4.JPA와 비교하기
// 개발자
jpa.persist(album);
// JPA
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
개발자가 persist()
를 호출하면서 객체를 넣어주면 JPA 가 필요한 INSERT 문을 생성해서 DB 에 쿼리를 날려주게 됩니다.
// 개발자
Album album = jpa.find(Album.class, albumId);
// JPA
SELECT
I.*, A.*
FROM ITEM I
JOIN ALBUM A
ON I.ITEM_ID = A.ITEM_ID
find()
에 객체 타입과 ID 를 넘겨주면 JPA 가 쿼리를 작성해서 조회를 해주게 되고, 개발자는 자바 컬렉션에서 가져오는 것처럼 조회할 수 있습니다.
member.setTeam(team);
jpa.persist(member);
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
연관관계를 사용하려면 원래는 외래키를 넣고 했어야 하는데, JPA 에서는 참조를 사용할 수 있습니다.
member.setTeam()
으로 team 을 세팅한 후에 member 를 저장합니다. 그 후 find()
를 통해 member 를 꺼냈을 때 member.getTeam()
으로 team 객체를 사용할 수 있습니다.
// 신뢰할 수 있는 엔티티
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); // 자유로운 객체 그래프 탐색
member.getOrder().getDelivery();
}
}
// 동일 보장
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; //같다.
JPA 는 알아서 쿼리를 작성해주면서 이전에 SQL 의 조회범위에 따른 신뢰성 문제를 해결해줍니다. 그 결과 자유로운 객체 그래프 탐색이 가능해집니다.
또 JPA 는 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장하기 때문에 find()
로 두 번 조회한 후에 ==
로 비교해도 동일하다고 판단됩니다.
같은 트랜잭션 안에서는 같은 엔티티를 반환하기 때문에 약간의 조회 성능이 향상됩니다.
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
첫 번째의 find()
에서는 SQL 쿼리가 DB 에 날라가게 되고, 조회 결과를 JPA 가 들고있게 됩니다. 또 find()
를 호출하면 JPA 가 SQL 을 날리지 않고 JPA 가 메모리 상에서 들고있는 결과를 반환해줍니다.
그래서 ==
로 비교했을 때 두 객체가 동일하다고 판단됩니다. 단, 같은 트랜잭션 안에서만 성립합니다.
트랜잭션을 커밋할 때까지 INSERT SQL 을 모음
JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//-- 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋
트랜잭션이 시작되고 데이터를 버퍼에 계속 보관합니다. 트랜잭션이 커밋되면 버퍼에 저장된 것들에 대한 Insert 쿼리를 한 번에 날라가게 되고, 그 후에 트랜잭션이 커밋됩니다.
지연 로딩 : 객체가 실제 사용될 때 로딩
즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
만약 Member 와 Team 을 항상 함께 사용하는 경우라면 SQL 한 번에 Member 와 Team 을 조회하는 것이 DB 도 여러 번 갈 필요도 없고, 네트워크 통신도 줄이기 때문에 좋습니다.
하지만 Member 를 사용할 때 Team 을 함께 사용하지 않는 경우라면 Member 만 조회하는 것이 성능 상 조금 더 좋기 때문에 Member 만 가지고 오는 것이 좋습니다.
그래서 JPA 는 상황에 따라 사용할 수 있도록 지연로딩과 즉시로딩을 지원합니다. 지연로딩에서 member 를 사용할 때 Member 만 조회하고 Team 은 조회하지 않습니다. 그 후 Team 을 사용할 때 Team 에 대한 프록시 객체의 값을 채워주게 됩니다.