Search

왜 JPA를 써야 하는가

Tags
JPA
Date
2023/09/27

개요

본 포스팅은 김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편을 기반으로 작성되었습니다.

1. JPA란?

JPA는 Japa Persistence API의 줄임말입니다. 자바 진영의 ORM 기술 표준을 일컫습니다.
여기서 ORM은 Object Relational Mapping으로 한국말로 직역하면 객체 관계 매핑이 됩니다.
ORM특징으로는
객체는 객체대로 설계
관계형 데이터베이스는 관계형 데이터베이스대로 설계
ORM 프레임워크는 둘 사이의 중간다리 역할
 그렇다면 ORM 기술은 왜 쓰이게 된 걸까요?
과거 자바 진영은 Jdbc를 시작으로 MyBatis, JdbcTemplate을 사용하기 시작했고 현대에 들어서는 JPA를 적극 사용하고 있습니다.
생산성 저하
JdbcTemplate이나 MyBatis를 통해 개발하면 개발자는 SQL 매핑만 주구장창 할 수 밖에 없는데요, 이는 생산성의 저하로 이어집니다!
패러다임의 불일치
또한 객체와 관계형 데이터베이스 간 패러다임이 맞지 않습니다.
객체는 필드나 메서드를 묶어 캡슐화 해서 사용하는게 목표이고, 관계형 데이터베이스는 데이터를 정교화 해서 저장하는게 목표이기 때문입니다.
데이터베이스는 결국 SQL만을 인식할 수 있기 때문에 객체에서 관계형 데이터 베이스로 전환 하는 과정에서 개발자의 SQL 작업에 의존할 수 밖에 없는것이죠!
영한님은 이를 SQL 중심적인 개발이라고 말합니다.
 객체와 관계형 데이터베이스의 차이는?
첫 번째로 상속입니다.
객체에는 상속 관계가 있지만, 관계형 데이터베이스는 상속 관계가 없습니다!
물론 유사한 관계는 존재합니다. 이를 슈퍼타입 서브타입 관계라고 합니다.
위 표를 보면 공통적인 속성들은 Item 테이블에, 추가적인 속성들은 Movie 테이블에 명시된걸 확인할 수 있습니다!
이를 작성하고자 한다면 두 테이블의 삽입 SQL을 작성해야 하고, 조회한다면 조인을 이용해 Item 테이블과 Movie 테이블을 조회하고 결과 값을 수작업으로 Album 객체와 Item 객체에 넣어주어야 하는 것이죠.
반복적인 개발은 개발자의 생산성을 저하 시킵니다!
만약 자바로 이를 조회한다면 어떻게 할까요?
// 삽입 시 list.add(Movie); // 조회 시 Movie movie = list.get(movieId); // 다형성 조회 시 Item item = list.get(movieId);
Java
복사
두 번째로 연관 관계가 있습니다.
객체는 참조할 수 있지만, 관계형 데이터베이스는 PK, FK를 사용해야 합니다.
// 객체 Team team = member.getTeam() // 데이터베이스 SELECT * FROM Member m JOIN Team t ON m.team_id == t.team_id
Java
복사
객체는 양방향 참조를 할 수 없습니다. member.getTeam()은 되지만 team.getMembers()는 안됩니다. (물론 별도의 속성을 명시하면 가능합니다)
관계형 데이터베이스는 PK, FK를 통해 양방향 참조가 가능합니다.
상속, 연관 관계 외에도 데이터 타입, 데이터 식별 방법의 차이가 존재합니다.
객체와 관계형 데이터베이스 사이에는 모델링 과정에서의 문제도 나타납니다.
// 객체 Team team = member.getTeam() // 데이터베이스 SELECT * FROM Member m JOIN Team t ON m.team_id == t.team_id
Java
복사
위에서 작성했던 둘의 차이입니다. 이를 모델링 하면 어떻게 다를까요?
관계형 데이터베이스
class Member { String id; Long teamId; // Team 대신 teamId가 들어감 String username; } class Team { Long id; String name; }
Java
복사
객체
class Member { String id; Team team; // 참조로 연관관계를 맺음 String username; Team getTeam() { return team; } } class Team { Long id; String name; }
Java
복사
만약 객체 모델링을 그대로 가져와서 관계형 데이터베이스에 들어갈 SQL을 사용하려면 다음과 같은 문제가 생깁니다.
Member member = new Member(); // 멤버를 가져오고 Team team = member.getTeam(); // 멤버에 있는 팀 참조를 가져오고 Long teamId = team.getId(); // 팀에서 아이디를 구한다
Java
복사
과정이 복잡하지 않나요? 객체는 다음과 같습니다.
Member member = list.get(memberId); Team team = member.getTeam();
Java
복사
객체는 자유롭게 객체 그래프를 탐색 할 수 있어야 합니다.
예를 들어, Member → Order → OrderItem → Item 자유롭게 탐색할 수 있어야 합니다.
하지만 관계형 데이터베이스에서는 처음 실행하는 SQL에 따라 탐색 범위가 결정됩니다.
예를 들어, Member 테이블과 Team 테이블만을 Join 했다면 Order 객체를 조회하는 것을 불가능합니다.
이는 엔티티 신뢰 문제를 일으키는데 이는 다음과 같습니다.
class MemberService { ... Member member = memberDao.find(memberId); member.getOrder() // null이 들어 있을 수도 있음 }
Java
복사
그렇다고 해서 SQL문을 작성할 때 모든 객체를 미리 로딩할 수 없습니다. 따라서 SQL을 직접 다루면 진정한 계층 분할이 어려워 집니다.
객체와 관계형 데이터베이스는 참조 값이 다릅니다. 이는 코드로 설명해보겠습니다.
String memberId = "100"; Member member1 = memberDAO.getMember(memberId); Member member2 = memberDAO.getMember(memberId); member1 == member2; // False
Java
복사
이는 getMember()가 항상 new로 새로운 객체를 생성하기 때문입니다.
class MemberDAO { public Member getMember(String memberId) { String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?"; ... //JDBC API, SQL 실행 return new Member(...); } }
Java
복사
하지만 자바는 동일한 참조 값을 보장합니다.
String memberId = "100"; Member member1 = list.get(memberId); Member member2 = list.get(memberId); member1 == member2; // True
Java
복사

2. JPA는 어떻게 동작하는가?

JPA는 애플리케이션과 JDBC 사이에서 동작합니다.
위 그림을 보면 개발자가 직접 JDBC API를 호출하는 것이 아닌 JPA가 자동으로 JDBC API를 호출하는 방식인 것을 확인할 수 있습니다.
JPA에서 엔티티를 저장하는 과정
만약 MemberDAO가 Member 객체를 저장하고 싶다하면
1.
JPA가 Entity를 분석하고
2.
Insert SQL 생성 한 뒤
3.
JDBC API를 사용하여 DB에 저장
이 때 위 모든 과정을 JPA가 해결해 줄 뿐만 아니라 위에서 언급했던 패러다임의 불일치를 해결 해줍니다.
JPA에서 엔티티를 조회하는 과정
만약 Member객체를 조회하고 싶다면 JPA가
1.
적절한 조회 쿼리를 만들고
2.
JDBC API를 통해 데이터베이스로 부터 결과를 조회
3.
ResultSet 매핑을 수행
4.
Entity Object를 반환
이 역시 마찬가지로 패러다임의 불일치를 해결해줍니다.

3. 왜 JPA를 써야 하는가?

첫 번째로 SQL 중심적인 개발에서 객체 중심적인 개발로 나아갈 수 있습니다.
더 이상 개발자가 SQL 매핑에 신경쓰지 않아도 됩니다.
두 번째로 생산성이 향상됩니다.
후에 JPA 코드를 다루면 알겠지만 CRUD. 즉, Create, Read, Update, Delete가 한 줄의 코드로 가능합니다.
public class MemberService(){ ... memberJpaRepository.save(member); // Create memberJpaRepository.findById(memberId); // Read member.setName(""); // Update memberJpaRepository.deleteById(memberId); // Delete
Java
복사
세 번째로 유지보수가 간편 해집니다.
SQL을 사용하면 기존 객체의 필드가 수정된다면 모든 SQL 쿼리를 작업해야 합니다. 하지만 JPA는 이를 자동으로 수정해줍니다.
네 번째로 객체와 데이터베이스 간 패러다임 불일치를 해결해줍니다.
개발자가 한 줄의 코드로 명령을 내리면 JPA가 SQL문을 자동으로 날려 처리해줍니다.
다섯 번째로 성능을 최적화 할 수 있습니다.
버퍼는 컴퓨터 시스템에서 데이터 처리의 효율성을 높이기 위한 중간 저장소 역할을 합니다. JPA도 마찬가지 인데요, 객체와 관계형 데이터 베이스 중간에 위치하여 성능 최적화를 도와줍니다.
1.
같은 트랜섹션 안에서는 동일성을 보장합니다.
Long memberId = 100L; Member member1 = memberJpaRepository.findById(memberId) // SQL 쿼리 Member member2 = memberJpaRepository.findById(memberId) // 캐시 안에서 가져옴 member1 == member2 // true
Java
복사
첫 번째로 조회가 실행된 뒤 두 번째 조회를 요청하면 새로운 SQL 쿼리가 나가는 것이 아닌 캐싱 된 값을 반홥합니다. 이는 약간의 조회 성능을 향상 시켜 줍니다.
2.
트랜섹션을 지원하는 쓰기 지연과 JDBC BATCH SQL
쓰기 지연이란 트랜섹션 내에서 데이터베이스에 변경 혹은 삭제를 요구할 때 이를 즉시 반영하지 않고 트랜섹션이 커밋되는 순간에 일괄적으로 처리하는 것을 말합니다.
또한 JDBC BATCH SQL을 사용하여 여러 SQL문을 한 번에 데이터베이스로 전송할 수 있습니다.
이러한 과정을 통해 효율성과 성능을 높일 수 있습니다.
3.
기본적으로 지연 로딩을 사용하여 성능 최적화를 진행합니다.
즉시 로딩지연 로딩의 차이는 말 그대로 즉시 Join SQL을 통해 조회 하는가 실제로 객체 필드에 접근할 때 조회 하는가 입니다.
쉽게 얘기하자면 즉시 로딩의 경우 Member만 조회함 에도 불구하고 Team 객체까지 모두 조회하는 것을 말합니다. 반대로 지연 로딩의 경우 Member만 조회한다면 Member만 조회하는 쿼리를 생성하고 후에 Team 객체에 접근할 때 Team 객체를 조회하는 쿼리를 생성하는 것입니다.