ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 데이터베이스를 기반으로 JPA의 연관관계 살펴보기 <1>
    프레임워크/Spring Boot 2024. 2. 6. 20:10

     

     

    소스코드: https://github.com/blog-example/-JPA-_relationship_mapping

    이번에는 아티스트를 새로 등록하는 시나리오를 진행하면서 관련된 Entity 간의 연관관계를 맺는 법을 살펴보겠습니다.

     


     

     

    0.  기본 시나리오 생성

    우선 연관관계를 생각하지 않고 데이터베이스 레벨에서 테이블을 생성하고, JPA에서 Entity를 만들어보겠습니다.

     

    아티스트는 솔로가수 혹은 아이돌 그룹을 생각해주시면 됩니다. 멤버는 솔로일 경우에 솔로 자신, 그룹일 경우 그룹의 멤버입니다.

    연관 관계가 없는 상태의 아티스트와 멤버 ERD 입니다.

     

    이를 표현하는 SQL을 사용해서 데이터베이스에 테이블을 생성해보겠습니다.

    CREATE TABLE IF NOT EXISTS artists(
        artist_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL
    );
    
    CREATE TABLE IF NOT EXISTS members(
        member_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        part VARCHAR(255) NOT NULL,
        artist_id BIGINT NOT NULL
    );

     

    이어서 스프링부트 프로젝트에서 JPA를 사용해서 Entity를 작성해주겠습니다.

    마찬가지로 연관관계를 맺지 않은 상태의 Entity들 입니다.

    // Artist.java
    import jakarta.persistence.*;
    
    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
      
      
      public Artist(String name) {
        this.name = name;
      }
    
    }
    // Member.java
    import jakarta.persistence.*;
    
    @Entity
    @Table(name = "members")
    public class Member {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "member_id")
      private long memberId;
    
      @Column(name = "name")
      private String name;
    
      @Column(name = "part")
      private String part;
    
      @Column(name = "artist_id")
      private long artistId;
      
      
      public Member(String name, String member, long artistId) {
        this.name = name;
        this.part = member;
        this.artistId = artistId;
      }
    
    }

     

    연관관계 없이 작성한 서비스 코드입니다.

    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class ArtistService {
    
      @PersistenceContext
      private final EntityManager entityManager;
    
      @Transactional
      public void createArtist() {
        Artist newJeans = new Artist("New Jeans");
        entityManager.persist(newJeans);
    
        Member minji = new Member("Minji", "vocal", newJeans.getArtistId());
        entityManager.persist(minji);
    
        Member hanni = new Member("Hanni", "vocal", newJeans.getArtistId());
        entityManager.persist(hanni);
    
        Member danielle = new Member("Danielle", "vocal", newJeans.getArtistId());
        entityManager.persist(danielle);
    
        Member haerin = new Member("Haerin", "vocal", newJeans.getArtistId());
        entityManager.persist(haerin);
    
        Member hyein = new Member("Hyein", "vocal", newJeans.getArtistId());
        entityManager.persist(hyein);
      }
    }

     

    아래와 같이 아티스트를 생성하는 1개의 쿼리와 멤버를 생성하는 5개의 쿼리가 나갑니다.

     

    아티스트와 관련된 멤버들이 잘 생성되는 것을 볼 수 있습니다.

     

     

     


     

    1. ORM 사용의 의미

    위 처럼 코드를 작성해도 원하는 데이터들이 데이터베이스에 잘 들어갑니다. DBMS가 두 테이블의 연관관계를 잘 관리해주고 member를 통해서 artist를 찾거나, artist를 통해서 member를 찾을 수 있습니다. 그렇다면 코드레벨에서는 어떨까요?

    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class ArtistService {
    
      @PersistenceContext
      private final EntityManager entityManager;
    
      @Transactional
      public void createArtist() {
        Artist newJeans = new Artist("New Jeans");
        entityManager.persist(newJeans);
    
        Member minji = new Member("Minji", "vocal", newJeans.getArtistId());
        entityManager.persist(minji);
    
        Member hanni = new Member("Hanni", "vocal", newJeans.getArtistId());
        entityManager.persist(hanni);
    
        Member danielle = new Member("Danielle", "vocal", newJeans.getArtistId());
        entityManager.persist(danielle);
    
        Member haerin = new Member("Haerin", "vocal", newJeans.getArtistId());
        entityManager.persist(haerin);
    
        Member hyein = new Member("Hyein", "vocal", newJeans.getArtistId());
        entityManager.persist(hyein);
        
        
        newJeans.getMembers()   // x
        member.getArtist()      // x
      }
    }

     

    의미적으로 artist와 member가 모두 생성되었지만 artist에서 member를 찾을 수도, 반대로 member에서 artist를 찾을 수도 없는 상황입니다. ORM은 데이터베이스 관련 로직을 객체 지향적으로 쓸 수 있도록 도와주는 역할을 하는데, 지금 코드에서는 Artist와 Member 사이의 관계를 코드 레벨에서는 파악할 수 있는 방법이 없습니다.

     


     

     

    2. 종류별 연관 관계

    JPA는 엔티티 간의 관계를 표현하기 위해 1:1, 1:N, N:N 3가지 방법을 사용합니다.

    1:N 관계부터 하나씩 살펴보도록 하겠습니다.

     

     

    일대다 관계 (1:N)

    가장 많이 사용되는 연관관계라고 생각합니다. 'OneToMany', '일대다' 혹은 '1:N' 으로 표현할 수 있습니다.

    Member to Aritst / Artist to Member / 양방향 순으로 살펴보겠습니다.

     

     

    1. Member to Artist (단방향)

    하나의 아티스트에는 여러명의 멤버가 있을 수 있다고 가정하고 두 테이블의 연관관계를 맺어주겠습니다.

     

    테이블을 생성하는 쿼리를 작성해보았습니다. members 테이블에서 artists 테이블을 참조하고 있습니다.

    FOREIGN KEY 라는 키워드를 사용해서 두 테이블의 연관관계를 지정하고 있습니다.

    CREATE TABLE IF NOT EXISTS artists(
        artist_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
    );
    
    CREATE TABLE IF NOT EXISTS members(
        member_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        part VARCHAR(255) NOT NULL,
        artist_id BIGINT NOT NULL,
        FOREIGN KEY (artist_id) REFERENCES artists(artist_id)
    );

     

     

    그렇다면 JPA에서는 어떻게 연관관계를 지정할까요?

    한 단계씩 천천히 Member Entity 변경을 진행해보겠습니다.

     

    Artist는 변경사항이 없습니다.

    // Aritst 엔티티
    import jakarta.persistence.*;
    
    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
    
    }

     

     

    JPA를 사용해서 연관관계를 맺기전에 다시 한번 생각해야하는 것은  "ORM은 데이터베이스가 하는 일을 객체 지향적으로 매핑하는 역할을 하는 것일 뿐, 데이터베이스가 하지 못하는 일을 할 수는 없다" 입니다.

     

    우선 데이터베이스에는 쿼리를 사용해서 artist_id를 artists 테이블과 연결했습니다.

    FOREIGN KEY (artist_id) REFERENCES artists(artist_id)

     

    동일한 동작이라고 볼 수 있는 것이 @JoinColumn 입니다. 이 어노테이션이 붙은 필드는 Join에 사용된다는 것입니다.

    먼저 artistId에 적용된 어노테이션을 변경해줍니다.

    // Member 엔티티
    import jakarta.persistence.*;
    
    @Entity
    @Table(name = "members")
    public class Member {
    
      ... 생략
    
      @JoinColumn(name = "artist_id")  // Column => JoinColumn으로 변경
      private long artistId;
    
    
      public Member(String name, String member, long artistId) {
        this.name = name;
        this.part = member;
        this.artistId = artistId;
      }
    
    }

     

    두번째로 데이터 베이스에서는 1:N 혹은 1:1을 표현할 때, 외래 키로 사용되는 컬럼의 UNIQUE 속성을 가지고 구분을 합니다.

    만약 member 테이블 내에 artist_id가 고유하다면 항상 1명의 멤버가 1개의 artist에 속하는 것이고 (1:1)

    그렇지 않다면 여러 명의 멤버가 1개의 artist에 속할 수 있는 것 입니다. (1:N)

     

    테이블 생성 쿼리를 보면, UNIQUE 속성이 적용되지 않았기 때문에 현재 1:N의 관계를 나타내고 있습니다.

    CREATE TABLE IF NOT EXISTS members(
        member_id BIGINT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        part VARCHAR(255) NOT NULL,
        artist_id BIGINT NOT NULL,
        FOREIGN KEY (artist_id) REFERENCES artists(artist_id)
    );

     

    이제 이 내용을 JPA로 가져와서 적용해보겠습니다.

    우리는 여러 명의 멤버가 1개의 artist에 속하는 상태입니다.

     

    저는 연관관계를 생각할 때 이런 흐름으로 생각합니다. 우선 영어로 어느 쪽에 Many가 있는지 생각해봅니다.

    "Many members can make up One artist." 라고 생각할 수 있고 이는 간단하게 "Many member To One artist" 입니다.

    따라서 @ManyToOne 어노테이션을 사용해서 연관관계를 표현할 수 있습니다.

    // Member 엔티티
    import jakarta.persistence.*;
    
    @Entity
    @Table(name = "members")
    public class Member {
    
      ... 생략
    
      @ManyToOne // 어노테이션 추가
      @JoinColumn(name = "artist_id")
      private long artistId;
    
    
      public Member(String name, String member, long artistId) {
        this.name = name;
        this.part = member;
        this.artistId = artistId;
      }
    
    }

     

    마지막 과정은 우리가 이 연관관계 매핑을 통해서 얻고 싶은 데이터가 무엇인지 생각해보는 것입니다.

    데이터 베이스에서는 member 테이블의 aritst_id를 artist 테이블과 연결함으로써 join을 통해서 member 테이블에서 artist의 정보를 얻을 수 있게 되었습니다.

    SELECT a.name FROM members m
    	INNER JOIN artist a ON m.artist_id = a.artist_id;

     

    그렇다면 JPA에서는 어떻게 artist의 정보를 얻을 수 있을까요?

    long 타입의 artistId 필드가 아닌 Artist 타입의 artist 필드를 사용해서 객체지향적으로 접근가능하게 만들어줍니다.

    // 완성된 Member 엔티티
    import jakarta.persistence.*;
    
    @Entity
    @Table(name = "members")
    public class Member {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "member_id")
      private long memberId;
    
      @Column(name = "name")
      private String name;
    
      @Column(name = "part")
      private String part;
    
      @ManyToOne 
      @JoinColumn(name = "artist_id")
      private Artist artist; // Artist 타입으로 변경
    
    
      public Member(String name, String member, Artist artist) {
        this.name = name;
        this.part = member;
        this.artist = artist;  // 생성자를 통해서 Artist 할당
      }
    
    }

     

    서비스 코드 역시 arstistId를 사용하던 부분을 모두 artist로 변경합니다.

      @Transactional
      public void createArtist() {
        Artist newJeans = new Artist("New Jeans");
        entityManager.persist(newJeans);
    
        Member minji = new Member("Minji", "vocal", newJeans);  // id => Aritst
        entityManager.persist(minji);
    
        Member hanni = new Member("Hanni", "vocal", newJeans);
        entityManager.persist(hanni);
    
        Member danielle = new Member("Danielle", "vocal", newJeans);
        entityManager.persist(danielle);
    
        Member haerin = new Member("Haerin", "vocal", newJeans);
        entityManager.persist(haerin);
    
        Member hyein = new Member("Hyein", "vocal", newJeans);
        entityManager.persist(hyein);
      }

     

    서비스 코드를 실행시켜보면 artist 생성을 위한 1개의 쿼리와 member 생성을 위한 5개의 쿼리가 나가는 것을 볼 수 있습니다.

     

    데이터베이스에서도 원하는대로 데이터가 잘 들어간 것을 확인할 수 있습니다.

     

    연관 관계를 맺음으로써 데이터베이스에서는 members 테이블의 artist_id를 사용해서 해당 member가 속해있는 artist 그룹의 정보에 접근하는 것이 가능해졌고, 그 것을 JPA에서는 @ManyToOne 을 통해서 구현하고 있습니다.

    (member를 기준으로 artist의 정보를 조회)

    // 민지가 속한 그룹의 이름은 뭐야?
    
    // DB 레벨
    SELECT a.name FROM members m
    	INNER JOIN artists a ON m.artist_id = a.artist_id
            WHERE m.name = "Minji"; //New Jeans
        
        
    // JPA 레벨
    Member minji = new Member("Minji", "vocal", newJeans);
    minji.getArtist().getName(); // New Jeans

     

     

    2. Artist to Member(단방향)

    여기서 조금만 더 생각해보겠습니다. 연관관계가 맺어진 상태라면, 데이터베이스에서는 위의 경우와는 반대로  artist를 기준으로 해당 그룹에 속한 멤버들을 조회하는 것도 가능합니다.

    그런데 지금 우리의 코드는 어떨까요? artist에서는 member에 대한 것을 알 수 있는 방법이 전무합니다.

    // New Jeans 라는 그룹은 어떤 멤버로 구성되어있어?
    
    // DB 레벨
    SELECT m.name FROM artists a
    	INNER JOIN member m ON a.artist_id = m.artist_id
            WHERE a.name = "New Jeans"; // minji, hanni, danielle, hyerin, haein
        
        
    // JPA 레벨
    Artist newJeans = new Artist("New Jeans");
    newJeans.getMembers() // 존재하지 않는 메소드

     

    그럼 다시 처음으로 돌아가서, 이번에는 artist를 기준으로 member들을 찾을 수 있도록 JPA의 연관관계를 설정해보겠습니다.

    Member Enttiy는 연관관계가 없던 상태로 돌아갑니다.

    // Member 엔티티는 다시 처음 상태로 돌아갑니다.
    @Entity
    @Table(name = "members")
    public class Member {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "member_id")
      private long memberId;
    
      @Column(name = "name")
      private String name;
    
      @Column(name = "part")
      private String part;
    
      @Column(name = "artist_id")
      private long artistId;
    
      public Member(String name, String member, long artistId) {
        this.name = name;
        this.part = member;
        this.artistId = artistId;
      }
    }

     

    Member에서 Artist를 매핑했던 순서를 되짚어가며 Artist 쪽에서 연관관계를 맺어주겠습니다.

    첫 번째는 @Column을 @JoinColumn으로 바꾸는 과정이었습니다.

    그런데 Artist에는 Member 처럼 조인에 사용되는 컬럼이 없습니다!

    잘 모르겠으니 변경하지 말고 다음 과정으로 넘어가보겠습니다.

    // 변경사항 없음
    
    @Getter
    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
    
      public Artist(String name) {
        this.name = name;
      }
    
    }

     

    두 번째는 연관관계를 의미하는 어노테이션을 추가하는 과정이었습니다. 이전에는 Member 기준에서 바라보았지만 이번에는 Artist 기준에서 생각해보겠습니다. "One artist can consist of Many members" 이니까 "One artist To Many member" 가 보입니다.

    마땅한 필드가 보이지 않으니 빈 공간에 한번 적어보겠습니다.

    @Getter
    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
      
      @OneToMany  // 일단 빈 공간에 어노테이션을 추가
    
      public Artist(String name) {
        this.name = name;
      }
    
    }

     

    마지막 과정은 원하는 데이터의 타입 설정이었습니다. 

    데이터베이스에서 Join을 한다면 해당 aritst의 모든 member가 조회됩니다.

    // DB 레벨
    SELECT m.name FROM artists a
    	INNER JOIN member m ON a.artist_id = m.artist_id
            WHERE a.name = "New Jeans"; // minji, hanni, danielle, hyerin, haein

     

    이렇게 복수의 대상을 자바에서 표현하기 적합한 타입은 무엇이 있을까요?

    List<Member>를 타입으로 사용하면 적절할 것 같습니다.

    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
    
      @OneToMany
      List<Member> members = new ArrayList<>(); // List<Member> 타입의 필드 생성
    
      public Artist(String name) {
        this.name = name;
      }
    
      public void addMember(Member member) { // member를 목록에 추가하는 메소드
    	this.members.add(member)
      }
    }

     

    그럼 이제 서비스 코드에 적용해보겠습니다.

    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class ArtistService {
    
      @PersistenceContext
      private final EntityManager entityManager;
    
      @Transactional
      public void createArtist() {
        Artist newJeans = new Artist("New Jeans");
        entityManager.persist(newJeans);
    
        Member minji = new Member("Minji", "vocal", newJeans.getArtistId());
        entityManager.persist(minji);
        newJeans.addMember(minji);
    
        Member hanni = new Member("Hanni", "vocal", newJeans.getArtistId());
        entityManager.persist(hanni);
        newJeans.addMember(hanni);
    
        Member danielle = new Member("Danielle", "vocal", newJeans.getArtistId());
        entityManager.persist(danielle);
        newJeans.addMember(danielle);
    
        Member haerin = new Member("Haerin", "vocal", newJeans.getArtistId());
        entityManager.persist(haerin);
        newJeans.addMember(haerin);
    
        Member hyein = new Member("Hyein", "vocal", newJeans.getArtistId());
        entityManager.persist(hyein);
        newJeans.addMember(hyein);
    
      }
    }

     

    서비스 코드가 성공적으로 실행은 되는데 나가는 쿼리가 이상합니다.

    artists_members 라는 테이블이 생성되고 데이터가 저장되고 있습니다.

    공식문서를 찾아봤지만 @JoinColumn을 쓰라고만 되어있지, 안쓰는 경우에 왜 조인 테이블이 생기는지에 대해서는 찾지 못하였습니다.

    Hibernate: insert into artists (name) values (?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into artists_members (artist_artist_id,members_member_id) values (?,?)
    Hibernate: insert into artists_members (artist_artist_id,members_member_id) values (?,?)
    Hibernate: insert into artists_members (artist_artist_id,members_member_id) values (?,?)
    Hibernate: insert into artists_members (artist_artist_id,members_member_id) values (?,?)
    Hibernate: insert into artists_members (artist_artist_id,members_member_id) values (?,?)

     

    하지만 원하는 동작이 아니니 공식 문서에 적혀있는대로 @JoinColumn을 추가해보겠습니다.

    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
    
      @OneToMany
      @JoinColumn(name = "artist_id") // JoinColumn 추가
      List<Member> members = new ArrayList<>();
    
      public Artist(String name) {
        this.name = name;
      }
    
      public void addMember(Member member) { 
    	this.members.add(member)
      }
    }

     

    이번에는 member를 insert 할 때, 이미 모든 정보를 알고 있음에도 불구하고 update 쿼리가 한번 더 발생하는 것을 알 수 있습니다.

    Hibernate: insert into artists (name) values (?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: update members set artist_id=? where member_id=?
    Hibernate: update members set artist_id=? where member_id=?
    Hibernate: update members set artist_id=? where member_id=?
    Hibernate: update members set artist_id=? where member_id=?
    Hibernate: update members set artist_id=? where member_id=?

     

    JoinColumn 옵션에 updatable = false를 추가하면 update 쿼리를 막을 수 있지만, 정말로 artist_id는 update가 필요없는지 확인할 필요가 있을 것 같습니다. 놓칠 위험이 많기 때문에 좋은 선택이라는 느낌이 들지는 않습니다.

    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
    
      @OneToMany
      @JoinColumn(name = "artist_id") // JoinColumn 추가
      List<Member> members = new ArrayList<>();
    
      public Artist(String name) {
        this.name = name;
      }
    
      public void addMember(Member member) { 
    	this.members.add(member)
      }
    }

     

    업데이트 쿼리 없이 원하는 쿼리가 나가는 것을 확인할 수 있습니다.

    Hibernate: insert into artists (name) values (?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)

     

    이제는 코드레벨에서도 해당 아티스트를 구성하고 있는 멤버를 확인할 수 있게 되었습니다.

    // New Jeans 라는 그룹은 어떤 멤버로 구성되어있어?
    
    // DB 레벨
    SELECT m.name FROM artists a
    	INNER JOIN member m ON a.artist_id = m.artist_id
            WHERE a.name = "New Jeans"; // minji, hanni, danielle, hyerin, haein
        
        
    // JPA 레벨
    Artist newJeans = new Artist("New Jeans");
    newJeans.getMembers().get(0) // 첫번째 멤버 정보 확인 가능

     

     

    3. Artist to Member && Member to Artist (양방향)

    데이터베이스는 외래키를 사용해서 양쪽 테이블을 기준으로 어느 쪽에서도 원하는 데이터를 조회할 수 있습니다.

    하지만 위의 두 방법은 여전히 데이터베이스의 기능을 완벽히 구현하고 있지 않습니다.

    // DB 레벨
    SELECT m.name FROM artists a
    	INNER JOIN member m ON a.artist_id = m.artist_id
            WHERE a.name = "New Jeans"; // minji, hanni, danielle, hyerin, haein
            
    SELECT a.name FROM members m
    	INNER JOIN artist a ON m.artist_id = a.artist_id;

     

     

    그렇다면 다시 처음부터 JPA를 사용해서 양방향 매핑을 해보겠습니다. 

    Member 엔티티는 member to artist에서 했던 것 처럼 연관관계 어노테이션을 추가하고 JoinColumn을 추가해줍니다.

    // Member Entity
    @Entity
    @Table(name = "members")
    public class Member {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "member_id")
      private long memberId;
    
      @Column(name = "name")
      private String name;
    
      @Column(name = "part")
      private String part;
    
      @ManyToOne                         // ManyToOne 어노테이션 추가
      @JoinColumn(name = "artist_id")    // JoinColumn 으로 변경
      private Artist artist;
    
      public Member(String name, String member, Artist artist) {
        this.name = name;
        this.part = member;
        this.artist = artist;
      }
    }

     

    Arist는 조금 차이가 있습니다. artist to member 단방향에서 사용했던 JoinColumn은 member 쪽에서 사용되고 있기 때문에 추가할 필요가 없습니다. 대신에 Member의 어떤 필드와 연결이 되는지 mappedBy 옵션을 통해서 표시를 해줘야합니다.

    // Artist Entity
    @Entity
    @Table(name = "artists")
    public class Artist {
    
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Id
      @Column(name = "artist_id")
      private long artistId;
    
      @Column(name = "name")
      private String name;
    
      @OneToMany(mappedBy = "artist")            // member entity에서 사용되는 필드명 
      List<Member> members = new ArrayList<>();  // List 추가
    
      public Artist(String name) {
        this.name = name;
      }
    
      public void addMember(Member member) {     // member 추가를 위한 함수 생성 
        members.add(member);
      }
    }

     

    이제 서비스 코드를 수정해주겠습니다.

    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class ArtistService {
    
      @PersistenceContext
      private final EntityManager entityManager;
    
      @Transactional
      public void createArtist() {
        Artist newJeans = new Artist("New Jeans");
        entityManager.persist(newJeans);
    
        Member minji = new Member("Minji", "vocal", newJeans);
        entityManager.persist(minji);
        newJeans.addMember(minji);
    
        Member hanni = new Member("Hanni", "vocal", newJeans);
        entityManager.persist(hanni);
        newJeans.addMember(hanni);
    
        Member danielle = new Member("Danielle", "vocal", newJeans);
        entityManager.persist(danielle);
        newJeans.addMember(danielle);
    
        Member haerin = new Member("Haerin", "vocal", newJeans);
        entityManager.persist(haerin);
        newJeans.addMember(haerin);
    
        Member hyein = new Member("Hyein", "vocal", newJeans);
        entityManager.persist(hyein);
        newJeans.addMember(hyein);
      }
    }

     

     

    이번에는 별다른 추가 옵션 없이도 깔끔하게 원하는 쿼리가 나가는 것을 확인할 수 있습니다.

    Hibernate: insert into artists (name) values (?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)
    Hibernate: insert into members (artist_id,name,part) values (?,?,?)

     

    그리고 이제는 artist에서 원하는 member를 찾거나, member에서 artist의 정보를 찾는 것이 모두 가능해졌습니다.

    Member minji = new Member("Minji", "vocal", newJeans);
    minji.getArtist().getName(); // 아티스트 정보 확인 가능
    
    Artist newJeans = new Artist("New Jeans");
    newJeans.getMembers().get(0) // 첫번째 멤버 정보 확인 가능

     

    데이터의 일관성을 위해서 코드를 조금 개선해보겠습니다.

    member를 artist에 추가할 때, member의 aritst도 변경해주면 코드의 일관성을 지키는데 도움이 될 것입니다.

    // Artist Entity
    @Entity
    @Table(name = "artists")
    public class Artist {
    
      ...생략
      
      public void addMember(Member member) {     
        members.add(member);
        member.setAritst(this);  // member에 artist를 설정
      }
    }

     

     

    마무리

    양방향 매핑이 사용하기는 좋을 수 있으나 항상 최선의 선택은 아닐 수도 있습니다. 양방향 매핑을 하게 되면 코드가 더 복잡해지기 때문에 수반되는 단점들이 발생할 것이고, List에 들어가는 내용이 많아진다면 메모리 사용량도 고려해볼 필요가 있을 것 입니다.

    비즈니스의 요구사항을 고려해서 적절한 선택을 하는 것이 필요할 것 같습니다.

     

    다른 연관관계들은 다른 글에서 다뤄보도록 하겠습니다.

     

     

     

     


    참고

    https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/onetomany

    https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/joincolumn

    https://www.inflearn.com/questions/681929/%EC%9D%BC%EB%8C%80%EB%8B%A4-%EB%8B%A8%EB%B0%A9%ED%96%A5%EC%9D%80-%EB%AC%B4%EC%A1%B0%EA%B1%B4-%ED%94%BC%ED%95%B4%EC%95%BC-%ED%95%98%EB%82%98%EC%9A%94

     

     

     

     

Designed by Tistory.