개발이 취미인 사람

[JPA] - Entity 관계 설정 방법 본문

백앤드(Back-End)/Jpa

[JPA] - Entity 관계 설정 방법

RyanSin 2024. 12. 24. 01:08
반응형

- 개요

안녕하세요. 이번 시간에는 Entity 간에 관계 설정에 대해 알아보겠습니다.

 

혹시 이전 시간에 내용을 학습하고 오시지 못 하신 분들은 학습하고 오시는 걸 추천드리겠습니다.

[JPA] - Entity 정의 방법

 

[JPA] - Entity 정의 방법

개요안녕하세요. 이번 시간에는 Jpa에서 Entity를 정의하는 방법에 대해 알아보겠습니다. 혹시 지난 시간에 내용을 학습하고 오시지 못하신 분들은 아래 링크를 통해 학습하고 오시는 걸 추천드

any-ting.tistory.com

 

- Entity 관계설정

JPA에서 관계설정은 데이터베이스 테이블 간의 연관성을 엔티티 간에 매핑하는 기술입니다.

보통 관계를 설정하는 방식은 1 : 1, M : 1, M : N 관계를 설정합니다. (반대로 1 : N로 설정도 가능하지만 추천하지 않습니다.)

 

@OneToOne, @OneToMany, @ManyToOne@ManyToMany 어노테이션을 사용해서 관계를 설정합니다.

 

한쪽 방향으로 관계를 연결하는 방식을 단반향 연관관계라고 표현하고, 양쪽 방향으로 연결하는 방식은 양방향 연관관계라고 표현합니다.

 

그럼 예제를 통해 하나씩 알아가 보도록 하겠습니다.

 

1 : 1 관계

MemberEntity
package com.ryan.kotlinspirngjpa.jpa.entity

import jakarta.persistence.*
import org.hibernate.annotations.SQLDelete
import org.hibernate.annotations.SQLRestriction

@Entity
@Table(name = "member")
@SQLDelete(sql = "UPDATE member SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
@SQLRestriction("deleted_at is NULL")
class MemberEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "email", nullable = false, unique = true)
    val email: String,

    @Column(name = "password", nullable = false)
    val password: String,

    @Column(name = "name", nullable = false)
    private var name: String,

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_profile_id", referencedColumnName = "id")
    val profile: MemberProfileEntity,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    val company: CompanyEntity

) : BaseEntity() {
    fun getName() = this.name
    fun setName(name: String) {
        this.name = name
    }
}

 

위 Entity에서 주목해야 할 부분은 @OneToOne이라는 어노테이션을 활용해서 MemberProfileEntity와 1:1 관계 설정을 했다는 걸 보실 수 있습니다.

 

그럼 반대로 MemberProfileEntity는 어떻게 설정 되어있을까요?

MemberProfileEntity
package com.ryan.kotlinspirngjpa.jpa.entity

import jakarta.persistence.*

@Entity
@Table(name = "member_profile")
class MemberProfileEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "age", nullable = false)
    val age: Int,

    @Enumerated(EnumType.STRING)
    @Column(name = "gender", nullable = false)
    val gender: Gender,

    @Column(name = "biography", nullable = true)
    val biography: String? = null
) : BaseEntity()

 

MemberProfileEntity 코드를 확인해 보면 관계 설정에 대한 어노테이션이 존재하지 않습니다. 왜일까요?

 

JPA에서는 엔티티 간에 연관관계를 설정할 때 연관관계 주인이라는 개념이 있으며, 위 구조는 단방향 연관관계 설정을 뜻 합니다.

 

우리가 데이터베이스에서 두 테이블 간의 관계를 설정할 때 한쪽 테이블에 Foregin Key를 설정합니다.

 

Foregin Key를 관리하는 쪽을 연관관계 주인이라고 생각하시면 됩니다. 그렇다면 연관관계 주인이라는 설정은 어떻게 했을까요?

@JoinColumn(name = "member_profile_id", referencedColumnName = "id")

 

MemberEntity 코드를 보면 위와 같은 어노테이션을 보셨을 겁니다.

 

name이라는 설정은 실제 테이블 컬럼을 뜻하며, referencedColumnName 은 내가 참조하는 MemberEntity id 를 말합니다.

M : 1 관계

MemberEntity
package com.ryan.kotlinspirngjpa.jpa.entity

import jakarta.persistence.*
import org.hibernate.annotations.SQLDelete
import org.hibernate.annotations.SQLRestriction

@Entity
@Table(name = "member")
@SQLDelete(sql = "UPDATE member SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
@SQLRestriction("deleted_at is NULL")
class MemberEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "email", nullable = false, unique = true)
    val email: String,

    @Column(name = "password", nullable = false)
    val password: String,

    @Column(name = "name", nullable = false)
    private var name: String,

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_profile_id", referencedColumnName = "id")
    val profile: MemberProfileEntity,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id", referencedColumnName = "id")
    val company: CompanyEntity

) : BaseEntity() {
    fun getName() = this.name
    fun setName(name: String) {
        this.name = name
    }
}

 

M:1 관계는 우리가 생각하는 전통적인 테이블 연관관계 설정과 동일합니다. @ManyToOne 어노테이션이 M:1 관계를 나타냅니다.

 

어노테이션 설정을 쉽게 이해하기 위해서 다음과 같이 기억하면 좋습니다.

 

첫 Many는 대상을 뜻하고 One 타겟을 나타냅니다. MemberEntity 가 CompanyEntity에게 연관관계를 설정한다.

 

그럼 ComapnyEntity를 확인해보겠습니다.

CompanyEntity
package com.ryan.kotlinspirngjpa.jpa.entity

import jakarta.persistence.*
import java.time.Instant

@Entity
@Table(name = "company")
class CompanyEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    val status: CompanyStatus = CompanyStatus.ACTIVE,

    @Column(name = "name", nullable = false, unique = true)
    val name: String,

    @Column(name = "business_number", nullable = false)
    val businessNumber: String,

    @Column(name = "ceo_name", nullable = false)
    val ceoName: String,

    @Column(name = "founded_date", nullable = false)
    val foundedDate: Instant,

    @Column(name = "email", nullable = false)
    val email: String,

    @Column(name = "phone_number", nullable = false)
    val phoneNumber: String,

    @Embedded
    val address: Address,

    @ManyToMany
    @JoinTable(
        name = "company_category_mapping",
        joinColumns = [JoinColumn(name = "company_id")],
        inverseJoinColumns = [JoinColumn(name = "category_id")]
    )
    val categories: MutableSet<CategoryEntity> = mutableSetOf()

) : BaseEntity()

 

CompanyEntity도 동일하게 양방향 설정이 아닌 단방향 설정으로 MemberEntity와 연결하지 않고 있습니다.

 

M : N 관계

다대다 설정은 실제 두 테이블만으로는 구현할 수 없기 때문에 중간에 두 테이블을 연결하는 맵핑 테이블을 만들어 구현합니다.

 

위에 CompanyEntity 코드를 확인하면 @JoinTable이라는 어노테이션을 확인할 수 있습니다.

 

실제 company_category_mapping이라는 맵핑 테이블과 연동을 할때 사용됩니다. 만약 위 어노테이션을 사용하지 않는다면 

중간 맵핑 엔티티를 만들어서 @ManyToMany 어노테이션을 설정해서 만들 수 있습니다.

 

그럼 CategoryEntity는 어떻게 구현되어 있을까요?

CategoryEntity
package com.ryan.kotlinspirngjpa.jpa.entity

import jakarta.persistence.*

@Entity
@Table(name = "category")
class CategoryEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(name = "name", nullable = false)
    val name: String,

    @ManyToOne
    @JoinColumn(name = "parent_id", referencedColumnName = "id")
    val parent: CategoryEntity?,

    @OneToMany(mappedBy = "parent")
    val childCategorise: List<CategoryEntity> = listOf(),

    @ManyToMany(mappedBy = "categories")
    val companies: MutableSet<CompanyEntity> = mutableSetOf()

) : BaseEntity()

 

CategoryEntity도 @ManyToMany 어노테이션을 통해 CompanyEntity와 연결하고 있습니다.

 

그런데 눈치가 빠르신 분들은 코드를 보고 이상하다고 생각하실 수 있습니다.

 

왜냐하면 CompanyEntity에서는 @JoinTable이라는 어노테이션을 사용했는데 CategoryEntity는 사용하지 않고 @ManyToMany 어노테이션에 mappedBy 라는 속성을 정의해서 사용하고 있습니다.

 

아까 위에서 연관관계 주인이라는 개념을 설명했습니다. 데이터베이스에서는 양방향 연관관계라는 개념이 존재하지 않습니다.

한쪽에 Foregin Key를 알고 있으면 양쪽 다 join 쿼리를 사용해서 조회가 가능하기 때문입니다.

 

JPA에서는 이 부분을 해결하기 위해 Foregin Key를 관리하지 않는 엔티티에 mappedBy라는 속성을 지정해서 위 기능을 구현합니다.

 

mappedBy라는 속성을 지정하면 해당 엔티티는 읽기 전용으로 설정 됩니다.(실제 값을 수정하는 로직을 작성하면... 피를 볼수 있습니다.)

 

정리를 하면 CompanyEntity에 @JoinTable이라고 어노테이션 설정은 연관관계 주인을 뜻하며 반대쪽 CategoryEntity는 읽기전용으로 설정합니다.

 

이번 시간에는 JPA에서 연관관계 설정에 대해 알아봤습니다.

 

꼭 실습을 통해 위 내용을 이해하시는 걸 추천드리겠습니다.

 

'백앤드(Back-End) > Jpa' 카테고리의 다른 글

[JPA] - Entity 정의 방법  (3) 2024.12.14
[JPA] - 영속성 컨텍스트(2)  (2) 2024.11.29
[JPA] - 영속성 컨텍스트(1)  (1) 2024.11.24
[JPA] - 기본 동작 방식과 단순 CRUD  (0) 2024.11.19