카테고리 없음

2024-05-14 [Kotlin+Spring 코틀린 스프링 금일 배운내용 정리]

Glen_check 2024. 5. 14. 23:57

1. @Embeddable / @Embedded

2. @GeneratedValue

3. mappedBy / @JoinColumn

4. 고아객체 자동 삭제하기

 

강의를 들으면서 회고하고 싶은 내용, 설명이 다소 부족하여 더 학습하고 싶은 내용들을 정리하여 블로그에 남겨두려 합니다.

 

1. @Embeddable / @Embedded

@Entity
@Table(name = "app_user")
class User(
    @Column(name = "email", nullable = false)
    val email: String,

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

    @Embedded
    var profile: Profile,

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

강의를 들으며 이해한 내용은 아래와 같습니다. 위 코드를 함께보면

User Entity 를 가지는 'User' 클래스가 있고, 'app_user' 테이블 내 nickname 컬럼도 필요하다고 가정해 봅시다.

그런데, nickname 외에도 추후 추가적인 사용자의 프로필 정보들이 필요할 수 있습니다. 즉 추후 변동 사항을 고려하여  @Embeddable 로 아래와 같은 데이터 클래스를 만들고

@Embeddable
data class Profile (
    @Column(name = "nickname")
    var nickname: String,
)

 

Entity 클래스 내 @Embdded 를 통해 Profile 이라는 데이터 클래스에 접근하여 컬럼을 만들 수 있습니다.

연관성이 높은 상세 데이터를 하나의 객체로 묶어 보다 객체 지향적으로 설계할 수 있습니다.

 

@Embedded : 값 타입을 정의하는 곳에 표시

@Embeddable : 값 타입을 사용하는 곳에 표시

 

정리

  • 임베디드 타입은 복합 값 타입으로 불리며 새로운 값 타입을 직접 정의해서 사용하는 JPA의 방법을 의미한다.
  • 상세한 데이터를 그대로 갖고 있는 것은 객체 지향적이지 않으며 응집력을 떨어뜨립니다. 이럴때 임베디드 타입을 사용하면 더욱 더 객체지향적인 코드를 만들 수 있습니다.

임베디드 타입과 null

  • 임베디드 타입 자체를 null로 지정했을 때와 임베디드 타입의 속성 값을 null로 설정했을 때는 모두 해당 column의 값이 null값으로 설정됩니다.

 

2. @GeneratedValue

JPA로 Talbe과 Entity를 매핑할 때 식별자로 사용할 필드 위에 @Id Annotation을 붙여 테이블의 Primary Key와 연결 시켜줄 수 있습니다.

이 때 컬럼 명을 따로 지정하지 않으면 관례에 따라 매핑되는 테이블 컬럼명은 camelCase로 작성된 필드명을 snake_case로 바뀐 테이블 컬럼을 찾아서 매핑시켜줍니다. EX) memberId -> member_id , orderItemId -> order_item_id

@Column 어노테이션을 활용하여 테이블의 pk 컬럼을 따로 지정할 수도 있습니다.

public class  User {
    @Id  @Column(name = "user_id") // 컬럼명 따로 지정
    private Long id;
    }

이렇게 @Id로 식별자 필드와 테이블의 PK를 매핑만 시켜놓으면, 식별자로 사용될 값을 일일히 수동으로 넣어줘야 하는 불편함이 있는데, @GeneratedValue 를 사용하면 이를 해결할 수 있습니다.

 

@GeneratedValue 어노테이션을 사용하면 식별자 값을 자동 생성 시켜줄 수 있습니다.

@GeneratedValue에는 3가지 전략이 있고, JPA에게 전략 선택을 위임하는 옵션인 AUTO 옵션을 포함해, 총 4가지 옵션이 존재합니다.

 

이 중 강의에서 활용한 IDENTITY 전략을 예시로 들어보려 합니다.

{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null
}
  • 기본 키 생성을 데이터베이스에 위임합니다. 
  • IDENTITY 전략은 em.persist()로 객체를 영속화 시키는 시점에 곧바로 insert 쿼리가 DB로 전송되고, 거기서 반환받은 식별자 값을 가지고 1차 캐시에 엔티티를 등록시켜 관리합니다.

JPA는 보통의 경우에 Transaction이 commit 되는 시점 쓰기 지연 저장소에 모아놓은 SQL을 한 번에 DB로 전송하며 실행합니다.

(이렇게 해야 애플리케이션과 DB 사이에 네트워크를 오가는 횟수가 줄어들고, 성능면에서 이득을 볼 수 있습니다.)

 

하지만 IDENTITY전략은 DB에 기본 키 생성을 위임하므로, Mysql의 경우 AUTO_INCREMENT를 활용하여 생성하는데,

이 때, JPA 입장에선 DB에 INSERT SQL를 실행하기 전엔 도저히 AUTO_INCREMENT되는 값을 알 수 없으므로, persist() 시점에 insert 쿼리가 실행되는 것입니다. (영속성 컨텍스트로 엔티티를 관리하려면 1차 캐시에 Id값을 key 값으로 들고 있어야 하기 때문)

 

동일한 내용을 보충하여 적어보자면,

 

1차 캐시에는 @Id와 @Entity로 지정한 것들이 들어갑니다. 그런데 기본키의 GeneratedValue가 IDENTITY 타입이면 id생성을 데이터베이스에게 위임하기 때문에, JPA는 1차 캐시에 넣을 때 id 필드가 뭔지 알 수 없습니다.

따라서, 보통 JPA는 트랜잭션 커밋시점에 insert문을 실행하지만 IDENTITY일 때는 보통과 달리 persist()를 호출하는 시점에 데이터베이스에 insert문을 날립니다. (이래야 1차 캐시(영속성컨텍스트) 에 올릴 때 JPA가 id를 알고 올릴 수 있으니깐)

그래서 이렇게 persist를 호출하여 영속성컨텍스트에 올리는 시점에 쿼리문을 날려서 auto_increment가 된 id값을 알고 1차캐시에 @Id를 올립니다.

 

해당 내용을 이해하면, 왜 id가 nullable 인지 확인할 수 있습니다.

 

3. mappedBy / @JoinColumn

연관관계? 연관관계 주인?

객체에서 양방향 연관 관계 관리 포인트가 두 개일 때는 테이블과 매핑을 담당하는 JPA입장에서 혼란을 주게 됩니다.

강의를 예시로 User에서 CourseApplication을 수정할 때 어디에 수정이 일어날지 혼란이 발생합니다.

이때 두 객체 사이의 연관 관계의 주인을 정해서 명확하게 "User에서 CourseApplication를 수정할 때만 FK를 수정하겠다"라고 정하는 것입니다.

 

두 객체 간 연관 관계를 나타낼 때 M:N 관계가 아닌 이상 한 객체에서 테이블의 FK를 관리해야 합니다.
이때, 연관관계의 주인이란 FK를 관리하는 객체를 의미합니다.

 

@JoinColumn

@JoinColumn은 DB 관점으로 보았을 때, 본인이 외래 키를 관리하면서 상대 Table의 PK(Join할 때 사용)를 명시해주는 역할을 합니다.

@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    val user: User

위 설명한 것 처럼 연관관계의 주인은  FK를 관리하는 쪽입니다. 즉, @JoinColumn을 쓰는 쪽의 Class가 연관관계의 주인이 됩니다.

 

mappedBy

@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
    val courseApplications: MutableList<CourseApplication> = mutableListOf()

mappedBy 또한 특정 관계와의 연관관계를 나타낼 때 사용하는데, @ManyToOne과는 달리 연관관계의 주인이 아닌 쪽의 Class 내에서 사용합니다.
보통은 양방향 연관 관계에서 사용합니다.

 

4. 고아 자식 객체 자동 삭제하기 (orphanRemoval)

강의 자료에 이해하기 쉽게 잘 나와 있어, 강의 자료 내용 일부를 발췌하여 글을 적습니다.

 

Cascade는 자식 객체에 영속성을 어떻게 전파할 것 인지의 옵션입니다.

Casecade를 ALL 로 설정하고, 아래와 같은 코드를 작성하면 삭제되는 Lecture는 어떻게 될까요?

val course = em.find(Course::class.java, 1L)
course.lectures.removeFirst()
em.flush()

CasecadeType.ALL에는 CascadeType.REMOVE가 존재하니, 해당 Lecture 데이터는 DB에서 삭제가 될까요?

아닙니다.아래와 같은 SQL문이 실행됩니다.

UPDATE lecture SET course_id = NULL WHERE course_id = 1;
  • JPA는 이를 단지 관계가 끊어진 걸로 보기에, 삭제는 일어나지 않습니다. 그런데, 이래도 괜찮을까요? course가 지정되지 않은 lecture가 생겨 이는 데이터의 무결성을 해친다고 할 수 있습니다.
  • 이렇게 관계가 끊어졌을 때, 자식을 ‘고아 상태’ 라고 표현할 수 있습니다. 이런 고아 상태의 자식을 제거할 수 있는 옵션이 있습니다. 바로 orphanRemoval 입니다. 아래처럼 @OneToMany Annotation에 설정을 하면, 관계가 끊어졌을 시 JPA가 자동으로 자식 데이터를 삭제해 줍니다. 즉, DELETE 문이 호출됩니다.
@OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval=true)
var lectures: MutableList<Lecture> = mutableListOf()