[Spring] Entity

diense_kk·2024년 12월 10일
0

SpringBoot

목록 보기
9/11
post-thumbnail

엔티티란

JPA에서 엔티티는 데이터베이스 테이블을 자바의 클래스로 매핑한 객체이다.
각 엔티티 인스턴스는 DB 테이블의 한 행(row)에 해당한다.
@Entity를 붙인 클래스는 JPA가 관리할 수 있는 객체로 등록된다.
JPA는 엔티티를 영속성 컨텍스트에서 관리하면서 데이터베이스와 동기화하고, 개발자는 SQL을 직접 작성하지 않아도 메서드 호출만으로 CRUD 작업을 수행할 수 있다.

엔티티는 반드시 식별자를 가져야 된다. JPA에서는 @Id를 사용해 식별자를 지정한다.
@Id 어노테이션이 붙은 필드는 JPA에서 영속성 컨텍스트에서 엔티티를 관리하는 기준이 된다.

연관관계

연관관계JPA Annotation
1:1@OneToOne
1:N@OneToMany
N:1@ManyToOne
N:M@ManyToMany

연관관계 정의 규칙

크게 3가지를 생각해야된다.
1) 방향 : 단방향, 양방향 (객체 참조)
2) 연관 관계의 주인 : 양방향일 때, 연관관계에서 관리 주체
3) 다중성 : 다대일, 일대다, 일대일, 다대다

@OneToMany, @ManyToOne 사용법

사용자(User)가 글(Post)을 작성하고 댓글(Comment)를 달 수 있다.

@Entity
@Getter @Setter
public class User {
	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "user_id")
    private Long userId;

    @OneToMany
    private List<Post> posts;

    @OneToMany
    private List<Comment> comments;
}

@Entity
@Getter @Setter
public class Post {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private User user;

    @OneToMany
    private List<Comment> comments;
}

@Entity
@Getter @Setter
public class Comment {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    private Post post;

    @ManyToOne
    private User user;
}

양방향과 단방향

DB 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다.
따라서 DB는 단방향이나 양방향으로 나눌 필요가 없다.
하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
선택은 비즈니스 로직에서 두 객체가 참조가 필요한지 여부를 고민해보면 된다.
user.getPosts처럼 참조가 필요하면 User -> Posts 참조
posts.getUser()처럼 참조가 필요하면 Posts -> User 참조

만약 참조가 필요없다면 하지 않으면 된다.

그냥 무조건 양방향 관계를 맺으면 쉽지 않나?

객체 입장에서는 양방향 매핑을 했을 때 오히려 복잡해질 수 있다.
User 엔티티는 일반적인 비즈니스 애플리케이션에서 굉장히 많은 엔티티와 연관 관계를 갖는다.
이러면 User 엔티티는 엄청나게 많은 테이블과 연관관계를 맺게 되어 복잡성이 증가한다.

기본적으로 단방향 매핑으로 하고 나중에 양방향 객체 탐색이 꼭 필요한 경우에 추가하면 된다.

양방향과 단방향 연관관계

단방향 - 한쪽 엔티티에서만 연관관계를 설정한다.
양방향 - 양쪽 엔티티 모두 연관관계를 설정한다.

@OneToMany 기준
단방향은 상대 엔티티에 @ManyToOne이 없는 경우이다.
양방향은 상대 엔티티에 @ManyToOne이 있는 경우이다.

@ManyToOne 기준
단방향은 상대 엔티티에 @OneToMany가 없는 경우이다.
양방향은 상대 엔티티에 @OneToMany가 있는 경우이다.

둘의 양방향은 기준만 다를 뿐 차이는 없다.

단방향과 양방향 상관 없이 @OneToMany가 붙어있는 엔티티가 부모 엔티티이다.
쉽게 생각하면 FK를 가진 쪽이 자식 엔티티이다.

연관관계의 주인을 지정하는 것은 양방향 관계 중, 제어의 권한을 갖는 실질적인 관계가 어떤 것이닞 JPA에게 알려주는 것이다.
관계의 주인은 연관관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제할 수 있지만, 주인이 아니면 조회만 가능하다.

post.setUser(user); -> // 관계 설정 가능
user.getPosts().add(post) -> DB 반영 안됨

양방향 연관관계에서는 mappedBy 속성을 사용해 어느 쪽이 주인이며(FK를 관리) 어느 쪽이 연관관계의 주인을 따라가는지 지정해야 된다.
mappedBy는 주인이 아닌 쪽에 붙인다.
User 엔티티와 Posts 엔티티를 예로 들면

@Entity
public class Post {
    @ManyToOne
    @JoinColumn(name = "user_id") // DB의 컬럼명과 같아야된다.
    private User user;
}
  
@Entity
public class User {
	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "user_id")
    private Long userId;
    @OneToMany(mappedBy = "user") // 주인 엔티티의 필드명과 같아야된다.
    private List<Post> posts;
}

쉽게 말해 FK를 가진 쪽이 자식 엔티티이며, 관계를 주도한다(연관관계의 주인이다).
자식 엔티티는 외래 키의 값을 업데이트하거나 관리할 수 있는 권한이 있다.

물론 단방향의 경우, 주인은 자연스럽게 해당 어노테이션이 존재하는 엔티티가 된다.
이 경우에는 그렇게 주인 개념이 강하지는 않다.
하지만 FK를 들고있는 Many쪽의 자식 엔티티가 주인이 되는 것이 더 자연스럽다.

따라서, @OneToMany 단방향을 사용하여 부모 엔티티가 주인이 되기 보다는 양방향 연관관계를 이용해서 자식 엔티티가 FK를 관리하는 것이 권장된다.

@ManyToOne 단방향

@JoinColumn 어노테이션과 함께 쓰이며, 이때 @JoinColumn은 엔티티 테이블에 FK 컬럼을 정의해준다.

/* Post.java */
@ManyToOne
@JoinColumn
private User user

@OneToMany 단방향

엔티티를 참조할 수 있는 매핑이 부모 엔티티에 존재하지만, FK는 자식 엔티티 테이블에 존재하는 연관관계이다.

@JoinColumn 없이 사용할 경우 Hibernate에서 자체적으로 중간 테이블을 생성하여 연관관계를 관리하게 된다.

옵션

fetch

해당 객체를 DB에서 조회할 때, 연관관계에 있는 엔티티의 정보를 언제 끌어올지에 대한 옵션이다.

1) Lazy Fetch
연관관계에 있는 엔티티에 접근할 때 DB에 쿼리를 날려 엔티티를 조회한다.
접근하지 않는 경우에는 쿼리가 발생하지 않는다.

2) Eager Fetch
조회 여부에 상관없이 쿼리가 발생한다.

@OneToMany의 기본값은 Lazy Fetch이며, @ManyToOne의 기본값은 Eager Fetch이다.
Eager, Lazy 값에 상관없이 단건 조회가 아닌 경우에는 N+1 문제가 발생할 수 있다.

영속성 전이 cascade

특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티들에 대해 영속성을 전파시키는 옵션이다.
CascadeType은 6가지가 있다.

1) PERSIST - 저장 메서드 호출 시 연관된 엔티티도 저장 (user 만들고 posts 만들어서 add해둔 경우)
2) REMOVE - 삭제 메서드 호출 시 연관된 엔티티도 삭제 (user 삭제시 posts도 삭제 -> Comment도 삭제되는거임)
3) MERGE - 병합 메서드 호출 시 연관된 엔티티도 병합 ()
4) REFRESH - 새로고침 메서드 호출 시 인스턴스의 값을 다시 읽어옴 (user 값 변경 후 시도하면 변경된 값이 무효화 되고, DB에서 최신 상태를 다시 읽어온다.)
5) DETACH - detach 메서드 호출 시 연관된 엔티티들까지 준영속 상태로 변환 (user를 준영속 상태로 만들면 posts도 준영속 상태가 되어 값을 변경하더라도 DB에 적용 X)
6) ALL - 위에 항목 전부 포함됨

영속성 전파를 설정하게 되면, 객체에 해당 작업이 이루어질 때, 자식 엔티티에도 작업이 전파된다.

영속성 컨텍스트란?

영속성 컨텍스트는 "엔티티를 영구 저장하는 환경"이다.
엔티티를 저장하거나 조회할 때 EntityManager는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
JPA는 트랜잭션을 커밋할 때 영속성 컨텍스트에 새로 저장된 Entity를 데이터베이스 자동으로 반영해준다. (영속 상태에서 데이터를 변경하고 따로 저장하지 않아도 자동 반영)

JPA에서의 영속성

영속 상태란 JPA에서 엔티티가 영속성 컨텍스트에 의해 관리되고 있는 상태로, DB와의 동기화를 JPA가 보장하는 상태이다.
JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있느냐이다.
영속성 컨텍스트가 유지된 상태에서 엔티티의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경 내용을 반영하게 된다.
이러한 개념을 더티 체킹이라고 한다.

  • 더티 체킹 -> 상태 변경 검사

동작

EntityManagerFactory를 빈으로 등록 -> EntityManager를 생성 -> 트랜잭션 시작 -> 엔티티를 영속 상태로 변경 -> 필요한 작업 수행 -> 트랜잭션 종료

설계시 주의점

1) 가급적 Setter를 사용하지 말자
Setter가 모두 열려있는 경우, 변경 포인트가 너무 많아서 유지보수가 어렵다.

2) 지연로딩으로 설정하자
즉시 로딩은 연관 테이블까지 모두 조회하기 때문에 예측이 어렵고 어떤 SQL이 실행될지 추적하기 어렵다.

3) 컬렉션은 필드에서 초기화하자
컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
NPE 문제에서 안전하다.

ID (UUID?)

클라이언트와 서버 사이에서 데이터를 확인하기 위해 PK를 주고받는 것은 보안적인 측면에서 위험하다고 한다.

http://www.domain.com/user/info?userid=1
이러한 URL이 있을때 파라미터로 들어가는 userid 값만 바꿔도, 다른 사람의 정보를 확인할 수 있는 것을 예측할 수 있다.
따라서 PK값을 그대로 넘겨주는 것은 바람직하지 않다.

서버 내에서 Token의 userId와 파라미터로 들어온 userid값을 비교하는 방식으로 해결 가능하지만, 트래픽이 많아져 서버를 늘리게 된다면 글로벌한 환경에서 고유한 값을 유지할 수 있도록 관리해야 된다.

UUID

UUID는 고유성이 보장되는 표준 규약이다.

Java.util 에서 제공하는 UUID 클래스는 UUIDv4인데 해당 방식은 단순 랜덤으로 값을 생성한다.
하지만, MySQL에선 기본적으로 B-Tree로 데이터를 관리하기 때문에 항상 정렬된 상태를 유지한다.
기본적으로 삽입되는 데이터의 기본키를 기준으로 구조를 재배치하는데 auto_increment와 같은 순차적인 값을 넣을때는 재배치를 하지 않지만 UUID와 같은 순서가 보장되지 않는 경우에는 재배치를 하게 된다.

UUID는 기본적으로 16진수 32개로 이루어진 16바이트의 크기를 가지지만 이를 DB에 그대로 문자열로 저장한다면 32자리이기 때문에 32바이트가 된다.
이는 bigint auto_increment보다 큰 용량을 차지하게 된다.

최적화 방법

UUIDv4가 아닌 UUIDv1을 사용한다면 Timestamp를 기반으로 값을 생성하여, 순차적인 값을 생성하기 때문에 재배치를 하지 않아도 된다.

총 128bits로 구성되어 32개의 문자가 5 묶음으로 구분되어 있는 형태이다.
V1, V2 형태)
Timestamp - Timestamp - Timestamp & Version - Variant & Clock sequence - Node id
(버전이 높다고 좋은 건 아님)

위에서 언급 했듯이 문자열 형식으로 저장하게 되면 32바이트가 된다.
MySQL에서는 binary라는 타입을 제공하기 때문에 binary(16) 타입으로 UUID를 저장해야 된다.
JPA Entity에서 UUID 또는 byte[] 타입으로 사용하면 된다.
하지만 JPA는 binary(16)을 UUID로 변환해주긴 하지만 UUID v4로 생성되기 때문에 UUID v1을 사용하는 경우에는 @Converter를 정의하거나 @Id에 @Convert를 적용해야 된다.

UUIDv4를 사용하기를 원하는 경우에는 DB에서는 auto_increment를 사용하고, UUID를 사용하는 엔티티 id 컬럼에 보조 인덱스를 거는 방식으로 성능 저하를 완화할 수 있다.

하지만 트래픽이 정말 많지 않고, 데이터가 아무리 생각해도 수백, 수천만 건이 생기지 않은 서비스가 아니라면 단순히 auto_increment PK를 엔티티의 ID로 사용하거나, UUID를 PK와 엔티티의 ID로써 사용하여도 큰 문제는 없을 것이다.

0개의 댓글