← Posts

ORM · JPA · Spring

ORM

자바 객체와 DB 테이블 사이를 손으로 나르지 않는다. JPA로 개념을 잡고, ORM이 만드는 SQL까지 본다.

목차
  1. 객체와 테이블 사이를 손으로 나른다
  2. ORM은 객체와 테이블을 이어준다
  3. 매핑을 선언한다
  4. SQL 대신 객체로 다룬다
  5. 연관관계도 매핑한다
  6. 불일치는 공짜로 사라지지 않는다
  7. ORM도 결국 SQL을 만든다
  8. 정리

객체와 테이블 사이를 손으로 나른다

users 테이블에서 사용자 하나를 읽어 User 객체로 만든다고 하자. ORM 없이 JDBC로 하면 이렇게 생겼다.

java
String sql = "SELECT id, email, name FROM users WHERE id = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setLong(1, id);
    ResultSet rs = ps.executeQuery();
    if (rs.next()) {
        User user = new User();
        user.setId(rs.getLong("id"));         // 컬럼 하나를
        user.setEmail(rs.getString("email")); // 꺼내서
        user.setName(rs.getString("name"));   // 필드에 넣는다
        return user;
    }
}

가운데 세 줄이 하는 일은 테이블의 칸을 객체의 필드로 옮겨 담는 것뿐이다. 그런데 이 일이 테이블마다, 쿼리마다 되풀이된다. 컬럼을 하나 추가하면 이걸 쓰는 자리를 다 찾아 한 줄씩 더해야 한다. 저장할 때는 반대 방향으로 또 나른다.

객체와 테이블은 생김새가 달라서, 둘 사이에는 늘 이 “옮겨 담기”가 낀다. 지겹고, 빠뜨리기 쉽다.

ORM은 객체와 테이블을 이어준다

ORM(Object-Relational Mapping)은 객체와 테이블을 이어주는 도구다. 옮겨 담는 일을 대신 해준다.

이어주는 규칙은 단순하다.

diagramdiagram
객체 세계테이블 세계
클래스테이블
필드컬럼
객체 하나행(row) 하나

이 대응만 한 번 선언해두면, 앞에서 손으로 나르던 일을 ORM이 알아서 한다. 자바에서 ORM의 표준 규격이 JPA이고, 그걸 실제로 구현한 것이 Hibernate다. Spring Boot는 기본으로 Hibernate를 쓴다.

매핑을 선언한다

대응을 알려주는 건 애너테이션 몇 개다.

java
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String email;

    private String name;
    // 생성자·getter 생략
}
  • @Entity - 이 클래스는 테이블과 이어진다는 표시
  • @Table(name = "users") - 어느 테이블인지. 클래스 이름과 같으면 생략해도 된다
  • @Id - 이 필드가 기본 키(행을 구분하는 값)
  • @Column - 컬럼 세부. 이름이 필드와 같으면 생략해도 된다

선언은 여기까지다. “email을 어느 컬럼에서 꺼내라”를 코드로 쓰지 않는다. 매핑을 한 번 적어두면 나르기는 ORM이 한다.

SQL 대신 객체로 다룬다

이제 사용자를 저장하고 조회하는 코드를 보자. Spring Data JPA를 쓰면 이렇다.

java
public interface UserRepository extends JpaRepository<User, Long> {
}

이게 전부다. 본문이 없는 인터페이스 하나인데, save·findById 같은 메서드가 이미 들어 있다. 쓰는 쪽은 이렇게 된다.

java
User user = new User("kim@example.com", "김");
userRepository.save(user);        // ORM이 INSERT INTO users ... 를 만든다

User found = userRepository.findById(1L).orElseThrow();
                                  // ORM이 SELECT ... FROM users WHERE id = 1 을 만든다

SELECTINSERT도 손으로 쓰지 않았다. 객체를 저장하라고, 아이디로 찾아오라고 말했을 뿐이고 SQL은 ORM이 만든다.

diagramdiagram

앞에서 ResultSet에서 한 줄씩 꺼내던 그 일이 통째로 사라졌다. 이것이 ORM이 주는 값어치다. 개발자는 객체로 생각하고, 테이블과의 변환은 도구에 맡긴다.

연관관계도 매핑한다

필드와 컬럼만 잇는 게 아니다. 객체 사이의 관계도 잇는다.

주문(Order)이 회원(Member)에 속한다고 하자. 객체 세계에선 OrderMember참조로 들고, 테이블 세계에선 ordersmember_id라는 외래 키가 있다. ORM은 이 둘을 애너테이션으로 잇는다.

java
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne                       // 주문 여럿이 회원 하나에 속한다
    @JoinColumn(name = "member_id")  // 이 외래 키가
    private Member member;           // 이 객체 참조가 된다
}

이러면 조인 SQL을 손으로 안 쓰고도 order.getMember().getName()처럼 객체를 타고 관계를 넘나든다. ORM이 member_id로 회원을 찾아 채워준다.

편해 보이지만, 바로 여기서 뒤에서 볼 문제가 싹튼다. 객체를 타고 넘나드는 그 한 줄이, SQL로는 조회 하나이기 때문이다.

불일치는 공짜로 사라지지 않는다

여기서 한 가지는 짚고 가야 한다. 객체와 테이블은 애초에 생각하는 방식이 다르다.

  • 잇는 방법이 다르다. 객체는 다른 객체를 참조로 가리킨다(order.getMember()). 테이블은 외래 키 컬럼(member_id)으로 가리킨다.
  • 모양이 다르다. 객체엔 상속도 있고 컬렉션도 있다. 테이블은 그냥 평평한 행이다.

이 간극을 객체-관계 임피던스 불일치라고 부른다. ORM은 이 간극을 메워주지만, 메우는 값을 치른다. 객체를 다루는 편한 코드 뒤에서 ORM이 SQL을 만드는데, 그 SQL이 늘 우리가 기대한 모양은 아니다.

그래서 ORM을 쓸 때 실무에서 가장 먼저 배우는 습관이 하나 있다.

ORM도 결국 SQL을 만든다

편한 객체 코드에 가려져 있을 뿐, 밑에서 도는 건 SQL이다. 그래서 실무에선 ORM이 어떤 SQL을 만드는지 눈으로 본다. 그걸 안 보면 대표적으로 이 문제에 걸린다.

주문 목록을 뽑아서 각 주문의 회원 이름을 찍는 코드다.

java
List<Order> orders = orderRepository.findAll();   // (1) 주문 전체를 SELECT

for (Order order : orders) {
    System.out.println(order.getMember().getName());  // 주문마다 회원을 꺼낸다
}

Order가 회원을 **지연 로딩(lazy)**으로 물고 있으면, getMember()는 그때그때 회원을 조회한다. 그래서 주문이 100개면 쿼리가 1번(주문) + 100번(회원) 나간다. 코드는 반복문 하나인데 DB는 101번 두드려진다.

diagramdiagram

이게 N+1 문제다. ORM이 객체 코드를 SQL로 바꾸다 보니, 객체로는 자연스러운 반복문 하나가 뒤에서 쿼리 폭탄이 된다. 객체만 보고 SQL을 안 보면 안 보이는 함정이다.

해결은 “회원을 같이 가져와라”라고 알려주는 것이다.

java
@Query("select o from Order o join fetch o.member")
List<Order> findAllWithMember();   // 주문과 회원을 한 번의 SELECT로

이러면 쿼리가 1번으로 준다. 요점은 특정 해법이 아니라 태도다. ORM은 SQL을 감춰주는 게 아니라 대신 써주는 것이고, 실무에선 그 SQL을 확인하는 습관이 ORM을 제대로 쓰는 첫걸음이다.

정리

ORM이란객체와 테이블을 이어주는 도구. 옮겨 담기를 대신한다
대응클래스=테이블, 필드=컬럼, 객체=행
어떻게 쓰나매핑을 애너테이션으로 선언(@Entity), SQL 대신 객체로 저장·조회
자바에선규격은 JPA, 구현은 Hibernate (Spring Boot 기본)
공짜가 아니다객체와 테이블은 생각하는 방식이 달라(임피던스 불일치) 간극을 메우는 값을 치른다
그래서 실무에선ORM이 만드는 SQL을 본다. 안 보면 N+1 같은 함정에 걸린다

객체로 생각하고 SQL은 도구에 맡기되, 그 도구가 무슨 SQL을 만드는지는 놓지 않는 것. 그게 ORM을 쓰는 감각이다.

관련 글