패러다임의 불일치
관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다.
그리고 객체지향에서 이야기하는 추상화, 다형성, 상속 같은 개념이 없다.
객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다.
-> 객체와 관계형 데이터베이스의 패러다임 불일치 문제라 한다.
예시)
객체 상속 모델
테이블 모델
∴ DTYPE 칼럼을 사용해서 어떤 자식 테이블과 관계가 있는지 정의
※ 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.
객체 모델 코드
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;
}
Album 객체를 저장하려면 이 객체를 분해해서 두 SQL문을 만들어야 한다.
insert into item ...
insert into album ...
등등 다른 객체도 마찬가지이다.
즉, JDBC API를 사용해서 이 코드를 완성하려면 부모 객체에서 부모 데이터만 꺼내 insert SQL문을 작성하고 자식 객체에서 자식 데이터만 꺼내서 insert SQL문을 작성해야 한다.
이런 과정들이 모두 패러다임의 불일치를 해결하려고 소모하는 비용이다.
JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다.
이전 내용에서 설명한 persist() 메소드를 사용해서 객체를 저장하면 된다.
jpa.persist(album);
jpa는 다음 SQL을 실행해서 객체를 item, album 두 테이블에 나누어서 저장한다.
insert into item...
insert into album...
album 객체를 조회할 때는 앞서 설명한 find() 메소드를 사용한다.
String albumId = "albumId";
Album album = jpa.find(Album.class, albumId);
JPA는 item과 album 두 테이블을 조인해서 필요한 데이터를 조회하고 그 결과를 반환한다.
select I.*, A.*
from Item I
join ALBUM A on I.item_id = A.item_id
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.
반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.
참조를 사용하는 객체와 외래 키를 사용하는 관계형 데이터베이스 사이의 패러다임 불일치는 극복하기 굉장히 어렵다.
Member 객체는 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다.
따라서 이 참조 필드에 접근하면 Member와 연관된 Team을 조회할 수 있다.
ex) member.getTeam();
class Member {
Team team;
public Team getTeam() {
return team;
}
}
class Team {
}
Member 테이블은 외래 키 칼럼을 사용해서 TEAM 테이블과 관계를 맺는다.
이 외래 키를 사용해서 Member 테이블과 TEAM 테이블을 조회하면 MEMBER 테이블과 연관된 TEAM 테이블을 조회할 수 있다.
또 다른 어려운 문제로, 객체는 참조가 있는 방향으로만 조회할 수 있다.
반면에 테이블은 외래 키 하나로 방향에 관계 없이 조회가 가능 하다.
패러다임 불일치를 해결하기 위해 객체를 테이블에 맞추어 모델링 해보자.
class Member {
String id;
Long teamId;
String userName;
}
class Team {
Long id;
String name;
}
이렇게 객체를 테이블에 맞추어 모델링하면 객체를 테이블에 저장하거나 조회할 때는 편리하다.
하지만, 객체는 연관된 객체의 참조를 보관해야 다음처럼 참조를 통해 연관된 객체를 찾을 수 있다.
Team team = member.getTeam();
이렇게 하면 Member 객체와 연관된 Team 객체를 참조를 통해서 조회할 수 없다.
결국 객체지향의 특징을 잃어버리게 된다.
그렇다면 이번에는 객체지향 모델링으로 해보자
class Member {
String id;
Team team;
String userName;
public Team getTeam() {
return team;
}
}
class Team {
Long id;
String name;
}
이제 회원객체에서 연관된 팀을 조회할 수 있다.
하지만, 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다.
- 저장
객체를 데이터베이스에 저장하려면 team 필드를 TEAM_ID 외래 키 값으로 변환해야 한다.
member.getId(); -> Member PK 저장
member.getTeam().getId(); -> TEAM FK, PK 저장
member.getUserName(); -> userName 저장 - 조회
TEAM_ID 외래 키 값을 Member 객체의 team 참조로 변환해서 객체에 보관해야 한다.
JPA는 연관관계에 관련된 패러다임의 불일치 문제를 해결한다.
member.setTeam(team);
jpa.persist(member);
회원과 팀의 관계를 설정하고 회원 객체를 persist() 메소드를 통해 저장하면 된다.
JPA는 team의 참조를 외래 키로 변환해서 적절한 SQL을 데이터베이스에 전달한다.
객체 그래프 탐색
객체 연관관계가 그림과 같이 설계되어 있다고 가정하자.
객체는 마음껏 객체 그래프를 탐색할 수 있어야 한다.
그런데 예를 들어 다음과 같은 SQL을 실행했다고 하자.
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
MemberDAO에서 member 객체를 조회할 때 member.getTeam() 은 성공하지만, member.getOrder() 는 null 값이 나올 것 이다.
SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.
회원 조회 비즈니스 로직 코드
class MemberService {
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); // member -> team 그래프 탐색이 가능한지 확실하지 않음
member.getOrder().getDelivery(); // member -> order -> delivery 그래프 탐색이 가능한지 확실하지 않음
위의 코드를 보고 이 객체와 연관된 Team, Order, Delivery 방향으로 객체 그래프를 탐색할 수 있을지 없을지는 알 수 없다.
결국 데이터 접근 계층인 DAO를 직접 확인하여 SQL을 직접 확인해야 한다.
JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다.
앞에서 말했다싶이 JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다.
이 기능은 실제 객체를 사용하는 지점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라고 한다.
//처음 조회한 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDate(); // Order를 사용하는 시점에 SELECT ORDER SQL
JPA는 연관된 객체를 즉시 함께 조회(즉시 로딩) 할 지 아니면 실제 사용되는 시점에 지연해서 조회(지연 로딩)할지 간단한 설정으로 정의할 수 있다. (나중에 따로 정리할 예정)
비교
데이터베이스는 기본 키의 값으로 각 로우를 구분한다.
객체는 동일성비교( == )와 동등성비교( equals )라는 두 가지 비교 방법이 있다.
MemberDAO 코드
class MemberDAO {
public Member getMember(String memberId) {
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
//JDBC API 실행
return new Member(...);
}
}
조회된 회원 비교
String memberId = "id";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
//다르기 때문에 false 반환
if(member1 == member2)
return true;
return false;
member1과 member2는 같은 데이터베이스 로우에서 조회했지만, 객체 측면에서 볼 때 둘은 다른 인스턴스이다.
이런 패러다임의 불일치를 위해 데이터베이스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다.
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.
예시 테스트 코드
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void 동일성_동등성_체크() {
//given
UserEntity userEntity = userRepository.save(new EasyRandom().nextObject(UserEntity.class));
//when
UserEntity savedUserEntity1 = userRepository.findByIdentity(userEntity.getIdentity()).orElse(null);
UserEntity savedUserEntity2 = userRepository.findByIdentity(userEntity.getIdentity()).orElse(null);
if(savedUserEntity1 == savedUserEntity2) {
assertTrue(true);
}
//then
assertEquals(savedUserEntity1, savedUserEntity2);
}
}
위의 코드와 디버그를 통해 두 객체의 동일성이 보장되는 것을 확인할 수 있다.
정리
객체 모델과 관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르다.
JPA는 패러다임의 불일치 문제를 해결해주고 정교한 객체 모델링을 유지하게 도와준다.
'Spring JPA' 카테고리의 다른 글
영속성 관리 - 6 (0) | 2021.06.29 |
---|---|
영속성 관리 - 5 (0) | 2021.06.26 |
JPA 시작 - 4 (0) | 2021.06.25 |
JPA 소개 - 3 (0) | 2021.06.23 |
JPA 소개 - 1 (0) | 2021.06.22 |
댓글