JPA는 객체와 관계형 데이터베이스 간의 차이를 중간에서 해결해주는 ORM(Object-Relational Mapping) 프레임워크이며, 자바 진영의 ORM 기술 표준이다.
JPA의 장점
- 지루하고 반복적인 CRUD SQL을 알아서 처리해줄 뿐만 아니라 객체 모델링과 관계형 데이터베이스 사이의 차이점도 해결해준다.
- 개발자는 SQL을 직접 작성하는 것이 아니라 어떤 SQL이 실행될지 생각만 하면된다.
- 성능에 대한 걱정은 네이티브 SQL 기능을 사용해 직접 SQL을 작성할 수 있다.
- SQL이 아닌 객체 중심으로 개발할 수 있으므로 생산성과 유지보수가 확연히 좋아지며 테스트를 작성하기도 편리해진다.
- 데이터베이스가 변경되어도 코드를 거의 수정하지 않고 데이터베이스를 손쉽게 변경할 수 있다.
SQL을 직접 다룰 때 발생하는 문제
SQL을 직접 다룰 떄의 문제점을 알아보자
자바와 관계형 데이터베이스를 사용해서 회원 관리 기능을 개발해보자
회원 테이블은 이미 만들어 져있따고 가정하고 회원을 CRUD하는 기능을 개발해보자.
- 회원 객체
public class Member{
private String memberId;
private String name;
}
- 회원 객체를 데이터베이스에서 관리할 목적의 회원용 DAO
public class MemberDAO {
public Member find(String memberId){...}
}
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");
Member member = new Member();
member.setMemberId(memberId);
member.setName(name);
회원 조회 기능을 완성했다.
다음으로 회원을 수정하고 삭제하는 기능을 추가하려면 SQL을 작성하고 JDBC API를 사용하는 일을 반복해야 한다.
만약 회원 객체를 DB가 아닌 자바 컬렉션에 보관한다면?
list.add(member);
다음 한줄로 객체를 저장할 수 있다.
데이터베이스는 객체 구조와 다른 데이터중심의 구조를 가진다.
객체를 데이터베이스에 직접 저장하거나 조회할 수 없다.
객체지향 애플리케이션과 DB 중간에서 SQL과 JDBC API를 사용해서 변환 작업을 직접 해주어야 한다.
CRUD하려면 많은 SQL과 JDBCAPI를 코드로 작성해야 한다.
테이블이 100개라면 끔찍하다.
만약 필드를 추가한다고 하면 수정해야 할 코드들도 많을 것이다.
- 연관 객체추가
만약 회원은 한 팀에 필스러 소속되어야 한다고 가정 해보자.
class Member {
private String memberId;
private String name;
private Team team; // 팀
}
class Team {
private String teamName;
...
}
기존의 find는 작동은 하지만 소속 팀의 값은 null을 출력할 것이다.
소속된 팀까지 함께 조회하려면 메소드를 추가 해야하거나 수정을 해야한다.
SELECT M.MEMBER_ID, M.NAME, T.TEAM_ID, T.TEAM_NAME
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID;
findWithTeam() 메소드에 위 SQL 쿼리를 적용시켜서 메소드를 추가 했다고 가정한다.
결국 Member 객체가 연관된 Team 객체를 사용할 수 있을지 없을지는 사용하는 SQL에 달려있다.
또한 어떤 쿼리가 실행되는지 데이터 접근 계층을 사용해서 DAO를 열어서 확인해야한다.
Member나 Team처럼 비지니스 요구사항을 모델링한 객체를 엔티티라 하는데, SQL에 모든 것을 의존하는 상황에서는 개발자들이 엔티티를 신뢰하고 사용할 수 없다.
DAO를 열어서 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 일일이 확인해야 한다.
그러므로 진정한 의미의 계층 분할이 아니다.
물리적으로는 SQL과 JDBC API를 데이터 접근 계층에 숨기는 데 성공했을지는 몰라도 논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다. 이런 강한 의존관계 때문에 CRUD 메소드를 생성할 때나 필드를 추가할 때도 DAO의 CRUD코드와 SQL 대부분을 변경해야하는 문제가 발생한다.
대표적 문제점은 세 가지는
- 진정한 의미의 계층 분할이 어렵다.
- 엔티티를 신뢰할 수 없다.
- SQL에 의존적인 개발을 피하기 어렵다.
JPA와 문제 해결
JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때 SQL을 직접 작성하는 것이아니라 JPA가 제공하는 API를 사용하면 된다.
jpa.persist(member); // 저장
Stirng memberId = "id";
Member member = jpa.find(Member.class, memberId); // 조회
member.setName("name"); // 수정
Team team = member.getTeam(); // 연관된 객체 조회
persist() 메소드는 객체를 데이터베이스에 저장한다.
이 메소드를 호출하면 JPA가 매핑정보를 보고 적절한 INSERT SQL을 생성해서 데이터베이스에 전달한다.
find() 메소드는 객체 하나를 데이터베이스에서 조회한다.
객체와 매핑벙조를 보고 적절한 SELECT SQL을 생성해서 데이터베이스에 전달하고 그 결과로 Member 객체를 생성해서 반환한다.
별도의 수정 메소드는 제공하지 않는 대신에 객체를 조회해서 값을 변경만 하면 트랜잭션을 커밋할 떄 데이터베이스에 적절한 UPDATE SQL이 전달된다.
연관된 객체 조회는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다.
이처럼 JPA는 SQL을 개발자 대신 작성해서 실행해주는 것 이상의 기능들을 제공한다.
패러다임의 불일치
객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 따라서 객체 구조를 테이블 구조에 저장하는 데는 한계가 있다.
상속
객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다.(일부 데이터베이스는 상속 기능을 지원하지만 객체의 상속과는 약간 다르다고 한다.)
그나마 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계 할 수 있다.
- 객체 모델 코드
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;
}
JDBC API를 사용하여 코드를 완성하려면 ALBUM, MOVIE 객체를 저장하려면 객체를 분해해서 두 SQL을 만들어야 한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
INSERT INTO ITEM ...
INSERT INTO MOVIE ...
작성해야할 코드량도 만만치 않을 것이고 자식 타입에 따라서 DTYPE도 저장해야한다.
조회하는 것도 쉬운일이 아니다. ALBUM 조회한다면 ITEM과 ALBUM 테이블을 조인해서 조회한 다음 그 결과로 ALBUM 객체를 생성해야한다.
이런 과정이 패러다임 불일치를 해결하려고 소모하는 비용이다.
만약 해당 객체들을 데이터베이스가 아닌 자바 컬렉션에 보관한다면 타입에 대한 고민 없이 해당 컬렉션을 그냥 사용하면 된다.
list.add(album);
list.add(movie);
Album albubm = list.get(albumId);
JPA와 상속
persist() 메소드를 사용해서 객체를 저장하면 된다.
jpa.persist(album) // album 객체 저장
그러면 JPA는 SQL을 실행해서 객체를 ITEM, ALBUM 테이블에 나누어 저장한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
Album 객체를 조회하려면 find() 메소드를 사용한다.
String albumId = "id001"
Album album = jpa.find(Album.class, albumId);
그러면 JPA는 두 테이블을 조인해서 필요한 데이터를 조회하고 반환한다.
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUBM A ON I.ITEM_ID = A.ITEM_ID;
연관관계
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.
테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.
class Member {
Team team
Team getTeam() {
return team();
}
}
class Team {
...
}
객체는 참조가 있는 방향으로만 조회할 수 있다.
member.getTeam()은 가능하지만 team.getMember()는 참조가 없으므로 불가능하다.
하지만 테이블은 외래 키 하나로 MEMBER JOIN TEAM도 가능하지만 TEAM JOIN MEMBER또 가능하다.
- 객체를 테이블에 맞추어 모델링
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 fk의 값을 그대로 보관하는 teamId 필드에는 문제가 있다.
관계형 DB는 조인이라는 기능이 있어서 외래 키의 값을 보관해도 되지만
객체는 연관된 객체를 참조를 보관해야한다.
객체지향 모델링
객체는 참조를 통해서 관계를 맺는다.
class Member {
String id; //MEMBER_ID 컬럼
Team team; //참조로 연관관계를 맺는다.**
String username; //USERNAME 컬럼
}
class Team {
Long id; //TEAM_ID PK 사용
String name; //NAME 컬럼 사용
}
Member.team 필드를 확인하면 외래 키의 값을 그대로 보관하는 것이 아니라. Team의 참조를 보관한다. 이제 회원과 연관된 팀을 조회할 수 있다.
Team team = member.getTeam(); // member의 team 객체를 리턴한다.
이처럼 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지않다.
객체 모델은 참조만 있으면 되고, 테이블은 외래 키만 있으면 된다. 결국 개발자가 중간에서 변환 역할을 해야한다.
저장
member.getId(); // MEMBER_ID PK에 저장
member.getTeam().getID(); // TEAM_ID FK에 저장
member.getUsername(); .. USERNAME 컬럼에 저장
객체를 DB에 저장하려면 team 필드를 TEAM_ID 외래 키 값으로 변환 해야한다.
외래키 값을 찾아서 INSERT SQL을 만들어야 하는데
MEMBER 테이블에 저장할 TEAM_ID 외래 키는
TEAM 테이블의 기본 키이므로 member.getTeam().getId()로 구할 수 있다.
조회
조회 할 떄는 TEAM_ID 외래 키 값을 Member 객체의 team 참조로 변환해서 객체에 보관해야 한다.
먼저 다음 SQL과 같이 MEMBER와 TEAM을 조회하자.
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SQL의 결과로 객체를 생성하고 연관관계를 설정해서 변환하면 된다.
public Member find(String memberId) {
//SQL 실행
...
Member member = new Member();
...
//DB에서 조회한 회원 관련 정보를 모두 입력
Team team = new Team();
...
//회원과 팀 관계 설정
member.setTeam(team);
return member;
}
JPA와 연관관계
JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다.
member.setTeam(team); // 회원과 팀 연관관계 설정
jpa.persist(member); // 회원과 연관관계 함께 저장
개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하면 된다.
객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리해준다.
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
객체 그래프 탐색
객체에서 회원이 소속된 팀을 조회할 떄 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이것을 객체 그래프 탐색이라 한다.
Team team = member.getTeam();
SQL을 직접다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는 지 정해진다.
객체지향 개발자에겐 큰 제약이다.
- 회원 조회 비지니스 로직
class MemberService {
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam(); // member -> team 객체 그래프 탐색이 가능한가?
member.getOrder().getDelivery(); // ???
}
}
MemberService는 DAO를 통해 객체를 조회했지만 객채 그래프를 탐색할 수 있을지 없을지는 이 코드만 보고는 전혀 예측할 수 없다.
그객체 그래프 탐색이 가능한지 알아보려면 DAO를 열어서 SQL을 직접 확인해야한다. 결국 엔티티가 SQL에 논리적으로 종속되어서 발생하는 문제이다.
JPA와 객체 그래프 탐색
JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다.
실제 객체를 사용하는 시점까지 DB 조회를 미룬다고 해서 지연 로딩이라 한다. JPA는 지연 로딩을 투명(transparent)하게 처리한다.
- 지연 로딩 사용
//처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDaate(); //Order를 사용하는 시점에 SELECT ORDER SQL
order.getOrderDate() 같이 실제 Order 객체를 사용하는 시점에 JPA는 DB에서 ORDER 테이블을 조회한다.
아까 코드의 process처럼 Member를 사용할 떔다 Order를 함꼐 사용하면, 한테이블 씩 조회하는 것 보다는
조회하는 시점에 SQL 조인을 사용해서 Member와 Order를 함꼐 조회하는 것이 효과적이다.
JPA는 연관된 객체를 즉시 함꼐 조회할지 아니면 실제 사용되는 시점에 지연해 조회할지 간단한 설정으로 정의할 수 있다.
비교
데이터베이스는 기본키 값으로 각 로우(row)를 구분한다.
객체는 동일성 identity 비교와 동등성equality 비교라는 두가지 비교 방법이 있다.
- 동일성 비교 "==" -> 인스턴스의 주소 값을 비교한다.
- 동등성 비교 equals() -> 객체 내부의 값을 비교한다.
- MemberDAO
class Member {
public Member getMember(String memberId) {
String sql = "SELECT .......";
//JDBC API, SQL 실행
return new Member(...); //실행할 때마다 새로운 객체를 생성**
}
}
- 조회한 회원 비교하기
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; // 다르다.
같은 로우에서 조회했지만 객체 측면에서 볼때 다른 인스턴스이다.
만약 객체를 컬렉션에 보관했다면 동일성 비교에 성공한다.
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; // 같다
이런 패러다임 불일치 문제를 해결하기 위해 데이터베이스의 같은 로우를 조회할 떄마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다.
JPA와 비교
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1 == member2; //같다
객체 모델과 관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르다.
정교한 객체 모델링을 할 수록 패러다임의 불일치 문제가 커진다.
결국 객체 모델링은 힘을 잃고 데이터 중심의 모델로 변해간다.
자바 진영에서 패러다임의 불일치 문제를 해결하기 위해 많은 노력을 기울여온 결과물이 JPA이다.
JPA란?
JPA는 자바 진영의 ORM 기술 표준이다. 애플리케이션과 JDBC 사이에서 동작한다.
ORM?
ORM(Object-Reational Mapping)은 객체와 관계형 데이터베이스를 매핑한다는 뜻이다.
객체를 데이터베이스에 저장할 때 INSERT SQL을 직접 작성하는 것이 아니라 자바의 컬렉션에 저장하듯 ORM 프레임워크에 저장하면 된다.
JPA를 사용해서 객체를 조회하는 코드
Member member = jpa.find(memberId); // 조회
ORM 프레임워크는 단순한 매핑뿐 아니라 다양한 패러다임 불일치 문제들도 해결해준다.
객체 측면에서는 정교한 객체 모델링이 가능하고
관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면 된다.
어떻게 매핑해야 하는지 매핑 방법만 ORM 프레임워크에게 알려주면된다.
개발자는 데이터중심인 관계형 데이터베이스를 사용해도 객체지향 애플리케이션 개발에 집중하면 된다.
왜 JPA를 사용해야 하는가?
생산성
JPA를 사용하면 자바 컬렉션에 저장하듯, JPA에게 저장할 객체를 전달하면 된다. 지루하고 반복적인 JDBC API나 SQL 작성은 JPA가 대신 처리해준다.
jpa.persist(member); // 저장
Member member = jpa.find(memberId) // 조회
더 나아가서는 CREATE TABLE 같은 DDL 문을 자동으로 생성해주는 기능도 있다.
데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전 시킬 수 있다.
유지보수
SQL을 직접 다루면 필드를 하나만 추가해도 관련된 CRUD 코드를 수정 및 변경해야 했다.
JPA를 사용하면 JPA가 대신 처리해주므로 필드를 추가하거나 삭제해도 수정해야 할 코드가 줄어든다.
패러다임의 불일치 해결
JPA는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임 불일치 문제를 해결해준다.
성능
JPA는 애플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 기회를 제공한다.
JPA는 애플리케이션과 데이터베이스에 계층이 하나 더 있으므로 최적화 관점에서 시도해 볼 수 있는 것들이 많다.
String memberId = "ID"
Member member1 = jpa.find(memberId);
Member member2 = jpa.find(memberId);
JDBC API를 사용해서 해당 코드를 직접 작성했다면 회원을 조회할 때마다 SQL을 사용해 DB와 두번 통신 했을 것이지만
JPA를 사용하면 한 번만 데이터베이스에 전달하고 두번 쨰 조회한 회원 객체를 재사용한다.
데이터 접근 추상화와 벤더 독립성
애플리케이션은 처음 선택한 DB 기술에 종속되고 다른 데이터 베이스로 변경하기 어렵다.
JPA는 그림처럼 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 DB 기술에 종속되지 않도록 한다. DB를 변경하면? JPA에게 다른 DB를 사용한다고만 알려주면 된다.
ORM에 대한 궁금증과 오해
성능이 느리진 않나요?
현재 시대에 자바가 느리다고 말하는 것과 비슷하다. JPA는 다양한 성능 최적화 기능을 제공해서 잘 이해하고 사용하면 SQL을 직접 사용할 떄보다 더 좋은 성능을 낼 수도 있다. 또한 JPA의 네이티브 SQL 기능을 사용해서 SQL을 직접 호출하는 것도 가능하다.
하지만 JPA를 잘 이해하지 못하고 사용하면 N+1 같은 문제로 심각한 성능 저하가 발생할 수 있다. N+1 문제는 예를 들어 SQL 1번으로 회원 100명을 조회했는데 각 회원마다 주문한 상품을 추가로 조회하기 위해 100번의 SQL을 추가로 실행하는 것을 말한다. 이러한 문제는 JPA를 약간만 공부하면 어렵지 않게 해결할 수 있다.
통계 쿼리처럼 매우 복잡한 SQL은 어떻게 하나요?
JPA는 통계 쿼리 같이 복잡한 쿼리보다 실시간 처리용 쿼리에 더 최적화 되어있다. 정말 복잡한 통계 쿼리는 SQL을 직접작성하는것이 더 쉬운 경우가 많다. 따라서 JPA가 제공하는 네이티브 SQL을 사용하거나, Mybatis, JdbcTemplate 같은 SQL 매퍼 형태의 프레임워크를 혼용하는 것도 좋은 방법이다.
학습곡선이 높다고 하던데요?
JPA는 학습곡선이 높다. 객체와 관계형 데이터베이스를 어떻게 매핑하는지 학습 후에 JPA의 핵심 개념들을 이해해야 한다.
예제들을 복사해서 사용하기만 하면 금방 한계에 부딪힌다.
JPA의 핵심 개념인 영속성 컨텍스트에 대한 이해가 부족하면 SQL을 직접 사용해서 개발하는 것보다 못한 상황이 벌어질 수 있다.
JPA가 어려운 근본적 이유는 ORM이 객체지향과 관계형 데이터베이스라는 두 기둥 위에 있기 떄문이다.
기초가 부족하면 어려울 수 밖에 없다.
'JPA' 카테고리의 다른 글
JPA 연관관계 매핑 기초 (0) | 2024.06.25 |
---|---|
JPA 엔티티 매핑 (0) | 2024.06.23 |
JPA 영속성 관리 (0) | 2024.06.20 |
JPA 시작 (2) | 2024.06.08 |