김영한님이 지음, [자바 ORM 표준 JPA 프로그래밍] 책을 읽고 정리한 필기입니다.📢
패러다임의 불일치
객체 : 속성(필드)과 기능(메소드)을 가지고, 다른 객체를 참조, 자식의 상속의 상태가 존재
관계형 데이터베이스 : 데이터 중심으로 구조화, 집합적인 사고 요구, 추상화 상속 다형성 개념 X
객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법이 다름, 따라서 객체 구조를 테이블 구조에 저장하는데는 한계가 존재한다.
상속 문제
객체 상속 모델
테이블 모델
데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//객체 모델 코드
abstract class Item{
Long id;
String name;
int price;
}
class Album extends Item{
String artist;
}
class Movie extends Item{
String director;
String actor;
}
class Book extends Item{
String author;
String isbn;
}
1
2
3
# ALBUM 객체 저장 시
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
문제점
- 자식 객체를 저장하려면 두 SQL을 만들어야 하기 때문에 작성해야할 코드량이 많음
- 자식 타입에 따라서
DTYPE
도 저장해야 함 - 조회 시 부모 테이블과 자식 테이블을 조인 후 가져와야 함
JPA를 사용할 경우
JPA는 위와 같은 문제를 개발자 대신 해결해 준다. 자바 컬렉션에 객체를 저장하듯이 JPA에게 객체를 저장하면 된다.
1
2
// ALBUM 객체 저장 시
jpa.persist(album);
JPA는 다음 SQL을 싱행해서 객체를 ITEM
, ALBUM
두 테이블에 나누어 저장한다.
1
2
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
조회 시
1
2
String albumId = "id100";
Album album = jpa.find(Album.class, albumId);
JPA는 두 테이블을 조인해서 필요한 데이터를 조회하고 그 결과를 반환한다.
1
2
3
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID
연관관계 문제
객체와 관계형 데이터 베이스 사이의 패러다임 불일치 중 가장 극복하기 어려운문제가 연관관계 문제이다.
객체 연관관계
객체 : 객체는 참조를 사용해서 다른 객체와 연관관계를 가지고, 참조에 접근해서 연관된 객체를 조회한다.
- 객체는 참조가 있는 방향으로만 조회할 수 있다.(
member.getTeam()
가능,team.getMember()
불가능)
테이블 연관관계
관계형 데이터베이스 : 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고, 조인을 사용해서 연관된 테이블을 조회한다.
- 테이블은 외래 키 하나로 양방향 조회가 가능하다.(
MEMBER JOIN TEAM
,TEAM JOIN MEMBER
둘다 가능)
객체를 테이블에 맞추어 모델링할 경우
1
2
3
4
5
6
7
8
9
10
class Member{
String id; //MEMBER_ID 컬럼 사용
Long teamId; //TEAM_ID FK 컬럼 사용
String username; //USERNAME 컬럼 사용
}
class Team{
Long id; //TEAM_ID PK 사용
String name; //NAME 컬럼 사용
}
테이블에 저장하거나 조회할 때는 편리하지만 참조를 통해 연관된 객체를 찾을 수 없다. 이런 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 결국 객체지향의 특징을 잃어버리게 된다.
객체지향 모델링할 경우
객체는 참조를 통해 관계를 맺는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Member{
String id; //MEMBER_ID 컬럼 사용
Team team; //참조로 연관관계를 맺는다.
String username; //USERNAME 컬럼 사용
Team getTeam(){
return team;
}
}
class Team{
Long id; //TEAM_ID PK 사용
String name; //NAME 컬럼 사용
}
이 경우 객체를 테이블에 저장하거나 조회하기가 쉽지 않음, 객체 모델은 외래 키가 필요 없고 참조만을 필요로하고 반면에 테이블은 참조가 필요 없고 외래 키만 필요로하기 때문에 결국 중간에서 개발자가 이에 대한 변환 역할을 해줘야 한다.
저장
team
필드를 TEAM_ID
외래 키 값으로 변환하여 INSERT SQL을 만든다.
1
2
3
member.getId(); //MEMBER_ID PK에 저장
member.getTeam().getId(); //TEAM_ID FK에 저장
member.getUsername(); //USERNAME 컬럼에 저장
조회
TEAM_ID
외래 키 값을 Member
객체의 team
참조로 변환해서 객체에 보관한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Member find(String memberId){
//SQL실행
...
Member member = new Member();
//데이터베이스에서 조회한 회원 관련 정보를 모두 입력
...
Team team = new Team();
//데이터베이스에서 조회한 팀 관련 정보를 모두 입력
...
//*** 회원과 팀 관계 설정 ***
member.setTeam(team);
return member;
}
JPA와 연관관계
JPA는 연관관계와 관련된 패러다임의 불일 문제를 해결해준다.
1
2
member.setTeam(team); //회원과 팀 연관관계 설정
jpa.persist(member); //회원과 연관관계 함께 저장
JPA는 team
의 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달한다.
조회의 경우 외래키를 참조로 변환하는 일도 JPA가 처리해 준다.
1
2
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
객체 그래프 탐색
객체 연관관계의 객체 그래프
1
member.getOrder().getOrderItem()... //자유로운 객체 그래프 탐색이 가능해야 한다.
위와 같은 상황에서 SQL을 실행해서 회원과 팀에 대한 데이터만 조회했다면, member.getTeam();
은 성공하지만 member.getOrder()
은 데이터가 없으므로 탐색할 수가 없다.
객체는 마음껏 객체 그래프를 탐색할 수 있어야 하지만 SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.
1
2
3
4
5
6
7
8
class MemberService{
...
public void process(){
Member member = memberDAO.find(memberId);
member.getTeam(); //member->team 객체 그래프 탐색이 가능한가?
member.getOrder().getDelivery(); //??? (예측 불가)
}
}
memberDAO
내부의 SQL 문을 확인해야 Delivery
객체를 사용할 수 있는지 확인 가능하다는 불편함이 존재하고, 이는 곧 Entity가 SQL 에 종속적이라는 문제와 직결된다.
JPA에서의 객체 그래프 탐색
JPA 를 사용하면 객체 그래프를 마음껏 탐색할 수 있다. JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행하기 때문에 객체를 신뢰하고 마음껏 조회할 수 있다.
=> 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연로딩이라 한다.
1
2
3
4
5
6
7
//지연 로딩 사용
//처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderData(); //Order를 사용하는 시점에 SELECT ORDER SQL
비교 문제
관계형 데이터베이스의 비교 방법 : 기본 키의 값으로 각 ROW를 구분
객체 : 동일성 비교와 동등성 두 가지 비교 방법 존재
- 동일성 비교 : == 키워드를 사용, 인스턴스의 주소 값을 비교
- 동등성 비교 :
equal()
메소드를 사용, 객체 내부의 값을 비교
1
2
3
4
5
6
7
8
class MemberDAO{
public Member getMember(String memberId){
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
...
//JDBC API, SQL 실행
return new Member(...);
}
}
1
2
3
4
5
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; //다르다.
member1
과 member2
는 같은 데이터베이스 ROW에서 조회했지만 객체 측면에서 볼 때 둘은 다른 인스턴스하는 패러다임 불일치 문제가 발행한다.
JPA에서의 비교
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.
1
2
3
4
5
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; //같다.(같은 트랜잭션일 경우)
정리
- 객체 모델과 관계형 데이터 베이스 모델은 지향하는 패러다임이 서로 다름
- 파러다임의 차이를 극복하려면 개발자가 너무 많은 시간과 코드가 소비 됨
- JPA에서는 이런 패러다임의 불일치를 해결하여 개발자에게 편의를 제공
댓글남기기