HoonK212 GitHub_Blog

[JPA] JPA 기초 3

JPA 기초 3

단방향 연관관계 매핑

2022-11-27-JPA-01.png

  • 객체 연관관계
    • 회원 객체(Member)는 Member.team 필드(멤버 변수)로 팀 객체와 연관관계를 맺는다.
    • 회원은 Member.team 필드를 통해 팀을 알 수 있지만, 팀 객체로 소속된 회원들을 알 수 없기 때문에 단방향 관계이다. (member → team 의 조회는 member.getTeam()으로 가능하지만, team → member 를 접근하는 필드는 없다.)
    • 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가하여 참조를 보관하면 되지만, 엄밀히 말하면 양방향이 아닌 서로 다른 단방향 관계 2개가 된다.
  • 테이블 연관관계
    • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
    • 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있기 때문에 양방향 관계이다. (MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM 과 TEAM JOIN MEMBER 둘 다 가능)
  • 연관관계 사용

    @Entity
    public class Member {
    
      @Id @GeneratedValue
      private Long id;
    
      @Column(name = "USERNAME")
      private String name;
    
      @ManyToOne
      @JoinColumn(name = "TEAM_ID")
      private Team team;
    
      // Getter, Setter ...
    }
    
    @Entity
    public class Team {
    
      @Id @GeneratedValue
      private Long id;
      private String name;
    
      // Getter, Setter ...
    }
    
    • 연관관계 저장

      //팀 저장
      Team team = new Team();
      team.setName("TeamA");
      em.persist(team);
      
      //회원 저장
      Member member = new Member();
      member.setName("conas");
      member.setTeam(team); //단방향 연관관계 설정, 참조 저장
      em.persist(member);
      
      • member에 teamId를 넣지 않고, team을 넣어준다.
    • 참조로 연관관계 조회 - 객체 그래프 탐색
      //조회
      Member findMember = em.find(Member.class, member.getId());
      //참조를 사용해서 연관관계 조회
      Team findTeam = findMember.getTeam();
      
    • 연관관계 수정

      // 새로운 팀B
      Team teamB = new Team();
      teamB.setName("TeamB");
      em.persist(teamB);
      
      // 회원1에 새로운 팀B 설정
      member.setTeam(teamB);
      

@ManyToOne

  • 다대일(N:1) 관계라는 매핑 정보 (위에서 봤던 회원과 팀은 다대일 관계)
  • 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
  • @ManyToOne 속성 2022-11-27-JPA-02.png

@JoinColumn

  • 외래 키를 매핑할 때 사용
  • 생략 가능
    • 생략 시, 외래 키를 찾을 때 기본 전략을 사용
    • 기본 전략 : 필드명_참조하는 테이블의 컬럼명
  • @JoinColumn 속성 2022-11-27-JPA-03.png

양방향 연관관계 매핑

2022-11-27-JPA-04.png

@Entity
public class Member {

 @Id
 @Column(name = "MEMBER_ID")
 private String id;

 private String username;

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

 // 연관관계 설정
 public void setTeam(Team team) {
   this.team = team;
 }

 // Getter, Setter ...
}
@Entity
public class Member {

 @Id
 @Column(name = "MEMBER_ID")
 private String id;

 private String username;

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

 // 연관관계 설정
 public void setTeam(Team team) {
   this.team = team;
 }

 // Getter, Setter ...
}
  • Team과 Member는 일대다 관계이므로 Team 엔티티에 List members를 추가
  • 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용
  • @OneToMany 어노테이션의 mappedBy 속성은 양방향 매핑일 때 반대쪽 매핑의 필드 이름을 값으로 설정하여 사용
  • 일대다 컬렉션 조회

    // member1: (id=1, username="회원1", team="team1")
    // member2: (id=2, username="회원2", team="team1")
    
    Team team = em.find(Team.class, "team1");
    List<Member> members = team.getMembers(); // (팀 -> 회원) 객체 그래프 탐색
    
    for (Member member : members) {
    System.out.println("member.username = " + member.getUsername());
    }
    
    // === 결과 ===
    // member.username = 회원1
    // member.username = 회원2
    

연관관계의 주인

  • 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
  • 그에 반해 객체는 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 하는 것이 한계이다.
  • 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이다. 따라서 둘 사이에 차이가 발생
  • JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있으며, 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 주인은 mappedBy 속성을 사용하지 않으며, 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
  • Member.team, Team.members 둘 중 연관관계의 주인은? 2022-11-27-JPA-05.png

    class Member {
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    // ...
    }
    
    - 팀 -> 회원(Team.members) 방향
    ~~~ java
    class Team {
    
      @OneToMany
      private List<Member> members = new ArrayList<>();
      // ...
    }
    
    • 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것
      • Member.team을 주인으로 선택하는 경우 (테이블에 외래 키가 있는 곳)
        • 자기 테이블에 있는 외래 키를 관리하면 된다.
      • Team.members를 주인으로 선택하는 경우
        • 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. (Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리할 외래 키는 MEMBER 테이블에 있기 때문)
      • Member.team을 주인으로 선택하고 Team.members에는 mappedBy=”team” 속성을 사용하여 주인이 아님을 설정해야 한다.
      • mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말한다.
      • 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지 못한다.
      • 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가지며, 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다. 2022-11-27-JPA-06.png

양방향 연관관계 저장

public void save() {

 // 팀1 저장
 Team team1 = new Team("team1", "팀1");
 em.persist(team1);

 // 회원1 저장
 Member member1 = new Member("member1", "회원1");
 member.setTeam(team1);  // 연관관계 설정 team1 -> team1
 em.persist(member1);

 // 회원2 저장
 Member member2 = new Member("member2", "회원2");
 member2.setTeam(team1); // 연관관계 설정 member2 -> team1
 em.persist(member2);
}

2022-11-27-JPA-07.png

  • 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장
  • team1.getMembers().add(member1); // 무시됨 (연관관계의 주인이 아님)
    • 위와 같은 코드가 추가로 있어야 할 것 같지만 Team.members는 연관관계의 주인이 아니므로 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.

양방향 연관관계의 주의점

Member member1 = new Member("member1", "회원1");
em.persist(member1);

Member member2 = new Member("member2", "회원2");
em.persist(member2);

Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);

em.persist(team1);

2022-11-27-JPA-08.png

  • 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문에 위와 같은 현상 발생

객체 관점의 양방향 연관관계

  • 사실은 객체 관점에서는 연관관계의 주인이 아닌 곳을 포함하여 양쪽 방향에 모두 값을 입력해주는 것이 안전하다.
  • JPA를 사용하지 않는 순수한 객체 상태에서 문제가 발생한다.
public class Member{

  private Team team;

	public void setTeam(Team team){

	 if(this.team != null) {
			// this.team이 null이 아니면 이 member객체는 team이 있음을 의미
	   this.team.getMembers().remove(this);		// 해당 팀의 멤버에서 삭제
	 }
	 this.team = team;
	 team.getMembers().add(this);
	}
  // ...
}
public void test() {

  Team team1 = new Team("team1", "팀1");
  em.persist(team1);

  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);
  em.persist(member1);

  Member member2 = new Member("member2", "회원1");
  member2.setTeam(team1);
  em.persist(member2);
}
  • 위의 setTeam()과 같이 수정하여 양방향 관계를 모두 설정하도록 한다. (연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.)
  • 한 번에 양방향 관계를 설정하는 메서드를 ‘연관관계 편의 메서드’라 한다.

연관관계 매핑 총 정리

  • (참고 : https://velog.io/@conatuseus/JPA-다양한-연관관계-매핑)

임베디드 타입

  • JPA에서 새로운 값 타입을 직접 정의해서 사용하는 것
  • 하이버네이트는 임베디드 타입을 컴포넌트(Components)라 한다.
// 임베디드 타입 사용
@Entity
public class Member {

  @Id @GeneratedVAlue
  private Long id;
  private String name;

  @Embedded
  private Period workPeriod;	// 근무 기간

  @Embedded
  private Address homeAddress;	// 집 주소
}
// 기간 임베디드 타입
@Embeddable
public class Peroid {

  @Temporal(TemporalType.DATE)
  Date startDate;
  @Temporal(TemporalType/Date)
  Date endDate;
  // ...

  public boolean isWork (Date date) {
    // .. 값 타입을 위한 메서드를 정의할 수 있다
  }
}
@Embeddable
public class Address {

  @Column(name="city") // 매핑할 컬럼 정의 가능
  private String city;
  private String street;
  private String zipcode;
  // ...
}

2022-11-27-JPA-09.png

  • 장점
    • 재사용 및 높은 데이터 응집도
    • 위의 Period 객체에서 isWork() 메서드처럼 해당 값 타입만 사용하는 의미있는 메서드 생성 가능
  • 사용법
    • 기본 생성자 필수
    • @Embeddable : 값 타입을 정의하는 곳에 표시
    • @Embeded : 값 타입을 사용하는 곳에 표시
  • 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존한다.
  • 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이다.

임베디드 타입과 테이블 매핑

2022-11-27-JPA-10.png

  • 임베디드 타입은 엔티티의 값일 뿐이므로 값이 속한 엔티티의 테이블에 매핑한다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 잘 설계한(객체와 테이블을 세밀하게 매핑한) ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
  • @AttributeOverride: 속성 재정의

    // 같은 임베디드 타입을 가지고 있는 회원
    @Entity
    public class Member {
    
      @Id @GeneratedValue
      private Long id;
      private String name;
    
      @Embedded
      Address homeAddress;
    
      @Embedded
      Address companyAddress;
    }
    
    • 위와 같은 경우에 테이블에 매핑하고자 하는 컬럼명이 중복됨
    • 아래와 같이 @AttributeOverride를 사용하여 매핑 정보 재정의
    @Entity
    public class Member {
    
      @Id @GeneratedValue
      private Long id;
      private String name;
    
      @Embedded
      Address homeAddress;
    
      @Embedded
      @AttributeOverrides({
        @AttributeOverride(name="city", column=@Column(name="COMPANY_CITY")),
        @AttributeOverride(name="street", column=@Column(name="COMPANY_STREET")),
        @AttributeOverride(name="zipcode", column=@Column(name="COMPANY_ZIPCODE"))
      })
      Address companyAddress;
    }
    
    • 생성된 테이블은 아래와 같다.
    CREATE TABLE MEMBER (
      COMPANY_CITY varchar(255),
      COMPANY_STREET varchar(255),
      COMPANY_ZIPCODE varchar(255),
      city varchar(255),
      street varchar(255),
      zipcode varchar(255),
      ...
    )
    
    • 특성
      • @AttributeOverride를 사용하면 엔티티 코드가 복잡해진다. (자주 사용하지 않음)
      • @AttributeOverride는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.

“[JPA] JPA 기초” 시리즈는 인프런 김영한 강사님의 강좌를 기반으로 쓰여졌습니다.

(참고 : https://www.inflearn.com/users/@yh)