기존 시스템들에서 어떤 방식을 통해 데이터를 접근했는지, 어떤 문제점들이 있었는지 살펴보고, JPA가 이를 어떻게 해결했는지 살펴봅시다.
JPA이전 관계형 데이터베이스의 데이터를 가져오기 위해서는 JDBC API를 통해 SQL 쿼리를 직접 날려주는 방식을 사용했습니다. 이는 직관적인것을 떠나 매우 직접적인 방법이고, 여러가지 문제점과 불편함이 존재합니다.
public class Member{
private String memberId;
private String name;
private String tel; // !! 비즈니스 로직 변경으로 전화번호 필드 추가
}
데이터에서 하나의 필드(컬럼) 이 추가될 때, SQL을 직접 매핑해 개발하는 방식에서는 삽입,조회,변경,삭제 시 많은 코드 변경이 필요합니다.
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) values(?,?,?);";
'''
pstmt.setString(3,member.getTel());
String sql = "SELECT MEMBER_ID, NAME, TEL from MEMBER WHERE MEMBER_ID = ?";
String tel = rs.getString("TEL");
member.setTel("XXX");
SELECT M.MEMBER_ID, M.NAME, M.TEL, T.TEAM_ID, T.TEAM_NAME
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
많은 비즈니스 로직에서 데이터 간 연관관계가 존재하는데, 위같이 SQL에 의존하는 개발을 하는 경우, 진정한 계층 분할이 어렵다는 문제가 있습니다.
DAO를 통해 데이터에 접근하는 로직만 분리했을 뿐이지, 필드를 추가할때만 해도 SQL과 CRUD와 관련된 코드를 추가해야하기 때문입니다. 이때문에 엔티티를 신뢰할 수 없게됩니다.
JPA는 위에서 소개한 문제들을 효율적으로 해결합니다.
String memberId = "MyeonggGyu Lee";
Member member = jpa.find(Member.class, memberId);
member.setName("Changed_Name");
Team team = member.getTeam();
String teamName= team.getName();
관계형 데이터베이스와 객체지향 프로그래밍 언어는 그 목적이 다른만큼 데이터의 기본 단위인 레코드와 객체의 기능과 표현 방법이 다릅니다. 따라서, 개발자가 이를 중간에서 해결해주어야 하지만, 이를 일일히 해결하는것은 너무 많은 리소스를 투입해야하는 작업입니다.
이같은 패러다임 불일치로 생기는 문제 또한 여러가지가 있으며, JPA가 어떤 방법으로 이를 해결하는지 살펴보겠습니다.
객체의 경우 상속이라는 기능을 통해 다형성을 제공하지만, 대부분의 데이터베이스는 상속 기능이 없습니다.
아래 그림과 같이 서브타입과 슈퍼타입을 사용하면 그나마 해결이 가능하지만, 이는 많은 불편함을 초래합니다.
위의 경우, 만약 ALBUM 객체를 저장하기 위해서는 두 개의 INSERT문이 필요합니다.
INSERT INTO ITEM(ITEM_ID, NAME, PRICE, DTYPE) VALUES(?,?,?);
INSERT INTO ALBUM(ITEM_ID,ARTIST) VALUES(?,?);
만약 데이터베이스를 사용하지 않고 자바 컬렉션(Ex, ArrayList)에 저장한다면, list.add(album)처럼 부모 객체에 대해 고려하지 않고도 데이터를 저장할 수 있습니다.
이처럼 상속으로 인한 자바 객체와 관계형 데이터베이스 사이의 패러다임 불일치문제는 분명히 존재하는것을 알아보았습니다 .
JPA에 상속하는 객체를 저장하면 상속에 의한 패러다임 불일치문제를 개발자 대신 해결해줍니다.
JPA의 EntityManager에 객체를 저장하면, ITEM,ALBUM 테이블에 데이터를 각각 나누어 저장합니다.
jpa.persist(album);
INSERT INTO ITEM(ITEM_ID, NAME, PRICE, DTYPE) VALUES(?,?,?);
INSERT INTO ALBUM(ITEM_ID,ARTIST) VALUES(?,?);
이후 album 객체를 찾고자 할 때, ITEM, ALBUM 테이블을 조인해서 데이터를 조회하고, 그 결과를 반환합니다 .
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON A.ITEM_ID = I.ITEM_ID;
객체는 참조를 통해서 다른 객체와 연관관계를 가지고, 참조에 접근하면서 연관관계의 객체를 조회합니다.
반면 관계형 데이터베이스의 테이블은 외래 키를 통해 다른 테이블과 연관관계를 가지고, JOIN을 통해 연관된 테이블을 조회합니다.
class Member{
Team team;
...
Team getTeam(){
return team;
}
}
member.getTeam();
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
위에서 다룬 상속에서의 패러다임 불일치문제는 서브타입/슈퍼타입을 통해 객체지향적으로 데이터를 다룰 수 있었지만, 연관관계로 인한 패러다임 불일치 문제는 객체지향적으로 풀어내기에 꽤 복잡합니다.
객체는 다른 연관관계를 조회하고자 할 때, 외래키 대신 참조만 있으면 되고, RDB의 테이블에서는 참조 대신 외래키만 있으면 조회가 가능합니다.
또한, 객체는 member.getTeam()을 통한 member → team 으로의 단방향 조회만 가능하지만, team에서 member에 대한 참조가 없으므로, team.getMember()을 통한 team → member 조회가 불가능합니다.
(RDB에서는 외래키를 통해 양방향으로 연관관계를 Join할 수 있습니다)
(직접구현) 개발자는 객체에서의 연관관계만 지정해주면, team의 참조를 외래키로 변환해 INSERT SQL문을 데이터베이스에 전달합니다.
(JPA 사용)JPA에서는 이런 연관관계로 인한 패러다임 문제 역시 해결해줍니다.
member.setTeam(team);
jpa.persist(member);
jpa.flush();
(직접구현) 조회시에는 TEAM_ID 외래 키 값을 Member 객체의 team 참조로 변활해 객체로 저장합니다.
public Member find(String memberId){
Member member = new Member();
//SQL 실행 후, 멤버 데이터 입력
Team team = new Team();
// team 외래키를 통해 팀 정보 조회 후, 팀 관련 정보 입력
member.setTeam(team);
//팀 연관관계 생성
return meber;
}
(JPA 사용) 마찬가지로 jpa entitymanager를 통해 find하면, 위의 과정을 간단하게 수행할 수 있습니다.
Member member = jpa.find(Member.class, memberId);
Team team. = member.getTeam();
실제 데이터의 연관관계가 복잡하게 얽혀있는 구조를 생각해보자. 예를 들어 아래와 같다고 해보자.
만약 DAO를 통해 SQL을 직접 조작한다고 해보자. 아래와 같은 자유로운 객체그래프 탐색이 가능할끼?
member.getOrder.getOrderItem().get….
만약 member을 조회하는 기능에서 order까지 참조로 저장하였다면, getOrder까지는 성공할 것이다. 하지만 그 이후부터는 데이터가 존재하지 않으므로 탐색할 수 없다.
비즈니스 로직에 따라 객체 그래프를 자유롭게 탐색할 수 있어야하지만, DAO내부에서 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지가 정해집니다. 결국 객체 그래프를 탐색할때마다 새로운 메서드와 SQL을 만들어야하는 복잡하고 불편한 상황이 연출됩니다.
memberDAO.getMember();
memberDAO.getMemberWithOrderWithDelivery();
...
앞서 해결했던 문제들과 달리, 객체 그래프 탐색 문제는 소스코드 몇줄을 추가한다 해서 해결될 수 없습니다. member와 관련된 모든 객체들을 메모리에 올려놓으면, 엄청난 메모리 낭비가 발생하기 때문이죠.
JPA는 실제 객체 사용 시점까지 데이터베이스 조회를 미루는 지연로딩을 통해 해당 문제를 해결합니다.
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.gerOrderDate(); // Order객체를 사용하는 시점에 SELECE Order 쿼리를 날림.
데이터베이스에서는 기본 키(PK)를 통해 데이터를 비교합니다. 반면 객체는 동일성과 동등성을 통해 데이터를 비교합니다.
동일성 비교는 “==”을 통해 비교합니다. 객체의 주소값을 비교하는 비교방법입니다.
동등성 비교는 “equals()”메서드를 통해 비교합니다. 객체 내부 값을 비교합니다.
SQL을 통해 Java에서 데이터베이스 레코드를 여러 번 불러왔을 때, 비교과정에 대해 살펴보겠습니다.
String memberId = "12";
Member m1 = memberDAO.getMember(memberId);
Member m2 = memberDAO.getMember(memberId);
if(m1 == m2) System.out.println("Equal");
else System.out.println("Not Equal");
// ouput : Not Equal
m1과 m2는 같은 member id를 통해 조회했으므로 그 내용은 같겠지만, 동일한 객체는 아닙니다. equals메서드를 통해 내용만 비교하면 되지 않나 라는 생각을 할 수 있지만, 만약 두 객체가 다른 스레드에서 각각 생성되고, 변경된다고 가정하면, 예상하지 않은 결과를 가져올 것입니다.
JPA에서는 같은 트랜잭션일 때, 같은 객체가 조회되는 것을 보장한다. 이는 엔티티메니저의 식별자를 통해 가능하다.
Java Persistence API의 약자로, 자바 진영의 ORM 기술.
Object Relational Mapping으로, 객체와 관계형 데이터베이스를 매핑하는 기술로, 앞서 설명했던 패러다임 불일치 문제를 해결해준다.
복습질문 정답
알아야하는 부분들 정답