본문 바로가기
Spring JPA

연관관계 매핑 기초 - 8

by 홍굴이 2021. 7. 6.

단방향 연관관계

더보기

회원과 팀이 있다.
회원은 하나의 팀에만 소속될 수 있다.
회원과 팀은 다대일 관계이다.

  • 객체 연관관계
    - 회원 객체와 팀 객체는 단방향 관계
  • 테이블 연관관계
    - 회원 테이블과 팀 테이블은 양방향 관계
  • 객체 연관관계와 테이블 연관관계의 가장 큰 차이
    - 참조를 통한 연관관계는 언제나 단방향이다.
    - 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
       즉, 서로 다른 단뱡향 관계 2개다.
    - 반면에, 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.

객체 관계 매핑

회원 엔티티

@Entity
public class Member {
	
    	@Id
        @Column(name = "member_id")
        private String id;
        
        private String userName;
        
        @ManyToOne
        @JoinColumn(name = "team_id")
        private Team team;
        
 }

팀 엔티티

@Entity
public class Team {
	
    @Id
    @Column(name = "team_id")
    private String id;
    
 }

@ManyToOne

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 함 true
fetch 글로벌 페치 전략 설정 @ManyToOne = FetchType.EAGER
@OneToMany = FetchType.LAZY
cascade 영속성 전이 기능 사용  
targetEntity 연관된 엔티티의 타입 정보를 설정
거의 사용 x
 

@JoinColumn

속성 기능 기본값
name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블의 기본 키 칼럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 칼럼명 참조하는 테이블의 기본 키 칼럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다.
테이블 생성할 때만 사용
 
unique
nullable
insertable
updatable
columnDefinition
table
@Column의 속성과 같다.  

조회

  • 객체 그래프 탐색
  • 객체지향 쿼리 사용

객체지향 쿼리를 사용한 조회

String jpql = "select m from Member m join m.team t where t.name=:teamName";

List<Member> memberList = entityManager.createQuery(jpql, Member.class).setParameter("teamName", "팀").getResultList();

 

양방향 연관관계

  • 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. -> 데이터베이스 수정 사항이 없음
  • 객체 관계는 단방향 매핑을 추가해야한다.

회원 엔티티

@Entity
public class Member {
	
    	@Id
        @Column(name = "member_id")
        private String id;
        
        private String userName;
        
        @ManyToOne
        @JoinColumn(name = "team_id")
        private Team team;
        
 }

팀 엔티티

@Entity
public class Team {
	
    @Id
    @Column(name = "team_id")
    private String id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> memberList = new ArrayList<>();
 }

 

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.

엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다.

JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.

 

양방향 매핑의 규칙: 연관관계의 주인

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.

반면에 주인이 아닌 쪽은 읽기만 할 수 있다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

양방향 연관관계의 주의점 및

순수한 객체까지 고려한 양방향 연관관계

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.

public void save(EntityManager entityManager) {
        Member member = Member.builder()
                .id("member")
                .username("회원")
                .build();
        
        entityManager.persist(member);
        
        Team team = Team.builder()
                .id("team")
                .teamName("팀")
                .build();
        
        team.getMemberList().add(member);
        
        entityManager.persist(team);
    }

다음과 같이 저장하고 조회하면 member의 팀 값은 null로 나온다.

 

즉, 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

 

양방향 매핑 순환 구조의 문제

실제로 양방향 매핑을 통해서 엔티티를 관리하다보면 무한 순환되는 문제를 발견할 수 있다.

 

회원 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "member_name")
    private String memberName;

    @ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
    @Setter
    private Team team;

    @Builder
    public Member(String memberName, Team team) {
        this.memberName = memberName;
        this.team = team;
    }
}

팀 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_team")
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "team_name")
    private String teamName;

    @OneToMany(mappedBy = "team")
    @Setter
    private List<Member> memberList = new ArrayList<>();

    @Builder
    public Team(String teamName, List<Member> memberList) {
        this.teamName = teamName;
        this.memberList = memberList;
    }
}

테스트 코드

class 양방향매핑_Test {

    @Test
    void findByMemberName() {

        Team team = Team.builder()
                .teamName("영준 팀")
                .build();

        teamRepository.save(team);

        Member member = Member.builder()
                .memberName("영준")
                .build();

        member.setTeam(team);

        List<Member> memberList = new ArrayList<>();
        memberList.add(member);

        team.setMemberList(memberList);
        memberRepository.save(member);

        Member savedMember = memberRepository.findByMemberName(member.getMemberName()).orElse(null);

        assertNotNull(savedMember);
    }
}

위와 같이 테스트코드를 작성하여 확인해보면 무한 순환으로 연결되는 것을 확인할 수 있다.

이 문제를 해결하는 방법 몇가지를 소개하겠다.

  • Entity로 반환하지 않고, DTO를 적극 활용하기
  • @JsonIgnore : Json으로 직렬화 할 속성에서 무시 해버리기
  • @JsonManagedReference, @JsonBackReference : 직렬화 방향을 설정하여 해결
  • @JsonIdentityInfo : 순환 참조 될 대상의 식별키로 구분해 더 이상 순환참조되지 않게 한다.

@JsonIgnore

@JsonIgnore
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
@Setter
private Team team;
@JsonIgnore
@OneToMany(mappedBy = "team")
@Setter
private List<Member> memberList = new ArrayList<>();

Json으로 출력해보면 아예 무시되어 나오는 것을 확인할 수 있다.

 

@JsonManagedReference, @JsonBackReference

@JsonManagedReference
@ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
@Setter
private Team team;
@JsonBackReference
@OneToMany(mappedBy = "team")
@Setter
private List<Member> memberList = new ArrayList<>();

Json으로 출력하면 정확하게 나온다.

 

@JsonIdentityInfo

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "member_name")
    private String memberName;

    @ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
    @Setter
    private Team team;

    @Builder
    public Member(String memberName, Team team) {
        this.memberName = memberName;
        this.team = team;
    }
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_team")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "team_name")
    private String teamName;

    @OneToMany(mappedBy = "team")
    @Setter
    private List<Member> memberList = new ArrayList<>();

    @Builder
    public Team(String teamName, List<Member> memberList) {
        this.teamName = teamName;
        this.memberList = memberList;
    }
}

 

위의 방식들과는 다르게 연관관계를 아예 단방향으로만 양방향을 만드는 방법이있다.

양방향으로 만들 엔티티 사이에 엔티티를 하나 더 만들어 간접적으로 양방향으로 만드는 방법이다.

 

 

다음과 같은 방식으로 연관관계를 맺을 수 있다.

 

회원 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "member_name")
    private String memberName;

    @Builder
    public Member(String memberName) {
        this.memberName = memberName;
    }
}

 

팀 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_team")
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "team_name")
    private String teamName;

    @Builder
    public Team(String teamName) {
        this.teamName = teamName;
    }
}

회원 - 팀 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
@Table(name = "tbl_member_team")
public class MemberTeam {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(targetEntity = Member.class, fetch = FetchType.LAZY)
    private Member member;

    @ManyToOne(targetEntity = Team.class, fetch = FetchType.LAZY)
    private Team team;

    @Builder
    public MemberTeam(Member member, Team team) {
        this.member = member;
        this.team = team;
    }
}

테스트 코드

class 양방향매핑_Test {

    @Test
    void findByMemberName() throws JsonProcessingException {

        Team team = Team.builder()
                .teamName("영준 팀")
                .build();

        teamRepository.save(team);

        Member member = Member.builder()
                .memberName("영준")
                .build();

        memberRepository.save(member);

        MemberTeam memberTeam = MemberTeam.builder()
                .member(member)
                .team(team)
                .build();

        memberTeamRepository.save(memberTeam);

        MemberTeam savedMemberTeam = memberTeamRepository.findById(1L).orElse(null);

        assertNotNull(savedMemberTeam);
    }
}

이렇게 연관관계를 맺을 경우 회원 엔티티와 팀 엔티티는 각각 테이블에서는 절대 알 수 없고, 회원-팀 엔티티를 통해서만 접근을 할 수 있다. 

'Spring JPA' 카테고리의 다른 글

고급 매핑 - 10  (0) 2021.07.12
다양한 연관관계 매핑 - 9  (0) 2021.07.09
엔티티 매핑 - 7  (0) 2021.07.03
영속성 관리 - 6  (0) 2021.06.29
영속성 관리 - 5  (0) 2021.06.26

댓글