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

TopOfTheHead·2025년 11월 10일

Spring JPA

목록 보기
3/10

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

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

  • ORM방향성의 특징으로 연관관계1:1 / 1:N / N:1 / N:M을 가진다.
    。일반 RDBMS1:1 / 1:N / N:M만 존재

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

  • 연관관계 주인(Relational Ownership)은 외래키 테이블
    1:N 관계N연관관계 주인이 된다.
    RDBMS와 동일하게 연관관계에서 1:N인 경우 NFK( 외래키 )가 있는쪽으로 설정

    연관관계에서 부모 Entity를 기준으로 Mapping을 수행하며 자식 EntityFK 필드가 존재
    ▶ 일반적으로 부모 Entity자식 Entity간 관계는 1:N 관계로서 부모 Entity는 여러 자식 Entity 객체를 가질 수 있고 자식 Entity는 하나의 부모 Entity 객체를 가진다.

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에서 추가로 선언

의존관계 주인 테이블FK 필드에서 @JoinColumn을 지정하지 않는 경우, Hibernate에 의해 임의의 필드명으로 설정됨.

@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 제약조건을 생성하지 않도록 설정

  • @JoinColumn( referencedColumnName = "부모테이블에서 참조할 필드명" )
    의존관계 정의 시 FK컬럼부모 테이블의 어떤 컬럼의존할 것인지 지정하는 속성
    ▶ 정의하지 않는 경우 부모테이블 PK를 참조하도록 지정

    부모 테이블에서 PK 이외의 컬럼참조 시 설정하는 속성
	@ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "item_code", referencedColumnName = "code")
    private Item item;

자식테이블item_code 컬럼부모테이블code 컬럼의존

@ManyToOne, @OneToMany에서 연관관계Entity초기화 전략
FETCH : 연관관계 객체를 가져오는 방법

연관관계 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로 할당

  • 주의사항 : 편의 메서드

    • 자식Entity객체가 생성 된 경우 양방향관계부모 Entity@OneToMany List<> 필드에도 생성된 Entity 객체를 추가해야한다.
      。해당 mapping메서드 ( mapTo??() )를 부모Entity 내 구현하여 추후 Mapping Table 용도의 Entity 객체가 새로 생성된 경우 해당 메서드를 통해 반영
    // 매핑용 메서드
        public void mapToOrder(OrderEntity order){
    		this.orders.add(order); // 부모 List<자식> 필드에 자식 객체 추가
              order.setMember(this); // 자식객체의 부모 필드에 부모 객체 추가
    	}
    • 자식 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<> 필드에 추가

    편의 메서드
    연관관계 객체필드 초기화 역할의 메서드
    protected로 설정하여 외부 패키지에서 접근하지 못하도록 설정

     public Member(
    		String name,
    		String password,
    		String email,
    		Address address
    	){
    		this.name = name;
    		this.password = password;
    		this.email = email;
    		addresses.add(address);
    		address.setMember(this);
    	}
    	// 편의 메서드
    	protected void setAddress(Address address) {
    		if(address != null){
    			this.addresses.add(address);
    			address.setMember(this);
    		}
    	}


  • 자식 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을 구현하여 처리하므로 사용하지 않는다.

@ManyToMany를 적용하는 경우 Hibernate는 자동으로 Mapping table을 생성.
@JoinTable을 통해 해당 Mapping Table에 대한 설정을 수행

 // JPA는 @ManyToMany 지정 시 Hibernate가 자동으로 Mapping table을 생성.
	// @JoinTable(name = "지정할매핑테이블이름", joinColumns(name="조인할ON컬럼명"))
	@ManyToMany(fetch = FetchType.EAGER)
	@JoinTable(
		name = "products_categories",
		joinColumns = @JoinColumn(name = "product_id"),
		// 반대편에서 조인할 ON컬럼명
		inverseJoinColumns = @JoinColumn(name = "category_id")
	)
	private List<Category> categories = new ArrayList<>();
  @ManyToMany(fetch = FetchType.EAGER, mappedBy = "categories")
	private List<Product> products = new ArrayList<>();
profile
공부기록 블로그

0개의 댓글