[1장] JPA 소개

1. JPA 자바 ORM 기술 표준


JPA 소개

JPA는 지루하고 반복적인 CRUD SQL을 알아서 처리해줄 뿐만 아니라 객체 모델링과 관계형 데이터베이스 사이의 차이점도 해결해주었다. 그리고 JPA는 실행 시점에 자동으로 SQL을 만들어서 실행하는데, JPA를 사용하는 개발자는 SQL을 직접 작성하는 것이 아니라 어떤 SQL이 실행될지 생각만 하면 된다. 참고로 JPA가 실행하는 SQL은 쉽게 예측할 수 있다. 반복적인 CRUD SQL을 작성하고 객체를 SQL에 매핑하는 데 시간을 많이 허비한다. 이미 많은 개발자들은 오랫동안 비슷한 고민을 해왔고 문제를 해결하려고 많은 노력일 기울였는데, 그 노력의 결정체가 바로 JPA다.

장점

  • CRUD SQL을 작성하지 않아서 반복적인 SQL문에 대해 보일러플레이트를 줄인다.
  • 조회된 결과를 객체로 매핑하는 작업도 대부분 자동으로 처리주므로 데이터 저장 계층에 작성해야 할 코드가 현저히 줄어든다.
  • 애플리케이션이 SQL이 아닌 객체 중심으로 개발하니 생산성과 유지보수가 좋아졌고 테스트를 작성하기도 편리해 졌다.
  • JPA로 인해 코드를 수정하지 않고 데이터베이스를 손쉽게 변경할 수 있다.

더 많은 시간을 쓰자.

귀찮은 문제는 JPA에게 맡기고 더 좋은 객체 모델링과 더 많은 테스트를 작성하는데 우리의 시간을 쓰자.

2. SQL을 직접 다룰 때 발생하는 문제점


반복, 반복 그리고 반복

// DB에 회원 테이블이 이미 있다는 가정...

// 회원 객체
public class Member {
		...
}

// DAO
public class MemberDAO {
		public Member find(String memberId){...}
}

// 회원 조회
String sql = "SELECT member_id, name FROM member m WHERE member_id = ?"
// JDBC API로 SQL 실행
ResultSet rs = stmt.excuteQuery(sql);
// 조회 결과를 Member 객체로 매핑
String memberId = rs.getString("member_id")
String name = rs.getString("name")
Member member = new Member();
member.setMemberId(memberId);
...

회원을 조회하는 기능과 등록을 만들고 수정, 삭제를 만들다보면 SQL을 작성하고 JDBC API를 사용하는 비슷한 일을 반복해야 한다. 회원 객체를 데이터베이스가 아닌 자바 컬렉션에 보관한다면 컬렉션은 한 줄로 객체를 저장할 수 있다.

list.add(member);

하지만 데이터베이스는 객체 구조와는 다른 데이터 중심 구조를 가지므로 객체를 데이터베이스에 직접 저장하거나 조회할 수는 없다. 결국 개발자는 객체지향 애플레케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용해서 변환작업을 직접 해줘야한다.

결국 모든 CRUD 더 나아가 테이블의 숫자가 100개라면 무수히 많은 CRUD에 대한 반복적인 SQL을 작성해야 한다. 데이터 접근 계층(DAO)을 개발하는 일은 이렇듯 지루함과 반복의 연속이다.

SQL에 의존적인 개발

요구사항이 추가되었을 때 절차

회원의 연락처도 함께 저장해달라는 요구사항이 추가되었다. 다음과 같은 절차를 따라야 한다.

  1. 테이블에 연락처 컬럼을 추가
  2. 회원 객체에 연락처에 대한 필드를 추가
  3. SQL 쿼리문에 연락처를 저장할 수 있게 수정
  4. 조회 코드에서 SQL 연락터를 가져오도록 쿼리문 수정
  5. 조회 코드에서 setter로 연락처를 멤버 클래스로 전달하도록 수정
  6. 번외로 연관 관계 맵핑이라면 조회에 대한 쿼리문도 연관관계맵핑이 되도록 새로 짜야한다.

자바 컬렉션에 보관했다면…

필드를 추가 한다고 해서 이렇게 많은 코드를 수정할 필요가 없다.

list.add(member); // 등록
Member member = list.get(xxx); // 조회
member.setTel("xxx") // 수정

문제점

  • 특정 객체가 연관된 객체가 존재할 때 객체를 사용할 수 있을지 없을지는 전적으로 사용하는 SQL에 달려 있다. 이런 방식의 가장 큰 문제는 데이터 접근 계층을 사용해서 SQL을 숨겨도 어쩔 수 없이 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다.
  • 객체끼리 연관 관계일 때 비즈니스 요구사항을 모델링한 객체를 엔티티라 하는데 SQL 중심 개발로 인해 모든 것을 의존하는 상황에서는 엔티티를 신뢰하고 사용할 수 없다.
  • 강한 의존관계 때문에 회원을 조회할 때는 물론이고 객체에 필드 하나 추가할 때도 DAO는 CRUD코드와 SQL 대부분을 변경해야 하는 문제가 생긴다.

요약

  • 진정한 의미의 계층 분할이 어렵다.
  • 엔티티를 신뢰할 수 없다.
  • SQL에 의존적인 개발을 피하기 어렵다.

JPA와 문제 해결

어떻게 해결할까?

JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 사용하면 된다. 그러면 JPA가 사용하는 데이터베이스에 맞게 적절한 SQL을 생성해서 데이터베이스에 전달한다.

CRUD API

// 저장 기능
jpa.persist(member);

// 조회 기능
Stirng memberId = "1";
Member member = jpa.find(Member.class, memberId); //조회

// 수정 기능
Member member = jpa.find(Member.class, memberId); 
member.setName("이름변경") // 수정

// 연관된 객체 조회
Mmeber member = jpa.find(Memeber.class, memberId);
Team team = member.getTeam(); // 연관된 객체 조회
  • persist()
    • 이 메소드를 호출하면 JPA가 개체와 매핑정보를 보고 적절한 INSERT SQL을 생성해서 데이터베이스에 전달한다.
  • find()
    • JPA는 객체와 매핑정보를 보고 적절한 SELECT SQL을 생성해서 데이터베이스에 전달하고 그 결과로 지정한 객체를 생성해서 반환한다.
  • 수정기능
    • JPA는 별도의 수정 메소드를 제공하지 않는다. 대신 객체를 조회해서 값을 변경만 하면 트랜잭션이 커밋할 때 데이터베이스에 적절한 UPDATE SQL이 전달 된다.
  • 연관된 객체 조회
    • JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다.

3. 패러다임 불일치


불일치 문제

애플리케이션은 발전하면서 그 내부의 복잡도가 점점 커져왔습니다. 애플리케이션 개발에서 복잡성은 계속 증가하며, 객체지향 프로그래밍은 이를 관리하는데 도움을 주지만 객체 저장과 관리는 어려운 문제입니다. 해당 문제는 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 객체지향의 추상화, 상속, 다형성과 같은 개념을 지원하지 않으므로 관계형 데이터베이스에 객체를 저장할 때 두 패러다임의 불일치로 인해 생기는 어려움 나타납니다. 즉, 객체와 관계형 데이터베이스는 다른 목적과 표현 방법을 가지고 있어서 두 개를 조화롭게 사용하는 것이 복잡하고 한계가 있습니다.

JDBC API 상속

객체는 상속이라는 기능이 있지만 테이블은 상속이라는 기능이 없다. 데이터베이스 모델링에서 이야기하는 슈퍼타입 서브타입 관계를 사용하면 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.

  • 객체 상속 모델
  • 테이블 모델

객체 모델 코드

abstract class Item {
		Long id;
		String name;
		int price;
}

class Album extends Item {
		String artist;
}

class Movie extends Item {
		Stirng director;
		String actor;
}

class Book extends Item {
		String author;
		String isbn;
}

Album,Movie 객체를 저장하려면 이 객체를 분해해서 다음 두 SQL을 만들어야 한다.

INSERT INTO ITEM ...
INSERT INTO ALBUM ...
  • 저장은 쉬운일이 아니다.
    • JDBC API를 사용해서 이 코드를 완성하려면 부모 캑체에서 부모 데이터만 꺼내서 ITEM용 INSERT SQL을 작성하고 자식 객체에서 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 하는데 코드량이 만만치 않다. 그리고 자식 타입에 따라서 DTYPE도 저장해야 한다.
  • 조회도 쉬운일은 아니다.
    • Album을 조회한다면 ITEM과 ALBUM테이블을 조인해서 조회한 다음 그 결과로 Album 객체를 생성해야 한다.

이런 과정이 결국 패러다임 불일치를 해결하려고 소모하는 비용이다.

컬렉션에 보관한다면

자식이나 타입에 대한 고민 없이 해당 컬렉션을 그냥 사용하면 된다.

list.add(album);
list.add(movie);
Album album = list.get(albumId);

JPA와 상속

JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 마치 자바 컬렉션에 객체를 저장하듯이 JPA에게 객체를 저장하면 된다.

JPA를 사용해서 item을 상속한 Album 객체를 저장하기.

// JPA 객체 저장
jpa.persist(album);

// JPA가 만들어주는 SQL 생성 로직
INSERT INTO ITEM ...
INSERT INTO ALBUM ...

// 조회
String albumId = "1";
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

연관 관계

객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다. 반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.

  • 객체와 외래키를 사용하는 관계형 데이터베이스 사이의 패러디임 불일치는 객체지향 모델링을 거의 포기하게 만들 정도로 극복이 어렵다.
  • Member 객체는 Member.team 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다. 따라서 이 참조 필드에 접근하면 Meber와 연괸된 Team을 조회 할 수 있다.
  • Member 테이블은 MEMBER.TEAM_ID 외래 키 컬럼을 사용해서 TEAM 테이블과 관계를 맺는다. 이 외래 키를 사용해서 테이블끼리 조인을 하면 주가되는 테이블과 연관된 테이블을 조회할 수 있다.
  • JPA가 만들어주는 JOIN QUERY
SELECT M.*, T,*
	FROM MEMBER M
	JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

객체는 참조가 있는 방향으로만 조회할 수 있다. 참조하지 않은 반대 방향으로 조회가 불가능하다.

  • 연관 관계
  • 객체 연관 관계 코드
class Member {
		Team team;
		...
		Team getTeam() {
				return team;
		}
}

class Team{
		...
}
// member -> team 접근
member.getTeam(); 

객체를 테이블에 맞추어 모델링

  • 테이블에 맞춘 모델링 코드
// 객체를 테이블에 맞추어 모델링 코드
class Member {
		String id;       // MEMBER_ID 컬럼 사용
		Long teamId;     // TEAM_ID FK 컬럼 사용
		String username; // USERNAME 컬럼 사용
}

class Team {
		Long id;     // TEAM_ID PK 사용
		String name; // NAME 컬럼 사용
}

객체를 관계형 데이터베이스에 맞추면 CRUD 작업이 편리하지만 객체 간의 참조와 관계가 복잡해집니다. 이로 인해 객체 지향의 핵심 특성이 퇴색될 수 있습니다. 모델링 시 객체와 데이터베이스 간의 균형을 고려하는 것이 중요합니다.

  • 참조를 사용하는 객체 모델
// 참조를 사용하는 객체 모델
class Member {
		String id;       // MEMBER_ID 컬럼 사용
		Team team;       // 참조로 연관관계를 맺는다.
		String username; // USERNAME 컬럼 사용

		Team getTeam() {
				return team;
		}
}

class Team {
		Long id;     // TEAM_ID PK 사용
		String name; // NAME 컬럼 사용
}

Team team = member.getTeam();

객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회가 쉽지 않다. 그러한 이유로 객체 모델은 외래 키가 필요 없고 참조만 있으면 되지만 테이블은 참조가 필요 없고 외래 키만 있으면된다. 결국 개발자가 중간에 변환 작업을 해야한다.

  • 변환 작업
// INSERT SQL을 만들때 필요한 값 받아오기
member.getId();
member.getTeam().getId();
member.getUsername();

// 조회
public Member find(Stirng memberId) {
		// SQL 실행
		...

		// DB에서 조회한 회원 관련 정보를 모두 입력
		Member member = new Member();
		...

		// DB에서 조회한 팀 관련 정보를 모두 입력
		Team team = new Team();
		...

		// 회원과 팀 관계 설정
		member.setTeam(team);
		return member
}

패러다임 불일치를 해결하려고 소모하는 비용으로 자바 컬렉션에 회원 객체를 저장한다면 이런 비용이 들지 않는다.

  • JPA와 연관 관계
member.setTeam(team); // 회원과 팀 연관관계 설정
jpa.persist(member);  // 회원과 연관관계 함께 저장

개발자는 회원과 팀 관계를 설정하고 회원 객체를 저장하면 된다. JPA는 team의 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달 한다.

  • 객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리해준다.
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

객체 그래프 탐색

객체에서 연관 관계에 대해 조회할 때 참조를 사용해서 연관된 객체를 찾는과정을 객체 그래프 탐색이라 한다.

  • 객체 연관 관계
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다.

  • 개발자에게는 너무 큰 제약으로 비즈니스 로직에 다라 사용하는 객체 그래프가 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수 없기 때문이다.
  • 객체와 연관된 방향으로 객체 그래프를 탐색할 수 있을지 없을지는 코드만 보고는 예측이 안되서 결국 SQL을 직접 확인 해야한다. 결국 이런 문제는 엔티티가 SQL에 논리적으로 종속되어서 발생하는 문제다.

JPA와 객체 그래프 탐색

JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다. JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다.

  • 해당 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩(Lazy Loding) 이라 한다.
// 지연 로딩 코드
// 처음 조회 시점에 SELECT MEMBER SQL;
Member member = jpa.find(Member.class, memberId);

Order order = member.getOrder();
order.getOrderDate(); // Order를 사용하는 시점에 SELECT ORDER SQL

한 테이블씩 조회하는 것보다 Member를 조회하는 시점에 SQL 조인을 사용해서 함께 조회하는게 효과적이다.

JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 죄할지를 간단한 설정으로 정의할 수 있다.

비교

  • Data Base 비교

데이터베이스는 기본 키 값의 로우를 구분한다.

  1. 동일성 비교 ==, 객체 인스턴스의 주소 값을 비교
  2. 동등성 비교 equals() 메소드를 사용해서 객체 내부의 값을 비교

따라서 테이블의 로우를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있다.

패러다임의 불일치 문제를 해결하기 위해 데이터에비스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여기에 여러 트랜잭션이 동시에 실행되는 상황까지 고려하면 문제는 더 어려워진다.

  • JPA와 비교
    • JPA는 같은 트랙잭션일 때 같은 객체가 조회되는 것을 보장한다. 그래서 동일성 비교를 해보면 성공한다.

4. 결국 JPA란 무엇인가?

JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준이다. 애플리케이션과 JDBC 사이에서 동작한다.

ORM이란 무엇인가?

ORM(Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.

  • ORM 프레임워크를 사용하면 쿼리를 작성하지 않고 객체를 마지 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 된다.

SQL을 개발자 대신 생성해서 데이터베이스에 전달 해주는 것뿐만 아니라 다양한 패러다음의 불일치 문제들도 해결해준다. 따라서 객체 측면에서는 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스에 맞도록 모델링 하면 된다.

  • 맵핑 방법만 ORM 프레임워크에 알려주면 된다.

JPA 소개

Enterprise JAVA Beans라는 표준 기술을 만들었는데 그 안에 엔티티 빈이라는 ORM 기술도 포함되어 있었다. 이 기술은 복잡하고 성숙도가 떨어졌으며 J2EE 서버에서만 동작했다. 이때 하이버네이트(Hibernate)라는 오픈소스 ORM 프레임워크가 등장했다. 이전 기술에 비해 가볍고 실욕적인 데다 기술 성숙도도 높았다. 그리고 애플리케이션 서버 없이도 동작해서 많은 개발자가 사용하기 시작했다. 결국 EJB 3.0에서 하이버네이트를 기반으로 새로운 자바 ORM 기술 표준이 만들어졌는데 이것이 바로 JPA다

왜 JPA를 사용해야 할까?

  • 생산성
    • 자바 컬렉션에 객체를 저장하듯이 JPA에게 저장할 객체를 전달하면 된다. 반복작업인 CRUD에 대해 JPA가 대신 처리해줌으로써 개발자는 비즈니스 로직에 더 집중할 수 있다.
  • 유지보수
    • 엔티티에 필드를 하나만 추가해도 관련된 모든 코드는 다 수정해야되는데 JPA를 사용하면 이러한 과정을 모두 JPA가 대신 처리해줌으로써 수정해야할 코드가 현저히 줄어든다. 결과적으로 관리 영역이 줄어드니 유지보수 영역도 줄어들어 관리가 수월해진다.
  • 패러다임 불일치 해결
    • JPA는 기존에 해결하기 어려운 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임 불일치 문제를 해결해 준다.
  • 성능
    • JPA는 애플리케이션과 데이터베이스 사이에 다양한 성능 최적화 기회를 제공한다.
    • JPA는 이전에 사용한 CRUD를 반복적으로 사용할 때 조회한 객체를 재사용한다. (처음은 조회 다음은 재사용)
    • 하이버 네이트는 SQL 힌트도 넣을 수 있는 기능도 제공한다.
  • 데이터 접근 추상화와 벤더 독립성
    • JPA는 애플리케이션과 데이터베이스 사이에 추상화 된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않게 한다.
    • H2로 로컬로 개발하다가 다른 데이터베이스로 옮겨 사용하는데 어렵지 않게 바로 사용할 수 있다.
LIST