본문 바로가기

프로그래밍/JAVA

[JPA] 연관관계 (1)

728x90
반응형

엔티티들은 대부분 다른 엔티티와 연관관계가 있다

예를 들어 위와 같은 ERD가 있다고 가정하고 영화 엔티티에는 감독이 누구인지 알기 위해 감독 엔티티와 연관관계가 있고 영화 엔티티는 리뷰, 리뷰 정보 등의 또 다른 엔티티와 관계가 있다

그런데 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다

객체 매칭 관계에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다

 

그래서 우리는 연관관계를 정의할 때 생각해야 할 것은 크게 3가지가 있다

 

방향(Direction) : 단방향, 양방향 이 있다

데이터베이스 테이블은 외래 키 하나로 양쪽 테이블 조인이 가능하기 때문에 단방향이니 양방향이니 나눌 필요가 없다

하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향이라 한다

예를 들어 영화 -> 감독 또는 감독 -> 영화 둘 중 한쪽만 참조하는 것을 단방향이라 하고 영화 -> 감독, 감독 -> 영화 양쪽 모두 참조하는 것을 양방향이라 한다

 

설계 단계에서 방향이 확실하게 나눠져 있다면 그대로 구현을 하면 되겠지만 그렇지 않은 경우 기본적으로 단방향으로 하고 나중에 역방향으로도 객체 탐색이 필요하다고 한다면 양방향으로 추가하는 것이 좋다

 

다중성(Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 

위 ERD를 기준으로 설명해 보면 하나의 영화에는 여러 명의 감독이 같이 작업을 할 수도 있다 반대로 한 명의 감독이 여러 영화를 찍을 수도 있다 이런 경우 영화와 감독은 다대다 관계를 갖게 된다

리뷰 정보에는 평균 평점, 리뷰수 등의 정보가 있는데 하나의 영화에는 이런 정보를 하나만 가질 수 있고 리뷰 정보 또한 하나의 영화에만 속하기 때문에 일대일 관계이다

마지막으로 영화는 여러 개의 리뷰를 가질 수 있지만 리뷰는 하나의 영화를 상대로 작성이 되기 때문에 다대일, 일대다 관계가 형성된다

 

연관관계의 주인(Owner) : 객체를 양뱡향 연관관계로 만들면 연관관계의 주인을 정해야 한다

제어의 권한(데이터의 수정, 삽입, 삭제)을 갖는 실질적인 관계가 어떤 것인지 JPA에게 알려 줘야 한다

외래 키를 갖고 있는 객체를 연관관계의 주인으로 정하면 된다

 

일대일(1:1) 단방향

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
@NoArgsConstructor
@Data
public class Movie {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String category;

    private Long director_id;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}

Movie 테이블에는 기본적인 PK설정 말고는 따로 할 건 없다

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@NoArgsConstructor
@Data
public class MovieReviewInfo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private float averageReviewScore;

    private int reviewCount;

    @OneToOne
    private Movie movie;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}

MovieReviewInfo 테이블을 보면 Movie 객체를 @OneToOne 설정을 한 것을 알 수 있다

 

아래 테스트 코드를 작성 후 로그를 확인해 보자

import com.example.jpablog.domain.Movie;
import com.example.jpablog.domain.MovieReviewInfo;
import com.example.jpablog.repository.MovieRepository;
import com.example.jpablog.repository.MovieReviewInfoRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;

@SpringBootTest
public class JpaTest {

    @Autowired
    private MovieRepository movieRepository;

    @Autowired
    private MovieReviewInfoRepository movieReviewInfoRepository;

    @Test
    void test() {
        Movie movie = new Movie();
        movie.setTitle("어벤저스");
        movie.setCategory("SF");
        movie.setDirector_id(1l);
        movie.setCreatedAt(LocalDateTime.now());
        movie.setUpdatedAt(LocalDateTime.now());

        MovieReviewInfo movieReviewInfo = new MovieReviewInfo();
        movieReviewInfo.setMovie(movieRepository.save(movie));
        movieReviewInfo.setAverageReviewScore(4.5f);
        movieReviewInfo.setReviewCount(5);
        movieReviewInfo.setCreatedAt(LocalDateTime.now());
        movieReviewInfo.setUpdatedAt(LocalDateTime.now());

        movieReviewInfoRepository.save(movieReviewInfo);

        movieRepository.findAll().forEach(System.out::println);

        movieReviewInfoRepository.findAll().forEach(System.out::println);

        Movie result = movieReviewInfoRepository.findAll().get(0).getMovie();

        System.out.println(result);

    }

}

우리는 갖고 있는 데이터가 없기 때문에 테스트 코드에서 Movie 테이블에 데이터를 생성해주고 MovieReviewInfo 테이블도 데이터를 생성해 준 뒤 로그로 하나씩 찍어본 결과이다

 

우선 JPA 가 생성해주는 DDL을 살펴보면

Hibernate: 
    
    create table movie (
       id bigint generated by default as identity,
        category varchar(255),
        created_at timestamp,
        director_id bigint,
        title varchar(255),
        updated_at timestamp,
        primary key (id)
    )
Hibernate: 
    
    create table movie_review_info (
       id bigint generated by default as identity,
        average_review_score float not null,
        created_at timestamp,
        review_count integer not null,
        updated_at timestamp,
        movie_id bigint,
        primary key (id)
    )
Hibernate: 
    
    alter table movie_review_info 
       add constraint FKh3xsiwodcofso7c4bk3pj654y 
       foreign key (movie_id) 
       references movie

movie_review_info 테이블에서 movie_id 가 생성이 된 후에 movie_id를 FK 설정을 해준 것을 볼 수 있다

Movie 객체에 @OneToOne 설정만 해줬을 뿐인데 JPA 가 자동으로 이런 DDL을 만들어 생성을 해준 것이다

 

그럼 실제 데이터를 뽑아온 로그를 확인해 보면 

Movie(id=1, title=어벤저스, category=SF, director_id=1, createdAt=2021-07-18T13:47:34.582, updatedAt=2021-07-18T13:47:34.582)

MovieReviewInfo(id=1, averageReviewScore=4.5, reviewCount=5, movie=Movie(id=1, title=어벤저스, category=SF, director_id=1, createdAt=2021-07-18T13:47:34.582, updatedAt=2021-07-18T13:47:34.582), createdAt=2021-07-18T13:47:34.718, updatedAt=2021-07-18T13:47:34.718)

Movie(id=1, title=어벤저스, category=SF, director_id=1, createdAt=2021-07-18T13:47:34.582, updatedAt=2021-07-18T13:47:34.582)

 

 

처음 줄에는 우리가 movieRepository.findAll(). forEach(System.out::println)으로 찍은 Movie 데이터가 잘 찍혀 있는 걸 확인할 수 있고 그다음 줄에는 MovieReviewInfo 역시 잘 찍혀 있는 것을 확인할 수 있다

마지막 줄을 보면 Movie 객체를 참조해 getMovie()로 Movie 데이터를 다시 한번 찍어 주었다

이렇게 MovieReviewInfo 테이블에서는 Movie 객체를 이용해 Movie 데이터를 참조할 수 있는 걸 확인했다

 

일대일(1:1) 양방향

@OneToOne(mappedBy = "movie")
@ToString.Exclude
private MovieReviewInfo movieReviewInfo;

Movie 테이블에 위와 같은 소스를 추가해 주었다

 

그 뒤 테스트 코드 역시 아래와 같이 수정해 주면 된다

MovieReviewInfo result = movieRepository.findAll().get(0).getMovieReviewInfo();

System.out.println(result);

이번엔 Moview 테이블에서 MoviewReviewInfo 테이블을 참조해 결과를 가져오는 걸 확인할 수 있다

 

위에서 봐야 할 건 mappedBy 속성을 준 것인데 만약 저 속성이 없다면 Movie 테이블에 movie_rieview_info_id라는 칼럼이 생성이 될 것이다 그리고 그것을 FK로 설정을 해주는데 양쪽 테이블 모두 서로의 기본키를 외래 키로 바라보고 있게 되는 상황이 발생하는 것이다

그래서 원래의 설계에 맞게 한쪽에만 외래 키를 제공해 주기 위해 mappedBy 속성을 사용해 줘야 하고 이것이 연관관계의 주인을 설정하는 것이다

연관관계의 주인인 쪽은 mappedBy를 사용하지 않고 주인이 아닌 쪽에 mappedBy 속성을 정의해주면 된다

mappedBy 속성에 movie를 준 이유는 MovieReviewInfo 테이블에 외래 키로 movie_id를 갖고 있다

그리고 MovieReviewInfo 클래스를 보면 @OneToOne 설정으로 private Movie movie; 를 선언해 주었는데 여기서 movie 가 우리가 선언한 movie 가 되는 것이다 즉 FK를 갖고 있는 테이블에 외래 키로 사용할 객체 변수명을 주면 된다

뭔가 설명하기가 어려운 느낌이다...ㅠ

 

그리고 @ToString. Exclude라는 어노테이션이 있는데 이것은 무한루프에 빠지는 걸 막기 위해 설정해 놓은 것이다

lombok을 이용해 ToString을 생성하거나 json으로 직렬화 할 때 생기는 경우인데 양방향 일 때 서로의 객체를 끝없이 참조하다 보니 무한루프에 빠져 StackOverFlow가 발생해 버리는 경우이다

 

내용이 너무 길어져 나머지 연관관계는 다음 포스팅으로 넘어가 보도록 하자

 

[JPA] 연관관계(2)

 

[JPA] 연관관계(2)

[JPA] 연관관계 (1) [JPA] 연관관계 (1) 엔티티들은 대부분 다른 엔티티와 연관관계가 있다 예를 들어 위와 같은 ERD가 있다고 가정하고 영화 엔티티에는 감독이 누구인지 알기 위해 감독 엔티티와 연

ldevlog.tistory.com

 

728x90
반응형