[JPA] 배워서 엔터프라이즈 개발하자 1일차 - JPA 소개
본 글은 김영한님의 [인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의와 자바 ORM 표준 JPA 프로그래밍 책을 기반으로 정리해 작성하였습니다.
0. 소개
과거 주로 자바로 애플리케이션을 개발하고 관계형 데이터베이스(이하 RDB)를 데이터 저장소로 사용하던 개발자들은 객체지향의 장점을 포기하고 객체를 단순히 테이블에 맞추어 데이터 전달 역할만 하도록 개발하는 경우가 많았다.
쉽게말해 객체지향의 장점 중 하나로 잘 설계된 독립적인 클래스를 재사용하고, 상속할 수 있다는 장점이 있는데, 객체를 RDB에 적용시키려 할 때는 일일히 CRUD(등록, 수정, 삭제, 조회) 용 SQL을 반복해서 작성해야했고, 상속 관계의 경우 부모, 자식 클래스에 대한 SQL문을 각각 작성해야 한다는 문제점이 발생했다는 것이다.
그러던 중 객체와 RDB 간의 차이를 중간에서 해결해주는 ORM(Object Relational Mapping) 프레임워크가 등장했고, 우리가 지금 공부할 JPA는 자바 진영의 ORM 기술 표준이 되시겠다.
JPA를 사용하는 개발자는 SQL을 직접 작성하는 것이 아니라 어떤 SQL이 실행될지 생각만 하면 되고, 객체 중심으로 개발하기 때문에 생산성과 유지보수 측면에서 효율성이 높아졌으며 테스트를 작성하기도 편해졌다고 한다.
또한 우리같은 학생 입장에서는 이게 회사가서도 많이 쓸까? 싶은 의문을 먼저 해결한 후에야 공부를 하기 마련인데, JPA의 경우 취업 왕 스프링 프레임워크도 스프링 데이터 JPA라는 기술로 JPA를 적극적으로 지원하고 전자정부 표준 프레임워크의 ORM 기술도 JPA를 사용한다고하니 최근 많은 대학생들이 공부하는 분야중 하나이다.
1. SQL을 직접 다룰 때 발생하는 문제점
데이터베이스에 데이터를 관리하려면 당연히 SQL문을 작성하고, 사용해야 한다. 자바로 작성한 애플리케이션은 JDBC API를 사용해서 SQL을 데이터베이스에 전달하게 된다.
일반적으로 자바에서 사용할 회원(Member) 객체를 만들고 이를 데이터베이스에서 조회, 등록하는 코드는 다음과 같다.
public class Member{
private Long id;
private String name;
}
//회원 조회
public Member find(Long id){
// 1. sql문 작성
String sql = "SELECT ID, NAME FROM MEMBER M WHERE ID = ?";
// 2. JDBC API 사용하여 SQL문 실행
ResultSet rs = stmt.executeQuery(sql);
// 3. 조회 결과를 Member 객체로 매핑
String id = rs.getString("ID");
String name = rs.getString("NAME");
Member member = new Member();
member.setid(id);
member.setname(name);
}
//회원 등록
public void save(Member m){
// 1. sql문 작성
String sql = "INSERT INTO MEMBER(ID, NAME) VALUES(?, ?)";
// 2. 회원 객체의 값을 꺼내서 등록 SQL에 전달한다.
pstmt.setString(1, m.getid());
pstmt.setString(2, m.getname());
// 3. JDBC API를 사용해서 SQL 실행
pstmt.executeUpdate(sql);
}
이 상태에서 만약에 회원(Member) 클래스에 연락처라는 필드가 하나 추가되었다고 생각해보자. 수정된 코드는 이렇다.
public class Member{
private Long id;
private String name;
private String tel; //추가
}
//회원 조회
public Member find(Long id){
// 1. sql문 작성
String sql = "SELECT ID, NAME FROM MEMBER M WHERE ID = ?";
// 2. JDBC API 사용하여 SQL문 실행
ResultSet rs = stmt.executeQuery(sql);
// 3. 조회 결과를 Member 객체로 매핑
String id = rs.getString("ID");
String name = rs.getString("NAME");
String tel = rs.getString("TEL"); //수정
Member member = new Member();
member.setid(id);
member.setname(name);
member.settel(tel); //수정
}
//회원 등록
public void save(Member m){
// 1. sql문 작성
String sql = "INSERT INTO MEMBER(ID, NAME, TEL) VALUES(?, ?, ?)"; //수정
// 2. 회원 객체의 값을 꺼내서 등록 SQL에 전달한다.
pstmt.setString(1, m.getid());
pstmt.setString(2, m.getname());
pstmt.setString(3, m.gettel()); //수정
// 3. JDBC API를 사용해서 SQL 실행
pstmt.executeUpdate(sql);
}
멤버 객체가 그리 크지도 않았는데도 바뀐 부분이 상당히 많다. 우선 컬럼이 추가되었기 때문에 getter, setter 관련 부분을 모두 추가해줘야 한다.
하지만 이게 다가 아니다. 위 코드에서 은근슬쩍 위에 껴 있는 find, save 함수들의 경우 DAO(Data Access Object)라는 클래스를 따로 만들고 여기에 트랜잭션 관련 로직 코드들을 저장하게 되는데(위에선 find, save 함수들이 이에 해당) 위처럼 컬럼들이 추가하게 되면 DAO에 속한 코드들을 다 뜯어 고쳐야 한다는 문제점이 있다.
JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 사용하면 된다. 그러면 JPA가 개발자 대신에 적절한 SQL을 생성해서 데이터베이스에 전달한다. 다음 JPA 예시 코드를 살펴보자.
// 1. 저장
jpa.persist(member);
// 2. 조회
String memberId = "suhwan";
Member member = jpa.find(Member.class, memberId);
// 3. 수정
Member member = jpa.find(Member.class, memberId);
member.setName("babo");
// 4. 연관된 객체 조회
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
정말 간단하게 바뀌는 것을 볼 수 있다. 자세한 내용은 앞으로 쓸 포스팅 내용에서 천천히 알게 될 것이다.
2. 패러다임의 불일치
RDB는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 그리고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다. 객체와 RDB는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이를 객체와 RDB의 패러다임 불일치 문제라고 한다.
따라서 객체 구조를 테이블 구조에 저장하는 데는 한계가 있다. 이런 문제들은 개발자가 해결해야 하는데, 문제는 너무 많은 시간과 코드를 소비하는 데 있다.
이번에는 패러다임의 불일치를 해결하는 JPA의 코드들을 살펴보겠다.
1) 상속
다음과 같은 상속 관계 객체 or 테이블이 있다고 하자. 객체를 저장하고 조회하는 코드는 다음과 같다.
//상속 관계의 객체 저장 방법
//1. JPA를 사용해 Album 객체 저장
jpa.persist(album);
// 사실 JPA는 다음 SQL를 실행하는 것이다.
INSERT INTO ITEM ~
INSERT INTO ALBUM ~
//2. JPA를 사용해 Album 객체 조회
String albumid = "id";
Album album = jpa.find(Album.class, albumid);
// 사실 JPA는 다음 SQL를 실행하는 것이다.
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID;
2) 연관관계
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다.
반면 테이블은 외래 키를 사용해서 다른 테이블과 조인한 후 연관된 정보를 조회한다.
이러한 차이는 생각보다 큰데, 예를 들어member를 이용해 team을 조회하는 동작을 코드로 표현하면 다음과 같다.
//객체의 경우 (Member.class에 getTeam() 함수가 있을 때)
Member member = new Member();
member.getTeam();
//테이블의 경우 (Member 테이블에 Team 테이블의 외래 키 컬럼이 있을 때)
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
객체는 연관된 객체의 참조를 반드시 보관해야하지만, 테이블의 경우 외래 키를 이용해 조인을 하면 된다.
즉, 만약에 Team 객체가 getMember() 함수와 같은 관련 멤버를 조회할 수 있는 기능이 없다면 Team -> Member 조회는 불가능하지만 테이블은 외래 키를 이용해 조인하면 구할 수 있다.
이렇게 둘 사이의 연관관계의 차이점도 개발자가 중간에서 변환 역할을 해야 한다.
JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다. 다음 코드를 보자.
member.setTeam(team); //회원과 팀 연관관계 설정
jpa.persist(member); //회원과 연관관계 함께 저장
개발자는 회원과 팀의 관계를 설정하고 회원 객체를 저장하기만 하면된다. JPA는 team의 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달한다.
3) 객체 그래프 탐색
사실 위에서 설명한 두 문제들은 개발자들이 어떻게 열심히하면 극복할 수 있는 문제들이다. 이번엔 극복하기 어려운 패러다임의 불일치 문제를 알아보자.
객체들이 이렇게 관계지어있을 때 객체를 이용해 자유롭게 탐색할 수 있는데, 이를 객체 그래프 탐색이라 한다.
만약 Member에서 Item까지 찾기 위해서는 (특정 조건에서) 다음과 같은 코드가 나올 수도 있다.
Item i = member.getOrder().getOrderItem().Item();
객체는 마음껏 객체 그래프를 탐색할 수 있어야 한다. 그런데 테이블도 이게 가능할까?
SQL을 직접 다루게 되면 처음 실행하는 SQL에 따라 객체 그래프의 범위가 정해진다. (JOIN 조건에 따라)
객체지향 개발자에게 이는 아주 큰 문제인데, 왜냐하면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데, 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없기 때문이다.
하지만 JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이런 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라 한다.
정리하자면 JPA는 패러다임의 불일치 문제를 해결해주고 정교한 객체 모델링을 유지하게 도와준다.
지금까지는 JPA를 문제 해결 위주로 살펴보았고, 본격적으로 JPA에 대해 알아보자.
3. JPA란 무엇인가?
JPA는 자바 진영의 ORM 기술 표준이다. 그림처럼 애플리케이션과 JDBC 사이에서 동작한다.
ORM(Object Relational Mapping)은 이름 그대로 객체와 RDB를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다. 즉, 개발자들이 마치 객체를 자바 컬렉션에 저장하는 것처럼(List.add(item) 같이) 만들게 도와준다.
따라서 객체 측면에서는 정교한 객체 모델링을 할 수 있고, RDB는 데이터베이스에 맞도록 모델링하면 된다.
덕분에 개발자는 데이터 중심인 RDB를 사용해도 객체지향 애플리케이션 개발에 집중할 수 있다.
자바 진영에도 다양한 ORM 프레임워크들이 있는데 그 중에 하이버네이트(Hibernate) 프레임워크가 가장 많이 사용되며, 거의 대부분의 패러다임 불일치 문제를 해결해주는 ORM 프레임워크이다.
JPA의 장점
- 1. 생산성 - 지루하고 반복적인 코드와 CRUD용 SQL을 개발자가 직접 작성하지 않고, CREATE TABLE 같은 DDL(Data Definition Language)도 자동 생성해주는 기능이 있다.
- 2. 유지보수 - SQL에 의존적인 개발이 된다면 하나의 컬럼이 추가되면 많은 SQL을 고쳐야 한다. 하지만 객체 지향으로 변할 시 유지보수해야 하는 코드 수가 줄어든다.
- 3. 패러다임의 불일치 해결
- 4. 성능 - JPA에서는 플러시 등 여러 기능들을 사용해 성능 최적화 기회를 제공한다. 자세한 내용은 뒤에서 살펴볼 예정이다.
* 잼민이의 질문 모음
(정리하면서 생긴 궁금증과 책에 있는 Q&A 내용들을 정리해두었습니다.)
Q1. 그럼 JPA = Hibernate 인가요?
A1. 그렇지 않다. Hibernate는 인터페이스로만 구성된 JPA를 실제로 사용하게 도와주는 구현체일 뿐이다.
* 참고 : JPA, Hibernate, Spring Data JPA의 차이점
Q2. 그럼 JPA를 사용하기 위해 Hibernate는 필수 요소이군요!
A2. 그렇지 않아 잼민아. JPA 구현체로써 Hibernate가 가장 많이 사용될 뿐이지, DataNucleus, EclipseLink 같은 것들도 있단다.
Q3. 자동으로 이것 저것 해준다던데, 성능이 느리진 않나요?
A3. JPA의 다양한 성능 최적화 기능을 잘 이해하고 사용하면 SQL을 직접 사용할 때보다 더 좋은 성능을 낼 수도 있다. 마치 지금 시대에 자바가 느리다고 말하는 것과 비슷한 질문이야 잼민아.
Q4. 이거 쓰면 SQL과 데이터베이스는 잘 몰라도 되나요?
A4. 매핑을 올바르게 하려면 객체와 RDB 모두 이해해야 한단다. 이것들을 모르고 ORM 프레임워크를 사용한다는 것은 ORM의 본질을 잘못 이해한 것.