개발세발은 안되요

[자바 ORM표준 JPA 프로그래밍] 01장 : JPA 소개 본문

책/자바 ORM 표준 JPA 프로그래밍

[자바 ORM표준 JPA 프로그래밍] 01장 : JPA 소개

금호박 2024. 8. 30. 23:32
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();

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

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

 

1.1.1 반복, 반복 그리고 반복

  기능을 추가하기 위해서는 SQL을 작성하고 JDBC API를 사용하는 비숫한 일을 반복하게 된다. 하지만 객체를 데이터베이스가 아닌 자바 컬렉션에 보관한다면 훨씬 간단해진다. 하지만 데이터베이스는 객체 구조와는 다른 데이터 중심의 구조를 가지기 때문에 객체를 데이터베이스에 직접 저장하거나 조회할 수는 없다. 따라서 개발자가 객체지향 애플리케이션과 데이터베이스 중간에서 SQL과 JDBC API를 사용해서 변환 작업을 해주어야 한다.

 

  문제는 객체를 데이터베이스에 CRUD 하려면 많은 SQL과 JDBC API를 코드로 작성해야 한다는 점이다. 그리고 테이블마다 이런 비슷한 일을 반복해야 하는데, 개발하려는 애플리케이션에서 사용하는 데이터베이스 테이블이 100개라면 무수히 많은 SQL을 작성해야 하고, 이런 비슷한 일을 100번은 더 반복해야 한다.

회원 등록용 API - 데이터베이스
String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES(?,?");
psmt.setString(1, member.getMemberId());
psmt.setString(2, member.getMemberNa,e());
psmt.executeUpdate(sql);​



회원 등록용 API - 자바 컬렉션

list.add(member);

 

 

 

1.1.2 SQL에 의존적인 개발

  필드를 추가한다거나 하는 수정 사항이 생겼을 때 객체를 데이터베이스에 저장한 경우 많은 코드를 수정해야 한다. 이 경우 여러 실수나 에러가 발생할 수 있는데, 이런 방식의 가장 큰 문제는 데이터 접근 계층을 사용해서 SQL을 숨겨도 어쩔 수 없이 DAO를 열어서 어떤 SQL이 실행되는지 확인해야 한다는 점이다.

 

  비즈니스 요구사항을 모델링한 객체를 엔티티라 하는데, SQL에 의존적인 상황에서는 개발자들이 엔티티를 신뢰하고 사용할 수 없다. 대신 DAO를 열어서 어떤 SQL이 실행되고 어떤 객체들이 함께 조회되는지 일일이 확인해야 한다. (= 이것은 진정한 의미의 계층 분할이라고 할 수 없다.) 

 

  물리적으로 SQL과 JDBC API를 데이터 접근 계층에 숨기는 데 성공했더라도 논리적으로는 엔티티와 아주 강한 의존관계를 가지고 있다. 이런 강한 의존관계 때문에 조회할 때는 물론이고 객체에 필드를 하나 추가할 때에도 DAO의 CRUD 코드와 SQL 대부분을 변경해야 하는 문제가 발생한다.

 

 

 

1.1.3 JPA와 문제 해결

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

 

1. 저장 기능

jpa.persist(member); // 저장

  

  persist() 메소드는 객체를 데이터베이스에 저장한다. 이 메소드를 호출하면 JPA가 객체와 매핑정보를 보고 적절한 INSERT SQL을 생성해서 데이터베이스에 저장한다. 매핑정보는 어떤 객체를 어떤 테이블에 관리할지 정리한 정보이다.

 

 

2. 조회 기능

String memberId = "helloId";
Member member = jpa.find(Member.class, memberId); // 조회

 

  find() 메소드는 객체 하나를 데이터베이스에서 조회한다. JPA는 객체와 매핑 정보를 보고 적절한 SELECT SQL을 생성해서 데이터베이스에 전달하고 그 결과로 Member 객체를 생성해서 반환한다.

 

 

3. 수정 기능

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

 

  JPA는 별도의 수정 메소드를 제공하지 않는데, 대신 객체를 조회해서 값을 변경하면 트랜잭션을 커밋할 때 데이터베이스에 적절한 SQL이 전달된다.

 

 

4. 연관된 객체 조회

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

 

 

 

1.2 패러다임의 불일치

  관계형 데이터베이스는 데이터 중심으로 구조화되어있고, 객체지향에서 이야기하는 추상화, 상속, 다형성과 같은 개념이 없다. 객체과 관계형 데이터베이스는 지향하는 목적이 다르기 때문에 둘의 기능과 표현 방법도 다른데, 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라고 한다. 
  JPA는 패러다임의 불일치 문제를 해결해주고 정교한 객체 모델링을 유지하게 도와준다. JPA를 통해 완화 가능한 문제로는 상속, 연관관계, 객체 그래프 탐색, 비교 등이 있다.

 

  객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공하기 때문에 현대의 복잡한 애플리케이션은 대부분 객체지향 언어로 개발한다. 비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다. 그러나 문제는 도메인 모델을 저장할 때 발생한다.

  특정 유저가 시스템에 회원 가입하면 회원이라는 객체 인스턴스를 생성한 후 이 객체를 메모리가 아닌 어딘가에 영구보관해야 한다.

 

 

  객체는 속성(필드)와 기능(메소드)를 가진다. 객체의 기능은 클래스에 정의되어 있기에 객체 인스턴스의 상태인 속성만 저장했다가 필요할 때 불러와서 복구하면 된다. 객체가 단순하면 객체의 모든 속성 값을 꺼내서 파일이나 DB에 저장하면 되지만, 상속이나 참조를 하고 있다면 객체의 상태를 저장하기는 어렵다.

  회원 객체를 저장해야 하는데 회원 객체가 팀 객체를 참조하고 있다면 회원 객체를 저장할 때 팀 객체도 함께 저장해야 한다. 단순히 회원 객체만 저장하면 참조하는 팀 객체를 잃어버리는 문제가 발생한다.

 

 

 자바는 이 문제를 고려하여 객체를 파일로 저장하는 직열화 기능과 저장된 파일을 객체로 복구하는 역 직열화 기능을 지원하지만, 직열화된 객체를 검색하기 어렵다는 문제가 있으므로 현실성이 없다. 현실적인 대안은 관계형 데이터베이스에 객체를 저장하는 것인데, 관계형 데이터베이스는 데이터 중심으로 구조화되어있고 객체지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다. (= 둘의 목적이 서로 다름. 패러다임 불일치 문제)

 

  애플리케이션은 자바라는 객체지향 언어로 개발하고 데이터는 관계형 데이터베이스에 저장해야 한다면, 패러다임의 불일치 문제를 개발자가 중간에서 해겨해야 하는데, 문제는 객체와 데이터베이스 사이의 패러다임 불일치 문제를 해결하는 데 너무 많은 시간과 코드를 소비하는 데 있다.

 

 

1.2.1 상속

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

객체 상속 모델

 

테이블 모델

 

  JDBC API를 사용해서 객체를 저장하려면 작성해야 하는 코드가 매우 많다. 

  각각의 객체를 저장하는 코드를 작성하고, 부모 객체에서 부모 데이터만 꺼내서 전용 INSERT SQL을 작성하고 자식 객체에서 자식 데티어만 꺼내어 또 INSERT SQL을 작성해야 한다. 또 자식 타입에 따라서 DTYPE도 저장해야 한다. 만약 조회를 하려 한다면 부모와 자식 테이블을 조인해서 조회한 후 그 결과로 자식 객체를 생성해야 한다.

 

 

  만약 객체들을 데이터베이스가 아닌 자바 컬렉션에 보관한다면, 부모자식이나 타입에 대한 고민 없이 해당 컬렉션을 그냥 사용하면 된다.

list.add(album);
list.add(movie);

Album album = list.get(albumId);

 

 

JPA와 상속

  JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 

 

  객체를 저장하려 할 경우, 앞서 설명한 persist() 메소드를 사용해서 객체를 저장한다.

jpa.persist(album);

 

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

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

 

 

 객체를 조회하려 할 경우, 앞서 설명한 find() 메소드를 사용해서 객체를 조회한다.

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

 

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

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

 

 

1.2.2 연관관계

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

  또 객체는 참조가 있는 방향으로만 조회할 수 있지만, 테이블은 외래 키 하나로 양방향 조회가 가능하다.

 

  객체를 테이블에 맞추어 모델링할 경우 객체를 테이블에 저장하거나 조회할 때는 편리하다. 관계형 데이터베이스는 조인이라는 기능이 있으므로 외래 키의 값을 그대로 보관해도 되지만 객체는 연관된 객체의 참조를 보관해야 참조를 통해 연관된 객체를 찾을 수 있다. 하지만 객체를 테이블에 맞추어 모델링하면 외래 키의 값을 필드에 그대로 보관하게 되고, 따라서 참조를 통해서 조회할 수 없게 된다. 이런 방식을 따르게 되면 좋은 객체 모델링은 기대하기 어렵고 결국 객체지향의 특징을 잃어버리게 된다.

 

 반면 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기가 쉽지 않다. 객체는 필드로 연관관계를 맺고 테이블은 외래 키로 연관관계를 맺기 때문에, 객체 모델은 외래 키가 필요없고 단지 참조만 있으면 된다. 그러나 테이블은 참조가 필요 없고 외래 키만 있으면 된다. 결국 개발자가 중간에서 변환 역할을 해야 한다. 이 과정에서 많은 비용이 소모된다. 만약 자바 컬렉션에 회우너 객체를 저장한다면 이런 비용이 전혀 들지 않는다.

 

JPA와 연관관계

  예를 들어 다음의 코드를 개발자는 작성하면 된다.

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

 

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

 

  객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리해준다.

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

 

 

 

1.2.3 객체 그래프 탐색

  객체에서 회원이 소속된 팀을 조회할 때는 참조를 사용해서 연관된 팀을 찾으면 된느데, 이것을 객체 그래프 탐색이라 한다. 객체는 마음껏 객체 그래프를 탐색할 수 있어야 하는데, SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다

  이것은 객체지향 개발자에겐 매우 큰 제약이다. 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없다. 결국 어디까지 객체 그래프 탐색이 가능한지 알아보려면 데이터 접근 계층인 DAO를 열어 SQL을 직접 확인해야 한다. 또 DAO에 조회하는 메소드를 상황에 따라 여러 벌 만들어 사용해야 한다.

 

 

JPA와 객체 그래프 탐색

  JPA를 사용하면 객체 그래프를 마음껏 탐색할 수 있다.

member.getOrder().getOrderItem()... // 자유로운 객체 그래프 탐색

 

  JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미운다고 해서 지연 로딩이라 한다.

 

  이런 기능들을 사용하기 위해 객체에 JPA와 관련된 어떤 코드들을 심어야 하는 것은 아니다. JPA는 지연 로딩을 투명하게 처리한다.

Member 객체를 보면 getOrder() 메소드의 구현 부분에 JPA와 관련된 어떤 코드도 직접 사용하지 않는다.
class Member{
	private Order order;
    
    public Order getOrder() {
    	retirn order;
    }
}​

 

 

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

  예를 들어 다음은 지연 로딩을 사용하는 코드이다. 여기서 마지막 줄의 order.getOrderDate() 같이 실제 Order 객체를 상요하는 시점에 JPA는 데이터베이스에서 ORDER 테이블을 조회한다.
// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);

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

 만약 Member 을 사용할 때마다 Order을 함께 사용하면, 위와 같이 한 테이블씩 조회하는 것보다는 Member를 조회하는 시점에 SQL조인을 사용해서 Member와 Order를 함께 조회하는 것이 효과적이다.

 

 

1.2.4 비교

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

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

  따라서 테이블의 로우를 구분하는 방법과 객체를 구분하는 방법에는 차이가 있다. 이런 이유로 데이터베이스의 같은 로우를 조회했지만 객체의 동일성 비교에는 실패할 수 있다. 만약 객체를 컬렉션에 보관한다면 동일성 비교에는 성공했을 것이다. 그러나 이런 패러다임의 불일치 문제를 해결하기 위해 데이터베이스의 같은 로우를 조회할 때마다 같은 인스턴스를 반환하도록 구현하는 것은 쉽지 않다. 여기에 여러 트랜잭션이 동시에 실행되는 상황까지 고려하면 문제를 더 어려워진다.

 

JPA와 비교

  JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다. 

// 회원 조회 - 실패
String memberId = "100Id";
Member member1 = memberDao.getMember(memberId);
Member member2 = memberDao.getMember(memberId);

member1 == member2; // false. 같은은 DB로우에서 조회했지만, 객체 측면에서는 다른 인스턴스.


// 객체를 컬렉션에 보관 - 성공. 그러나 비용 높음.
Member member1 = list.get(0);
Member member2 = list.get(0);

member1 == member2; // true


// JPA 사용 - 성공.
Stirng memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);

member1 == member2; // true

 

 

1.2.5  정리

  객체 모델과 관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르고, 개발자가 이 패러다임의 차이를 극복하기 위해 많은 시간과 코드를 소비한다. 또 정교한 객체 모델링을 할수록 패러다임의 불일치 문제는 더욱 커지고, 이 틈을 메우기 위해 개발자가 소모해야 하는 비용도 더 커진다. 결국 객체 모델링은 힘을 잃고 점점 데이터 중심의 모델로 변해간다.

  JPA는 패러다임의 불일치 문제를 해결해주고 정교한 객체 모델링을 유지하게 도와준다.

 

 

 

1.3 JPA란 무엇인가?

  JPA는 자바 진영의 ORM 표준 기술이다. 

JPA 동작 방식

 

 

  ORM은 객체와 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.

  ORM 프레임워크를 사용하면 객체를 데이터베이스에 저장할 때 INSERT SQL을 직접 작성하지 않고, 객체를 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 된다. ORM 프레임워크가 적절한 SQL을 생성해서 데이터베이스에 객체를 저장해준다.

 

 

  ORM 프레임워크는 SQL을 개발자 대신 생성해서 데이터베이스에 전달해주는 것뿐만 아니라 앞서 이야기한 다양한 패러다임의 불일치 문제들도 해결해준다. 덕분에 개발자는 데이터 중심인 관계형 데이터베이스를 사용해도 객체지향 애플리케이션 개발에 집중할 수 있다.

 

  성숙한 객체지향 언어에는 대부분 ORM 프레임워크들이 있는데, 각각의 프레임워크마다 제공하는 기능의 수준은 매우 다양하다. 자바 진영에도 다양한 ORM 프레임워크들이 있는데 그 중 하이버네이트 프레임워크가 가장 많이 사용된다. 하이버네이트는 거의 대부분의 패러다임 불일치 문제를 해결해주는 성숙한 ORM 프레임워크이다.

 

 

1.3.1 JPA 소개

   하이버네이트라는 ORM 프레임워크는 기존 EJB의 ORM 기술과 비교해서 가볍고 실용적인 데다 기술 성숙도도 높았다. 또 자바 엔터프라이즈 애플리케이션 서버 없이도 동작해서 많은 개발자가 사용하기 시작했고, EJB 3.0에서 하이버네이트를 기반으로 새로운 자바 ORM 기술 표준이 만들어졌는데 이것이 JPA이다.

EJB
  하이버네이트의 등장 이전 존재했던 기술 표준이다. 그 안에는 엔티티 빈이라는 ORM 기술이 포함되어 있었지만, 기술 성숙도가 떨어지고 자바 엔터프라이즈(J2EE) 애플리케이션 서버에서만 등장했다.

 

 

 

 

JPA 표준 인터페이스와 구현체

    JPA는 자바 ORM 기술에 대한 API 표준 명세이다. 즉 JPA를 사용하려면 JPA를 구현한 ORM 프레임워크를 선택해야 한다.  JPA라는 표준 덕분에 특정 구현 기술에 대한 의존도를 줄일 수 있고 다른 구현 기술로 손쉽게 이동할 수 있는 장점이 있다. 

 

 JPA 버전별 특징은 간략하게 정리하면 다음과 같다.

  1. JPA 1.0 2006년 : 초기 버전. 복합 키와 연관관계 기능이 부족했다.
  2. JPA 2.0 2009년 : 대부분의 ORM 기능을 포함하고 JPA Creteria가 추가되었다.
  3. JPA 2.1 2013년 : 스토어드 프로시저 접근, 컨버터, 엔티티 그래프 기능이 추가되었다.

 

1.3.2 왜 JPA를 사용해야 하는가?

1. 생산성

  JPA를 사용하면 자바 컬렉션에 객체를 저장하듯이 JPA에게 저장할 객체를 전달하기 때문에, SQL을 작성하고 JDBC API를 사용하는 반복적인 일을 직접 할 필요가 없다. 또 DDL 문을 자동으로 생성해주는 기능들도 제공하기 때문에 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전시킬 수 있다.

 

 

2. 유지보수

  SQL을 직접 다루면 엔티티에 필드를 하나만 추가해도 관련된 등록, 수정, 조회 SQL과 결과를 매핑하기 위한 JDBC API 코드를 모두 변경해야 하지만, JPA를 사용하면 이런 과정을 JPA가 대신 처리해주기 때문에 필드를 추가하거나 삭제해도 수정해야 할 코드가 줄어든다. 즉 유지보수해야 하는 코드 수가 줄어든다. 또 JPA가 패러다임의 불일치 문제를 해결해주기 때문에 객체지향 언어가 가진 장점들을 활용해서 유연하고 유지보수하기 좋은 도메인 모델을 편리하게 설계할 수 있다.

 

 

3. 패러다임의 불일치 해결

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

 

 

4. 성능

  JPA는 애플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 기회를 제공한다. JPA는 애플리케이션과 데이터베이스 사이에서 동작하는데, 이렇게 애플리케이션과 데이터베이스 사이에 계층이 하나 더 있으면 최적와 관점에서 시도해볼 수 있는 것이 많다. 또 하이버네이트는 SQL 힌트를 넣을 수 있는 기능도 제공한다.

String memberId = "helloId";
Member member1 = jpa.find(memberId);
Member member2 = jpa.find(memberId);

  
  JDBC API를 사용해서 코드를 직접 작성했다면 회원을 조회할 때마다 SQL을 사용해서 데이터베이스와 두 번 통신했을 것이다. 
  JPA를 사용하면 회원을 조회하는 SQL을 한 번만 데이터베이스에 전달하고 두 번째는 조회한 회원 객체를 재사용한다.

 

 

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

  관계형 데이터베이스는 같은 기능도 벤더마다 사용법이 다른 경우가 많기 때문에, 애플리케이션은 처음 선택한 데이터베이스 기술에 종속되고 다른 데이터베이스로 변경하기는 매우 어렵다. 그러나 JPA는 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 한다. 

 

 

6. 표준

  JPA는 자바 진영의 ORM 기술 표준이기에 다른 구현 기술로 쉽게 변경할 수 있다.