JPA - 일대다 연관관계

이유석·2023년 1월 9일
3

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
https://github.com/yuseogi0218

0개의 댓글