JPA 소개

신윤섭·2023년 6월 25일

JPA 정복하기

목록 보기
1/3

항상 써왔던 JPA에 대해서 더욱 심도있게 공부해 보고자 이 시리즈를 시작하게 되었다. 지금까지 JPA를 사용하여 개발할 때는 '일단 되게 하자'라는 생각으로 어노테이션이나 메소드의 동작 방식에 대해 이해하려 하지 않고 코드를 작성해 나갔다. 최근 친구들과 사이드 프로젝트를 진행하면서 JPA를 사용하여 앱을 개발중인데 정작 내가 도입하자고 한 JPA에 대해서 설명하려니 모르는게 너무 많다고 느껴졌다. 이번 기회에 JPA를 제대로 공부하고 정리하여 역량을 키워야겠다.

이 글은 김영한님께서 지으신 '자바 ORM 표준 JPA 프로그래밍'의 내용을 바탕으로 작성하였습니다.

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

데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가지므로 객체를 데이터베이스에 직접 저장하거나 조회할 수 없다. 따라서 개발자가 객체지향 애플리케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용해서 변환 작업을 직접 해주어야 하는데, 문제는 객체를 데이터베이스에 CRUD하려면 너무 많은 SQL과 JDBC API를 코드로 작성해야 한다는 점이다. 이렇듯 데이터 접근 계층(DAO)을 개발하는 일은 지루함과 반복의 연속이다.

또한, 요구사항이 추가되거나 변경된다면 사용하는 SQL을 필히 확인해야 한다. 이는 SQL에 의존적이며 DAO를 통해 SQL을 숨기는 행동의 의미가 없어지게 만들고, 이렇게 SQL에 모든 것을 의존하는 상황에서는 개발자들이 엔티티(비즈니스 요구사항을 모델링한 객체)를 신뢰하고 사용할 수 없다. 이것은 진정한 의미의 계층 분할이 아니다.

요약

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

JPA와 문제 해결

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

다음 코드는 JPA가 제공하는 CRUD API다.

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를 사용하면 연관된 객체를 마음껏 조회할 수 있다. */

2. 패러다임의 불일치

객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라 한다. 애플리케이션은 자바라는 객체지향 언어로 개발하고 데이터는 관계형 데이터베이스에 저장해야 한다면, 패러다임의 불일치 문제를 개발자가 중간에서 해결해야 한다. 문제는 이런 객체와 관계형 데이터베이스 사이의 패러다임 불일치 문제를 해결하는 데 너무 많은 시간과 코드를 소비한다는 점이다.

패러다임의 불일치로 인해 발생하는 문제와 JPA를 통한 해결책은 다음과 같다.

1) 상속

객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다(일부 데이터베이스는 상속 기능을 지원하지만 객체의 상속과는 약간 다르다).

//객체 모델 코드

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;
}

위의 코드를 보면 Album, Movie, Book 객체는 모두 Item 객체를 상속한다.
Album 객체를 저장하려면 이 객체를 분해해서 다음 두 SQL을 만들어야 한다.

INSERT INTO ITEM ...
INSERT INTO ALBUM ...

위의 코드를 완성하려면 부모 객체에서 부모 데이터만 꺼내서 ITEM용 INSERT SQL을 작성하고 자식 객체에서 자식 데이터만 꺼내서 ALBUM용 INSERT SQL을 작성해야 하므로 코드량이 많아진다. 조회하는 것도 쉬운 일이 아니다. Album을 조회한다면 ITEM과 ALBUM테이블을 조인해서 조회한 다음 그 결과로 Album 객체를 생성해야 한다. 이런 과정이 모두 패러다임의 불일치를 해결하려고 소모하는 비용이다.

JPA와 상속

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

JPA를 사용해서 Item을 상속한 Album 객체를 저장해보자.

jpa.persist(album);

JPA는 다음 SQL을 실행해서 객체를 ITEM, ALBUM 두 테이블에 나누어 저장한다.

INSERT INTO ITEM...
INSERT INTO ALBUM...

Album 객체를 조회해보자.

String albumId = "id100";
Album album = jps.find(Album.class, albumId);

JPA는 ITEM과 ALBUM 두 테이블을 조인해서 필요한 데이터를 조회하고 그 결과를 반환한다.

SELECT I.*, A.*
	FROM ITEM I
    JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID

2) 연관관계

객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다. 반면에 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다. 이 패러다임 불일치는 극복하기 매우 어려운 문제이다.

class Member {
	Team team;
    ...
    Team getTeam() {
    	return team;
    }
}

class Team {
	...
}

member.getTeam(); //member -> team 접근

위의 코드를 보면 Member 객체는 Member.team 필드에 Team 객체의 참조를 보관해서 Team 객체와 관계를 맺는다. 이 참조 필드에 접근하면 Member와 연관된 Team을 조회할 수 있다.

SELECT M.*, T.*
	FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

위의 SQL을 보면 MEMBER MEMBER.TEAM_ID 외래 키 컬럼을 사용해서 TEAM 테이블과 관계를 맺는다. 이 외래 키를 사용해서 MEMBER 테이블과 TEAM 테이블을 조인하면 MEMBER 테이블과 연관된 TEAM 테이블을 조회할 수 있다.

JPA와 연관관계

JPA는 연관관계와 관련된 패러다임의 불일치 문제를 해결해준다.

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

Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

3) 객체 그래프 탐색

객체에서 회원이 소속된 팀을 조회할 때는 다음처럼 참조를 사용해서 연관된 팀을 찾으면 되는데, 이것을 '객체 그래프 탐색'이라 한다.

Team team = member.getTeam();

객체는 마음껏 객체 그래프를 탐색할 수 있어야 하지만 SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이것은 객체지향 개발자에겐 너무 큰 제약이다. 왜냐하면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그레프를 함부로 탐색할 수는 없기 때문이다.

JPA와 객체 그래프 탐색

JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다. JPA는 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미루는 '지연 로딩'을 하기 때문에 연관된 객체를 신뢰하고 마음껏 조회할 수 있다.

4) 비교

데이터베이스는 기본 키의 값으로 각 로우(row)를 구분한다. 반면에 객체는 동일성(identity) 비교와 동등성(equality) 비교라는 두 가지 비교 방법이 있다.

  • 동일성 비교 : 객체 인스턴스의 주소 값을 비교 (==)
  • 동등성 비교 : 객체 내부의 값을 비교 (equals() 메소드)

이렇듯 테이블의 로우를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있기 때문에 데이터베이스의 같은 로우를 조회했지만 객체의 동일성 비교에는 실패하는 경우도 존재한다.

JPA와 비교

JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다. 그러므로 다음 코드에서 member1과 member2는 동일성 비교에 성공한다.

String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2; //같다.

3. JPA란 무엇인가?

JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준이다.

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

ORM 프레임워크를 사용하면 개발자는 데이터 중심인 관계형 데이터베이스를 사용해도 객체지향 애플리케이션 개발에 집중할 수 있다.

1) JPA 소개

JPA는 자바 ORM 기술에 대한 API 표준 명세다. 쉽게 이야기해서 인터페이스를 모아둔 것이다. 따라서 JPA를 사용하려면 JPA를 구현한 ORM 프레임워크를 선택해야 한다. JPA를 구현한 ORM 프레임워크 중 하이버네이트가 가장 대중적이다.

2) 왜 JPA를 사용해야 하는가?

생산성

JPA를 사용하면 자바 컬렉션에 객체를 저장하듯이 JPA에게 저장할 객체를 전달하면 된다. INSERT SQL을 작성하고 JDBC API를 사용하는 지루하고 반복적인 일은 JPA가 대신 처리해준다.

유지보수

개발자가 작성해야 했던 SQL과 JDBC API 코드를 JPA가 대신 처리해주므로 유지보수해야 하는 코드 수가 줄어든다.

패러다임의 불일치 해결

JPA는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임의 불일치 문제를 해결해준다.

성능

JPA는 애플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 기회를 제공한다.

데이터 접근 추상화와 벤더 독립성

애플리케이션은 처음 선택한 데이터베이스 기술에 종속되고 다른 데이터베이스로 변경하기는 매우 어려운데 JPA는 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 한다. 만약 데이터베이스를 변경하면 JPA에게 다른 데이터베이스를 사용한다고 알려주기만 하면 된다.

표준

JPA는 자바 진영의 ORM 기술 표준이다. 표준을 사용하면 다른 구현 기술로 손쉽게 변경할 수 있다.

0개의 댓글