Загружаем каталог…
Загружаем каталог…
[JPA] 다양한 연관관계 매핑 - 다대일, 일대다, 일대일, 다대다 김영한님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리한 글입니다. 실습 환경: PostgreSQL + Hibernate 6(jakarta) 1. 연관관계 매핑 시 고려사항 3가지 연관관계를 매핑할 때는 항상 이 3가지를 생각하면 됩니다. 1 다중성 관계 어노테이션 예 다대일 (N:1) @ManyToOne 회원 여러 명 → 팀 하나 일대다 (1:N) @OneToMany 팀 하나 → 회원 여러 명 일대일 (1:1) @OneToOne 회원 하나 ↔ 사물함 하나 다대다 (N:M) @ManyToMany 회원 여러 명 ↔ 상품 여러 개 💡 다중성이 헷갈리면 반대로 생각해 보면 됩니다. 다대일의 반대는 일대다, 일대일의 반대는 일대일입니다. 2 단방향, 양방향 방향 테이블 외래 키 하나로 양쪽 조인 가능 → 사실 방향이라는 개념이 없음 객체 참조용 필드가 있는 쪽으로만 참조 가능. 한쪽만 참조하면 단방향, 서로 참조하면 양방향 3 연관관계의 주인 테이블은 외래 키 하나 로 두 테이블이 연관관계를 맺습니다. 객체 양방향 관계는 A → B , B → A 처럼 참조가 2군데 있습니다. 둘 중 테이블의 외래 키를 관리할 곳 을 지정해야 합니다. 역할 연관관계의 주인 외래 키를 관리하는 참조 (등록, 수정) 주인의 반대편 외래 키에 영향을 주지 않음. 단순 조회만 가능 2. 다대일 [N:1] 다대일 단방향 [객체] Member ──────→ Team - team [테이블] MEMBER.TEAM_ID (FK) ─── TEAM.TEAM_ID (PK) @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; } @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; } 가장 많이 사용하는 연관관계 입니다. DB에서 외래 키가 있는 N 쪽에 참조를 두니 객체와 테이블이 자연스럽게 맞아떨어집니다. 다대일 양방향 [객체] Member ←─────→ Team - team - members @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") // 읽기 전용 private List<Member> members = new ArrayList<>(); } 외래 키가 있는 쪽(Member)이 연관관계의 주인 입니다. 양쪽이 서로 참조하도록 개발합니다. (편의 메서드로 양쪽 세팅) 테이블에는 아무 변화가 없습니다. Team에 members 필드만 추가된 것입니다. ✅ 실무 기본 공식: 다대일 단방향으로 시작 → 필요하면 다대일 양방향 3. 일대다 [1:N] 일대다 단방향 이번에는 반대로 "일(1)"이 주인 이 되는 경우입니다. [객체] Team ──────→ Member - members (team 필드 없음) [테이블] TEAM ─── MEMBER.TEAM_ID (FK) @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID를 관리 private List<Member> members = new ArrayList<>(); } @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; // Team 참조 없음 } 무엇이 이상할까? 테이블의 일대다 관계에서 외래 키는 항상 N(MEMBER) 쪽 에 있습니다. 그런데 객체는 Team이 외래 키를 관리 합니다. 내 테이블이 아닌 반대편 테이블의 외래 키를 관리 하는 특이한 구조가 됩니다. 실제로 나가는 쿼리 Member member = new Member(); member.setUsername("member1"); em.persist(member); Team team = new Team(); team.setName("teamA"); team.getMembers().add(member); // Team을 건드렸는데... em.persist(team); tx.commit(); insert into member (username, member_id) values (?, ?) -- TEAM_ID는 null로 들어감 insert into team (name, team_id) values (?, ?) update member set team_id = ? where member_id = ? -- ← 추가 UPDATE! Member를 INSERT할 때는 Member가 Team을 모르니 TEAM_ID를 넣을 수 없습니다. Team을 저장할 때 members 컬렉션을 보고 MEMBER 테이블을 다시 UPDATE 합니다. 일대다 단방향의 단점 엔티티가 관리하는 외래 키가 다른 테이블에 있습니다. 연관관계 관리를 위해 추가로 UPDATE SQL 이 실행됩니다. 코드는 Team을 수정했는데 MEMBER 테이블에 UPDATE가 나가니 실무에서 헷갈립니다. 테이블이 수십 개면 쿼리 추적이 정말 어렵습니다. ⚠️ @JoinColumn 을 꼭 써야 합니다. 빼먹으면 JPA가 조인 테이블 방식 을 사용해서 TEAM_MEMBERS 같은 중간 테이블을 하나 더 만들어 버립니다. ✅ 결론: 일대다 단방향보다는 다대일 양방향을 사용하자. "Member에 Team 참조가 필요 없는데요?"라고 해도, 약간의 객체지향적 손해를 보더라도 DB 설계에 맞춰 Member에 참조를 두고 다대일 양방향으로 가는 것이 운영하기 훨씬 쉽습니다. 일대다 양방향 @Entity public class Member { // ... @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) // 읽기 전용 private Team team; } 이런 매핑은 공식적으로 존재하지 않습니다. insertable = false, updatable = false 로 읽기 전용 필드 를 만들어서 양방향처럼 쓰는 꼼수입니다. 둘 다 주인처럼 동작하면 꼬이니까 Member 쪽을 강제로 읽기 전용으로 막은 것입니다. ✅ 결론: 다대일 양방향을 사용하자. 4. 일대일 [1:1] 특징 일대일 관계는 그 반대도 일대일 입니다. 주 테이블이나 대상 테이블 중 어디에 외래 키를 둘지 선택 할 수 있습니다. 주 테이블에 외래 키 대상 테이블에 외래 키 외래 키에 유니크(UNIQUE) 제약 조건 을 추가해야 일대일이 보장됩니다. 💡 주 테이블 / 대상 테이블이란? 더 많이 조회하는 쪽, 비즈니스의 중심이 되는 쪽이 주 테이블입니다. 예제에서는 회원(MEMBER)이 주 테이블, 사물함(LOCKER)이 대상 테이블입니다. 1 주 테이블에 외래 키 - 단방향 [객체] Member ──────→ Locker - locker [테이블] MEMBER.LOCKER_ID (FK, UNI) ─── LOCKER.LOCKER_ID (PK) @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker; } @Entity public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; } 다대일( @ManyToOne ) 단방향 매핑과 거의 같습니다. 어노테이션만 @OneToOne 으로 바뀌고 유니크 제약이 붙는 차이입니다. 💡 Hibernate 6.2부터는 @OneToOne 의 외래 키 컬럼에 유니크 제약 조건을 자동으로 만들어 줍니다. 2 주 테이블에 외래 키 - 양방향 @Entity public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; @OneToOne(mappedBy = "locker") // 읽기 전용 private Member member; } 다대일 양방향처럼 외래 키가 있는 곳(Member)이 연관관계의 주인 입니다. 반대편은 mappedBy 를 적용합니다. 3 대상 테이블에 외래 키 - 단방향 [객체] Member ──────→ Locker - locker [테이블] MEMBER ─── LOCKER.MEMBER_ID (FK, UNI) Member 객체의 locker 로 LOCKER 테이블의 외래 키를 관리 하고 싶은 경우입니다. JPA가 지원하지 않습니다. (일대다 단방향은 지원했지만 일대일은 안 됨) 4 대상 테이블에 외래 키 - 양방향 @Entity public class Locker { // ... @OneToOne @JoinColumn(name = "MEMBER_ID") // 주인 private Member member; } @Entity public class Member { // ... @OneToOne(mappedBy = "member") // 읽기 전용 private Locker locker; } Locker.member를 주인 으로 잡으면 됩니다. 사실 12의 주 테이블 외래 키 양방향을 뒤집은 것 과 매핑 방법이 같습니다. 💡 일대일 규칙 요약 : 내 엔티티에 있는 외래 키는 내가 직접 관리 합니다. 다른 테이블의 외래 키는 관리할 수 없습니다. 일대일 정리: 어디에 외래 키를 둘까? 주 테이블에 외래 키 대상 테이블에 외래 키 구조 주 객체가 대상 객체를 참조하듯, 주 테이블에 FK를 두고 대상 테이블을 찾음 대상 테이블에 FK가 존재 선호 객체지향 개발자 전통적인 DB 개발자 장점 JPA 매핑이 편리. 주 테이블만 조회해도 대상 데이터가 있는지 확인 가능 나중에 일대일 → 일대다로 바뀔 때 테이블 구조 유지 단점 값이 없으면 FK에 null 허용 프록시 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨 일대다로 바뀌는 상황 예시 "회원 한 명이 사물함을 여러 개 쓸 수 있게 해 주세요." 대상 테이블(LOCKER)에 FK 가 있었다면 → 유니크 제약만 지우면 끝. 그대로 일대다 구조입니다. 주 테이블(MEMBER)에 FK 가 있었다면 → MEMBER.LOCKER_ID를 지우고 LOCKER에 MEMBER_ID를 새로 만들어야 합니다. 반대로 "사물함 하나를 여러 회원이 같이 쓸 수 있게"로 바뀐다면 주 테이블 FK가 유리합니다. 어떤 방향으로 확장될지 예측해서 결정 하는 게 핵심입니다. 대상 테이블 FK는 왜 지연 로딩이 안 될까? (간단히) Member를 조회할 때 locker 필드에 값이 있는지 없는지 알아야 null을 넣을지, 프록시를 넣을지 정할 수 있습니다. 그런데 FK가 LOCKER 테이블에 있으니 MEMBER만 조회해서는 알 수 없습니다. 결국 LOCKER를 어차피 조회해야 하므로 지연 로딩이 의미가 없어지고 즉시 로딩됩니다. 프록시와 지연 로딩은 다음 장에서 자세히 다룹니다. 5. 다대다 [N:M] 테이블과 객체의 차이 관계형 DB는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다. → 연결 테이블 을 추가해서 일대다, 다대일 관계로 풀어내야 합니다. MEMBER ─── MEMBER_PRODUCT ─── PRODUCT - MEMBER_ID (PK, FK) - PRODUCT_ID (PK, FK) 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능 합니다. Member (List<Product>) ←→ Product (List<Member>) @ManyToMany 매핑 @Entity public class Member { // ... @ManyToMany @JoinTable(name = "MEMBER_PRODUCT") // 연결 테이블 지정 private List<Product> products = new ArrayList<>(); } @Entity public class Product { @Id @GeneratedValue private Long id; private String name; @ManyToMany(mappedBy = "products") // 양방향일 때 private List<Member> members = new ArrayList<>(); } @ManyToMany 사용 @JoinTable 로 연결 테이블 지정 단방향, 양방향 모두 가능 ⚠️ 다대다 매핑의 한계 → 실무에서 사용 X 편리해 보이지만 실무에서는 쓰지 않습니다. 연결 테이블이 단순히 연결만 하고 끝나지 않습니다. 주문 시간, 수량 같은 데이터가 들어올 수 있습니다. MEMBER_PRODUCT - MEMBER_ID (PK, FK) - PRODUCT_ID (PK, FK) - ORDERAMOUNT ← 이런 컬럼을 추가하고 싶은데 - ORDERDATE ← @ManyToMany로는 매핑할 방법이 없음 @ManyToMany 의 연결 테이블은 JPA가 숨겨서 관리 하므로 추가 컬럼을 넣을 수 없습니다. 연결 테이블이 숨겨져 있어서 예상하지 못한 조인 쿼리 가 나가기도 합니다. ✅ 다대다 한계 극복: 연결 테이블을 엔티티로 승격 @ManyToMany → @OneToMany + @ManyToOne 으로 풀어냅니다. Member 1 ──── * MemberProduct * ──── 1 Product - member - product - orderAmount - orderDate @Entity public class MemberProduct { @Id @GeneratedValue @Column(name = "MEMBER_PRODUCT_ID") private Long id; // ★ 별도의 PK @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product; private int orderAmount; private LocalDateTime orderDate; } @Entity public class Member { // ... @OneToMany(mappedBy = "member") private List<MemberProduct> memberProducts = new ArrayList<>(); } 이제 연결 테이블도 평범한 엔티티 이므로 원하는 컬럼을 마음껏 추가할 수 있습니다. 실제로 이런 테이블은 결국 "주문(ORDER)" 같은 의미 있는 이름의 엔티티가 됩니다. 💡 연결 테이블의 PK는 (MEMBER_ID, PRODUCT_ID) 복합키 vs 별도 ID? 강의에서는 별도의 ID(대리키)를 쓰는 것을 권장 합니다. 복합키는 @IdClass 나 @EmbeddedId 로 매핑이 번거롭습니다. "같은 회원이 같은 상품을 두 번 주문"하는 요구사항이 생기면 복합키로는 막힙니다. 다른 테이블이 이 테이블을 참조할 때 FK가 컬럼 1개면 됩니다. 중복 방지가 필요하면 (MEMBER_ID, PRODUCT_ID)에 유니크 제약 을 따로 걸면 됩니다. 6. 실전 예제 3 - 다양한 연관관계 매핑 배송, 카테고리 추가 주문과 배송 은 1:1 ( @OneToOne ) 상품과 카테고리 는 N:M ( @ManyToMany ) 회원 1 ── * 주문 1 ── * 주문상품 * ── 1 상품 * ── * 카테고리 1 │ 1 배송 ERD 테이블 컬럼 MEMBER MEMBER_ID, NAME, CITY, STREET, ZIPCODE ORDERS ORDER_ID, MEMBER_ID (FK), DELIVERY_ID (FK) , ORDERDATE, STATUS DELIVERY DELIVERY_ID, CITY, STREET, ZIPCODE, STATUS ORDER_ITEM ORDER_ITEM_ID, ORDER_ID (FK), ITEM_ID (FK), ORDERPRICE, COUNT ITEM ITEM_ID, NAME, PRICE, STOCKQUANTITY CATEGORY CATEGORY_ID, PARENT_ID (FK) , NAME CATEGORY_ITEM CATEGORY_ID (FK), ITEM_ID (FK) 코드 Order - Delivery (일대일, 주 테이블에 FK) @Entity @Table(name = "ORDERS") public class Order { // ... 기존 필드 @OneToOne @JoinColumn(name = "DELIVERY_ID") // 주인 (ORDERS에 FK) private Delivery delivery; } @Entity public class Delivery { @Id @GeneratedValue @Column(name = "DELIVERY_ID") private Long id; private String city; private String street; private String zipcode; @Enumerated(EnumType.STRING) private DeliveryStatus status; @OneToOne(mappedBy = "delivery") // 읽기 전용 private Order order; } public enum DeliveryStatus { READY, COMP } 💡 왜 ORDERS에 FK를 뒀을까? 주문을 조회할 때 배송 정보를 같이 보는 경우가 많고, 주문이 비즈니스의 중심(주 테이블)이기 때문입니다. Category - Item (다대다) + Category 자기 참조 @Entity public class Category { @Id @GeneratedValue @Column(name = "CATEGORY_ID") private Long id; private String name; // 자기 자신을 참조 (부모 카테고리) @ManyToOne @JoinColumn(name = "PARENT_ID") private Category parent; // 자식 카테고리 목록 @OneToMany(mappedBy = "parent") private List<Category> child = new ArrayList<>(); // 다대다 (실습용. 실무에서는 연결 엔티티로 풀어야 함) @ManyToMany @JoinTable(name = "CATEGORY_ITEM", joinColumns = @JoinColumn(name = "CATEGORY_ID"), // 내 쪽 FK inverseJoinColumns = @JoinColumn(name = "ITEM_ID")) // 상대 쪽 FK private List<Item> items = new ArrayList<>(); } @Entity public class Item { // ... 기존 필드 @ManyToMany(mappedBy = "items") private List<Category> categories = new ArrayList<>(); } 💡 자기 참조(셀프 조인) "가전 > 주방가전 > 냉장고"처럼 계층 구조 를 표현할 때 씁니다. 같은 엔티티 안에서 parent (다대일)와 child (일대다)로 양방향 매핑하면 됩니다. 💡 @JoinTable 속성 joinColumns : 현재 엔티티(Category) 를 참조하는 FK inverseJoinColumns : 반대편 엔티티(Item) 를 참조하는 FK N:M 관계는 1:N, N:1로 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
다양한 연관관계 매핑 - 다대일, 일대다, 일대일, 다대다. [JPA] 다양한 연관관계 매핑 - 다대일, 일대다, 일대일, 다대다 김영한님의 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 정리한 글입니다. 실습 환경: PostgreSQL + Hibernate 6(jakarta) 1. 연관관계 매핑 시 고려사항 3가지 연관관계를 매핑할 때는 항상 이 3가지를 생각하면 됩니다. 1 다중성 관계 어노테이션 예 다대일 (N:1) @ManyToOne 회원 여러 명 → 팀 하나 일대다 (1:N) @OneToMany 팀 하나 → 회원 여러 명 일대일 (1:1) @OneToOne 회원 하나 ↔ 사물함 하나 다대다 (N:M) @ManyToMany 회원 여러 명 ↔ 상품 여러 개 💡 다중성이 헷갈리면 반대로 생각해 보면…
Открыть источник