https://github.com/MoochiPark/jpa/tree/master/chapter01/README.md
[ ▶ JPA ]
객체를 관계형 데이터베이스에 저장하려면 많은 시간과 코드를 소비해야 한다. 자바 진영에서는 이런 문제를 해결하기 위해 JPA라는 표준 기술을 사용한다. 자바 ORM (Object Relational Mapping) 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도화주고, 객체와 데이터베이스의 차이도 중간에서 해결해 준다.
MyBatis, 스프링의 JdbcTemplate 같은 SQL 매퍼를 사용하면 JDBC 코드를 많이 줄일 수 있지만, 여전히 등록, 수정, 삭제, 조회 (CRUD)용 SQL은 반복해서 작성하게 된다.
JPA는 지루하고 반복적인 CRUD SQL을 알아서 처리해줄 뿐만 아니라 객체 모델링과 관계형 데이터베이스 사이의 차이점도 해결해준다. JPA는 실행 시점에서 자동으로 SQL을 만들어서 실행하는데 개발자는 SQL을 직접 작성하는 것이 아니라 어떤 SQL이 실행될지 생각만 하면 된다.
[ ▷ SQL을 직접 다룰 때 발생하는 문제점 ]
관계형 데이터베이스는 가장 대중적이로 신뢰할 만한 데이터 저장소이며, 자바로 개발하는 애플리케이션은 대부분 관계형 데이터베이스를 데이저 저장소로 사용한다.
[ ▷ SQL 반복 문제 ]
해당 문제에 대한 예시로 회원 관리 기능을 개발한다고 가정한다.
▼ 회원 객체
public class Member {
private String memberId;
private String name;
...
}
▼ 회원 객체를 데이터베이스에 관리할 목적으로 DAO (Data Access Object)를 만든다.
public class MemberDao {
public Member find(final String memberId) {
...
}
}
MemberDao의 find() 메서드를 완성하여 회원을 조회하는 기능을 개발한다면 보통 다음과 같은 순서로 개발한다.
1. 회원 조회용 SQL을 작성한다.
SELECT member_id, name FROM member where member_id = ?
2. JDBC API를 사용해서 SQL을 실행한다.
ps = conn.preparedStatement(query);
rs = ps.executeQuery();
3. 조회 결과를 Member 객체로 매핑한다.
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setName(rs.getString("name"));
...
회원 조회 기능을 완성하였으니, 회원 등록 기능을 만든다.
public class MemberDao {
public Member find(final String memberId) {
...
}
public void save(final Member member) {
...
}
}
1. 회원 등록용 SQL을 작성한다.
String query = "INSERT INTO member(member_id, name) VALUES(?, ?)";
2. 회원 객체의 값을 꺼내서 등록 SQL에 전달한다
pstmt.setString(1, member.getMemberId());
pstmt.setString(2, member.getName());
3. JDBC API를 사용해서 SQL을 실행한다.
pstmt.excuteUpdate(query);
다음으로 회원을 수정하고 삭제하는 기능도 추가한다면, SQL을 작성하고 JDBC API를 사용하는 비슷한 일을 반복하게 된다. 만약 사용하고 있는 객체들을 데이터베이스가 아닌 자바 컬렉션에 보관한다면 다음 한 줄로 객체를 저장할 수 있다.
list.add(member);
하지만 데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가지므로 객체를 데이터베이스에 직접 저장하거나 조회할 수 없다. 따라서 개발자가 객체지향 애플리케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용하여 직접 변환해주어야 한다.
문제는 객체를 데이터베이스에 CRUD 하려면 너무 많은 SQL과 JDBC API를 코드로 작성해야 한다는 점이다. 그리고 테이블마다 이런 비슷한 작업을 반복해야 한다. 데이터 접근 계층 (DAO)를 개발하는 일은 이렇듯 지루하고 반복적이다.
[ ▷ SQL에 의존적인 개발 ]
여기에 요구사항으로 회원의 연락처도 함께 저장해야 한다고 가정하자.
▼ 회원 클래스에 연락처 필드 추가
public class Member {
private String memberId;
private String name;
private String tel; // 추가
...
}
1. 등록 코드 변경
연락처를 저장할 수 있도록 INSERT SQL을 수정해야 한다.
String sql = "INSERT INTO member (member_id, name, tel) values(?, ?, ?)";
2. 조회 코드 변경
제대로 출력이 가능하도록 조회 SQL에 연락처 칼럼을 추가해야 한다.
SELECT member_id, name, tel FROM member WHERE member_id = ?
또한 연락처의 조회 결과를 Member 객체로 추가로 매핑해야 한다.
String tel = rs.getString("TEL");
member.setTel(tel);
3. 수정 코드 변경
UPDATE SQL에서 TEL 칼럼을 추가해야만 연락처가 정상적으루 수정될 것이다. 그렇다면 또 다시 UPDATE SQL과 MemberDAO.update()의 일부 코드를 변경해서 연락처가 정상적으로 수정되도록 해야한다.
하지만 회원 객체를 데이터베이스가 아닌 자바 컬렉션에 보관했다면 필드를 추가한다고 해서 이렇게 많은 코드를 수정할 필요가 없어진다.
list.add(member); // 등록
Member member = list.get(xxx); //조회
member.setTel("xxx"); // 수정
4. 연관된 객체가 존재할 경우
만약 회원은 어떤 한 팀에 필수로 소속되어야 한다는 요구사항이 생겼다고 가정하자. 팀 모듈을 전단하는 개발자가 Member 객체에 team 필드를 추가했다, 이 때 회원 정보를 출력할 떄 연관됨 팀 이름도 함께 출력하는 기능을 추가한다고 가정한다.
▼ 회원 클래스에 연관된 팀 추가
public class Member {
private String memberId;
private String name;
private String tel;
private Team team;
...
}
member.getTeam().getTeamName();
이 코드를 추가해서 화면에 팀 이름을 출력하도록 해도, member.getTeam()의 값은 항상 null 일 것이다.
- 회원을 조회하는 find() 메서드는 기본 동작 (회원 정보만 조회)만 수행
- findWithTeam()이라는 회원과 연관된 팀을 함께 조회하는 메서드 발견
- DAO를 열어서 SQL을 확인하고 원인 발견
- 회원 조회 코드를 memberDao.find()에서 memberDao.findWithTeam()으로 변경하여 해결
여기서 SQL의 문제점을 정리할 수 있다.
Member나 Team처럼 비즈니스 요구사항을 모델링한 객체를 엔티티라고 하는데, 지금처럼 SQL에 모든 것을 의존하는 상황에선 개발자들이 엔티티를 신뢰하고 사용할 수 없다. DAO를 열어 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 일일히 확인해야 한다. 이것은 진정한 의미의 계층 분할이 아니다.
물리적으로 SQL과 JDBC API를 데이터 접근 계층에 숨겼어도 논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다.
이런 강한 의존관계 때문에 회원을 조회할 때 물론이고 회원 객체에 필드를 하나만 추가해도 DAO의 CRUD 코드, SQL의 대부분을 변경해야 하는 문제가 발생한다. 애플리케이션에서 SQL을 직접 다룰 때 발생하는 문제는 아래와 같다.
- 진정한 의미의 계층 분할이 어렵다.
- 엔티티를 신뢰할 수 없다.
- SQL에 의존적인 개발을 피하기 어럽다.
[ ▷ JPA와 문제 해결 ]
JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 사용하면 된다. 그러면 JPA가 개발자 대신 적절한 SQL을 생성해서 데이터베이스에 전달한다.
▲ 저장 기능
jpa.persist(member);
persist() 메서드는 객체를 데이터베이스에 저장한다. 이 메서드를 호출하면 JPA가 객체와 매핑정보를 보고 적절한 INSERT SQL을 생성해서 데이터베이스에 전달한다.
▲ 조회 기능
String memberId = "helloId";
Member member = jpa.find(Member.class, memberId);
find() 메서드는 객체 하나를 데이터베이스에서 조회한다. JPA는 객체와 매핑정보를 보고 적절한 SELECT SQL을 생성해서 데이터베이스에 전달하고 그 결과로 Member 객체를 생성해서 반환한다.
▲ 수정 기능
Member member = jpa.find(Member.class, memberId);
member.setName("이름변경");
JPA는 별도의 수정 메서드를 제공하지 않는다. 대신에 객체를 조회해서 값을 변경만 하면 트랜잭션을 커밋할 때 데이터베이스에 적절한 UPDATE SQL이 전달된다.
▲ 연관된 객체 조회
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연곤된 객체를 마음껏 조회할 수 있다.
[ ▷ 패러타임의 불일치 ]
애플리케이션은 발전하면서 그 내부의 복잡성도 점점 커진다. 복잡성을 제어하지 못하면 결국 유지보수하기 어려운 애플리케이션이 된다.
객체지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다. 그렇기에 현대의 복잡한 애플리케이션은 대부분 객체지향 언어로 개발된다. 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다. 문제는 이렇게 정의한 도메인 모델을 저장할 때 발생한다.
예를 들어 회원 객체를 저장해야 하는데 회원 객체가 팀 객체를 참조하고 있다면, 회원 객체를 저장할 때 팀 객체도 저장해야 한다. 관계형 데이터베이시는 데이터 중심으로 주도화되어 있고, 집합적인 사고를 요구한다. 또한 객체 지향에서의 추상화, 상속, 다형성 같은 개념이 없다.
객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라고 한다. 따라서 객체 구조를 테이블 구조로 자정하는데는 한계가 잇다.
애플리케이션은 자바로 개발하고 데이터는 관계형 데이터베이스에 저장해야 한다면, 패러다임의 불일치 문제를 중간에서 개발자개 해결해야 한다.
[ ▷ 상속 ]
객체는 상속이라는 기능을 가지고 있지만, 테이블은 상속이라는 기능이 없다.
▼ 객체 상속 모델
그나마 데이터베이스 모델링에서의 슈퍼타입 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 설계할 수 있다.
▼ 테이블 모델
ITEM 테이블의 DTYPE 컬럼을 사용해서 어떤 자식 테이블과 관계가 있는지 정의되어 있다. 예를 들어 DTYPE의 값이 MOVIE이면 영화 테이블과 관계가 있다.
만약 Album 객체를 저장하려면 이 객체를 분해해서 두 SQL을 만들어야 한다. Movie 객체도 마찬가지이다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
JDBC API를 사용해서 이 코드를 완성하려면 부모 객체에서 부모 데이터만 꺼내서 ITEM용 INSERT SQL을 작성하고 자식 객체에서 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 한다. 이러면 작성할 코드가 많아진다.
조회하는 것도 마찬가지로 쉽지 않다. 이런 과정이 패러다임 불일치를 해결하려고 소모하는 비용이다. 해당 객체들을 데이터베이스가 아닌 자바 컬렉션에 보관한다면 아과 같이 부모 자식이나 타입에 대한 고민 없이 사용하면 된다.
list.add(album);
list.add(movie);
Album album = list.get(albumId);
[ ▷ JPA와 상속 ]
JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 마치 컬렉션에 객체를 저장하듯이 JPA에게 객체를 저장하면 된다.
JPA를 사용하여 Item을 상속한 Album 객체를 저장하려면, persist() 메서드는 사용하면 된다.
jpa.persist(album);
JPA는 다음 SQL을 실행햐서 객체를 ITEM, ALBUM 두 테이블에 나누어 저장한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
다음으로 Album 객체를 조회한다면, find() 메서드를 사용해서 객체를 조회하면 된다.
String albumId = "id100";
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 객체는 Member.team 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다. 따라서 이 참조 필드에 접근하면 Member와 연관된 Team을 조회할 수 있다.
class Member {
Team team;
...
Team getTeam() {
return team;
}
}
class Team {
...
}
member.getTeam(); // member -> team 접근
MEMBER 테이블을 MEMBER.TEAM_ID 외래 키 컬럼을 사용해서 TEAM 테이블과 관계를 맺는다. 이 외래 키를 사용해서 MEMBER 테이블과 TEAM 테이블을 조인하면 MEMBER 테이블과 연관된 TEAM 테이블을 조회할 수 있다.
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
객체는 참조가 있는 방향으로 조회할 수 있는 반면에 테이블은 외래 키 하나로 MEMBER JOIN TEAM도 가능하지만 반대의 경우도 가능하다.
[ ▷ 객체를 테이블에 맞추어 모델링 ]
객체와 테이블을 차이를 알기 위해선 객체를 단순히 테이블에 맞추어 모델링하면 된다.
class Member {
String id; // MEMBER_ID 컬럼 사용
Long teamId; // TEAM_ID FK 컬럼 사용
String username; // USERNAME 컬럼 사용
...
}
class Team {
Long id; // TEAM_ID PK 사용
String name; // NAME 컬럼 사용
...
}
이렇게 테이블의 컬럼을 그대로 가져와서 모델링하면 객체를 테이블에 저장하거나 조회할 때 편리하다.
하지만 여기 teamId 필드에는 문제가 있다. 관계형 데이터베이스는 조인이라는 기능이 있으므로 외래 키 값을 그대로 보관해도 되지만, 객체는 연관된 객체의 참조를 보관해야 다음처럼 참조를 통해 연관된 객체를 찾을 수 있다.
Team team = member.getTeam();
Member.teamId 필드처럼 TEAM_ID 외래 키까지 테이블에 맞추어 모델링한다면 Member 객체와 연관된 Team 객체를 참조를 통해서 조회할 수 없어 결국엔 객체지향의 특징을 잃어버리게 된다.
[ ▷ 객체 지향 모델링 ]
객체는 참조를 통해 관계를 맺는다. 아래와 같이 참조를 사용하도록 모델링해야 한다.
▼ 참조를 사용하는 객체 모델
class Member {
String id;
Team team; // 참조로 연관관계를 맺음
String username;
Team getTeam() {
return team;
}
...
}
하지만 이처럼 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다. Member 객체는 team 필드로 연관관계를 맺고 MEMBER 테이블은 TEAM_ID 외래 키로 연관관계를 맺기 때문이다.
▲ 객체 모델
- 외래 키가 필요 없음
- 참조만 있으면 됨
▲ 테이블
- 참조가 필요 없음
- 외래 키만 있으면 됨
▲ 저장
객체를 데이터베이스에 저장하려면 team 필드를 TEAM_ID 외래 키 값으로 변환해야 한다.
TEAM_ID는 TEAM 테이블의 기본 키이므로 member.getTeam().getId()로 구할 수 잇다.
member.getTeam().getId(); // TEAM_ID FK에 저장
▲ 조회
조회할 때는 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(final String memberId) {
// SQL 실행
...
Member member = new Member();
...
// 데이터베이스에서 조회한 회원 관련 정보를 모두 입력
Team team = new Team();
...
// 데이터베이스에서 조회한 팀 관련 정보를 모두 입력
// 회원과 팀 관계 설정
member.setTeam(team);
return member;
}
위의 과정들은 전부 패러다임 불일치를 해결하려고 소모되는 비용이다. 만약 컬렉션에 회원 객체를 저장한다면 이런 비용이 전혀 들지 않는다.
[ ▷ JPA와 연관관계 ]
JPA는 연관관계에 관련된 패러다임 불일치 문제를 해결해 준다.
member.setTeam(team); // 회원과 팀 연관관계 설정
jpa.persist(member); // 회원과 연관관계 함께 저장
개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하면 된다. JPA는 team의 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달한다.
객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리한다.
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
[ ▷ 객체 그래프 탐색 ]
객체에서 회원이 소속된 팀을 조회할 때는 아래의 그림처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이를 객체 그래프 탐색이라고 한다.
Team team = member.getTeam();
객체는 마음껏 객체 그래프를 탐색할 수 있어야 한다.
member.getOrder().getOrderItem()... // 자유로운 객체 그래프 탐색
예를 들어 MemberDao에서 member 객체를 조회할 아래의 SQL을 실행해서 회원과 팀에 대한 데이터만 조회햇다면 member.getTeam()은 성공하지만 다음처럼 다른 객체 그래프는 데이터가 없으므로 탐색할 수 없다.
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
member.getOrder() // null
결국 SQL을 직접 다루며면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이것은 객체 지향 개발에 큰 제약인데. 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데, 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없기 때문이다.
class MemberService {
..
public void process() {
Member member = memberDao.find(memberId);
member.getTeam(); // member->team 객체 그래프 탐색이 가능할까?
member.getOrder().getDelivery(); // ??
}
}
위 코드에서 memberDao를 통해서 member 객체를 조회했지만 이 코드만 보고 Team, Order, Delivery 방향으로 객체 그래프 탐색을 할 수 있을지 없을지는 예측할 수 없다. 결국 Dao를 열어서 SQL을 직접 확인해야만 한다.
그렇다고 member와 연관된 모든 객체 그래프를 메모리에 올려두는 것은 현실성이 없다. 결국 memberDao에 회원을 조회하는 메서드를 상황에 따라 여러 개 만들어서 사용해야 한다.
memberDao.getMember();
memberDao.getMemberWithTeam();
memberDao.getMemberWithOrderWithDelivery();
[ ▷ JPA와 객체 그래프 탐색 ]
JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다. 앞에서 나왔듯이 JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 조회할 수 잇다. 이 기능을 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라고 한다.
JPA는 지연 로딩을 투명 (transport)하게 처리한다. 아래의 코드를 보면 메서드 구현 부분에 JPA에 관련된 어떤 코드도 직접 사용하지 않는다.
▼ 투명한 엔티티
class Member {
private Order order;
public Order getOrder() {
return order;
}
...
▼ 지연 로딩 사용
// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDate(); // Order를 사용하는 시점에 SELECT ORDER SQL
만약 Member를 사용할 때마다 Order를 사용한다면, JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에서 지연해서 조회할지 간단한 설정으로 정의할 수 있다.
[ ▷ 비교 ]
데이터베이스는 기본 키의 값으로 각 로우를 구분한다. 반면 객체는 동일성 (identity)비교와 동등성 (equality) 비교 두 방법이 있다.
- 동일성 비교는 == 비교. 객체 인스턴스의 주소 값을 비교한다.
- 동등성 비교는 equals() 메서드를 사용해서 객체 내부의 값을 비교한다.
따라서 테이블의 로우를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있다.
▼ MemberDao 코드
class MemberDao {
public Member getMember(final String memberId) {
String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
...
// JDBC API, SQL 실행
return new Member(...);
}
}
▼ 조회한 회원을 비교하는 코드
String MemberId = "100";
Member member1 = memberDao.getMember(memberId);
Member member2 = memberDao.getMember(memberId);
member1 == member2 ==> false
member1과 member2는 같은 데이터베이스 로우에서 조회하였지만, 객체 측면에선 다른 인스턴스이다.
따라서 객체의 동일성 비교에는 실패한다. 만약 객체를 컬렉션에 보관했다면 비교에 성공한다.
Member member1 = list.get(0);
Member member2 = list.get(0);
member1 == member2 ==> true
이런 패러더임 불일치 문제를 해결하기 위해 데이터베이스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다.
[ ▷ JPA와 비교 ]
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다. 그러므로 아래 코드에서 member1과 member2 는 동일성 비교에 성공한다.
String MemberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2 ==> true
객체 비교하기는 분산 환경이나 트랜잭션이 다른 상황까지 고려하면 더 복잡해진다.
[ ▷ 정리 ]
- 객체 모델과 관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르다. 문제는 이 차이를 극복하려면 개발자가 너무 많은 시간과 코드를 소비해야 한다.
- 더 어려운 문제는 객체지향의 특성한 정교한 객체 모델링을 할수록 패러다임 불일치 문제가 더 커진다.
- 자바 진영에서는 패러다임 불일치 문제를 해결하기 위해 많은 노력을 기울여왔다. 그리고 그 결과물이 JPA이다.
- JPA는 패러다임 불일치 문제를 해결해주고 정교한 객체 모델링을 유지하게 도와준다.
[ ▷ JPA란 무엇인가? ]
JPA (Java Persistence API)는 자바 진영의 ORM 기술 표준이다.
▼ JPA
JPA는 애플리케이션과 JDBC 사이에서 동작한다.
ORM (Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임 불일치 문제를 개발자 대신 해결해 준다. 예를 들어 객체를 데이터베이스에 저장할 때 INSERT SQL를 직접 작성하는 것이 아니라 객체를 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 된다. 그러면 ORM 프레임워크가 적절한 INSERT SQL을 생성해서 데이터베이스에 객체를 저장해준다.
▼ JPA 저장
▼ JPA 조회
조회할 때도 JPA를 통해 객체를 직접 조회하면 된다.
ORM 프레임워크는 단순히 SQL을 개발자 대신 생성해서 데이터베이스에 전달해주는 것뿐만 아니라 앞서 이야기한 다양한 패러다임 불일치 문제들도 해결해 주어 객체 측면에서 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면된다. 그렇기에 개발자는 데이터 중심은 관계형 데이터베이스를 사용해도 객체지향 애플리케이션에 집중할 수 있다.
[ ▷ JPA 소개 ]
과거 자바 진영에서는 EJB (Enterprise Java Beans)라는 기술 표준을 만들었는데 그 안에는 엔티티 빈이라는 ORM 기술도 포함되어 있었다. 하지만 너무 복잡하고 기술 성숙도도 떨어졌으며 자바 엔터프라이즈 애플리케이션 서버에서만 동작했다. 이때 하이버네이트 (hibernate.org)라는 오픈 소스 ORM 프레임워크가 등장하였는데 EJB는 ORM 기술과 비교해서 가볍고 기술 성숙도도 높앗다. 또한 자바 엔터프라이즈 애플리케이션 서버 없이도 동작했기 때문에 많은 개발자들이 사용하기 시작했다.
결국 EJB 3.0에서 하이버네이트를 기반으로 새로운 자바 ORM 기술 표준이 만들어졌는데 이것이 바로 JPA이다.
▼ JPA 표준 인터페이스와 구현체
자바는 자바 ORM 기술에 대한 API 표준 명세이다. 쉽게 말하자면 인터페이스를 모아둔 것이다. 따라서 JPA를 사용하려면 JPA가 구현현 ORM 프레임워크를 선택해야 한다. 하이버네이트, EclipseLink, DataNucleus 중 하이버네이트가 가장 대중적이다.
JPA라는 표준 덕분에 특정 구현 기술에 대한 의존도를 줄일 수 있고 다른 구현 기술로 손쉽게 이동할 수 있다는 장점이 잇다.
[ ▷ JPA 를 사용하는 이유 ]
▲ 생산성
JPA를 사용하면 컬렉션에 객체를 저장하듯이 JPA에게 저장할 객체를 전달하면 된다. 지루하고 반복적인 을은 JPA가 대신 처리해준다.
jpa.persist(member);
Member member = jpa.find(memberId);
더 나아가 JPA에는 CREATE TABLE과 같은 DDL 문을 자동으로 생성해주는 기능도 있다. 이런 기능들을 사용하면 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전시킬 수 있다.
▲ 유지보수
SQL을 직접 다루면 엔티티에 필드 하나만 추가해도 관련된 코드들을 모두 변경해야 했다, 반면에 JPA는 이런 과정을 대신 처리해주므로 필드를 추가하거나 삭제해도 수정해야할 코드가 줄어든다.
또한 패러다임 불일치 문제를 해결해주어 객체지향 언어가 가진 장점들을 활용하여 유지 보수하기 좋은 도메인 모델을 편리하게 설계할 수 있다.
▲ 패러다임 불일치 해결
JPA는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임 불일치 문제를 해결해 준다.
▲ 성능
JPA는 애플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 기회를 제공한다. JPA는 애플리케이션과 데이터베이스 사이에서 동작하므로 최적화 관점에서 시도해볼 수 있는 것이 많다.
String memberId = "helloId";
Member member1 = jpa.find(memberId);
Meber member2 = jpa.find(memberId);
JDBC를 사용해서 해당 코드를 직접 작성했다면 회원을 조회할 떄마다 SELECT SQL을 사용해서 데이터베이스와 두 번 통신했을 것이다. JPA는 한 번만 데이터베이스에 전달하고 두 번째부터는 조회한 객체를 재사용한다.
▲ 데이터 접근 추상화와 벤더 독립성
관계형 데이터베이스는 같은 기등도 벤더마다 사용법이 다른 경우가 많다. 애플리케이션은 처음 선택한 데이터베이스 기술에 종속되고 다른 데이터베이스로 변경하기 매우 어렵다.
JPA는 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 특정 데이터베이스 기술에 종속되지 않도록 한다. 만약 데이터베이스를 변경하면 JPA에게 다른 데이터베이스를 사용한다고 알려주기만 하면된다.
▲ 표준
JPA는 자바 진영의 ORM 표준이다. 표준을 사용하면 다른 구현 기술로 손쉽게 변경할 수 있다.
'JPA (Java Persistence API)' 카테고리의 다른 글
6장 다양한 연관관계 매핑 (0) | 2024.09.13 |
---|---|
5장 연관관계 매핑 기초 (0) | 2024.09.11 |
4장 엔티티 매핑 (0) | 2024.09.11 |
3장 영속성 관리 (0) | 2024.09.11 |
2장 JPA 시작 (0) | 2024.09.11 |