JPA 연관관계 , mapping table , @OneToOne, @OneToMany , @ManyToOne, @JoinColumn ,@ManyToMany

이정수·2025년 11월 10일

Spring JPA

목록 보기
4/9

JPA에서의 연관관계 Mapping :
JPA에서 DB Entity 에서 다른 연관관계에 있는 DB Entity 객체 를 가져올 때 어노테이션을 통해 연관관계를 설정.
RDBMS는 단순히 FK만 설정하면 자동으로 양방향 관계가 되므로 Join을 수행하면 원하는 Data를 가져올 수 있으나 JPADB Entity양방향 관계를 맺어야 Entity 간에 Entity객체를 가져올 수 있다.

。구현하고자 하는 서비스에 따라서 각 Entity단방향 / 양방향 관계여부 파악 후 연관관계 설정이 필요

연관관계에서 부모 Entity를 기준으로 Mapping을 수행하며 자식 EntityFK가 존재하는 Entity 쪽으로 설정
▶ 일반적으로 부모 Entity자식 Entity간 관계는 1:N 관계로서 부모 Entity는 여러 자식 Entity 객체를 가질 수 있고 자식 Entity는 하나의 부모 Entity 객체를 가진다.
RDBMS와 동일하게 연관관계에서 1:N인 경우 NFK( 외래키 )가 있는쪽으로 설정

특정 Entityfield에 존재하는 연관관계Entity 객체를 조회하는건 추가질의를 하는것과 동일
▶ 불필요한 Entity 객체 까지 포함한 연관관계의 모든 Entity 객체를 조회하는건 성능 상 문제가 발생할 수 있으므로 (fetch = FetchType.LAZY) 등의 전략을 잘 설정해야한다

M:N 관계에서는 사이에 추가로 Mapping Table을 구현하여 각 EntityMapping Table Entity1:N 관계 구축

  • Entity연관관계 종류

    단방향 관계 :
    。단방향으로의 DB Entity만 참조가 가능한 관계
    @OneToOne의 경우 자식 Entity에서 부모 Entity를 참조할 수 있음.
    ex) Member Entity에서 Team Entity의 정보를 가져올 수 있지만, 반대는 불가능

    양방향 관계 :
    。양 쪽의 DB Entity가 서로 참조가 가능한 관계
    @OneToMany & @ManyToOne의 경우 부모Entity자식 Entity에서 서로 참조가능
    ex) Member EntityTeam Entity 간 에서 서로의 Entity를 참조가능

DB Entity연관관계 시 주의사항

  • DB Entity영속화되기전인 비영속 상태인 경우 POJO와 차이가 없다.
@OneToMany(mappedBy="member", fetch = FetchType.EAGER)
private List<OrderEntity> orders = new ArrayList<>();

。다음처럼 DB Entity Class연관관계를 정의하는 field가 정의되어 있을때, 해당 fieldDB Entity 객체save(DBEntity객체)를 통해 영속성 컨텍스트에 등록되어 영속화된 시점에서 DBQuery를 전달하여 연관관계초기화를 수행한다.
FetchType.EAGER의 경우 DB에서 List<OrderEntity>를 가져와서 초기화
FetchType.LAZY의 경우 Hibernate에 의해 프록시객체를 생성하여 초기화

。이때 해당 DB Entity가 가 수행되지않아 영속화되기전인 비영속 상태인 경우 POJO로서 해당 field영속화 될때까지 비어있는 new ArrayList<>() 상태로 존재
▶ 이러한 다른 Entity의존성이 존재하지 않는 상태의 비영속 상태인 경우 단위테스트가 가능

  • 부모 Entity가 먼저 영속성 컨텍스트에 등록된 후 자식 Entity영속성 컨텍스트에 등록
// 자식 Entity 이므로 외래키 Field에 @ManyToOne을 선언
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name="memberId") // DB Table의 외래키 field명 설정
	private MemberEntity member;

부모 EntityEntityManager.save()를 통해 영속성 컨텍스트에 등록되기 전 자식 Entity가 등록될 경우 자식 Entity ClassFK Field부모 Entity 객체가 정의되지 않으므로 오류가 발생한다.
▶ 반드시 부모 Entity를 먼저 등록

JPADB Entity연관관계 Mapping 용도의 JPA 어노테이션 종류 :
JPA( jakarta.persistence )에 포함

@Entity를 선언한 Entity Class외래키 용도의 Field에 선언하여 DB Entity연관관계를 설정

@OneToOne, @OneToMany , @ManyToOne, @ManyToMany
@OneToMany , @ManyToOne를 주로 사용

@OneToOne
Entity1:1 관계연관관계를 정의

자식Entity부모 Entity data typeField에 선언
자식EntityFK Column을 관리

부모 Entity에서는 @OneToOne(mappedBy="자식테이블연관필드명")으로 설정
cascade = CascadeType.ALL 등의 제약조건을 추가 설정 가능

    // 자식 Entity
	@OneToOne
	@JoinColumn(name = "order_product_id", nullable = false)
	private OrderProductEntity orderProduct;

@OneToMany(mappedBy="자식Entity의 @ManyToOne필드명")
DB Entity양방향 관계 설정 시 부모 EntityField에 선언하는 어노테이션
1:N 관계를 Mapping할 때 다수의 자식 Entity들을 저장하는 List<자식EntityClass> data typefield에 선언

부모 Entity에서 @OneToMany를 선언하여 연관관계 부모를 지정하여 양방향 Mapping부모 Entity에서 FK Column중복생성하지 않도록 방지
FK자식 Entity에서만 가져야 하므로

(fetch = FetchType.LAZY)가 기본값으로 설정됨
부모 Entity 생성 시점에서 연관관계자식 EntityDB에서 가져오지않고 Hibernate에 의해 프록시 객체로 초기화

  • (cascade = CascadeType.ALL, orphanRemoval = true)
    부모 Entity가 삭제된 경우 연관된 자식 Entity도 함께 삭제하도록 JPA 레벨에서 제약조건 설정
    ORM에서만 적용되며 실제 DB Table에서는 적용되지 않음.

    orphanRemoval = true를 추가설정하여 부모 Entity자식 Entity를 더 이상 참조하지않을때 고아가 된 자식 Entity 자동으로 삭제

  • (mappedBy="자식Entity의 @ManyToOne필드명")를 설정하는 이유?
    @OneToMany만 선언하는 경우 M:N 관계라고 판단하여 JPA에 의해 자동으로 Mapping Table이 생성
    ▶ 해당 경우를 방지하기 위해 1:N 관계인 경우 자식 Entity@ManyToOne + @JoinColumn 필드명으로 설정 시 Mapping Table을 생성하지 않는다.

@ManyToOne
DB Entity양방향 관계로서 1:N 관계를 설정 시 자식 Entity에서 Mapping할 때 부모 Entity 객체를 저장하는 field에 선언하는 어노테이션
▶ 주로 @JoinColumn과 함께 사용

외래키관리주체를 결정하며 선언된 EntityDB TableFK Field가 생성됨
자식 EntityMapping자식테이블외래키 Column이 생성되므로

(fetch = FetchType.EAGER)가 기본값으로 설정됨
자식 Entity 생성 시점에서 연관관계부모 Entity도 전부 DB에서 가져와서 생성

  • (optional = false)
    。해당 FK 필드NOT NULL로 설정

@JoinColumn(name = "FK필드명") :
@OneToOne, @ManyToOne을 통한 연관관계 정의 시 자식Entity 쪽에서 매핑DB Table에 생성될 FKColumn명을 지정하는 어노테이션
1:1 또는 1:N 연관관계 설정 시 자식 Entity@OneToOne , @ManyToOne이 선언된 외래키 Field에서 추가로 선언

@JoinColumn 예시

	@ManyToOne
	@JoinColumn(
		name = "route_id",
		nullable = false,
		foreignKey = @ForeignKey(
		name = "fk_routelink_route",
		foreignKeyDefinition = "FOREIGN KEY(route_id) REFERENCES route(id) ON DELETE CASCADE"
		)
	)
	private Route route;

。 다음의 경우 @ForeignKey 를 통해 설정된 FKOrder EntityDB Table에서 user_id라는 명칭으로 FK Field가 생성
영속성 컨텍스트를 통해 Entity를 삭제하는 경우가 아닌 JPQL 또는 Native SQL을 통해 삭제를 수행 시 다음처럼 @ForeignKey를 통해 DB Table에 직접 제약조건을 설정.
영속성 컨텍스트를 통해 삭제 시 @OneToMany(cascade = CascadeType.ALL,orphanRemoval = true) 를 정의

  • @JoinColumn( nullable = false )
    。선언된 FK FieldNOT NULL 속성으로 DB TABLE에 설정

  • @JoinColumn( foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT) )
    Entity매핑DB Table 생성 시 FK 제약조건을 생성하지 않도록 설정

@ManyToOne, @OneToMany에서 연관관계Entity초기화 전략
연관관계 Entity 객체lazily loaded / eagerly fetched의 여부를 결정

연관관계 Entity가 필요한 경우 FetchType.EAGER를 사용하고, 필요없을 경우 FetchType.LAZY를 사용

  • (fetch = FetchType.LAZY)
    @OneToManydefault 속성

    。특정 EntityDB에서 조회하는 시점에서 연관관계Entity조회하지않고 Hibernate가 생성한 프록시 객체로 대체하는 방식
    ▶ 해당 Entity 객체에서 연관관계 Entity객체로 접근하는 시점에서 DBQuery를 전송하여 해당 연관관계 Entity객체를 가져와서 할당

    EAGER 방식에 비해 불필요한 Entity까지 조회하지 않으므로 성능문제가 크게 발생하지 않아 실무에서 주로사용

    。이때 연관관계Entity를 추가 조회하는 시점에서 반복조회가 발생 시 N+1 문제가 발생할 수 있음
    연관관계 Entity가 필요한 경우 FetchType.EAGER를 선언

  • (fetch = FetchType.EAGER)
    @ManyToOnedefault 속성

    。특정 EntityDB에서 조회하는 시점에서 연관관계Entity도 함께 즉시 조회하여 가져오는 방식

    。불필요한 Entity까지 모두 조회하며 연관관계가 복잡하면 성능문제가 발생
    연관관계 Entity가 필요없는 경우 FetchType.LAZY 사용

1:1 연관관계 구현 예시
자식 Entity부모 EntityFK Column을 가진다.

자식 Entity Class에서만 부모 Entity Data Type을 참조하는 Field@OneToOne을 선언

@Getter
@Entity(name = "review")
@NoArgsConstructor(access = PROTECTED)
public class ReviewEntity extends BaseEntity {
	@Column(nullable = false)
	@Enumerated(EnumType.STRING)
	private ReviewStatus status;
	@Column(nullable = false)
	private String content;
          // 자식Entity 클래스에서 1:1 관계 정의
	@OneToOne
	@JoinColumn(name = "order_product_id", nullable = false)
	private OrderProductEntity orderProduct;
	public void update(String content){ this.content = content; }
	public void delete(){ this.status = ReviewStatus.REMOVED; }
	protected ReviewEntity(String content, ReviewStatus status) {
		ValidationUtil.validateNotNullAndBlank(content,"내용");
		this.status = status;
		this.content = content;
	}
	public static ReviewEntity create(final String content){
		return new ReviewEntity(content, ReviewStatus.ENABLED);
	}
	public void mapToOrderProduct(OrderProductEntity orderProduct){
		this.orderProduct = orderProduct;
	}
}

1:N 연관관계 구현 예시

  • 부모 Entity
    MemberOrder연관관계 = 1:N
    Member부모 Entity이므로 여러 자식 Entity 객체가 정의된 List<OrderEntity> Field@OneToMany 선언
@Getter
@Entity
@Table(name="Member")
public class MemberEntity extends BaseEntity {
    // 부모 Entity이므로 여러 자식 Entity 객체를 가진다
	@OneToMany(mappedBy="member")
	private List<OrderEntity> orders = new ArrayList<>();
    // 자식 Entity객체가 생성 시 부모 Entity의 List 필드에 넣는
    // 매핑용 메서드
    public void mapToOrder(OrderEntity order){
		orders.add(order);
	}
}

@OneToManydefault(fetch = FetchType.LAZY)이므로 MemberEntity가 생성되더라도 List<OrderEntity>DB에서 가져오지않고 프록시 객체로 채워짐
List<OrderEntity>를 사용하는 시점에서 DB에서 해당 OrderEntity객체를 가져와서 orders로 할당

  • 주의사항 : mapTo??() 메서드

    • 자식Entity객체가 생성 된 경우 양방향관계부모 Entity@OneToMany List<> 필드에도 생성된 Entity 객체를 추가해야한다.
      。해당 mapping메서드 ( mapTo??() )를 부모Entity 내 구현하여 추후 Mapping Table 용도의 Entity 객체가 새로 생성된 경우 해당 메서드를 통해 반영
    // 매핑용 메서드
        public void mapToOrder(OrderEntity order){
    		orders.add(order);
    	}
    • 자식 Entity 객체를 생성 시 해당 Entity객체 내부의 static factory method를 통해 Entity객체를 생성하면서 부모 Entity매핑용메서드를 호출하여 전달
       // 자식 Entity 이므로 외래키 Field에 @ManyToOne을 선언
    	@ManyToOne(fetch = FetchType.LAZY)
    	@JoinColumn(name="memberId") // DB Table의 외래키 field명 설정
    	private MemberEntity member;
    	private OrderEntity(Receiver receiver, LocalDateTime deliveredAt, MemberEntity member, OrderStatus orderStatus) {
    		this.receiver = receiver;
    		this.deliveredAt = deliveredAt;
    		this.member = member;
    		this.orderStatus = orderStatus;
    	}
    	 // 정적 팩토리 메서드
    	public static OrderEntity of(Receiver receiver, MemberEntity member){
    		var order = new OrderEntity(
    			receiver,
    			LocalDateTime.now().plusDays(3),
    			member,
    			OrderStatus.PENDING
    		);
    		member.mapToOrder(order); // 부모 Entity 객체에 생성된 OrderEntity객체 추가
    		return order;
    	}

    생성자캡슐화되어있고 정적 팩토리 메서드에 의해 객체 생성이 대리로 수행되며 해당 메서드 내에서 생성된 OrderEntity 객체OrderEntity객체@ManyToOne으로 선언된 부모 Entity@OneToMany List<> 필드에 추가



  • 자식 Entity
    Order자식 Entity이므로 단일 부모 Entity 객체가 정의된 MemberEntity Field@ManyToOne@JoinColumn 선언
@Entity
@Getter
@Table(name = "Orders")
public class OrderEntity extends BaseEntity {
	// 자식 Entity 이므로 외래키 Field에 @ManyToOne을 선언
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name="memberId") // DB Table의 외래키 field명 설정
	private MemberEntity member;
   	private OrderEntity(Receiver receiver, LocalDateTime deliveredAt, MemberEntity member, OrderStatus orderStatus) {
		this.receiver = receiver;
		this.deliveredAt = deliveredAt;
		this.member = member;
		this.orderStatus = orderStatus;
	}
	 // 정적 팩토리 메서드
	public static OrderEntity of(Receiver receiver, MemberEntity member){
		var order = new OrderEntity(
			receiver,
			LocalDateTime.now().plusDays(3),
			member,
			OrderStatus.PENDING
		);
		member.mapToOrder(order); // 부모 Entity 객체에 생성된 OrderEntity객체 추가
		return order;
	}
}

@ManyToOnedefault(fetch = FetchType.EAGER)이므로 OrderEntity가 생성 시 MemberEntity도 함께 DB에서 가져와서 할당
(fetch = FetchType.LAZY)로 따로 설정가능

。 이후 자식 Table부모 Table참조하는 FK Field가 생성됨을 확인가능

M:N 연관관계 구현 예시
Order 는 여러개의 Product를 가질수 있다.
Product는 여러개의 Order를 가질 수 있다.
▶ 두 DB Entity는 서로 M:N 관계를 만족하므로 중간에 Mapping Table 역할의 OrderProductEntity를 정의

OrderEntityProductEntityMapping Table에 대해 1:N 관계이므로 List<OrderProductEntity> 객체를 가지는 @OneToMany이 선언된 Field를 정의

OrderProductEntity는 각각 OrderEntityProductEntity를 참조하는 단일 Entity 객체를 가지는 @ManyToOne + @JoinColumn이 선언된 field를 정의


  • Mapping Table 역할의 DB Entity 정의
    Mapping Table이므로 양쪽 부모 Entity를 각각 참조하는 fk field 선언
    @ManyToOne + @JoinColumn으로 선언한다.
@Entity
@Getter
@NoArgsConstructor
@Table(name="OrderProduct")
public class OrderProductEntity extends BaseEntity {
	private Long quantity;
	// Mapping Table이므로 양쪽 Table을 1:1로 참조하는 field를 갖는다.
	@ManyToOne
	@JoinColumn(name = "order_id")
	private OrderEntity order;
	@ManyToOne
	@JoinColumn(name = "product_id")
	private ProductEntity product;
	private OrderProductEntity(Long quantity, OrderEntity order, ProductEntity product) {
		this.quantity = quantity;
		this.order = order;
		this.product = product;
	}
	public static OrderProductEntity of(Long quantity, OrderEntity order, ProductEntity product){
		OrderProductEntity orderProduct = new OrderProductEntity(quantity,order,product);
		// OrderProduct 생성 시 각각 부모Entity인 ProductEntity , OrderEntity에 반영
		order.mapToOrderProduct(orderProduct);
		product.mapToOrderProduct(orderProduct);
		return orderProduct;
	}
}
  • Mapping Table 기준 양쪽 부모 Entity 정의
    OrderEntityProductEntityMapping Table에 대해 1:N 관계이므로 List<OrderProductEntity> 객체를 가지는 @OneToMany이 선언된 Field를 정의
    ▶ 각각 부모 Entity이므로 OrderProductEntity객체 생성 시 해당 @OneToMany 필드객체로 추가하는 메서드도 구현
@Getter
@Entity
@NoArgsConstructor
@Table(name= "Product")
public class ProductEntity extends BaseEntity {
	private String name;
	private Long price;
	private Long stock;
	@Enumerated(EnumType.STRING)
	private ProductStatus status;
    // Mapping Table 역할의 Entity와 1:N 관계에서 부모
	@OneToMany(mappedBy="product")
	private List<OrderProductEntity> orderProductEntities = new ArrayList<>();
    // OrderProduct 생성 시 수작업으로 양방향 관계에 있는 Entity의
	// @OneToMany가 선언된 List 필드에 추가해야한다.
    public void mapToOrderProduct(OrderProductEntity orderProductEntity){
		orderProductEntities.add(orderProductEntity);
	}
}
@Entity
@Getter
@Table(name = "Orders")
public class OrderEntity extends BaseEntity {
	@Embedded
	private Receiver receiver;
	@Enumerated(EnumType.STRING)
	private OrderStatus orderStatus;
	private LocalDateTime deliveredAt;
	// Mapping Table 역할의 Entity와 1:N 관계에서 부모
	@OneToMany(mappedBy = "order")
	List<OrderProductEntity> orderProductEntities = new ArrayList<>();
    // OrderProduct 생성 시 수작업으로 양방향 관계에 있는 Entity의
	// @OneToMany가 선언된 List 필드에 추가해야한다.
    public void mapToOrderProduct(OrderProductEntity orderProductEntity){
		orderProductEntities.add(orderProductEntity);
	}
}


Mapping Table에 각각 부모DB Table을 참조하는 FK Field가 생성


주의사항 :

  • OrderProductEntity가 생성 된 경우 양방향관계부모 Entity@OneToMany List<> 필드에도 생성된 Entity 객체를 추가해야한다.
    。해당 mapping메서드부모Entity 내 구현하여 추후 Mapping Table 용도의 Entity 객체가 새로 생성된 경우 해당 메서드를 통해 반영
      // 부모 Entity 내 반영
public void mapToOrderProduct(OrderProductEntity orderProductEntity){
		orderProductEntities.add(orderProductEntity);
	}
  • 자식 Entity 객체를 생성 시 해당 Entity객체 내부의 static factory method를 통해 Entity객체를 생성하면서 부모 Entity매핑용메서드를 호출하여 전달
@ManyToOne
	@JoinColumn(name = "order_id")
	private OrderEntity order;
	@ManyToOne
	@JoinColumn(name = "product_id")
	private ProductEntity product;
	private OrderProductEntity(Long quantity, OrderEntity order, ProductEntity product) {
		this.quantity = quantity;
		this.order = order;
		this.product = product;
	}
	public static OrderProductEntity of(Long quantity, OrderEntity order, ProductEntity product){
		OrderProductEntity orderProduct = new OrderProductEntity(quantity,order,product);
		// OrderProduct 생성 시 각각 부모Entity인 ProductEntity , OrderEntity에 반영
		order.mapToOrderProduct(orderProduct);
		product.mapToOrderProduct(orderProduct);
		return orderProduct;
	}

OrderProduct 객체 생성 시 연관관계의 양쪽 부모 EntityList 필드에도 추가하는 메서드 실행

@ManyToMany N:M :
。실무에서는 1:N or N:1을 통해 중간에 Mapping Table을 구현하여 처리하므로 사용하지 않는다.

profile
공부기록 블로그

0개의 댓글