[JPA/김영한] 01. JPA 소개

Ogu·2024년 3월 2일
1

JPA 도입이 활성화되기 이전에, 자바로 애플리케이션을 개발하면 관계형 데이터베이스(RDS)를 다루며, SQL 반복 작성은 피할 수 없는 일이었습니다. 개발자들은 SQL과 JDBC API를 작성하는데 매우 많은 시간을 소요하였습니다.
iBatis(MyBatis)나 JdbcTemplate와 같은 SQL 매퍼가 등장하며 JDBC API 사용 코드를 줄일 수 있었지만, 여전히 CRUD의 SQL은 반복해서 작성해야 했고, 비생산적이었습니다.

많은 개발자들은 RDS 테이블과 객체의 모델링의 차이에서 발생하는 문제를 중간에서 해결하기 위해 SQL을 작성해야 했고, 많은 개발자가 같은 고민을 했습니다.

ORM은 이러한 객체와 관계형 데이터베이스 사이의 차이점을 중간에서 해결해주기 위하여 등장했습니다. JPA는 자바 진영 ORM 기술 표준입니다.

JPA는 CRUD SQL을 자동으로 생성해주며, 조회된 결과를 객체로 매핑하는 작업도 대부분 자동으로 처리해줍니다.
JPA를 통한 가장 큰 성과는 SQL이 아닌 객체 중심의 개발으로 생산성과 유지보수가 좋아지고, 테스트를 작성하기도 편리해졌다는 것입니다. JPA를 사용하면 DB 변경에서도 자유로워집니다.

이번 장에서는 SQL을 직접 다뤘을 때 발생했던 문제점과, 객체와 RDS 사이의 차이점에 대해 알아보겠습니다.

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

자바 애플리케이션에서는 RDS를 많이 사용하며, RDS에서 데이터를 관리하기 위해서는 SQL을 사용합니다.
이를 위해 자바 애플리케이션에서는 JDBC API를 사용해 SQL을 데이터베이스에 전달합니다.

1. 반복, 반복 그리고 반복

다음과 같이 회원 관리 기능을 위해 회원 테이블은 이미 만들어 져 있다고 가증하고, 회원 CRUD 기능을 개발해 봅시다.

회원 객체와 DAO는 다음과 같습니다.

회원 객체

public class Member {

    private String memberId;
    private String name;
}

회원용 DAO(데이터 접근 객체)

public class Member DAO {

    public Member find(String memberId) {...}
}

회원 조회

  1. 회원 조회용 SQL
`SELECT MEMBER_ID, NAME FROM MEMBER M WHERE MEMBER_ID = ?`
  1. JDBC API를 사용해서 SQL 실행
ResultSet rs = stmt.executeQuery(sql); 
  1. 조회 결과를 Member 객체로 매핑
String memberId = rs.getString("MEMBER_ID");
String name = rs.getString("NAME");

Member member = new Member();
member.setMemberId(memberId);
member.setName(name);

회원 등록

회원 등록 기능을 추가하기 위해서 DAO에 해당 save 메서드를 추가해 줍니다.

회원용 DAO에 등록 기능 save 메서드 추가

public class MemberDAO {

    public Member find(String memberId){...}
    public void save(Member member){...} //추가
}

1. 회원등록용 SQL 작성

String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES(?,?)";

파라미트 지정 방식

  • ? 와 같은 방식은 위치기반 파라미터 지정 방식입니다.
  • 직접 파라미터 이름을 명시하는 네임드 파라미터 방식도 존재합니다.

2. 회원 객체의 값을 꺼내서 등록 SQL에 전달

pstmt.setString(1, member.getMemberId());
pstmt.setString(2, member.getName());

3. JDBC API를 사용해서 SQL을 실행

pstmt.executeUpdate(sql);

마찬가지로 회원 수정 및 삭제하는 기능도 추가한다면, 마찬가지로 SQL을 작성하고, JDBC API를 사용하는 비슷한 일을 반복할 것입니다.

하지만 회원 객체를 DB가 아닌 자바 컬렉션에 보관한다면 어떨까요?
다음과 같이 한줄로 객체를 저장할 수 있을 것입니다.

list.add(member);

하지만 DB는 객체 구조와는 다른 데이터 중심의 구조를 가집니다. 객체를 데이터베이스에 직접 저장하거나 조회할 수는 없죠.

하지만 객체를 CRUD하려면 너무 많은 SQL과 JDBC API 코드를 작성을 반복해야 합니다.

2.SQL에 의존적인 개발

회원 관리 기능을 완료했는데, 연락처도 함께 저장해달라는 요구사항이 추가되었습니다.
필드가 하나가 추가되었지만, 해야할 작업들은 다음과 같습니다.

회원 테이블에 TEL 컬럼을 추가, 회원 객체에 tel 필드 추가

public class Member {

    private String memberId;
    private String name;
    private String tel; // 추가 
}

등록 코드 변경

1. 연락처를 저장할 수 있도록 INSERT SQL 수정

String sql = "INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES(?,?,?)";

2. 회원 객체의 연락처 값을 꺼내서 등록 SQL에 전달

pstmt.setString(3, member.getTel());

조회 코드 변경

1. 회원 조회용 SQL을 수정

SELECT MEMBER_ID, NAME, TEL FROM MEMBER WHERE MEMBER_ID = ?

2. 연락처 조회 결과를 Member 객체에 추가로 매핑

String tel = rs.getString("TEL");
member.setTel(tel); // 추가

수정 코드 변경

화면에 연락처가 잘 출력이 되고, 기능이 잘 동작한다고 생각했지만, 수정 기능이 작동하지 않는 버그가 발견되었습니다. MemberDAO.update(member) 메서드에 수정할 회원 정보와 연락처를 잘 전달했지만, UPDATE SQL에 TEL 컬럼을 추가하지 않아 발생한 문제였습니다.

따라서 UPDATE SQLMemberDAO.update()의 일부 코드를 변경해 연락처 수정 기능이 정상 동작되도록 합니다.

만약 객체를 DB가 아닌 자바 컬렉션에 보관했다면, 필드 하나 추가 때문에 이렇게 전반적인 코드를 수정할 필요는 없을 것입니다.

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

만약 어떠한 한 팀에 소속되어야 한다는 요구사항이 추가된다면 어떨까요?
마찬가지로 테이블과 객체에 field를 추가하고, DAO에 메서드를 추가해야 할 것입니다.

만약 해당 기능에 오류가 발생한다면, DAO의 메서드 코드를 열어 SQL을 직접 확인해야 원인을 알 수 있습니다.
즉, 데이터 접근 계층을 두어 SQL을 숨겨도, 어쩔 수 없이 SQL을 열어 어떤 SQL이 실행되는지 확인해야 합니다. 즉 물리적으로는 SQL과 JDBC API를 데이터 접근 계층에 숨기는데 성공했지만, 논리적으로는 엔티티와 아주 강한 의존 관계를 가지고 있는 셈입니다.

Member나 비즈니스 요구사항을 모델링한 객체를 엔티티라 하는데, 이처럼 SQL에 모든 비즈니스 로직을 의존한다면, 개발자들이 엔티티를 신뢰하고 사용할 수 없습니다.

애플리케이션에서 SQL을 직접 다룰 때의 문제점

따라서 애플리케이션에서 SQL을 직접 다루는 경우, 다음과 같은 문제점이 발생합니다.

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

3. JPA와 문제 해결

그렇다면 JPA에서는 이러한 문제를 어떻게 해결할까요?

JPA를 사용하면, 객체를 DB에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하지 않고 JPA가 제공하는 API를 통해 적절한 SQL을 대신 생성하여 DB에 전달합니다.

저장 기능

jpa.persist(member); // 저장
  • persist() : 객체를 DB에 저장
  1. 객체와 매핑 정보를 보고 적절한 INSERT SQL 생성
  2. DB에 전달

조회 기능

String memberId = "helloId";
Member member = jpa.find(Member.class, memberId); // 조회
  • find() : 객체 하나를 DB에서 조회
  1. 객체와 매핑 정보를 보고 적절한 SELECT SQL을 생성
  2. SQL DB에 전달
  3. 결과로 Member 객체를 생성해서 반환

수정 기능

Member member = jpa.find(Member.class, memberId);
member.setName("이름변경") // 수정
  • JPA는 별도로 수정 메서드를 제공 X

JPA는 별도의 수정 메서드를 제공하지 않지만, 객체를 조회해서 값을 변경하고 트랜잭션을 커밋할 때, Dirty Checking을 수행하여 값이 변경되었다면, 적절한 UPDATE SQL을 생성해 DB에 전달합니다.

연관된 객체 조회

Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam(); // 연관된 개체 조회
  • JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행

2. 패러다임의 불일치

어플리케이션이 발전하면서 내부의 복잡성은 점점 커지며 유지보수의 중요성이 커졌습니다.

객체지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 정치들을 제공합니다.
반면, 관계형 데이타베이스데이터 중심으로 구조화되어 있으며, 집합적인 사고가 필요합니다. 추상화, 상속 다형성 같은 개념이 없습니다.

이러한 객체와 관계형 데이터베이스의 패러다임 차이 때문에 다양한 문제들이 발생합니다.

따라서 애플리케이션에서는 자바라는 객체지향 언어로 개발하고, 데이터는 관계형 데이터베이스에 저장해야 한다면, 패러다임의 불일치 문제를 개발자가 중간에서 해결해야 하지만 시간과 비용이 매우 큽니다.

패러다임의 불일치로 발생하는 문제점

1. 상속

객체는 상속의 기능을 가지고 있지만, 테이블은 상속의 기능이 없습니다.


그나마 데이터베이스 모델링에서 이야기하는 슈퍼 타입과 서브 타입 관계를 사용하면 오른쪽 그림과 같이 객체 상속과 유사한 형태로 테이블을 설계할 수는 있습니다.

Item 테이블의 DTYPE 컬럼을 사용하면 어떤 자식 테이블과 관계가 있는지 정의합니다. 예를 들어, DTYPE의 값이 MOVIE 이면, 영화 테이블과 관계가 있습니다.

이를 객체 모델 코드로 구현하면, 다음과 같습니다.

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 객체를 저장하려면, 이 객체를 분해해 다음과 같이 두 SQL을 만들어야 합니다.

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

INSERT INTO ITEM ...
INSERT INTO MOVIE ...

JDBC API를 이용해서 코드를 구현하려면 부모 객체에서 부모 데이터만 꺼내 ITEM용 INSERT SQL을 작성하고, 자식 객체에서 자식 데이터만 꺼내 ALBUM용 INSERT SQL을 작성해야 하지만, 효율적이지 않습니다. 그리고 자식 타입에 따라 DTYPE도 저장해야 하죠.

조회 또한 ITEM과 ALBUM 테이블을 조인해야 합니다.

JPA와 상속

JPA는 이러한 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해줍니다.
개발자는 단지 자바 컬렉션이 객체를 저장하듯 JPA에 객체를 저장합니다.

persist() 메서드로 jpa.persist(album);과 같이 album 객체를 저장하면 JPA는 자동으로 ITEM, ALBUM 두 테이블에 나누어 SQL을 실행해 저장하고, 조회도 마찬가지로 자동으로 JOIN SQL을 생성해 조회하고 그 결과를 반환합니다.

2. 연관관계

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

그리고, 객체는 참조가 있는 방향으로만 조회할 수 있지만, 테이블은 외래 키 하나로 양방향 조회(JOIN)가 가능합니다.

그렇다면 객체를 테이블에 맞추어 모델링 하거나, 객체지향 모델링을 하면 어떠한 문제가 발생할까요?
1. 객체를 테이블에 맞추어 모델링

class Member {

    String id;      // MEMBER_ID 컬럼 사용
    Long teamId;    // TEAM_ID FK 컬럼 사용
    String userName;
}

class Team {

    Long id;        // TEAM_ID PK 사용
    String name;
}

관계형 데이터베이스 방식에 맞추면 Member 객체와 연관된 Team 객체를 참조를 통해서 조회할 수 없습니다. 또한 이러한 방식은 객체지향의 특징을 잃어버리게 됩니다.

2. 객체를 테이블에 맞추어 모델링
객체는 참조를 통해 관계를 맺으므로, 다음과 같이 참조를 사용하도록 객체지향적으로 모델링해야 합니다.

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

    Team getTeam() {
        return team;
    }
}

class Team {
    Long id;
    String name;
}

하지만 객체지향 모델링을 사용하면 객체를 테이블에 저장하거나 조회하기 쉽지 않습니다. 객체 모델은 외래 키가 필요 없고, 단지 참조만 있으면 연관관계를 맺지만, 테이블은 참조가 필요 없고 외래 키만 있으면 되기 때문이죠.
결국, 개발자가 중간에서 변환 역할을 해야합니다.

JPA와 연관관계

JPA는 연관관계와 관련된 패러다임 불일치 문제를 다음과 같이 해결해줍니다.

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


단지 회원과 팀의 관계를 설정하고, 회원 객체를 저장합니다. JPA는 team의 참조를 외래 키로 변환해 적절한 INSERT SQL을 대신 생성하여 DB에 전달합니다.

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

3. 객체 그래프 탐색

객체에서 회원이 소속된 팀을 조회할 때 참조를 사용해서 연관된 팀을 찾는데, 이것을 객체 그래프 탐색이라 합니다.

다음과 같은 객체 연관관계가 설계되어 있다면, 객체 그래프 탐색 코드는 아래와 같습니다.

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

객체는 마음껏 객체 그래프를 참색할 수 있어야 하지만, SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해지게 됩니다.

이는 객체지향 관점에서 큰 리스크가 되는데, 비즈니스 로직에 따라 사용하는 객체 그래프가 다르므로 언제 끊어질지 모르는 객체 그래프를 함부로 탐색할 수 없기 때문입니다.

만약, 아래와 같이 memberDAO를 통해 member 객체를 조회한다면, 이 객체와 연관된 Team, Order, Deliver 방향의 객체 그래프 탐색의 가능성은 이 코드만 보고 파악할 수 없습니다.

// 회원 조회 비지니스 로직
class MemberService {
    ...
    public void process() {

        Member member = memberDAO.find(memberId);
        member.getTeam();   // member->team 객체 그래프 탐색 가능?
        member.getOrder().getDelivery();    // ???
    }
}

결국 데이터 접근 계층인 DAO를 열어 SQL을 직접 확인해야 합니다. 이 또한 엔티티가 SQL에 논리적으로 종속되어 발생하는 문제입니다.

member와 관련된 모든 객체 그래프를 DB에서 조회해 애플리케이션 메모리에 올려두는 것은 비효율적이므로, 보통 상황에 따른 여러 객체 그래프 탐색의 메서드를 경우에 따라 만들어 사용하곤 했습니다.

memberDAO.getMember(); // Member만 조회
memberDAO.getMemberWithTeam(); // Member와 Team 조회
memberDAO.getMemberWithOrderWithDelivery();  // Member, Order, Delivery 조회

JPA와 객체 그래프 탐색

JPA는 객체 그래프를 자유롭게 탐색할 수 있도록 해줍니다.

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

JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행합니다. 따라서 연관된 객체를 신뢰하고 마음껏 조회할 수 있습니다.

또한, JPA는 지연 로딩을 투명하게 처리합니다.

- 💡 지연 로딩 : 실제 객체를 사용하는 시점까지 조회를 미룸

// 투명한 엔티티
class Member {
    private Order order;

    public Order getOrder() {
        return order;
    }
}
// 지연로딩 사용
// 처음 조회 시점에 SELECT MEMBER SQL
Member member = jpa.find(Member.class, memberId);

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

4. 비교

데이터베이스와 객체의 비교 방법은 다음과 같이 차별점이 있습니다.

  • DB : 기본 키(식별자 ID)의 값으로 각 ROW 구별
  • 객체 : 동일성(identity), 동등성(equality) 비교

💡 동일성 비교와 동등성 비교

  • 동일성 비교 : == 비교, 객체의 인스턴스 주소 값 비교
  • 동일성 비교 : equals() 메서드를 사용해 객체 내부의 값 비교
class MemberDAO {

    public Member getMember(String memberId) {
        String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
        ...
        // JDBC API, SQL실행
        return new Member(...);
    }
}

// 조회한 회원 비교
String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2;     //다르다.

기본 키 값이 같은 회원 객체를 두번 조회했지만, 동일성 비교에서 false가 반환됩니다. member1과 member2는 같은 DB row에서 조회했지만, 객체 측면에서는 서로 다른 인스턴스이기 때문입니다. (인스턴스 주소 값이 다름)

JPA와 비교

JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장합니다.
따라서 다음 코드에서 member1과 member2는 동일성 비교에 성공합니다.

// 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 기술 표준입니다. JPA는 아래 그림과 같이 어플리케이션과 JDBC 사이에서 동작합니다.

ORM이란?

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

예를 들어 INSERT SQL을 직접 장성하는 대신 객체를 자바 컬렉션에 저장핟스 ORM 프레임워크에 저장합니다. 그러면 ORM 프레임워크가 적절한 INSERT SQL을 대신 생성해서 DB에 객체를 저장합니다.

JPA로 객체를 저장하는 코드는 다음과 같습니다.

jpa.persist(member);

조회의 경우도 JPA를 통해 객체를 직접 조회합니다.

JPA로 객체를 조회하는 코드는 다음과 같습니다.

Member member = jpa.find(memberId); // 조회

위의 방법으로 객체 측면에서는 정교한 객체 모델링을 할 수 있고, RDS는 DB에 맞도록 모델링만 하면 됩니다. 또한 매핑시에는 매핑 방법만 ORM 프레임워크만 알려주면 됩니다.

JPA 소개 - 하이버네이트

EJB(자바 엔터프라이즈 빈즈) 3.0에서 하이버네이트를 기반으로 새로운 자바 ORM 기술 표준이 만들어졌는데 이것이 바로 JPA입니다.
즉, JPA는 자바 ORM 기술에 대한 API 표준 명세입니다.

따라서 JPA를 사용하려면 JPA를 구현한 ORM 프레임워크를 선택해야 하는데, 그 중 JPA 2.1을 구현한 Hibernate(하이버네이트)가 가장 대중적입니다. JPA 2.1 구현체에는 하이버네이트 외에도 EclipseLink, DataNucleus 등이 있습니다.

버전별 특징

버전연도설명
JPA 1.0(JSR 220)2006년초기 버전, 복합 키와 연관관계 기능 부족
JPA 2.0(JSR 317)2009년대부분의 ORM 기능 포함, JPA Criteria가 추가
JPA 2.1(JSR 338)2013년스토어드 프로시저 접근, 컨버터, 엔티티 그래프 기능 추가

JPA를 사용하는 이유

1. 생산성

  • 자바 컬렉션에 객체를 저장하듯 JPA에게 저장할 객체를 전달
  • INSERT SQL을 작성하고 JDBC API 사용하는 지루하고 반복적인 일을 JPA가 대신 처리
jpa.persist(member);    // 저장
Member member = jpa.find(memberId);     // 조회
  • CREATE TABLE같은 DDL문 자동 생성
  • 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전

2. 유지보수

  • SQL에 의존적인 개발에서 벗어남
  • 개발자가 작성해야 했던 SQL과 JDBC API 코드를 JPA가 대신 처리해주므로 유지보수해야 하는 코드가 줄어듦

3. 패러다임 불일치 해결

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

4. 성능

  • JPA는 애플리케이션과 데이터베이스 사이에서 다양한 성능 치적화 기회 제공
  • 회원 조회의 경우, SELECT SQL 한번만 DB에 전달하고, 두 번째는 조회한 회원 객체 재사용

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

  • 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 함
  • DB를 변경하면 JPA에게 다른 DB를 사용한다고 알려주기만 하면 됨

정리

참고

  • 자바 ORM 표준 JPA 프로그래밍 (김영한 지음)
  • [인프런] 자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한)
profile
私はゲームと日本が好きなBackend Developer志望生のOguです🐤🐤

2개의 댓글

comment-user-thumbnail
2024년 3월 23일

정리가 너무 잘 되어있네여👍👍 나중에 많이 참고 해야겠어여

1개의 답글