
출처 - 인프런 김영한님 자바 ORM 표준 JPA 프로그래밍 - 기본편
인강의 내용이 있기 때문에 출처를 밝힙니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
오늘은 테이블 매핑, 필드와 컬럼 매핑, 기본 키 매핑 및 실습을 진행했다.
기록 시작 ~
JPA 객체와 테이블 매핑
JPA 에서 객체와 테이블을 매핑할 때, @Entity, @Table 어노테이션을 사용해서 매핑한다.
자세히 알아보자.
1. @Entity
테이블과 매핑할 객체 클래스 위에는 항상 @Entity 어노테이션을 사용해서 JPA 에게 이 객체를 매핑할 것을 알려줘야 한다.
엔티티는 항상 기본 생성자가 필수로 있어야 하고 Public 또는 Protected 를 접근 제어자로 사용한다.
final, enum, interface, inner 는 @Entity 어노테이션을 사용할 수 없다.
엔티티의 필드에는 final 을 사용할 수 없다.
왜 기본 생성자가 필수로 존재해야 할까 ?
JPA 는 기본 생성자를 통해 객체를 생성, Reflection 을 사용해 값을 매핑하도록 내부 구현이 되어있기 때문이다. 좀 더 풀어 작성하자면,
Java Reflection API 는 생성자의 인자 정보들을 가져올 수 없다. 따라서 아무런 인자가 없는 기본 생성자가 있어야만 객체를 생성할 수
있기 때문에 엔티티에 기본 생성자가 꼭 필요하다.
왜 필드에 final 을 사용할 수 없나 ?
Reflection 을 사용하면 생성자나 Setter 메서드를 사용하는데, final 을 사용하면 Setter 를 사용할 수 없기 때문에 final 을 사용하지
않는다. 그렇다고 롬북의 @Setter 를 사용하기 보다는, 보다 명확한 의미를 가지는 메서드를 엔티티에 만들어주는 것이 좋다.
왜 final, enum, interface, inner 클래스는 @Entity 를 사용할 수 없나 ?
강의 뒷 부분에 나오지만, Proxy 패턴과 관련이 있다.
Proxy 패턴을 사용하기 위해선 상속을 받을 수 있는 클래스여야 하는데, 위의 클래스들은 상속이 불가한 클래스들이기 때문이다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
@Id
private Long id;
private String name;
.. 중략
}
2. @Table
@Table 어노테이션을 사용하면, 매핑할 테이블을 지정할 수 있다.
아무것도 설정하지 않으면, 기본 값으로 Entity 이름을 사용한다. (Meber Entity -> Member Table)
@Table 어노테이션으로 사용하고 있는 DB 의 예약어를 회피할 때 사용하기도 한다.
예를 들어, 내가 사용하는 DB 에 Member 는 예약어라 사용이 불가하다고 가정하면, name 속성을 통해 이름을 지정해서 회피할 수 있다.
@Entity
// 이렇게 하면 Select * From Member 가 아닌, From Members 가 나가기 때문에 예약어 회피가 가능하다.
@Table(name = "Members")
public Member {
... 중략
}
3. DB 스키마 자동 생성 기능
JPA 에서는 애플리케이션 로딩 시점에 DB 테이블을 생성하는 기능을 제공한다.
5가지 옵션이 있는데 애플리케이션 운영 시에는 잘 사용하지 않고, 로컬 개발 시 자주 사용한다.
운영 서버에서 함부로 사용하면 위험한 기능들이 많으므로 웬만한 기능은 로컬 개발 시에만 사용하자.
첫 번째 옵션) Create
기존의 테이블을 모두 DROP 하고 다시 모든 테이블을 생성하는 기능이다.
모든 테이블을 밀어버리기 때문에 정말 주의해서 사용해야 한다. 따라서 애플리케이션 운영 시 절대 사용하지 않는다.
두 번째 옵션) Create-Drop
Create 옵션과 동일하지만, 애플리케이션 종료 시점에 다시 테이블을 DROP 한다.
역시 운영 시에는 사용하지 않는 옵션이다.
세 번째 옵션) Update
ALTER TABLE 과 유사하며, 테이블을 Drop 하는게 아닌, 엔티티의 변경된 부분이 있다면 변경분만 새로 테이블 컬럼에 추가한다.
변경분이 없다면 아무 일도 일어나지 않고 해당 옵션 역시 운영 시에는 절대 사용하지 않는다.
네 번째 옵션) Valudate
Entity 와 테이블이 정상적으로 매핑되어있는지 확인만 해준다.
따로 테이블이 없어진다거나 컬럼이 추가되지 않기 때문에 운영 서버에서도 사용이 가능한 옵션이다.
다섯째 옵션) None
사실 위 옵션들을 사용하지 않으면 주석 처리하거나 비워놔도 되지만, 관례적으로 None 을 적어둔다.
Yml 파일 기준으로 이렇게 설정해서 사용하고 싶은 옵션을 ddl-auto 옆에 하나 적어주면 된다.
spring:
jpa:
hibernate:
ddl-auto: create
create-drop
update
validate
none
필드와 컬럼 매핑 그리고 @Column
간단한 요구사항을 통해 예시 엔티티를 하나 만들어보자.
요구사항
1. 회원은 일반 회원과 관리자로 구분한다.
2. 회원 가입일과 수정일이 있다.
3. 회원을 설명할 수 있는 필드가 있다. 단, 길이제한은 없다.
@Setter 는 예시라서 사용했을 뿐 사용하지 않는게 가장 좋다.
@Getter
@Setter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
private Long id;
@Column(name = "name")
private String username;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Lob
private String description;
@Transient
private int temp;
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
}
DDL 생성 결과
Hibernate:
create table Member (
id bigint not null,
age integer,
createdDate timestamp,
description clob,
lastModifiedDate timestamp,
roleType varchar(255),
name varchar(255),
primary key (id)
)
하나씩 분리해서 살펴보자.
여기서 엔티티의 PK 값을 설정했다.
뒤에 적겠지만, 따로 @GeneratedValue 어노테이션이 없다면 직접 ID 값을 셋팅해야 한다.
타입이 Long 인 이유는 @GeneratedValue 적을 때 같이 서술하려고 한다.
@Id
private Long id;
여기서는 멤버의 이름을 설정했다.
@Column 어노테이션을 사용해서 객체에서는 memberName 을 사용하고 DB 테이블에서는 name 을 사용할 수 있도록 했다.
name 외에 다양한 옵션이 있는데, 가장 많이 사용하는 옵션은 nullable, length 정도로 null 제약 조건과 길이 제약을 걸 수 있는 옵션이다.
@Column(name = "name")
private String memberName;
여기서는 Enum 클래스인 RoleType 을 설정했다.
엔티티에 Enum 타입을 필드로 둘 때는 항상 @Enumerated 어노테이션을 활용해야하는데, 기본으로 EnumType.ORDINAL 이 있지만
절대 사용하면 안되고, 항상 EnumType.STRING 으로 설정해서 사용해야 한다 !
왜?
Ordinal 은 순서를, String 은 Enum 의 이름을 DB에 저장하는 방식이다.
만약 순서를 통해 Enum 을 DB 에 저장했다면 한 눈에 알아보기도 힘들 뿐더러, 요구사항이 늘어나서 Enum 클래스에 필드가 추가됬을 때
예전에 DB 에 저장된 순서는 최신화 되지 않기 때문에 순서가 꼬여 굉장히 큰 장애를 발생시킬 수 있는 여지가 돼기 때문이다.
따라서 무조건 EnumType.STRING 으로 설정하고 Enum 을 사용하도록 하자.
// 참고) RoleType Enum 클래스
public enum RoleType {
ADMIN, USER
}
---
@Enumerated(EnumType.STRING)
private RoleType roleType;
여기서는 회원을 설명하는 필드를 설정했다.
@Lob 은 Varchar 범위를 넘어서는 큰 컨텐츠에서 사용하는 어노테이션으로, 요구사항에서 회원 설명에 대해 길이 제한은 없다고 했기
때문에 해당 어노테이션을 사용했다.
문자라면 CLOB, 문자가 아닌 나머지는 BLOB 으로 매핑된다 !
@Lob
private String description;
여기서는 가입일과 수정일 필드를 설정했다.
자바 8의 LocalDate, LocalTime, LocalDateTiem API 가 나오기 전에는 @Temporal 어노테이션을 작성해서 DB 의 시간 타입과
매칭시켜줘야 했었지만, 자바 8 이후 부터는 필요가 없어져서 시간 관련 필드는 이렇게 작성해줘도 무방하다.
private LocalDateTime createDate;
private LocalDateTime lastModifiedDate;
여기는 @Transient 를 설명하기 위한 예시 필드이다.
이 어노테이션을 필드에 설정하면 매핑을 하지 않기 때문에 DB 에 저장하지 않고 당연히 조회도 되지 않는다.
주로 메모리 상에서 임시로 값을 보관하고 싶을 때 사용하는 어노테이션이다.
@Transient
private int temp;
기본 키 매핑
위에서 조금씩 언급된 @Id, @GeneratedValue 어노테이션에 대해 좀 더 알아보자.
1. @Id
@GeneratedValue 를 사용하지 않고 @Id 어노테이션 단독으로 사용하면, 내가 직접 기본 키를 셋팅해서 사용할 수 있다.
보통 RDB 를 사용하면, 오라클인 경우 sequence, MySQL 계열은 auto-increment 를 사용하기 때문에 @Id 만을 사용해 기본 키를
할당하여 사용하는 경우는 거의 없다고 봐도 무방하다.
2. @GeneratedValue
보통 엔티티의 PK 값을 설정할 때 @Id 와 함께 사용하는 어노테이션이다.
AUTO, IDENTITY, SEQUENCE 전략이 있는데, IDENTITY, SEQUENCE 두 가지 전략에 대해 알아보자.
(AUTO 전략은 설정한 DB 에 맞춰 자동으로 기본 키가 셋팅되는 전략)
IDENTITY 전략
PK 값을 설정할 때 흔히 사용하는 auto_increment 방식과 동일한 전략이다.
설정한 DB 에 맞춰 알맞은 DDL 이 생성되는 것을 확인할 수 있다.
@Setter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String username;
}
DDL 생성 결과 (H2)
Hibernate:
create table Member (
id bigint generated by default as identity,
name varchar(255),
primary key (id)
)
DDL 생성 결과 (MySQL)
Hibernate:
create table Member (
id bigint not null auto_increment,
name varchar(255),
primary key (id)
) engine=MyISAM
```
그런데 JPA 의 영속성 컨텍스트를 다시 상기해보면, 뭔가 이상한 점이 생각난다.
첫 번째)
auto_increment 는 DB 에 INSERT 쿼리를 실행한 이후에 비로소 ID 값을 확인할 수 있다.
두 번째)
JPA 의 영속성 컨텍스트에 존재하는 1차 캐시에 저장된 엔티티는 반드시 PK 값을 가진다.
세 번째)
JPA 는 보통 트랜잭션 커밋 시점에 INSERT 쿼리를 DB 로 보내 실행시키는데, auto-increment 를 사용하면 우리가 직접 PK 값을
셋팅할 수 없으므로, INSERT 쿼리에 PK 값을 NULL 로 셋팅된다.
엔티티가 영속화 되는 시점에 1차 캐시에 저장되는 엔티티는 반드시 PK 값이 존재해야 하는데, PK 값으로 NULL 이 셋팅된다라니?
즉, NULL 인 엔티티가 1차 캐시에 저장될 수 없는데 어떻게 동작하는 걸까 ?
이에 대한 해결 방안으로 IDENTITY 전략에서만 예외적으로 트랜잭션 커밋 시점이 아닌, 엔티티를 영속화 하는 시점에 바로 INSERT
쿼리를 DB 에서 실행시키게 된다. 아래 실행 결과를 보면 em.persist() 시점에 INSERT 쿼리가 실행되는 것을 알 수 있다.
이렇게 영속 시점에 DB 에서 INSERT 쿼리가 실행되면, DB 를 통해 식별자를 조회하여 영속성 컨텍스트의 1차 캐시에 값을 넣는다.
Member member = new Member();
member.setUsername("C");
System.out.println("======")
em.persist(member);
System.out.println("======")
tx.commit();
실행 결과
System.out.println("======")
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(id, name)
values
(null, ?)
System.out.println("======")
SEQUENCE 전략
주로 오라클 DB 에서 사용하는 전략이다.
시퀸스 이름을 따로 지정하지 않으면, 하이버네이트에서 제공하는 기본 시퀸스를 사용한다.
만약 테이블마다 다른 시퀸스를 사용하고 싶다면, 엔티티에서 따로 매핑하면 된다 !
@Entity
@SequenceGenrator(
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;
... 중략
}
권장하는 식별자 전략
식별자 전략에는 자연키와 대체키 라는 단어가 나오는데 뜻을 알고 가자.
1. 자연키 (Natural Key) 란 ?
테이블을 이루는 속성 가운데 의미를 담고 있는 키
즉, 비즈니스 적 의미를 가지고 있는 키라고 할 수 있다.(주민번호, 전화번호 등)
2. 대체키 (Surrogate Key) 란 ?
테이블의 이루는 속성을 PK 로 하지 않고 DB 에 독립적으로 할당된 키
즉, 비즈니스와 아무련 관련이 없는 임의로 만들어진 키라고 할 수 있다. (auto-increment, 오라클의 sequence 등)
PK 값은 다음 3 가지의 조건을 만족해야 한다.
1. 유일무이한 값이여야 한다.
2. NULL 이면 안된다.
3. 변하면 안된다.
특히 3번, 변하면 안된다 라는 조건은 생각하기 굉장히 까다롭다고 한다.
또한 이 세 가지의 조건을 유지할 수 있는 자연키는 찾기 굉장히 어렵고 까다로운데 여러가지 예시를 들어보자면,
첫 번째) 핸드폰 번호 ?
확실히 핸드폰 번호라면 곂치는 번호가 절대 존재하지 않기 때문에 자주 바뀔 가능성이 높다.
두 번째) 주소 ?
본인이 가입한 곳은 같은 주소에 있는 사람들은 PK 값이 곂치기 때문에, 거주 중인 다른 가족들이 가입할 수 없다.
세 번째) 주민번호 ?
주민번호야 말로 변하지 않고, 절대로 곂치지 않으니 PK 로 적합하지 않을까?
하지만 국가 정책의 변경으로 서비스 중인 DB 에 회원의 주민번호를 저장하지 못하게 막혀버린 사례가 있다. (영한님 사례라고 한다..)
따라서 비즈니스와 아무 관련이 없는 대체키와 Long 타입 및 키 생성전략을 적절히 사용해 PK 를 생성하고 사용하는게 젤 낫다 !
'JPA' 카테고리의 다른 글
[인강 복습] JPA 기본편 다양한 연관관계 매핑 (#5) (0) | 2022.07.06 |
---|---|
[인강 복습] JPA 기본편 연관관계 매핑 기초 (#4) (0) | 2022.07.05 |
[인강 복습] JPA 기본편 영속 컨텍스트 까지 (#2) (0) | 2022.06.12 |
[인강 복습] JPA 기본편 JPA 시작하기 까지 (#1) (0) | 2022.06.10 |