JPA는 무엇인가

5 minute read

김영한 : 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 공부한 내용입니다.


JPA (Java Persistence Api)

과거엔 EJB가 자바 표준 ORM이었다.
그러나 문제점이 많았고, 이를 해결하고자 Gavin King이 Hibernate를 만들게 된다.
이후 Java 진영에서 Hibernate를 자바 표준화하여 JPA를 만들었다.
(여담으로 비슷한 시기 Rod Johnson에 의해 Spring Framework가 탄생했다.)

JPA는 인터페이스의 모음이다.
(Hibernate, EclipseLink, DataNucleus)
대부분이 Hibernate를 사용한다.

과거 순수 JDBC를 사용해 데이터베이스에서 조회, 저장 등 작업을 할 때 굉장히 많고 복잡한 코드가 필요했다.

이후 JdbcTemplate 이나, Mybatis 와 같은 SQLMapper가 등장하면서
복잡한 과정은 줄어들었으나, Query를 하나하나 작성해야하는 것은 동일했다.

JPA는 개발자 대신 적절한 Query를 실행까지 해주는 역할을 가진다.


SQL 중심적인 개발의 문제점

객체지향언어를 사용해 객체를 DB에 저장하려 하지만
DB는 SQL문 만을 해석하기 때문에
실제 작업은 각 테이블 별로 CRUD Query 작업이 대다수이다.

public class Users {
    private String id;
    private String name;
}
SELECT ID, NAME FROM USERS;
INSERT INTO USERS (ID, NAME) VALUES ("LEE", "GICHEOL");
UPDATE USERS SET NAME = "EKKKK1";
DELETE FROM USERS WHERE ID = "LEE";

이러한 테이블은 CRUD 하나씩 쿼리를 가지기 때문에,
만약 이후에 다른 컬럼이 추가된다면 대다수 바뀔 부분이 너무나도 많다.


객체와 관계형 데이터베이스의 차이

  • 상속 : RDBMS는 객체의 상속관계와 같은 상속관계는 없다.
  • 연관관계 : 객체는 참조변수를 통해 접근하지만, RDBMS는 PK, FK를 통해 JOIN으로 접근한다.

가장 큰 문제점은 연관관계가 있는 테이블에 데이터를 저장할 때 각각의 테이블로 INSERT문을 날려야한다…

그러나 객체지향적으로 생각해보면 연관관계가 있는 클래스를 컬렉션에 담을때
제네릭과 다형성이 있기 때문에 전혀 문제가 없다.
예제를 보더라도 전혀 이상이 없다.

public class Item {}

public class Album extends Item {}

public class Main {
    public static void main(String[] args) {
        List<Item> item = new ArrayList<>();
        Album album = new Album();
        item.add(album);

        Album newAlbum = (Album) item.get(0);
    }
}

연관관계의 차이점

error

객체는 Member에서 Team으로 접근 할 수 있지만, Team에선 Member 참조가 없기 때문에 접근 할 수 없다.

테이블은 FK를 통해 양방향으로 접근 할 수 있다.

위 테이블대로 클래스를 설계하면 아래와 같다.

public class Member {
    String id;
    Long teamId;
    String username;
}

public class Team {
    Long id;
    String name;
}
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) 
VALUES (id, teamId, username); 

그러나 이러한 방법은 그다지 객체지향스럽지 못하다.
아래와 같이 객체지향스럽게 수정해보자.

public class Member {
    String id;
    Team team;
    String username;

    Team getTeam() {
        return team;
    }
}

public class Team {
    Long id;
    String name;
}
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) 
VALUES(id, member.getTeam().getId(), username);

저장은 어떻게든 됬으나, 문제는 조회이다.

SELECT M.*, T.*
  FROM MEMBER M
  JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
public Member find(String memberId) {
    // SELECT SQL 실행

    Member member = new Member();
    // DB에서 조회한 회원 정보를 저장하는 코드

    Team team = new Team(); 
    // DB에서 조회한 팀 정보를 저장하는 코드

    member.setTeam(team);
    
    return member;
}


Member 클래스에서 Team 참조변수로 받을 수 없어서 정말 불편한 코드가 나온다..

또한 객체는 객체 내부의 그래프를 자유롭게 탐색할 수 있어야 하는데,
SQL문에 따라 탐색 범위가 결정되어 그러지 못한다.

public class Member {
    String id;
    Team team;
    String username;

    Team getTeam() {
        return team;
    }
}

public class Team {
    Long id;
    String name;
}
SELECT *
  FROM MEMBER; 
member.getTeam(); // null

Member만 조회를 했기 때문에 아무리 클래스에 Team이 있더라도, member.getTeam()은 null이다.
이는 엔티티에대한 신뢰성 문제로 이어진다.
개념적으론 member.getTeam() 을 호출할 수 있을 것 같으나, 실제 SQL문을 확인하지 않는 이상 호출할 수 있을지, 못할지 판단하지 못한다.

그렇다고해서 모든 객체를 미리 로딩해두는건 매우 비효율적이다.

즉 진정한 계층 분할이 어렵다.


비교하기

일반적인 SQL을 사용하는 상황이다.

String memberId = "100";
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);

member1 == member2; // 다르다
public class MemberDAO {
    public Member getMember(String memberId) {
        String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID = ?";
        ...

        return new Member(...);
    }
}

그러나 자바 컬렉션에선 두 개의 참조변수는 동일하다.

String memberId = "100";
Member member1 = list.get(memberId);
Member member2 = list.get(memberId);

member1 == member2; // 같다

객체답게 모델링 할수록 오히려 매핑 작업만 늘어난다…
그래서 RDBMS에 맞춰서 작업하는게 오히려 간단하게 되버린다.

이런 문제를 해결해주는 것이 바로 JPA이다.


ORM?

Object-Relational Mapping (객체 관계 매핑)

ORM의 특징

  1. 객체는 객체대로, RDBMS는 RDBMS대로 설계한다.
  2. 둘의 차이는 ORM 프레임워크가 중간에서 매핑해준다.
  3. 대중적인 언어엔 대부분 ORM이 있다.

error

JPA가 JDBC API를 사용해서 SQL을 사용해 DB에 접근 후 결과를 반환해 준다.


JPA 동작

error

INSERT의 동작 예시이다.
JPA에게 Entity를 주면 JPA가 Entity를 분석 후 INSERT SQL을 생성해준다.
이후 JDBC API를 사용해 DB에 INSERT 한다.

이때 JPA는 패러다임의 불일치를 해결해준다.


JPA CRUD

  • 저장 : jpa.persist(member);
  • 조회 : Member member = jpa.find(memberId);
  • 수정 : member.setName(“LEEGICHEOL”);
  • 삭제 : jpa.remove(member);

여기서 신기한 것은 수정이다.
일반적으로 Mybatis 같은 SQL Mapper를 사용한다면,
수정이 필요한 데이터를 객체에 세팅을 한 후 DB에 밀어 넣어야한다.

그러나 JPA는 객체를 수정하면 update 쿼리가 발생한다.

public static void main(String[] args) {
    List<Users> list = new ArrayList<>();
    list.add(new Users());

    Users user = list.get(0);
    user.setAge(26);
    user.setName("LEEGICHEOL");

    System.out.println(list.get(0).getAge());
    System.out.println(list.get(0).getName());
}

우리가 자바 컬렉션을 사용할 때 위와 같은 상황에서 객체에 세팅하고 다시 컬렉션에 집어 넣어야 결과가 바뀌는가?

아니다. 세팅만 해줌으로써 객체는 이미 값이 변경되었다.
JPA에서도 마찬가지이다.


JPA와 패러다임의 불일치 해결

error

아무리 상속관계라 하여도 Mybatis에선 Album을 insert하기 위해
아래와 같이 두 개의 쿼리를 직접 작성해야 한다.

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

그러나 JPA에서는 JPA가 두 개의 쿼리로 나누어주기 때문에 단 한 문장으로 해결할 수 있다.

jpa.persist(album);

객체 그래프 탐색도 마찬가지다.
persist 한번이면 자유롭게 객체 그래프를 탐색할 수 있다.

member.setTeam(team);
jpa.persist(member);

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

이러한 일들을 JPA가 해주기 때문에 Entity를 신뢰할 수 있다.


JPA와 비교하기

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

member1 == member2;     // 같다

JPA는 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장한다.


JPA의 성능 최적화 기능

  1. 1차 캐시와 동일성 (Identity) 보장
  2. 트랜잭션을 지원하는 쓰기 지연 (Transactional write-behind)
  3. 지연 로딩 (Lazy Loading), 즉시 로딩

1. 1차 캐시와 동일성 (Identity) 보장

같은 트랜잭션 안에서는 같은 엔티티를 반환한다.
이로인해 약간의 조회 성능이 향상된다.

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

member1은 SQL을 통해 가져오지만, member2는 캐시에 저장된 값을 가져온다.

2. 트랜잭션을 지원하는 쓰기 지연 (Transactional write-behind)

2-1) 트랜잭션을 지원하는 쓰기 지연 - INSERT
  1. 트랜잭션을 커밋할 때까지 INSERT SQL을 모은다.
  2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL을 전송한다.
transaction.begin();

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//여기까지 INSERT SQL을 메모리에 저장

transaction.commit();
// 커밋하는 순간 DB에 SQL을 모아서 보낸다.
2-2) 트랜잭션을 지원하는 쓰기 지연 - UPDATE
  1. UPDATE, DELETE로 인한 ROW 락 시간 최소화
  2. 트랜잭선 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋
transaction.begin();

changeMember(memberA);
deleteMember(memberB);
비즈니스로직();             // 비즈니스 로직 수행 동안 DB Row 락이 걸리지 않는다.


transaction.commit();
// 커밋하는 순간 DB에 SQL을 모아서 보낸다.

3. 지연 로딩과 즉시 로딩

  • 지연 로딩 : 객체가 실제 사용될 때 로딩
  • 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
지연로딩
Member member = memberDAO.find(memberId); // MEMBER SELECT
Team team = member.getTeam();
String teamName = team.getName();         // TEAM SELECT

지연로딩은 team 객체가 실제로 사용될 때 (필요한 시점) Team에 대한 쿼리를 DB로 보낸다. (Proxy를 통해서)

즉시로딩
Member member = memberDAO.find(memberId); // MEMBER, TEAM JOIN SELECT
Team team = member.getTeam();
String teamName = team.getName();

Member를 조회할때 Team이 같이 쓰이는 경우가 많을 경우
지연로딩은 DB IO가 2번 발생하니 오히려 손해일 수 있다.
이런 경우 즉시로딩 옵션을 주면, Member와 Team을 Join해서 한번에 Query한다.


Tags:

Categories:

Updated:

Leave a comment