엔티티 모델링

HwangJerry·2023년 4월 10일
0

엔티티와 테이블 매핑


데이터베이스 스키마 자동 생성

JPA에서는 애플리케이션 실행 시점에 자동으로 DDL(Data Definition Language)를 생성하는 기능을 제공합니다. 이를 Automatic Schema Generation이라고 합니다. 이 기능을 사용하면 개발자가 직접 데이터베이스 스키마를 생성하거나 변경할 필요 없이, JPA가 엔티티 클래스와 매핑 정보를 바탕으로 DDL을 자동으로 생성하여 스키마를 생성하거나 변경할 수 있습니다.

DDL 생성 기능에 제약조건을 추가할 수도 있습니다. 예를 들어, 회원 이름은 필수로 설정하되, 이름이 10자를 초과하면 안된다고 가정하면 아래와 같이 설정할 수 있습니다.

import jakarta.persistence.*;

@Entity
@Table(name = "member")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, length = 10)
    private String name;

    // getters and setters
}

유니크 제약조건도 추가할 수 있습니다. 위 코드에 member 테이블에 name과 age 필드로 이루어진 유니크 제약 조건을 추가하는 방법은 다음과 같습니다.

@Entity
@Table(name = "member", uniqueConstraints = {@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, length = 10)
    private String name;

    // getters and setters
}

Automatic Schema Generation 기능은 JPA 구현체마다 다를 수 있고, 대표적인 구현체인 Hibernate의 경우 hibernate.hbm2ddl.auto 설정을 통해 제어할 수 있습니다.

hibernate.hbm2ddl.auto

옵션설명
create기존테이블 삭제 후 다시 생성 (DROP + CREATE)
create-dropcreate와 같으나 종료 시점에 테이블 DROP
update변경분만 반영 (운영 DB에는 절대 사용하면 안됨)
validate엔티티와 테이블이 정상 매핑되었는지만 확인
noneAutomatic Schema Generation을 사용하지 않음 (정해진 명령어는 아니지만 관례상 사용)

Automatic Schema Generation 기능은 개발 환경에서는 매우 유용하나, 운영 환경에서는 자동으로 스키마 생성 및 변경이 위험할 수 있습니다. 특히, 운영 장비에는 절대 create, create-drop, update 사용하면 안 됩니다. 자칫 잘못하면 테이블이 드랍되는 위험이 있으므로 운영 환경에서는 반드시 수동으로 데이터베이스 스키마를 생성하고 변경해야 합니다.

개발 초기 단계에서는 create 또는 update까진 로컬에선 사용할 수 있지만, 공용 개발 서버로 넘어가는 경우에는 create 등은 사용하면 안되고, validate나 update까지정도 가능할 수 있습니다. 스테이징과 운영 서버는 가급적 none을 권장하되 validate까지는 가능할 수 있습니다.

객체와 테이블 매핑

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity를 붙여서 선언해줍니다.

주의 사항

  1. 기본 생성자(파라미터가 없는 public 또는 protected 생성자)는 필수
  2. final 클래스, enum, interface, inner 클래스는 적용되지 않습니다.
  3. 저장할 필드에는 final을 사용할 수 없습니다.
@Entity
public class Member {
    @Id
    private Long id;
    private String name;
    // getter, setter ...
}

@Table 어노테이션은 JPA에서 엔티티 클래스와 데이터베이스 테이블을 매핑하기 위한 어노테이션입니다.

기본적으로 JPA는 엔티티 클래스의 이름을 기준으로 해당 클래스와 매핑할 데이터베이스 테이블의 이름을 결정합니다. 따라서 엔티티 클래스의 이름과 데이터베이스 테이블의 이름이 일치한다면 @Table 어노테이션을 사용하지 않아도 됩니다.

하지만 엔티티 클래스의 이름과 데이터베이스 테이블의 이름이 다르거나, 매핑 정보를 추가로 설정하고 싶은 경우에는 @Table 어노테이션을 사용합니다. @Table 어노테이션을 사용하면 다음과 같은 설정을 할 수 있습니다.

  • name: 매핑할 데이터베이스 테이블의 이름을 설정합니다.
  • catalog: 테이블이 속한 카탈로그 이름을 설정합니다.
  • schema: 테이블이 속한 스키마 이름을 설정합니다.
  • uniqueConstraints: 테이블의 유니크 제약 조건을 설정합니다.
@Entity
@Table(name = "member", catalog = "mydb", schema = "public", uniqueConstraints = {@UniqueConstraint(columnNames = {"name", "age"})})
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "name", nullable = false)
    private String name;
    @Column(name = "age", nullable = false)
    private Integer age;
    // getters and setters
}

필드와 컬럼 매핑

회원 테이블에 요구사항을 추가하여 필드를 구성해봅시다.

  1. 회원은 일반 회원과 관리자로 구분되어야 한다.
  2. 회원 가입일과 수정일이 있어야 한다.
  3. 회원을 설명할 수 있는 필드가 있어야 한다. 이 필드는 길이 제한이 없다.

위 요구사항을 반영한 엔티티 모델은 다음과 같다.

package com.example.hellojpa;
  
import jakarta.persistence.*;
import java.util.Date;

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "name")
    private String name;

    private Integer age;

    @Enumerated(EnumType.STRING)
    private RoleType roleType;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;

    @Lob
    private String description;

    // getter, setter ...
}

@Column

속성설명기본값
name필드와 매핑할 테이블의 칼럼 이름객체의 필드 이름
insertable, updatable등록, 변경 가능 여부True
nullable(DDL)null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다.True
unique(DDL)@Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다. (하지만, 이름을 반영하기 어려워서 잘 쓰지 않는다.)
columnDefinition(DDL)데이터베이스 컬럼 정보를 직접 줄 수 있다. ex) varchar(100) default 'EMPTY'필드의 자바 타입과 방언 정보를 사용
length(DDL)문자 길이 제약조건, String 타입에만 사용한다.255
precision, scale(DDL)BigDecimal 타입에서 사용한다(BigInteger도 사용가능). precision은 소수점을 포함한 전체 자릿수를, scale은 소수의 자릿수다. 참고로 double, float 타입에는 적용되지 않는다. 아주 큰 숫자나 정밀한 소수를 다루어야 할 때만 사용한다.precision = 19, scale = 2

@Enumerated

자바 enum 타입을 매핑할 때 사용. 단, ORDINAL 사용하지 말고 STRING으로 사용할 것.
  • EnumType.ORDINAL: enum 순서를 데이터베이스에 저장 (기본값)
  • EnumType.STRING: enum 이름을 데이터베이스에 저장 (권장)

@Temporal

날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용하는데, 요즘 하이버네이트에서는 LocalDate, LocalDateTime을 지원하고, 이를 사용하면 @Temporal은 생략 가능

  • TemporalType.DATE: 날짜, 데이터베이스 date타입과 매핑 (2023-04-10)
  • TemporalType.TIME: 시간, 데이터베이스 time타입과 매핑 (11:11:11)
  • TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예: 2013-10-11 11:11:11)

@Lob

데이터베이스 BLOB, CLOB 타입과 매핑할 때 사용합니다.

BLOB(Binary Large Object)과 CLOB(Character Large Object)는 데이터베이스에서 사용되는 데이터 유형입니다.

BLOB은 이진 데이터를 저장하는 데 사용되며, 예를 들어 이미지, 동영상, 오디오 등과 같은 이진 파일을 저장하는 데 적합합니다.

반면에, CLOB는 문자 데이터를 저장하는 데 사용되며, 예를 들어 긴 텍스트, 문서, HTML 또는 XML 문서 등과 같은 문자열 데이터를 저장하는 데 적합합니다.

  • @Lob에는 지정할 수 있는 속성이 없습니다.
  • 매핑하는 필드 타입이 문자면 CLOB 매핑, 나머지는 BLOB 매핑
    • CLOB : String, char[], java.sql.CLOB
    • BLOB : byte[], java.sql.BLOB

@Transient

  • 필드 매핑 X
  • 데이터베이스에 저장X, 조회X
  • 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용
@Transient
private Integer temp;



기본 키 매핑


기본 키 매핑 방법

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

직접 할당시에는 @Id만 사용하지만, 대개의 경우 @GeneratedValue을 같이 붙여서 기본 키를 자동 생성하여 사용합니다.

  • IDENTITY : 데이터베이스에 위임, MYSQL
  • SEQUENCE : 데이터베이스 시퀸스 오브젝트 사용, ORACLE
    • @SequenceGenerator 필요
  • TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
    • @TableGenerator 필요
  • AUTO : 방언에 따라 자동 지정, 기본값 (주로 사용)

기본 키 전략 권장사항

기본 키는 절대 변하면 안됩니다. 많은 시간이 흘러도 항상 유일하고 null이 아니여야 합니다. 가령, 주민등록번호조차 변동될 수 있으므로 대개의 자연키는 기본 키로 설정되기 어렵습니다. 따라서 대리키(대체키)를 사용해야 하는 것입니다. 자료형 또한 10억은 가볍게 넘겨도 유효해야 하므로 int, Integer 등은 적합하지 않고, 일반적으로 Long 타입을 권장합니다.

>>> 권장 : Long 타입 + 대체키 + 키 생성전략 사용

IDENTITY 전략

@Entity
public class Member {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

기본 키 생성을 데이터베이스에 위임하는 전략으로, 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용합니다.

MySQL의 AUTO_INCREMENT가 IDENTITY 전략인데, 이는 데이터베이스에 INSERT SQL을 실행한 이후에 ID값을 알 수 있습니다. 즉, DB에 반영이 된 이후에 PK 값을 확인할 수 있는 것이죠. 하지만 JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL을 실행하므로, IDENTITY 전략을 사용할 경우 em.persist() 시점에 즉시 INSERT SQL을 실행시키고 DB에서 식별자를 조회하는 방법으로 사용할 수 있습니다.

SEQUENCE 전략

@Entity
@SequenceGenerator(
		name = "MEMBER_SEQ_GENERATOR",
        sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스 시퀸스 이름,
        initialValue = 1, allocationSize = 1)
public class Member {
	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
    		generator = "MEMBER_SEQ_GENERATOR")
    private Long id;

키 생성 전용 테이블을 하나 만들어두고 데이터베이스 시퀀스를 흉내내는 전략입니다. 이는 모든 데이터베이스에 적용이 가능하지만, 성능이 좋지 않다는 치명적인 단점이 있습니다.



연관관계 매핑


JPA의 연관관계 매핑 기능을 통해 객체 중심 모델링과 테이블 중심 모델링의 패러다임 불일치를 해결한다.

단방향 매핑

Member, Team 엔티티 두 개가 있으며, 멤버 여러 명이 한 팀에 소속되는 연관관계가 있다고 가정해봅시다.

@Entity
public class Member {
  
  @Id @GeneratedValue
  private Long id;
  
  @Column(name = "username")
  private String name;
  private int age;
}
@Entity
public class Team {
  
  @Id @GeneratedValue
  private Long id;
  
  private String name;
}

위 엔티티는 서로 상관관계가 아직 반영되지 않은 상황입니다. 객체 지향 모델링에 따라 단방향으로 연관관계를 설정하면 다음과 같습니다.

@Entity
public class Member {
  
  @Id @GeneratedValue
  private Long id;
  
  @Column(name = "username")
  private String name;
  private int age;
  
  // 연관관계 추가
  @ManyToOne
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}
@Entity
public class Team {
  
  @Id @GeneratedValue
  private Long id;
  
  private String name;
}

단방향을 설정할 때에는 FK를 가진 엔티티에만 @JoinColumn 어노테이션과 같이, 다대일, 일대다, 일대일 등 어떤 연관관계인지에 따른 어노테이션을 붙이면 끝납니다. 이렇게 완성한 단방향 연관관계를 통해 다음과 같이 멤버에 새로운 팀을 설정할 수 있습니다.

// 새로운 팀 객체 생성
Team newTeam = new Team();
newTeam.setName("new team");
em.persist(newTeam);
  
// 회원에 새로운 팀 설정
member.setTeam(newTeam);

하지만 반대 방향으로 객체 그래프를 탐색하는 등의 기능을 수행할 일이 잦은 경우, 단방향으로는 부족할 수 있습니다. 이를 해결하기 위해 고안된 것이 양방향 매핑입니다.

양방향 매핑

양방향 매핑의 경우, 연관관계의 주인을 설정하는 것이 매우 중요합니다. 이를 설정한 모습은 다음과 같습니다.

@Entity
public class Member {
  
  @Id @GeneratedValue
  private Long id;
  
  @Column(name = "username")
  private String name;
  private int age;
  
  // Member 클래스는 동일
  @ManyToOne
  @JoinColumn(name = "TEAM_ID")
  private Team team;
}
@Entity
public class Team {
  
  @Id @GeneratedValue
  private Long id;
 
  private String name;
  
  // 양방향 연관관계 설정
  // "나는 반대 테이블의 team 이라는 이름에 매핑되어있어"
  @OneToMany(mappedBy = "team")
  List<Member> members = new ArrayList<Member>();
}

위와 같이 설정한 경우에 아래와 같이 조회가 가능합니다.

Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); // 역방향 조회

테이블 지향 모델링에서 두 테이블 간의 연관관계를 관리하기 위해서는 외래 키 하나를 설정하면 되지만, 객체 지향 모델링에서는 이를 사용할 수 없었습니다. 따라서 설정하는 '양방향 관계'는 사실 양방향 관계가 아닌, 서로 다른 단방향 관계 2개입니다. 하지만 DB 입장에선 둘 중 하나를 기준으로 외래 키를 관리해야 하므로 "연관관계 주인"이라는 개념이 등장하게 됩니다.

연관관계 주인

객체의 두 관계 중 하나를 연관관계의 주인으로 지정합니다. 연관관계의 주인이 외래 키를 관리하는 테이블이 되고, 해당 데이터의 등록과 수정을 담당합니다. 주인이 아닌 객체는 읽기만 가능합니다. 그렇다면 누가 주인이 될지 감이 잡히나요?

ERD 상의 외래 키를 가진 객체가 연관관계의 주인이 됩니다. 위 Member와 Team 객체 간의 연관관계에서는 Member가 연관관계의 주인이 되었습니다.

Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");

// 역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);

em.persist(member);

현재 예시에서의 연관관계의 주인은 Member 엔티티입니다. 하지만 위 코드에서는 team에 있는 Members 필드에만 멤버 인스턴스를 입력하고 있습니다. 이처럼 연관관계 주인에 값을 입력하지 않고 그 반대 방향에만 값을 입력해주면 안됩니다. 최소한 연관관계의 주인에 값을 우선 넣어주어야 하며, 순수한 객체 관계를 고려하면 항상 양쪽 모두에 값을 넣어주는 것을 권장합니다. 이러한 로직을 구현할 때에는 연관관계 편의 메소드를 사용하는 것을 권장합니다.

위 내용에 따라 개선한 코드는 다음과 같습니다.

// Member 클래스 내용 중 (예시)

@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

// 연관관계 편의 메소드
public void setTeam(Team team) {
	this.team = team;
    team.getMembers().add(this);
}
Team team = new Team();
team.setName("TeamA");
em.persist(team);

Member member = new Member();
member.setName("member1");

// 연관관계 주인에 먼저 값 입력 (연관관계 
member.setTeam(team);
em.persist(member);

// 이후 역방향에도 값 입력
team.getMembers().add(member);

em.persist(member);

위와 같이 양방향 매핑 시 주의할 점은, lombok을 이용한 toString() 메소드 사용 등의 관계에서 잘못하면 무한루프에 걸릴 수 있습니다. 따라서 가급적 toString() 메소드는 alt + insert를 통해 generate toString()으로 생성하여 문제가 발생하지는 않을지 체크하는 습관이 필요합니다.

사실, 단방향 매핑만으로도 이미 연관관계 매핑은 끝났다고 봐도 됩니다. 하지만 JPQL을 사용할 때 역방향으로 탐색하는 경우가 종종 발생하기 때문에 양방향 매핑 방법을 숙지해두는 것이 더 좋습니다. 단방향 매핑을 잘 구성해두고, 양방향은 필요할 때 추가해도 테이블에 영향을 주지 않으니 무방합니다.

*** 다대일, 일대다일대일 매핑 방법에 대하여 더욱 자세히 알고 싶다면 첨부 링크를 참조 (이후 별도로 정리하겠습니다.)

1:1 관계에서 외래키는 주테이블에 존재하는 것으로 하자. 그게 제일 편하다.

상속관계 매핑


관계형 데이터베이스 모델링은 기본적으로 상속관계를 구현할 수 없습니다. 하지만 슈퍼타입 서브타입 관계를 정의하는 모델링 기법을 활용하여 객체 상속 구조와 유사하게 RDB에 매핑할 수 있습니다.

슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 다음 3가지로 정리됩니다. 하나하나 살펴볼까요?

  • JOINED : 각각 테이블로 구축한 뒤 join 이용
  • SINGLE_TABLE : 단일의 통합 테이블로 구축
  • TABLE_PER_CLASS : 구현 클래스 별로 각각 서브타입 테이블 구축

JOINED 전략

장점단점
테이블 정규화성능 저하 (조회 시 조인을 많이 사용)
외래 키 참조 무결성 제약조건 활용 가능조회 쿼리가 복잡함
저장공간 효율화데이터 저장시 INSERT SQL 2번 호출

SINGLE_TABLE 전략

장점단점
성능 우수 (조인 불필요)항상 모든 필드를 채우지 않으므로, 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 함
조회 쿼리가 단순함단일 테이블에서 모든 데이터를 저장하므로 테이블이 커질 수 있어, 상황에 따라 조회 성능이 악화될 가능성 존재.

TABLE_PER_CLASS 전략

이 전략은 비추천
장점단점
서브 타입을 명확하게 구분하여 처리해야 할 때 효과적임여러 자식 테이블을 함께 조회할 때 UNION SQL이 필요하여 성능이 느림
not null 사용 가능자식 테이블을 통합해서 쿼리하기 어려움

@MappedSuperclass

@MappedSuperclass 어노테이션은 테이블과 관계 없이, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 수행합니다. @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속이 가능하여 이를 활용하여 모델링합니다. 이는 주로, 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용합니다.

profile
알고리즘 풀이 아카이브

0개의 댓글