삽질 주도 개발
article thumbnail
Published 2022. 11. 20. 00:25
JPA 연관 관계 매핑 기초 Spring

테이블에 맞추어 모델링

team table과 member table이 1:N 관계를 맺고 있다고 가정하자.

그렇다면 기본적으로 테이블의 ERD는 다음과 같다.

member-team erd

그렇다면 테이블에 맞춰 객체를 모델링해보면 다음과 같이 설계될 수 있다.

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

    public Team() {}

    // getter/setter
}
@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private Long teamId;

    public Member() {}

    // getter/setter
}

 

그렇다면 우리는 member의 team을 조회하기 위해서 다음과 같이 작성할 수 있다.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setTeamId(team.getId());
member.setUsername("silly");
em.persist(member);

Team findTeam = em.find(Team.class, member.getTeamId());
System.out.println("team = " + findTeam.getName());

식별자를 가지고 재조회를 하는 것은 객체 지향적인 방법이 아니다.

 

즉, 참조를 통한 그래프 탐색이 가능해야 한다. 이때 우리는 연관관계 매핑 어노테이션을 사용할 수 있다.

연관 관계는 다중성이 있는데, 이 개념을 알기 전에 보편적인 다대일의 개념에서 방향성을 먼저 알아보도록 한다.

 

단방향 연관 관계

우리는 @ManyToOne으로 연관 관계를 매핑할 수 있다.

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    public Member() {}

    // getter/setter
}

'다'쪽의 Member에서 '일'쪽의 Team과 매핑하기 때문에 '많은 Many', '에서 To', '하나 One'
join column은 Team의 team_id로 지정하여 외래 키 매핑한다.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("woogie");
member.setTeam(team);
em.persist(member);

Member findMember = em.find(Member.class, member.getId());
Team referTeam = findMember.getTeam();

 

양방향 연관 관계

종종 '다' 쪽에서 '일' 말고 반대로 '일' 쪽에서 '다'의 목록을 조회하는게 더 편할 경우가 있다.

 

@ManyToOne@OneToMany 은 함께 사용해서 객체 간의 양방향 참조를 맺을 수 있다.

 

기억해야 할 것은 테이블의 변화는 없다는 것이다. 객체 관계가 단방향이던 양방향이던 테이블 연관관계는 외래 키 하나로 조인해서 항상 양방향 관계를 맺고 있기 때문에 방향성이라는게 없다고 볼 수 있다. 

 

위의 예시의 Member는 그대로 두고, Team의 코드를 다음과 같이 수정하면 된다.

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

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

    public Team() {}

    // getter/setter
}
Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("woogie");
member.setTeam(team);
em.persist(member);

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();

 

mappedBy

주인이 아니면 mappedBy를 사용하면 된다고 생각할 수 있는데, ~를 대상(주인)으로 매핑이 된다는 뜻이니까 "주인이 아니다"라는 걸 생각하자

 

mappedBy = "team" 주인 객체의 team field를 대상으로 매핑된다.

 

연관 관계의 주인

양방향 연관 관계를 맺을 때 외래 키를 관리하는 클래스가 연관 관계의 주인이라고 한다.

예시 코드에서는 Member의 Team, Team의 List 이 둘 중 하나로 외래 키를 관리해야 한다.

즉, 객체의 두 관계 중 하나의 연관 관계를 주인으로 지정해야 하고 연관관계의 주인만 외래 키를 관리해야 한다.

연관 관계의 주인이 아닌 쪽은 읽기만 가능하다(변경 쿼리들은 동작 안됨).

보통 테이블 상 외래키를 가지는 테이블에 매핑된 엔티티가 연관 관계의 주인이 된다.

 

편의 메서드

 

양쪽 값을 한 번에 set하기 위해 편의 메서드를 만드는 것도 좋다. 

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setUsername("woogie");
member.setTeam(team);
em.persist(member);

위 코드를 보면 양방향 매핑시 연관관계 주인(member)에게 외래 키(team)만 세팅해도 되지만, 두 객체 모두 값을 set 해주는게 좋다.

왜냐하면 JPA에서 알아서 해주지만 "객체지향적으로 set을 해주지 않았는데 어떻게 찾을 수 있지?" 라는 생각이 들 수 있다.


또, 지연 로딩에서 이슈가 있는데, 트랜잭션 내에서 find(Team.class, team.getId()).getMembers()에서 1차 캐시에는 memberList가 들어가 있지 않아서 조회할 수 없다.

 

 

member에서 편의 메서드를 만드는 경우

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    public Member() {}

    // omitted

    // 관습적으로 사용되는 set보다 더 명시적인 이름이 있으면 그것을 사용하자.
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

 

team에서 편의 메서드를 만드는 경우

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

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

    public Team() {}

    // omitted

    public void addMember(Member member) {
        member.setTeam(this);
        members.add(member);
    }
}

 

무한 루프

양방향 관계에서 toString, lombok, Json 변환 객체같은 것을 사용하면 team <-> member를 계속 참조하기 때문에 무한 루프의 위험성이 크다.

 

@OneToMany 시 구현체를 할당하는 이유

하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 내장 컬렉션을 사용하도록 참조를 변경한다.

하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어 래퍼 컬렉션이라고도 부른다.

하이버네이트는 위 특징 때문에 컬렉션을 사용할때 즉시 초기화를 권장한다.

Collection<Member> members = new ArrayList<Member>();

 

정리

  • 시스템 설계 시 그냥 무조건 단방향 매핑으로 설계를 해라
  • 양방향 매핑은 객체 그래프 탐색 기능이 추가된 것 뿐 (대신 관리 비용은 훨씬 더 큼)
  • 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 된다.

 


Reference

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

'Spring' 카테고리의 다른 글

주관적인 생각이 그득한 detach, remove  (1) 2022.11.22
Jasypt로 properties 암호화  (0) 2022.11.20
spring boot 2.5 이후 DB 데이터 초기화 설정 변경 사항  (0) 2022.11.19
JPA flush  (1) 2022.11.19
엔티티 매핑  (0) 2022.11.18