JPA - 일대다 연관관계

이유석·2023년 1월 9일
4

JPA - Entity

목록 보기
6/14
post-thumbnail

일대다 연관관계

  • 일대다 (1:N) 관계는 다대일 관계의 반대 방향입니다.

  • 일 (1) 이 외래키를 관리하며, 연관관계의 주인 입니다.

  • 해당 관계는 표준 스펙에서 지원은 하지만, 실무에서 권장하지 않는 관계 입니다.
    일대다 연관관계를 사용하여 발생하는 문제점들을 통해서 그 이유를 알아보도록 하겠습니다.

테이블 모델링

이해를 돕기 위해, 회원(N, Member)팀(1, Team)의 관계를 예시로 들어보겠습니다.

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 다수의 회원은 하나의 팀에 소속될 수 있습니다.
  • 즉, 팀과 회원은 일(1), 다(N) 관계입니다.

일대다 단방향 (1:N)

객체 모델링

위 테이블 모델링 조건에 일대다 단방향 관계를 위한 추가 조건은 아래와 같습니다.

  • 팀 객체와 회원 객체는 일대다 단방향 관계입니다.
  • 팀 객체(Team)는 Team.members 필드를 통해서 팀에 속한 회원 객체 목록(List<Member>)에 접근할 수 있습니다.
  • 회원 객체(Member)는 회원이 속한 팀 객체(Team)에 접근할 수 없습니다.

위 객체 모델링을 보면, 테이블 모델링 상 외래키를 소유하고 있는 MEMBER 테이블의 반대편인 TEAM 테이블의 객체인 Team 클래스가 외래키를 관리하고 있는 특이한 모습입니다.

  • 테이블 모델링은, 일(1) 과 다(N) 관계에서 외래키는 항상 다(N)쪽 테이블에 존재한다.
  • 하지만 다(N)쪽인 Member 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없다.
  • 대신에 반대쪽 Team 엔티티에는 참조 필드인 members 가 있다.

객체 관계 매핑

해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.

Team 클래스 (일대다에서 에 해당합니다.)

@Entity
public class Team {

	@Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    @Column(name = "NAME")
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)
    pricate List<Member> members = new ArrayList<>();
    
    // Getter, Setter, Constructor, ...
}

Member 클래스 (일대다에서 에 해당합니다.)

@Entity
public class Member {
	
    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String username;
    
    // Getter, Setter, Constructor
}

일대다 단방향 매핑에서는 에 해당하는 클래스에 에 해당하는 클래스를 Collection 프레임워크로 감싼 형태의 참조 필드로 작성해주시면 됩니다.
이때, 해당 참조 필드위에 @OneToMany@JoinColumn(name = "외래키 이름")을 추가하여 줍니다.

@JoinColumn

외래키를 매핑할 때 사용합니다.

자세한 속성은 JPA 단방향 연관관계 에 있는 설명과 동일합니다.

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 합니다.
그렇지 않으면 JPA 는 연결 테이블을 중간에 생성하고, 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용하여 매핑합니다.

@OneToMany

일대다 관계에서 사용합니다.

속성기능기본값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야 한다true
mappedBy연관관계의 주인 필드를 선택한다.
fetch글로벌 패치 전략을 설정한다.
(자세한 내용은 추후에)
@ManyToOne=FetchType.EAGER (즉시 로딩), @OneToMany=FetchType.LAZY (지연 로딩)
cascade영속성 전이 기능을 사용한다.
(자세한 내용은 추후에)
orphanRemovaltrue 로 설정 시, 고아 객체를 즉시 삭제합니다. false (고아 객체를 삭제하지 않습니다.)
targetEntity연관된 엔티티의 타입 정보를 설정한다.
이 기능은 거의 사용하지 않음

연관관계 사용 (1:N)

연관관계를 등록, 삭제 하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보겠습니다.

저장

Member member1 = new Member(0L, "회원1");
Member member2 = new Member(1L, "회원2");

entityManager.persist(member1); // INSERT-MEMBER1
entityManager.persist(member2); // INSERT-MEMBER2

Team team1 = new Team(0L, "팀1");

team1.getMembers().add(member1);
team1.getMembers().add(member2);

entityManager.persist(team1); // INSERT-TEAM1, UPDATE-MEMBER1.FK, UPDATE-MEMBER2.FK
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
INSERT INTO MEMBER (MEMBER_ID, USERNAME) VALUES (0, '회원1');
INSERT INTO MEMBER (MEMBER_ID, USERNAME) VALUES (1, '회원2');

INSERT INTO TEAM (TEAM_ID, NAME) VALUES (0, '팀1');

UPDATE MEMBER SET TEAM_ID = 0 WHERE MEMBER_ID = 0;
UPDATE MEMBER SET TEAM_ID = 0 WHERE MEMBER_ID = 1;

실행되는 SQL 코드를 보면 @OneToMany 단방향 매핑의 단점을 알 수 있습니다.

  • 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있기 때문에, INSERT SQL 이후에 UPDATE SQL 을 사용하여 외래키 매핑을 해줘야 합니다.

문제점

  • 일단 추가로 발생하는 UPDATE SQL 로 인하여 성능상 좋아보이지 않습니다.
  • 구조적으로도, 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리해야 하므로 더 복잡합니다.

해결법은 우선 다대일 단방향 매핑으로 테이블 모델링과 일치시켜준 후, 필요에 따라 일대다 단방향 매핑을 추가한 다대일 양방향 매핑을 사용합니다.

삭제

orphanRemoval 옵션을 true 로 하여 고아 객체를 어떻게 관리하는지 살펴보겠습니다.

Team team = entityManager.find(Team.class, 0L);

entityManager.remove(team);
  • 위 코드를 실행하였을 때, 실행되는 SQL 은 아래와 같습니다.
UPDATE MEMBER SET TEAM_ID = null WHERE TEAM_ID = 0;

DELETE FROM MEMBER WHER MEMBER_ID = 0;
DELETE FROM MEMBER WHER MEMBER_ID = 1;

DELETE FROM TEAM WHER MEMBER_ID = 0;

고아객체 삭제 옵션을 사용하였기 때문에, team 객체의 삭제로 인하여 고아객체가 되어버린 member 객체 역시 삭제되었습니다.

일대다 양방향 (1:N, N:1)

이런 매핑은 공식적으로 존재하지 않습니다..
대신 다대일 양방향 매핑을 사용하도록 하자 (일대다 양방향 == 다대일 양방향)

일(1), 다(N) 관계에서는 항상 다(N)에 외래키가 있으므로 @OneToMany, @ManyToOne 둘 중에 연관관계의 주인은 항상 다 쪽인 @ManyToOne 입니다.

  • 이런 이유로 @ManyToOne 에는 연관관계의 주인을 설정해주는 mappedBy 속성이 존재하지 않습니다..

그렇다고 일대다 양방향 매핑이 완전히 불가능한 것 은 아닙니다.

객체 모델링

  • 해당 연관관계의 주인은 일(1) (Team) 입니다.
  • 그러므로, 연관관계의 외래키인 MEMBER 테이블의 TEAM_ID 의 관리는 Team 객체가 수행합니다.
  • Member 객체는 연관관계의 외래키인 MEMBER 테이블의 TEAM_ID 에 대해서 읽기만 가능하게 설정해야 합니다.

객체 관계 매핑

해당 객체 모델링을 코드로 나타내어 보도록 하겠습니다.

Team 클래스 (일대다에서 에 해당합니다.)

@Entity
public class Team {

	@Id
    @Column(name = "TEAM_ID")
    private Long id;
    
    @Column(name = "NAME")
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)
    pricate List<Member> members = new ArrayList<>();
    
    // Getter, Setter, Constructor, ...
}

Member 클래스 (일대다에서 에 해당합니다.)

@Entity
public class Member {
	
    @Id
    @Column(name = "MEMBER_ID")
    private Long id;
    
    @Column(name = "USERNAME")
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
    
    // Getter, Setter, Constructor
}
  • @ManyToOne과 @JoinColumn을 사용해서 연관관계를 매핑하면, 다대일 단방향 매핑이 되어버립니다. 그런데 반대쪽 Team에서 이미 일대다 단방향 매핑이 설정되어있습니다.

  • 이런 상황에서는 두 엔티티에서 모두 테이블의 FK 키를 관리 하게 되는 상황이 벌어집니다.
    그걸 막기 위해서 insertable, updatable 설정을 FALSE로 설정하고 읽기 전용 필드로 사용해서 양방향 매핑처럼 사용하는 방법입니다.

위 방법은 일대다 양방향 매핑이라기 보다, 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가하여 일대다 양방향 처럼 보이게 하는 방법입니다.

  • 따라서 일대다 단방향 매핑이 가지는 단점을 그대로 갖고있습니다.

결론은 다대일 양방향을 사용하면 편합니다.

  • 일대다 매핑은 읽기 전용으로 사용할 수 도 있으니 알아두는게 좋습니다.

소스코드

profile
소통을 중요하게 여기며, 정보의 공유를 통해 완전한 학습을 이루어 냅니다.

1개의 댓글

comment-user-thumbnail
6일 전

Enjoy every moment with elite Escorts Daryaganj, where your comfort and pleasure are the top priorities. These elegant companions know how to create perfect memories through charm, care, and sensuality. Whether you want a one-time thrill or regular indulgence, their quality service ensures you leave satisfied. Book now for magical moments.

답글 달기