[#1. SpringBoot JPA사용하기] JPA 엔티티 연관관계 매핑 개념 설명 . 예제

BlackBean99·2022년 1월 15일
2

DB

목록 보기
3/5

설명하기 전에 RDB와 JPA Entity의 차이를 이해해보자

RDB

테이블간의 foreign key ( 외래키 ) 로 연관관계를 맺고 JOIN 명령어를 통해서 테이블을 조회한다.

자 한 팀과 그 팀에 소속된 멤버의 예시를 들어보자
Team 테이블과 Member 테이블은 1:M관계다.
일반적으로 외래키는 M쪽인 Member 테이블에 존재하고 이 외래키를 통해 Member 의 Team, Team 과 Member를 조회할 수 있다.

Member JOIN Team
Team JOIN Member

이 두 명령어 모두 가능하다.

객체를 사용한다면 어떨까?
Team, Member의 Entity객체가 있다.
Member에 Team 멤버 변수를 두어서 관계를 맺는다.
이렇게 하면
[ O ] Member -> Team 연관관계
[ X ] Team -> Member 연관관계
즉, Team 테이블에서 소속(연관관계를 맺은 Member) 를 조회할 수 없다.

이러한 관계를 단방향 관계라고 한다.

@Entity
public class Team {
    private Long id;
    private String teamName;
    //Team은 자기와 연관된 Member를 알 수 없다.
}

@Entity
public class Member {
    private Long id;
    //Member는 자기와 연관된 Team을 알고있다.
    private Team team;
    private String memberName;
}

그럼 RDB처럼 양방향으로 할 수는 없을까?
단순하게 *단방향 관계 2개로 구현할 수 있다.
하지만 따지고보면 단방향 관계 2개다.

1. 다대일(M:1) 단방향 관계 구현

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String MemberName;
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
}

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String teamName;
}
  • @ManyToOne : N:1 관계를 표현하는 어노테이션이다. @ManyToOne이 붙은 엔티티가 M이고 반대 엔티티가 1일 때 붙인다.
  • @JoinColumn(name="TEAM_ID") : 외래키를 정의하는 어노테이션이다.

이 두 어노테이션을 자주 사용하니 해당 어노테이션의 Attributes를 자세히 알아볼까요?

@JoinColumn 속성

name매핑할 외래키 이름필드명_참조하는 테이블 기본기 칼럼명
referencedColumnName외래키가 참조하는 대상 테이블의 칼럼명참조하는 테이블 기본키 칼럼명(반대편 테이블의 기본키)
foreignKey(DDL)외래 키 제약조건 지정unique, nullable, insertable, updatable, columnDefinition, table

@ManyToOne 속성

optionalfalse 일때 연관된 엔티티가 항상 있어야 한다true
fetch글로벌패치 전략 설정@ManyToOne=FetchType.EAGER @OneToMany=FetchType.LAZY
cascade영속성 전이기능 사용
targetEntity연관된 엔티티 타입 정보 설정

이해를 했으니 매핑한 Entity로 Member, Team테이블에 값을 저장하는 코드를 보자.

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Team team = new Team();
        team.setTeamName("test");
        em.persist(team);

        Member member = new Member();
        member.setMemberName("memberTest");
        //member객체에 team객체를 넣어 관계를 맺는다
        member.setTeam(team);
        em.persist(member);
    }
}

다대일(M:1) 양방향 관계

일단 코드 먼저 보자 이제는 이해할 수 있을 것이다.

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String teamName;
    //양방향 매핑을 위해 추가
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String MemberName;
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
}

위의 코드를 보면 Team에 Member를 저장할 필드가 생성된 것을 볼 수 있고, @OneToMany 어노테이션이 사용됐다. 그리고 속성으로 mappedBy가 사용되었는데 mapppedBy는 양방향 매핑일 때 사용한다.
name 속성에는 반대쪽 매핑의 필드 이름값을 넣는다. 위 코드상에서는 Member의 Team객체의 team을 입력한다.

단방향 2개로 양방향 관계를 구현하기 위해서는 외래키 (FK) 가 2개 필요하다.
이를 JPA에서는 2개의 연관관계중 하나를 고르기 위해서 mappedBy를 사용한다.

어?? 그러면 Team 말고도 Member에서도 mappedBy를 해줘야 하는거 아닌가요? 2개의 연관관계의 Master Table을 설정해주는 것이기 때문에
- Member에 작성하지 않아도 Member엔티티에 외래키(FK)가 생성된다.

하지만 유의해야할 점이 있다!

자! 다음은 양방향 관계에서 데이터를 삽입하는 코드다.

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Team team = new Team();
        team.setTeamName("test");
        em.persist(team);

        Member member = new Member();
        member.setMemberName("memberTest");
        member.setTeam(team);
        em.persist(member);
    }
}

위 코드에서 Master Table은 Team이다.

연관관계주인 방향에서 데이터를 입력하면 연관관계 주인이 아닌(Team)에 Member 데이터를 넣지 않아도 데이터베이스에는 정상적으로 들어간다.
하지만 아래 코드를 봐라! Member에만 데이터를 넣었을 경우에는 정상적으로 데이터가 삽입되지 않는다.

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Team team = new Team();
        team.setTeamName("test");
        em.persist(team);

        Member member = new Member();
        member.setMemberName("memberTest");
        //관계주인인 member에는 team을 설정하지 않음
        //member.setTeam(team);
        
        //관계주인이 아닌 team에 member를 추가
        team.getMembers().add(member);
        em.persist(member);
    }
}

결과를 보면 Member 테이블에 외래키값이 들어가지 않는다.

이러한 오류를 방지하기 위해서는 양쪽 객체 둘다에게 데이터를 저장해야한다.

이렇게 비어있는 데이터가 생겨서 나중에 문제가 생긴다.
이러한 문제를 해결하기 위해서 양쪽 모두 객체 둘다 데이터를 저장하는게 맞다.

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Team team = new Team();
        team.setTeamName("test");
        em.persist(team);

        Member member = new Member();
        member.setMemberName("memberTest");

        member.setTeam(team);
        team.getMembers().add(member);

        em.persist(member);
    }
}

일대다(1:M) 단방향 관계

@OneToMany로 단방향 관계를 맺을때 @JoinColumn을 걸면 어떻게 될까? 즉, One을 MasterTable로 두는 것이다.

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String teamName;
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
}

이런 단방향 관계에서 Member엔티티에 외래키(FK)가 생성된다.

이런 방법은 권장하지 않는다!

주인이 외래키를 관리하지 않고 반대편에서 관리하면 어렵기 떄문이다.

또한 이로 인해 성능 이슈도 발생한다.

예를 들어 Member 객체를 저장한다 해보자.

Member 엔티티에는 Team엔티티에 대한 정보가 없다. 즉 Member 객체를 저장하면 INSERT 구문에서 외래키는 저장이 되지 않고 데이터베이스에 들어갈 것이다.

그리고 Team객체가 저장될 때 Team객체에 있는 연관 관계 정보를 보고 Member객체에 UPDATE구문으로 외래키가 저장될 것이다.
즉 한번 저장하는데 UPDATE도 항상 같이 호출된다.
그러므로 단방향 관계를 할 때는 일대다 보다는 다대일 단방향으로 하는 것이 좋다.


일대다(1:M) 양방향 관계

일대다 양방향 관계는 존재하지 않는다.
뭐 없는건 아닌데 따지고 보면

1:M관계의 주인은 항상 다(M)이기 때문에 일대다 양방향이나 다대일 양방향은 같은 말이다.

일대일(1:1) 단방향 관계

  • 일대일 관계는 양쪽이 서로 하나의 관계만 가지는 관계이다.

  • 일대일 관계에서는 외래키가 어디에 있든 상관이 없다.

Member 엔티티와 Locker 엔티티가 있고, 1:1 관계라 하자.

외래키는 Member 또는 Locker 엔티티에 존재할 수 있다.

연관관계 주인을 Member로 한 1:1 단방향 관계 코드를 보자.

@Entity
public class Locker {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

Locker를 단방향 관계 주인으로 하고 싶으면 @OneToOne을 Locker에 붙여주면 된다.

일대일(1:1) 양방향 관계

더이상의 설명은 생략한다! 코드를 보면 알거다

@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String memberName;

@OneToOne(mappedBy = "member")
private Locker locker;

}

@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String lockerName;

@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;

}
위 코드는 Locker를 연관관계 주인으로 한 일대일 관계 매핑이다.

Member를 연관관계 주인으로 하고 싶다면 mappedBy, @joinColumn위치를 서로 바꿔주면 된다.

다대다(M:M) 단방향 관계

RDB에서는 2개의 테이블로 설명하기는 어려워서 중간 테이블을 두어 관계를 맺는다.

RDB는 3개의 테이블로 설명을 하지만 객체를 사용하여 2개의 객체로 설명을 하도록 하겠다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
    joinColumns = @JoinColumn(name = "MEMBER_ID"),
    inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> product = new ArrayList<>();
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
}

기존의 관계와 다르게 MasterTable만 @ManyToMany를 사용하고 @JoinTable를 사용한다.
@JoinTable은 연결테이블을 지정한다.

이때, 3개의 테이블은


요로코롬 생겼꼬노
데이터를 삽입해봅시다.

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Product product = new Product();
        product.setProductName("testProduct");
        em.persist(product);

        Member member = new Member();
        member.setMemberName("testMember");
        member.getProduct().add(product);
        em.persist(member);
    }
}

다대다(M:M) 양방향 관계

양방향 연관주인 반대편에서도 @ManyToMany 를 붙여준다.
그럼 Master는 누군데???
나도 몰라 그러니 니가 정해!

  • 연관주인이 아닌 객체에 mappedBy를 붙여준다.

아래 코드에선 Master 가 아닌 Product Entity에 mappedBy를 붙여줍니다.

@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String productName;
    @ManyToMany(mappedBy = "product")
    private List<Member> members = new ArrayList<>();
}

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT",
    joinColumns = @JoinColumn(name = "MEMBER_ID"),
    inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
    private List<Product> product = new ArrayList<>();
}

다대다 양방향 관계일 때, 데이터를 넣어봅시다

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Product product = new Product();
        product.setProductName("testProduct");
        em.persist(product);

        Member member = new Member();
        member.setMemberName("testMember");

        member.getProduct().add(product);
				//양방향으로 서로 객체 추가
        product.getMembers().add(member);
        em.persist(member);
    }
}

그럼 JOIN할 필요 없이 이 관계를 막 써버리면 어때?

여러분의 꼼수는 통하지 않습니다.
데이터의 칼럼이 추가되지 않는 매우 안정적인 시스템이라면 상관 없을 겁니다.

  • 하지만 추가적인 칼럼이 필요할 경우 @ManyToMany는 더이상 사용할 수 없다!

이런 경우에 추가적인 칼럼이 필요하다면
새로운 연결 엔티티를 만들어서 일대다 다대일 관계를 직접 만들어야 한다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String memberName;

    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProduct = new ArrayList<>();
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
    @OneToMany(mappedBy = "product")
    private List<MemberProduct> memberProducts = new ArrayList<>();
}

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
    @Id
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @Id
    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private String test;
}

MemberProduct를 보면 @id, @JoinColumn을 동시에 사용하면 기본기 + 외래 키를 한번에 매핑했다.
그리고 @IdClass어노테이션이 있는데 이것은 JPA에서 복합 기본키를 매핑할 수 있게해준다.

@IdClass안에 MemberProductId.class가 들어있는데 이걸 식별자 클래스라 한다.

식별자 클래스를 만들때는 다음 특징을 만족해야한다.

  • Serializable을 구현해야 한다.
  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자 속성명은 같아야함(member, product)
  • equals, hashCode 메소드를 구현해야함
  • 기본 생성자가 있어야함
  • 식별자 클래스는 public이어야 함

데이터 삽입 예제

public class TestService {
    private final EntityManager em;
    @Transactional
    public void test() {
        Product product = new Product();
        product.setProductName("testProduct");
        em.persist(product);

        Member member = new Member();
        member.setMemberName("testMember");
        em.persist(member);

        MemberProduct memberProduct = new MemberProduct();
        memberProduct.setProduct(product);
        memberProduct.setMember(member);

        product.getMemberProducts().add(memberProduct);
        member.getMemberProduct().add(memberProduct);

        em.persist(memberProduct);
    }
}

이렇게 자신의 기본 키 + 외래 키로 사용하는 것을 식별 관계라 한다.
하지만 이렇게 사용했을 경우 @IdClass를 사용하는 등 사용 방법이 복잡하다는 단점이있다.

그래서 또다른 방법으로 복합키를 사용하지 않고 연관 테이블에 새로운 기본키를 만들고 외래키는 외래키로서만 사용되는 방법인데 이것을 비식별 관계라 한다.

그래서 MEMBER_PRODUCT의 기본키를 따로 설정하는 방법도 있다.

@Entity
public class MemberProduct {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    private String test;
}

Reference
https://cjw-awdsd.tistory.com/47

profile
like_learning

0개의 댓글