개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › JPA › 시작

JPA가 푸는 문제

SQL을 직접 쓰면 무엇을 손으로 채워야 하나

자바 객체는 참조로 서로 이어지고, 테이블의 행은 번호(외래 키)로 이어집니다. 모양이 다르므로 테이블의 행을 객체로 옮겨 담는 코드를 누군가는 써야 합니다.

실행으로 대조 · JDBC 코드와 Hibernate 7.4.5를 H2에서 열두 가지 조합으로 실행해 대조

이 실습에 나오는 개념

ORM
객체와 관계형 데이터베이스의 테이블을 연결(매핑)해 주는 기술이다. 개발자가 객체를 다루면 ORM이 그에 맞는 SQL을 만들어 실행한다. 반복적인 SQL 작성이 줄고 객체 중심으로 코드를 작성할 수 있지만, 어떤 SQL이 실행되는지 이해하지 못하면 성능 문제가 생긴다.
JDBC
자바 프로그램이 데이터베이스에 SQL을 보내고 결과를 받아 오는 가장 기본이 되는 표준 API다. 커넥션을 얻어 SQL 문장을 문자열로 넘기고, 물음표 자리에 값을 채워 실행한 뒤, 돌아온 결과(ResultSet)에서 컬럼 값을 하나씩 꺼낸다. SQL을 쓰는 일과 꺼낸 값을 객체의 필드에 옮겨 담는 일은 모두 개발자의 몫이다. JPA와 MyBatis도 내부에서는 JDBC로 SQL을 보낸다. 그래서 JDBC를 직접 쓰지 않더라도 그 위에서 어떤 일이 일어나는지 알아 두면 도움이 된다.
객체와 테이블의 불일치
자바 객체와 관계형 데이터베이스의 테이블이 데이터를 표현하는 방식이 서로 달라 생기는 어긋남이다. 객체는 다른 객체를 참조로 가리키고 참조를 따라 넘어가지만, 테이블의 행은 외래 키라는 번호로 다른 행을 가리키고 조인으로 이어 붙인다. 객체는 new를 할 때마다 서로 다른 객체이지만, 테이블에서는 기본 키가 같으면 같은 행이다. 객체에는 상속이 있지만 테이블에는 없다. SQL을 직접 쓰면 이 차이를 메우는 변환 코드를 개발자가 쓰게 되고, ORM은 그 변환을 대신 맡는다. 흔히 패러다임 불일치라고도 부른다.
JPA, Hibernate, Spring Data JPA
JPA는 자바의 ORM 표준 명세로, 어떤 기능을 어떻게 사용할지 정한 규칙(인터페이스)이다. Hibernate는 그 명세를 실제로 구현한 라이브러리다. Spring Data JPA는 JPA를 더 쉽게 쓰도록 리포지토리 인터페이스를 제공하는 스프링의 모듈이며 내부적으로 JPA(Hibernate)를 사용한다.
1차 캐시
영속성 컨텍스트 안에 엔티티를 식별자 기준으로 보관하는 저장소다. 같은 트랜잭션 안에서 같은 식별자로 다시 조회하면 DB에 쿼리를 보내지 않고 보관된 엔티티를 돌려준다. 그 결과 같은 트랜잭션 안에서 같은 식별자의 엔티티는 항상 같은 객체다. 기본적으로 트랜잭션이 끝나면 사라지므로(OSIV가 켜진 웹 요청에서는 요청이 끝날 때까지 유지된다) 애플리케이션 전체의 캐시와는 다르다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. orderDao.findById(1L): 직접 쓴 SELECT로 orders의 1번 행을 읽어 id와 amount를 객체에 옮겨 담습니다. 행에는 회원의 번호(member_id = 1)만 있고 회원의 이름은 없습니다. 회원 객체를 만들어 넣는 코드가 없으므로 order의 member 필드는 null로 남습니다.
  2. order.getMember().getName(): getMember()가 null을 돌려줍니다. null에 getName()을 불렀으므로 NullPointerException이 납니다.

실행 결과

코드

class Order {
    Long id;
    int amount;
    Member member;        // 객체에서는 회원 객체를 바로 가리키고 싶다
}

class OrderDao {
    Connection conn;

    Order findById(long id) throws SQLException {
        PreparedStatement ps = conn.prepareStatement(
            "select id, amount, member_id from orders where id = ?");
        ps.setLong(1, id);
        ResultSet rs = ps.executeQuery();
        rs.next();
        Order o = new Order();
        o.setId(rs.getLong("id"));
        o.setAmount(rs.getInt("amount"));
        long memberId = rs.getLong("member_id");   // 테이블에는 회원 번호만 있다
        // 회원 객체를 만들어 넣는 코드가 없다. o.member는 null로 남는다
        return o;
    }
}

Order order = orderDao.findById(1L);
System.out.println(order.getMember().getName());

이 모형의 전제

열두 가지 조합을 이 PC에서 실제로 실행해 대조했습니다(JDBC 쪽은 화면과 같은 구조의 DAO 코드를 H2 데이터베이스에, JPA 쪽은 Hibernate 7.4.5와 H2에 실행). 대조한 것은 코드에 적은 SQL 문장의 수, 줄마다 나간 SQL의 종류와 테이블, a == b의 값, 회원 이름 또는 예외, 다시 읽은 grade와 DB에 남은 grade입니다. "직접 채웠다"는 설정은 JDBC에서만 뜻이 있고, JPA에서는 결과에 영향이 없습니다. 화면의 SQL 문장은 읽기 쉽게 줄여 적은 것이며, Hibernate가 실제로 만드는 문장은 컬럼 순서와 별칭이 다릅니다. JPA의 @ManyToOne은 지연 로딩(LAZY)으로 두었습니다. 기본값인 즉시 로딩(EAGER)이면 em.find 한 번에 주문과 회원을 함께 읽습니다. JDBC 쪽의 코드 양은 이 예제의 DAO를 기준으로 한 것입니다. 스프링의 JdbcTemplate이나 MyBatis를 쓰면 반복되는 코드는 줄지만, SQL을 직접 쓰고 필드가 바뀔 때 SQL을 함께 고쳐야 한다는 점은 같습니다. "고친 곳 네 곳"은 화면의 코드에서 센 것입니다.

다음 실습: 영속성 컨텍스트

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요