JPA 엔티티 어노테이션 및 연관관계 매핑 코드 구현 정리

devdo·2022년 4월 9일
1

JPA

목록 보기
2/13
post-thumbnail

JPA 사용시, 엔티티 클래스를 만들어줘야 한다. 엔티티에 자주 사용하는, 꼭 써야하는 어노테이션들은 무엇인지 알아보자.

Entity 설계

예시로 사용할 엔티티 클래스는 다음과 같다.

보통 나는 엔티티에는 빌더패턴을 쓰기 때문에 @Builder도 같이 쓴다.
하지만 @Builder를 쓸 때 '생성자에 쓰는 것'이 권장된다.
자세한 건 https://velog.io/@mooh2jj/올바른-엔티티-Builder-사용법

JPA를 사용할려면 '디폴트 생성자' 가 필요하다. 그래서 @NoArgsConstructor를 써주는 것이다.

옵션으로 준access = AccessLevel.PROTECTED은 자바 개발자라면 다들 배웠을 접근제한자랑 같은 의미다.

PROTECTED는 다른 외부 패키지에 소속된 클래스가 접근하지 못하게 하는 것이다.

??) access = AccessLevel.PRIVATE 는 사용하면 안되나?

?? access = AccessLevel Proxy 객체와 관련되어 있다. JPA 지연로딩시, Proxy 객체를 사용하기에 외부에서 이 엔티티 클래스를 사용하면 오류가 나서 안정성이 안좋다.

👀 각 연관관계의 default 속성은 다음과 같다.

  • ☑️@ManyToOne : EAGER
  • ☑️@OneToOne : EAGER
  • @ManyToMany : LAZY
  • @OneToMany : LAZY

✳️ XXXToOne(FetchType.EAGER) 부분을 Lazy로 바꿔주어야 한다!


Entity 설정 어노테이션들

@Getter
// @Setter	// 실무에선 자제
@NoArgsConstructor(access = AccessLevel.PROTECTED) // entity를 만들기 위해서는 기본생성자 필요
@AllArgsConstructor		// 권장 x => 안쓰는 게 나음!
@Builder	// 권장x => 생성자에
@Entity
@ToString(exclude = "team")
// @ToString(of = {"id", "username", "age"})
public class Member {

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

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
    
    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;

        if (team != null) {
            changeTeam(team);
        }
    }

    private void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@ToString(exclude = "members")
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")		// mappedBy에서 쓰이는 변수명은 @ManyToOne Member 엔티티의 Team 변수명과 같아야 한다!
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}

연관관계 매핑 정리

양방향

  • ManyToMany(외래키, 조인관계를 직접 처리)

하지만 실제 실무에선 쓰지 않는 방식이다. 이런 조인 방식에서 만든 매핑테이블에는 추가 컬럼을 만들 수도 없기 때문이다.
다른 방법으로 직접 매핑테이블을 만들고 ManyToOne으로 만드는 방식으로 한다.

	// Category.class
    @ManyToMany
    @JoinTable(name = "category_item",
            joinColumns = @JoinColumn(name = "category_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id"))
    private List<Item> items = new ArrayList<>();
	// springboot_board_example
    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name = "user_roles",
    	joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
    	inverseJoinColumns = @JoinColumn(name="role_id", referencedColumnName = "id"))
    private Set<Role> roles;

✅ 직접 중간매핑 만들기(실제 현장에선 이렇게 만듦)
단방향매핑(@ManyToOne) 이 2개 만들면 된다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "cart_item")
public class CartItem {
    // cart - cart_item - item 중간 매핑 테이블
    // 1   :  N  _   M  :  1

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "cart_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "cart_id")
    @Setter
    private Cart cart;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    @Setter
    private Item item;

}

단방향

  • ManyToOne - 연관관계의 주인(외래키를 가진 곳) 설정, 자식테이블(ex. member - Team 관계중 member! )쪽에 있다고 보면 된다! 아래 글 참고

  • OneToMany - 외래키로 참조한 자식 테이블을 보통 List로 담을 때 사용, 필수는 아니다.

  • cascade = CascadeType.ALL영속성 전이를 해주는 옵션이다. 이 옵션을 설정해주면 외래키로 참조한 자식 테이블도 같이 지워줄 수 있다.
    추가로 고아객체도 추가설정할 수 있다.

	// Team.class : 부모테이블
    
    // Team.members는 읽기용도일뿐! 주인 아님!
    // 컬렉션일 때 cascade 처리(영속성 전이)
    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL
    , orphanRemoval = true)
    private List<Member> members = new ArrayList<>();

✅ 참고) 고아 객체

  • 고아 객체 : 부모 엔티티와 연관관계가 끊어져서 버려진 자식 엔티티

  • 고아객체 제거 기능은 참조하는 곳이 하나일 때만 사용

  • ⚠️ 예를 들어, OrderItem 엔티티를 Order 엔티티가 아닌 다른 곳에서 사용하고 있다면, 이 기능을 사용하면 안됨!

  • @OneToOne, @OneToMany 어노테이션에서 orphanRemoval = true 옵션 추가할 수 있음.


연관관계 주인 => ManyToOne 쪽

https://velog.io/@conatuseus/연관관계-매핑-기초-2-양방향-연관관계와-연관관계의-주인

엔티티를 양방향 연관관계로 설정한다면, 객체의 참조는 둘인데 외래 키는 하나입니다.

따라서 DB 테이블과 다르게 차이가 발생합니다.

그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까요?

이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리하게 하는데 이것을 연관관계의 주인이라 합니다.

  • ManyToOne(xxxToOne일시 Lazy즉시로딩으로 처리)
    // Member.class : 자식 테이블
    
    // 외래키를 가지고 있는 곳인 연관관계의 주인이다! Member.team이 주인임 
    // 즉, 자식테이블의 부모테이블 필드가 연관관계의 주인!
 	@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id") 		// @JoinColumn 로 외래키 걺
    private Team team;

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

양방향 연관관계 매핑 시 지켜야할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다.

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있습니다.

어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 됩니다.

	// Team.class : 부모테이블
    
    // Team.members는 읽기용도일뿐! 주인 아님!
	// mappedBy = "team" => Member.team 이 연관관계 주인 설정
    // OneToMany쪽에서 설정 
    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL
    , orphanRemoval = true)
    private List<Member> members = new ArrayList<>();
  • mappedBy 설정은 @OneToMany에서 설정하는 것이다.
  • mappedBy에서 쓰이는 변수명은 @ManyToOne Member 엔티티의 Team 변수명과 같아야 한다!
  • @ManyToOne이 항상 연관관계의 주인이 된다.
  • 연관관계의 주인은 외래 키가 있는 곳이다.

연관관계의 주인은 외래 키(FK)가 있는 곳! 자식테이블! @ManyToOne

연관관계의 주인은 테이블에 외래 키(FK)가 있는 곳으로 정해야 합니다.

🌟 결론적으로, 외래키를 가지고 있는 자식테이블에 연관관계의 주인인 @ManyToOne 이 있습니다!

여기서는 회원 테이블이 외래 키를 가지고 있으므로

Member.team이 주인이 됩니다.
주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정해야 합니다.
여기서 mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말합니다.


연관관계 편의 메서드

https://velog.io/@conatuseus/연관관계-매핑-기초-2-양방향-연관관계와-연관관계의-주인

연관관계의 주인만이 외래 키의 값을 변경할 수 있습니다!

public class Member {
...
private Team team;

public void setTeam(Team team){
	this.team = team;
	member.getTeam().add(member);
}

setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했습니다.
이렇게 수정한 메서드를 사용하는 코드를 보겠습니다.

Test

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);
}

이렇게 한 번에 양방향 관계를 설정하는 메서드를 연관관계 편의 메서드라 합니다.



소스출처

profile
배운 것을 기록합니다.

0개의 댓글