JPA는 자바 진영의 ORM 기술 표준이다.
ORM 프레임워크는 개발자가 직접 SQL을 작성할 때 생기는 문제를 해결하기 위해 탄생했다.
SQL을 직접 다룰 때 발생하는 문제점
자바로 개발하는 애플리케이션은 대부분 관계형 데이터베이스에 데이터를 저장한다. 관계형 데이터베이스를 이용해 데이터를 관리하려면 SQL을 사용한다. 자바 애플리케이션은 JDBC API를 사용해서 SQL을 데이터베이스에 전달한다. 이때 다양한 문제가 발생한다.
1. 반복
SQL을 직접 작성할 때는 비슷한 코드를 반복적으로 작성해야 한다. 기능을 수정하고 추가하기 위해 비슷한 SQL을 작성하고 JDBC API를 사용하는 일을 반복해야 한다.
객체를 데이터베이스가 아닌 자바 컬렉션에 보관한다면 단 한 줄로 객체를 저장할 수 있다.
하지만 데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가진다. 그렇기 때문에 객체를 데이터베이스에 직접 저장하거나 조회할 수 없다. 따라서 개발자가 객체지향 애플리케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용해 변환 작업을 직접 해줘야 한다.
2. SQL에 의존적인 개발
SQL을 직접 다루면, 데이터 접근 계층(DAO)을 사용해서 SQL을 숨겨도 기능을 수정하거나 문제를 고치기 위해 어쩔 수 없이 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다.
비즈니스 요구사항을 모델링한 객체를 엔티티라고 하는데, SQL에 모든 것을 의존하는 상황에서는 개발자들이 엔티티를 신뢰하고 사용할 수 없다. 대신 DAO를 열어서 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 확인해야 한다.
DAO를 이용하여 물리적으로는 SQL과 JDBC API를 숨기는 것에 성공했을지 모른다. 하지만 논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다. 즉, SQL을 직접 다룰 때는 SQL에 의존적인 개발을 피하기 어렵다.
패러다임의 불일치
애플리케이션은 발전하면서 내부의 복잡성도 점점 커진다. 지속 가능한 애플리케이션을 개발하는 일은 끊임없이 증가하는 복잡성과의 싸움이다. 복잡성을 제어하지 못하면 결국 유지보수하기 어려운 애플리케이션이 된다.
객체지향 프로그래밍은 추상화, 캡슐화, 정보 은닉, 상속, 다형성 등의 장치를 통해 시스템의 복잡성을 제어할 수 있다. 비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다.
문제는 도메인 모델을 저장할 때 발생한다. 객체는 속성(필드)과 기능(메소드)을 가진다. 객체의 기능은 클래스에 정의되어 있으므로 객체의 상태인 속성만 저장했다가 필요할 때 불러와서 복구하면 된다. 객체가 단순할 때는 객체의 모든 속성 값을 꺼내서 파일이나 데이터베이스에 저장하면 되기 때문에 큰 문제가 되지 않는다. 하지만 부모 객체를 상속받았거나, 다른 객체를 참조하고 있다면 객체의 상태를 저장하기는 쉽지 않다.
자바에는 객체를 파일로 저장하는 직렬화 기능과 저장된 파일을 객체로 복구하는 역직렬화 기능을 지원한다. 하지만 이 방법은 객체를 검색하기 어렵다는 문제가 있다.
현실적인 대안은 관계형 데이터베이스에 객체를 저장하는 것이다. 하지만, 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 또한 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 관계형 데이터베이스에는 존재하지 않는다.
정리하면, 객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르기 때문에 둘의 기능과 표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라고 한다. 패러다임 불일치 문제 때문에 객체 구조를 테이블 구조에 저장하는 데는 한계가 있다. 패러다임 불일치 문제를 해결하기 위해서는 개발자가 중간에서 많은 시간과 코드를 소비하여 해결해야 한다.
패러다임 불일치의 대표적인 문제점들은 다음과 같다.
1. 상속
객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없다. 그나마 데이터베이스 모델링에서 이야기하는 슈퍼타입, 서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블을 설계할 수 있다.
슈퍼타입, 서브타입 관계를 사용해도 여전히 패러다임 불일치를 해결하기 위한 비용이 들어간다. 부모 객체와 자식 객체에서 부모 데이터와 자식 데이터를 꺼내서 각각의 테이블에 저장하기 위한 SQL을 작성해야 한다. 또한 조회를 위해서도 부모와 자식 테이블을 조인하고 그 결과를 객체로 생성해야 한다.
만약 자바 컬렉션에 객체를 보관한다면 부모 자식이나 타입에 대한 고민 없이 한 줄로 해당 컬렉션을 사용하면 된다. 자바컬렉션은 객체지향 패러다임을 사용하기 때문이다.
2. 연관관계
객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회한다. 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회한다.
참조를 사용하는 객체와 외래 키를 사용하는 관계형 데이터베이스 사이의 패러다임 불일치는 객체지향 모델링을 거의 포기하게 만들 정도로 극복하기 어렵다.
2-1. 객체를 테이블에 맞추어 모델링
객체를 테이블에 맞추어 모델링하기 위해 참조 대신 외래 키를 객체에 저장한다고 해보자. 객체를 테이블에 저장하거나 테이블에서 조회할 때는 편리할 것이다. 관계형 데이터베이스에는 조인이라는 기능이 있으므로 외래 키 값을 그대로 보관해도 조회 과정에서 큰 어려움이 없다. 하지만 객체는 연관된 객체의 참조를 보관해야 참조를 통해 연관된 객체를 찾을 수 있다. 객체를 테이블에 맞추어 모델링하기 위해 참조 대신 외래 키를 저장하면 참조를 통해서 연관 객체를 조회할 수 없다.
이런 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 결국 객체지향의 특징을 잃어버리게 된다.
2-2. 객체지향 모델링
객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다. 개발자가 중간에서 변환을 해주어야 한다. 연관 객체를 외래 키 값으로 변환하여 테이블에 저장하고, 외래 키 값으로 연관 객체를 생성해 조회해야 한다. 이런 과정들은 패러다임 불일치를 해결하려고 소모하는 비용이다.
3. 객체 그래프 탐색
객체에서 참조를 사용해 연관된 객체를 찾는 것을 객체 그래프 탐색이라고 한다.
SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이것은 개발자에게 큰 제약이다. 언제 끊어질지 모르는 객체 그래프를 함부로 탐색할 수는 없기 때문이다. 어디까지 객체 그래프 탐색이 가능한지 알아보려면 DAO를 열어서 SQL을 직접 확인해야 한다. 이는 엔티티가 논리적으로 SQL에 종속되어서 발생하는 문제이다.
4. 비교
데이터베이스는 기본 키의 값으로 각 로우(row)를 구분한다. 반면 객체는 동일성(identity) 비교와 동등성(equality) 비교라는 두 가지 비교 방법이 있다.
동일성 비교는 == 비교다. 객체 인스턴스의 주소 값을 비교한다. 동등성 비교는 equals() 메소드를 사용하여 객체 내부의 값을 비교한다.
테이블의 로우를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있다. 따라서 데이터베이스의 같은 로우를 조회했지만 객체의 동일성 비교에는 실패할 수도 있다.
이런 패러다임 불일치 문제를 해결하기 위해 데이터베이스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여러 트랜잭션이 동시에 실행되는 상황까지 고려하면 더욱 쉽지 않다.
JPA와 문제 해결
SQL을 직접 다룰 때 발생하는 문제점과 객체지향과 관계형 데이터베이스의 패러다임 불일치로 인해 발생하는 문제점을 살펴보았다. JPA는 이런 문제들을 어떻게 해결했는지 살펴보자.
1. 개발자가 직접 SQL을 다루는 문제
개발자가 직접 SQL을 작성하지 않고 JPA가 제공하는 API를 사용한다. 그러면 JPA가 개발자 대신에 적절한 SQL을 생성해서 데이터베이스에 전달한다. 그렇게 개발자가 직접 SQL을 다루면서 생겼던 문제들이 해결된다.
JPA가 제공하는 CRUD API를 간단히 살펴보면 다음과 같다.
- 저장 기능
jpa.persist(member);
- 조회 기능
String memberId = "helloId";
Member member = jpa.find(Member.class, memberId);
- 수정 기능
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을 실행한다.
2. 패러다임 불일치 문제
2-1. 상속
개발자는 마치 자바 컬렉션에 객체를 저장하듯이 JPA에 객체를 저장하면 된다.
JPA를 사용하여 Item을 상속한 Album 객체를 저장해보자. persist() 메소드를 사용해서 객체를 저장하면 된다.
jpa.persist(album);
JPA는 두 SQL을 실행하여 객체를 ITEM 테이블과 ALBUM 테이블에 나누어 저장한다.
Album 객체를 조회할 때는 find() 메소드를 사용하면 된다.
String albumId = "id100";
Album album = jpa.find(Album.class, albumId);
JPA는 ITEM과 ALBUM 테이블을 조인해서 필요한 데이터를 조회하고 그 결과를 반환한다.
2-2. 연관관계
연관관계를 설정하고 JPA에 객체를 저장하면 된다. JPA는 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달한다. 객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리해준다.
member.setTeam(team);
jpa.persist(member);
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
2-3. 객체 그래프 탐색
JPA를 사용하면 객체 그래프 탐색 문제를 해결할 수 있다. JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 연관된 객체가 존재하지 않는 것을 걱정하지 않고 객체 그래프를 탐색할 수 있다. 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라고 한다.
JPA는 지연 로딩을 투명(transparent)하게 처리하기 때문에 추가적인 코드를 삽입할 필요가 없다.
아래 예시 코드는 객체 그래프를 탐색하는 코드이다. 지연 로딩을 위한 추가적인 코드가 없음을 알 수 있다.
// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);
Order order = member.getOrder();
order.getOrderDate(); // order를 사용하는 시점에 SELECT ORDER SQL
위 코드에서 order 변수를 선언하고 member.getOrder()를 실행할 때까지는 데이터베이스 조회가 발생하지 않는다. order 변수를 사용할 때(order.getOrderDate())에 비로소 데이터베이스 조회가 발생한다.
위와 같이 연관 객체를 사용할 때 데이터베이스 조회가 발생하기 때문에 객체 그래프 탐색 문제가 해결된다.
JPA는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 조회할지를 간단한 설정으로 정의할 수 있다.
2-4. 비교
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(Java Persistence API)는 자바 진영의 ORM 표준 기술이다. JPA는 애플리케이션과 JDBC 사이에서 동작한다.
ORM(Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
ORM 프레임워크를 사용하면, 객체 측면에서는 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면 된다. 그리고 둘을 어떻게 매핑해야 하는지 매핑 방법만 ORM 프레임워크에게 알려주면 된다.
자바 진영에는 다양한 ORM 프레임워크들이 있는데 그중에 하이버네이트 프레임워크가 가장 많이 사용된다. 하이버네이트는 거의 대부분의 패러다임 불일치 문제를 해결해주는 성숙한 ORM 프레임워크다.
JPA 소개
과거 자바 진영은 엔터프라이즈 자바 빈즈(EJB)라는 기술 표준을 만들었다. 그 안에는 엔티티 빈이라는 ORM 기술도 포함되어 있었다. 하지만 너무 복잡하고 기술 성숙도도 떨어졌으며 자바 엔터프라이즈(J2EE) 애플리케이션 서버에서만 동작했다.
이때 하이버네이트라는 오픈소스 ORM 프레임워크가 등장했다. EJB의 ORM 기술과 비교해서 가볍고 실용적이고 기술 성숙도도 높았다. 또한 J2EE 애플리케이션 서버 없이도 동작해서 많은 개발자가 사용하기 시작했다. 결국 EJB 3.0에서 하이버네이트를 기반으로 새로운 자바 ORM 기술 표준이 만들어졌는데 이것이 바로 JPA다.
JPA는 자바 ORM 기술에 대한 API 표준 명세다. 즉, 인터페이스를 모아둔 것이다. 따라서 JPA를 사용하려면 JPA를 구현한 ORM 프레임워크를 선택해야 한다. ORM 프레임워크 중 하이버네이트가 가장 대중적이다.
JPA라는 표준 덕분에 특정 구현 기술에 대한 의존도를 줄일 수 있고 다른 구현 기술로 손쉽게 이동할 수 있는 장점이 있다.
왜 JPA를 사용해야 하는가?
1. 생산성
SQL을 작성하고 JDBC API를 사용하는 지루하고 반복적인 일은 JPA가 대신 처리해준다. JPA를 사용하면 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전시킬 수 있다.
2. 유지보수
개발자가 작성해야 했던 SQL과 JDBC API 코드를 JPA가 대신 처리해주므로 유지보수해야 하는 코드 수가 줄어든다. 또한 JPA가 패러다임의 불일치 문제를 해결해주므로 객체지향 언어가 가진 장점을 활용해서 유연하고 유지보수하기 좋은 도메인 모델을 편리하게 설계할 수 있다.
3. 패러다임의 불일치 해결
JPA는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임의 불일치 문제를 해결해준다.
4. 성능
JPA는 애플리케이션과 데이터베이스 사이에서 동작한다. 이렇게 애플리케이션과 데이터베이스 사이에 계층이 하나 더 있으면 최적화 관점에서 시도해 볼 수 있는 것들이 많다.
5. 데이터 접근 추상화와 벤더 독립성
관계형 데이터베이스는 같은 기능도 벤더마다 사용법이 다른 경우가 많다. 애플리케이션은 처음 선택한 데이터베이스 기술에 종속되고 다른 데이터베이스로 변경하기는 매우 어렵다.
JPA는 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 한다.
6. 표준
JPA는 자바 진영의 ORM 기술 표준이다. 표준을 사용하면 다른 구현 기술로 손쉽게 변경할 수 있다.
참고 자료
- 자바 ORM 표준 JPA 프로그래밍
'IT 도서 정리 > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA] 6. 다양한 연관관계 매핑 (4) | 2024.07.10 |
---|---|
[JPA] 5. 연관관계 매핑 기초 (3) | 2024.07.03 |
[JPA] 4. 엔티티 매핑 (3) | 2024.07.02 |
[JPA] 3. 영속성 관리 (4) | 2024.06.29 |
[JPA] 2. JPA 시작 (3) | 2024.06.27 |