JPA 소개
SQL을 직접 다룰 때 발생하는 문제점
DB에 데이터를 관리하려면 SQL을 사용해야 하고 JAVA로 작성한 application은 JDBC API를 사용해서 SQL을 DB에 전달한다.
반복, 반복, 그리고 반복
SQL을 직접 다룰 때의 문제점을 알아보기 위해 JAVA와 RDBMS를 사용해서 회원 관리 기능을 개발해보자 회원 테이블은 이미 만들어져 있다고 가정한다. 먼저 JAVA에서 사용할 Member 객체를 만든다
public class Member {
private String memberId;
private String name;
}
다음으로 회원 객체를 DB에 관리할 목적으로 회원용 DAO(DB Access Object)를 만들자.
public class MemberDAO{
public Member find(String memberId){...}
}
이제 MemberDAO의 find() 메소드를 완성해서 회원을 조회하는 기능을 개발해보자
1. 회원 조회용 SQL을 작성한다.
SELECT MEMBER_ID, NAME FROM MEMBER M WHERE MEMBER_ID = ?
2. JDBC API를 사용해서 SQL을 실행한다.
ResultSet rs = stmt.executeQuery(sql);
3.조회 결과를 Member 객체로 매핑한다.
String memberId = rs.getString("MEMBER_ID");
String name = rs.getString("NAME");
String member = new Memeber();
member.setMemeberId(memberId);
member.setName(name);
회원 조회 기능을 완성했으니 회원 등록 기능을 만들자
public class MemberDAO{
public Member find(String memberId){...}
public void save(Member member){...}
}
1. 회원 등록용 SQL을 작성한다.
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES(?,?)";
2. 회원 객체의 값을 꺼내서 등록 SQL에 전달한다.
pstmt.setString(1, member.getMemberId());
pstmt.setString(2, member.getName());
3. JDBC API를 사용해서 SQL을 실행한다.
pstmt.executeUpdate(sql);
DB는 객체 구조와는 다른 데이터 중심의 구조를 가지므로 객체를 DB에 직접 저장하거나 조회할 수는 없다. 따라서 개발자가 객체지향 application과 DB 중간에서 SQL과 JDBC API를 사용해서 변환 작업을 직접 해주어야 한다.
문제는 객체를 DB에 CRUD하려면 너무 많은 SQL과 JDBC API를 코드로 작성해야 한다는 점이다.
그리고 테이블마다 이런 비슷한 일을 반복해야 하는데, 개발하려는 application에서 사용하는 DB table이 수백개라면 무수히 많은 SQL을 작성해야하고 이런 비슷한 일을 몇백번 반복해야 한다는 것이다.
DAO를 개발하는 일은 이렇듯 지루함과 반복의 연속이다.
SQL에 의존적인 개발
MemberDAO를 완성한 상태에서 갑자기 회원의 연락처도 함께 저장해달라는 요구사항이 추가되었다고 시나리오를 가정해보자.
일단 Member객체에 tel이라는 필드를 추가해야한다
public class Member {
private String memberId;
private String name;
private String tel;
}
연락처를 저장할 수 있도록 INSERT SQL도 수정해야 한다..
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES(?,?,?)";
그 다음 회원 객체의 연락처 값을 꺼내서 등록 SQL에 전달해야 한다..
pstmt.setString(3, member.getTel());
이런 방식의 가장 큰 문제점은 DAO를 사용해서 SQL을 숨겨도 어쩔 수 없이 DAO를 열어서 어떤 SQL이 실행되는지 확인하고 수정해야 한다는 점이다.
Member처럼 비즈니스 요구사항을 모델링한 객체를 Entity라고 하는데, 지금처럼 SQL에 모든 것을 의존하는 상황에서는 개발자들이 Entity를 신뢰하고 사용할 수 가 없다.
대신에 DAO를 열어서 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 일일이 확인해야 한다. 이것은 진정한 의미의 계층 분할이 아니다.
물리적으로 SQL과 JDBC API를 DAO에 숨기는데 성공했을지는 몰라도 논리적으로는 Entity와 아주 강한 의존관계를 가지고 있다. (Entity에 필드가 하나 추가되면 여러 SQL문을 바꾸고 DAO 로직을 수정해야 하듯이..)
application에서 SQL을 직접 다룰 때 발생하는 문제점을 요약하면 다음과 같다
- 진정한 의미의 계층 분할이 어렵다
- Entity를 신뢰할 수 없다.
- SQL에 의존적인 개발을 피하기 어렵다.
JPA와 문제 해결
JPA는 이런 문제들을 어떻게 해결할까? JPA가 문제를 어떻게 해결하는지 간단히 알아보자.
JPA를 사용하면 객체를 DB에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 사용하면 된다. 그러면 JPA가 개발자 대신에 적절한 SQL을 생성해서 DB에 전달한다.
jpa.persist(member); //저장
persist() 메소드는 객체를 DB에 저장한다. 이 메소드를 호출하면 JPA가 객체와 매핑정보를 보고 적절한 INSERT SQL을 생성해서 DB에 전달한다. 매핑정보는 어떤 객체를 어떤 table에 관리할지 정의한 정보이다.
JPA는 SQL을 개발자 대신 작성해서 실행해주는 것 이상의 기능들을 제공한다.
패러다임의 불일치
OOP(Object-Oriented Programming)은 추상화, 캡슐화, 상속, 다형성, 정보은닉등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다. 그래서 현대의 복잡한 application은 대부분 객체지향 언어로 개발한다.
비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다. 문제는 객체 인스턴스를 생성한 후에 이 객체를 메모리가 아닌 어딘가에 영구 보관해야 한다는 점이다.
객체가 단순하면 객체의 모든 속성 값을 꺼내서 파일이나 DB에 저장하면 되지만, 부모 객체를 상속받았거나, 다른 객체를 참조하고 있다면 객체의 상태를 저장하기는 쉽지 않다.
가장 좋은 방법은 DB에 객체를 저장하는 것인데, RDB는 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.
Object와 RDB는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스 패러다임 불일치 문제라고 한다. 문제는 이런 object와 RDB사이의 패러다임 불일치 문제를 개발자가 중간에서 해결해줘야 하는데 너무 많은 시간과 코드를 소비하는데 있다.(방금 예제로 보았듯이)
상속
다음 그림처럼 객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다.
그나마 DB 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다. 다음 그림에서 ITEM 테이블의 DTYPE 컬럼을 사용해서 어떤 자식 테이블과 관계가 있는지 정의했다. 예를 들어 DTYPE의 값이 MOVIE이면 영화 테이블과 관계가 있다.
Album 객체를 저장하려면 이 객체를 분해해서 다음 두 SQL을 만들어야한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
JDBC API를 사용해서 이 코드를 완성하려면 부모 객체에서 부모 데이터만 꺼내서 ITEM용 INSERT SQL을 작성하고 자식 객체에서 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 하는데, 위의 예제에서 보았듯이 작성해야 할 코드량이 만만치 않다. 그리고 자식 타입에 따라서 DTYPE도 저장해야 할 것이다..
이런 과정이 모두 패러다임의 불일치를 해결하려고 소모하는 비용이다.
JPA 상속
JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 마치 자바 컬렉션에 객체를 저장하듯이 JPA에게 객체를 저장하면 된다.
JPA를 사용해서 Item을 상속한 Album 객체를 저장해보자. 앞서 설명한 persist() 메소드를 사용해서 객체를 저장하면 된다.
jpa.persist(album);
JPA는 이 한줄의 코드로 다음의 SQL을 실행해서 객체를 ITEM, ALBUM 두 테이블에 나누어 저장한다.. 놀랍지 않나..?
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
연관관계
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다. 반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.
참조를 사용하는 객체와 외래 키를 사용하는 RDB 사이의 패러다임 불일치는 객체지향 모델링을 포기하게 만들 정도로 극복하기 어렵다. 다음 예제를 통해 문제점을 파악해보자
Member 객체는 Member.team 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다. 따라서 이 참조 필드에 접근하면 Member와 연관된 Team을 조회 할 수 있다.
public class Member {
Team team;
Team getTeam() {
return team;
}
}
class Team {
...
}
member.getTeam();//member -> team 접근
Member 테이블은 MEMBER.TEAM_ID 외래 키 컬럼을 사용해서 TEAM 테이블과 관계를 맺는다. 이 외래 키를 사용해서 MEMBER 테이블과 TEAM 테이블을 조인하면 MEMBER 테이블과 연관된 TEAM 테이블을 조회 할 수 있다.
조금 어려운 문제도 있는데, 객체는 참조가 있는 방향으로만 조회할 수 있다. 방금 예에서 member.getTeam()은 가능하지만 반대 방향인 team.getMember()는 참조가 없으므로 불가능하다. 반면에 테이블은 외래 키 하나로 MEMBER JOIN TEAM도 가능하지만 TEAM JOIN MEMBER도 가능하다
객체를 테이블에 맞추어 모델링 해보기
객체와 테이블의 차이를 알아보기 위해 객체를 단순히 테이블에 맞추어 모델링해보자.
public class Member {
String id; //MEMBER_ID 컬럼사용
Long teamId; //TEAM_ID FK컬럼 사용
String username; //USERNAME 컬럼 사용
}
class Team {
Long id; //TEAM_ID PK사용
String name; //NAME 컬럼 사용
}
객체를 테이블에 맞추어 모델링하면 객체를 테이블에 저장하거나 조회할 때는 편리하다. 그런데 여기서 TEAM_ID 외래 키의 값을 그대로 보관하는 teamId 필드에는 문제가 있다. RDB는 조인이라는 기능이 있으므로 외래 키의 값을 그대로 보관해도 된다. 하지만 객체는 연관된 객체의 참조를 보관해야 다음처럼 참조를 통해 연관된 객체를 찾을 수 있다.
Team team = member.getTeam();
객체를 테이블에 맞추어 모델링하면 이런식으로는 Team객체를 찾을 수 없다. teamId는 찾을 수 있다 그래도..
테이블에서는 teamId를 사용하여 teamId에 해당하는 team의 name을 찾을 수 있지만 객체를 테이블에 맞추어 모델링하면 teamId로는 해당하는 team의 name을 찾을 수 없는 것을 볼 수 있다.
이런 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 결국 객체지향의 특징을 잃어버리게 된다.
JPA와 연관관계
JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다. 다음 코드를 보자
member.setTeam(team); //회원과 팀 연관관계 설정
jpa.persist(member); //회원과 연관관계 함께 저장
개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하면 된다. JPA는 team의 참조를 외래 키로 변환해서 적절한 SQL을 DB에 전달한다. 객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리해준다.
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
지금까지 설명한 문제들은 SQL을 직접 다루어도 열심히 코드만 작성하면 어느정도 극복할 수 있는 문제들이었다.
연관관계와 관련해서 극복하기 어려운 패러다임의 불일치 문제를 알아보자.
객체 그래프 탐색
객체에서 회원이 소속된 팀을 조회할 때는 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이것을 객체 그래프 탐색이라고 한다. 객체 연관관계가 다음과 같이 설계되어 있다고 가정해보자
다음은 객체 그래프를 탐색하는 코드이다.
member.getOrder().getOrderItem()... //자유로운 객체 그래프 탐색
테이블에서도 다음과 같이 할 수 있을까?
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
예를 들어 MemberDAO에서 member 객체를 조회할때 이런 SQL을 실행해서 팀에 대한 데이터만 조회했다면 member.getTeam() 성공하지만 member.getOrder()는 Order 테이블을 조인하지 않았으므로 탐색할 수 없다.
SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.
Member member = memberDAO.find(memberId);
member.getTeam();
member.getOrder().getDelivery();
memberDAO.find() 메소드는 team 테이블과 member 테이블만 조회하는 SQL을 사용하고 있으므로 member.getOrder()는 당연히 안될 것이다.. 개발자는 이상함을 느끼고 다시 memberDAO 클래스에 들어가 SQL문을 확인하게 되는 것이다.
그렇다고 해서 find()메소드가 실행 될때마다 member와 연관된 모든 객체를 DB에서 조회해서 member 변수에 저장하는 것은 메모리 낭비가 심할 것이다..
그래서 개발자는 DAO에 다양한 객체그래프 탐색 메소드를 만들게 되는 끔찍한 상황이 벌어진다..
memberDAO.getMember(); //Member만 조회
memberDAO.getMemberWithTeam(); //Member와 Team 조회
memberDAO.getMemberWithOrderWithDelivery //Member와 Order와 Delivery 조회
...
JPA와 객체 그래프 탐색
JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다
JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행하는데, 실제 객체를 사용하는 시점까지 DB조회를 미룬다고해서 지연 로딩이라고 한다.
다음은 지연 로딩을 사용하는 코드다. 여기서 마지막 줄의 order.getOrderDate()와 같이 실제 Order 객체를 사용하는 시점에 JPA는 DB에서 ORDER 테이블을 조회한다.
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDate(); //Order를 사용하는 시점에 SELECT ORDER SQL
비교
DB는 PK로 각 record를 구분하는 반면에, 객체는 동일성(identity)와 동등성(equality) 비교라는 두 가지 비교 방법이 있다.
- 동일성 비교는 == 비교다. 객체 인스턴스의 주소 값을 비교한다.
- 동등성 비교는 equals() 메소드를 사용해서 객체 내부의 값을 비교한다.
따라서 테이블의 record를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있다.
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2;//False
기본 키 값이 같은 회원 객체를 두 번 조회했지만 동일성 비교의 값은 false이다. member1과 member2는 DB의 같은 레코드에서 조회되었지만 객체 측면에서 볼 때 둘은 다른 인스턴스이기 때문이다 (MemberDAO.getMember()를 호출할 때 마다 new Member를 return 한다.)
이런 패러다임의 불일치 문제를 해결하기 위해 DB의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여기에 여러 트랜잭션이 동시에 실행되는 상황까지 고려하면 문제는 더 어려워진다.
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2;//True
출처
김영한, "자바 ORM 표준 JPA 프로그래밍(2015)", 에이콘출판사