이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › JPA
연관관계는 무엇이고, @ManyToOne은 무엇을 해 주는가
주문은 누가 주문했는지를 알아야 합니다. 테이블에서는 주문 행이 회원의 번호를 열 하나(외래 키)에 적어 회원을 가리킵니다. 자바 객체에서는 Order가 Member 객체를 필드로 직접 가리킵니다. 이렇게 엔티티가 다른 엔티티를 가리키는 것이 연관관계입니다. 그 필드 위에 @ManyToOne을 적으면 JPA가 둘 사이를 이어 줍니다. 저장할 때는 Member 객체에서 id를 꺼내 외래 키 열에 넣고, 읽을 때는 그 번호로 Member를 찾아 필드에 넣어 줍니다. 그래서 order.getMember().getName()처럼 객체를 따라가면 됩니다. 이 실습은 member 테이블에 (1, 김철수)가 미리 들어 있는 DB에서, 주문을 저장했다가 다시 읽는 같은 코드를 필드 선언만 바꿔 실행합니다. @ManyToOne을 지우면 어떻게 되는지, 객체 대신 번호(Long memberId)만 필드로 두면 무엇이 달라지는지를 봅니다.
실행으로 대조 · Hibernate 7.4.5와 H2 2.4.240에서 네 가지 조합을 소스째 컴파일해 JPA를 실제로 시작하고 저장하고 다시 읽어 대조
@Entity
@Table(name = "orders")
public class Order {
@Id
private Long id;
// 연관관계: 이 주문을 한 회원을 객체로 가리킨다
@ManyToOne
private Member member;
}
// 쓰는 쪽: 주문을 저장하고, 다시 읽어서 주문한 회원의 이름을 본다
Member kim = em.find(Member.class, 1L);
em.persist(new Order(10L, kim));
// 저장(commit)을 마친 뒤, 새 EntityManager(em2)로 DB에서 다시 읽는다
Order o = em2.find(Order.class, 10L);
System.out.println(o.getMember().getName());
Hibernate 7.4.5와 H2 2.4.240(메모리 DB)에서 실행해 대조했습니다. 스프링은 쓰지 않고 Hibernate를 코드로 직접 시작했습니다(persistence.xml 없이 Configuration에 Member와 Order 클래스를 등록). 네 가지 조합을 모두, 화면의 Order 소스와 같은 글(package와 import 줄, 생성자와 getter만 더함)을 Member 소스와 함께 컴파일하고 자바 프로세스를 새로 띄워 확인했습니다. 화면에서 줄인 것은 package와 import 줄, Order의 생성자와 getter, Member 엔티티(id와 name 필드를 가진 평범한 엔티티), 그리고 EntityManager em과 em2를 얻는 줄입니다. 화면의 "쓰는 쪽"은 실제로는 main 안의 코드이고, em으로 저장하는 부분은 트랜잭션을 시작하고 끝내는(commit) 줄 사이에 있습니다. 그 줄은 화면에서 줄였습니다. 읽는 줄은 저장이 끝난 뒤, 새로 만든 em2로 실행했습니다. 같은 EntityManager로 읽으면 방금 저장한 객체를 DB가 아니라 메모리에서 돌려주어, 읽는 단계를 보일 수 없기 때문입니다. member 테이블의 (1, 김철수)는 JPA가 시작하면서 테이블을 만든 직후 JDBC로 직접 넣었습니다. 열 이름은 필드 이름의 대문자 앞에 _를 넣어 정하는 규칙(hibernate.physical_naming_strategy=PhysicalNamingStrategySnakeCaseImpl)을 설정했습니다. 스프링 부트 4.1.1의 spring-boot-hibernate jar가 기본 이름 규칙으로 가리키는 클래스와 같은 것이며(jar 안의 클래스 이름으로 확인했고 스프링 부트를 실제로 띄워 보지는 않았습니다), 이 설정이 없을 때 열 이름이 어떻게 되는지는 확인하지 않았습니다. 대조한 것은 JPA가 시작하다 실패하는지와 예외의 종류와 메시지 첫 줄, 시작 직후와 실행 뒤 H2에 있는 테이블과 열(INFORMATION_SCHEMA.COLUMNS), 외래 키 제약(INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS와 KEY_COLUMN_USAGE), StatementInspector로 잡은 SQL(저장 단계와 읽기 단계로 나눠 SELECT와 INSERT의 수와 글), 화면에 찍힌 글, 실행 뒤 orders 테이블의 행입니다. 테이블 이름은 H2가 대문자로 저장하는데 화면에는 소문자로 적었고, 외래 키가 가리키는 member 테이블은 엔티티 이름을 따라 Member로 적었습니다. 열의 차례는 H2에서 본 그대로이며 필드를 적은 순서와 같지 않습니다(@JoinColumn으로 buyer_id를 정하면 buyer_id가 id보다 앞에 놓였습니다). 문서가 정한 것은 Jakarta Persistence 3.2 명세 2.11절(연관관계를 나타내는 필드에는 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 가운데 하나를 붙인다), 2.12절(연관관계의 기본 매핑)과 2.12.3.2항(단방향 ManyToOne은 이 엔티티의 테이블에 상대 테이블의 기본 키를 가리키는 외래 키 열이 놓이고, 그 열의 이름은 필드 이름, 밑줄, 상대의 기본 키 열 이름을 이은 것), 그리고 ManyToOne과 JoinColumn의 API 문서(JoinColumn의 name을 적지 않았을 때의 기본 이름 규칙)입니다. 명세 본문에는 애너테이션 설명이 따로 없어 이 둘은 API 문서에서 읽었습니다. 이 문서들은 WebFetch로 열었고, 돌려받은 것은 원문이 아니라 요약이어서 문장을 하나하나 원문과 대조하지는 못했습니다. 아래는 문서가 정한 것이 아니라 이 버전에서 관찰한 동작입니다. @ManyToOne을 지우면 JPA는 시작할 때 MappingException을 내고(메시지 첫 줄은 Incorrect use of entity type 'app.Member' (possibly due to missing association mapping annotation)이고, 안쪽 원인은 Java 타입 'app.Member'에 맞는 JdbcType을 정하지 못했다는 것) 테이블이 하나도 만들어지지 않습니다. 이때 member 테이블도 만들어지지 않으므로 미리 넣는 행 (1, 김철수)도 없습니다. 연관관계가 있을 때 주문을 다시 읽는 SELECT는 1번이었고 member 테이블을 함께 읽는 문장(left join)이었으며, 번호만 두었을 때는 주문을 읽는 SELECT 1번과 우리가 부른 Member find의 SELECT 1번으로 모두 2번이었습니다. 이 SELECT가 언제 어떻게 나가는지의 세부는 공부 쪽 실습에서 다룹니다. 문장은 Hibernate 버전에 따라 글자가 다를 수 있어 화면에는 "이 실행에서 Hibernate 7.4.5가 만든 문장"이라고 밝혔습니다. 테이블 이름을 orders로 정한 @Table은 order가 SQL의 예약어이기 때문에 붙였고, 이 실습의 주제가 아닙니다. 필드는 id와 member(또는 memberId)만 다뤘고, 반대 방향(Member가 주문 목록을 가지는 양방향), 연관관계의 주인, 지연 로딩, 상대가 없는 경우(optional), cascade는 다루지 않았습니다. 이 실습의 연관관계는 Order에서 Member 쪽으로만 가리키는 단방향입니다. MySQL에서의 실행 대조는 하지 않았습니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.