@Entity
@Table(name="MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10)
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
@Transient
private String temp;
}
JPA를 활용해 Table과 매핑할 클래스에는 @Entity 어노테이션을 붙여 Entity로 활용할 것임을 명시해줘야 한다.
@Entity 어노테이션이 붙어 있는 객체만이 JPA에 의해 관리받으면서 DB와 통신할 수 있게 된다.
즉, 어노테이션이 붙어 있는 클래스만 영속성 컨텍스트의 1차 캐시에 등록될 수 있는 것이다.
이렇게 @Entity가 붙어 있는 클래스를 "엔티티 클래스(Entity Class)"라고 한다
@Entity를 적용하기 위해서는 몇 가지 주의 사항이 존재한다
추가로 Entity로 지정해줄 수 없는 클래스 종류도 존재하는데 대부분 Entity는 외부에서 접근 가능한 객체여야 하므로 public 접근 제어자를 가지는 경우가 대부분이라 그냥 public 클래스에서만 활용한다고 알고 있자
@Entity도 다른 Annotation과 마찬가지로 많은 Property 값을 가지고 있지만 활용할 상황도 별로 없고 개발에 오히려 방해되는 경우도 많아서 그냥 @Entity만 활용한다 정도로만 알고 있자
Table Annotation은 Entity 어노테이션과 떼어놓기 힘든 어노테이션이라 Entity 어노테이션과 같이 설명을 하겠다.
우리는 @Entity 어노테이션을 통해 엔티티 클래스를 지정했다. 그렇다면 이 엔티티 클래스는 어떤 이름의 DB Table과 매핑되게 될까?
Table 이름도 Column Name과 마찬가지로 JPA 자체적으로 정한 Naming Rule이 존재하여 만약 @Table로 내가 Mapping 할 Table이름을 지정해주지 않을 경우 규칙에 따라 Table을 찾을 것이다.
하지만 DB Table에 종속적인 개발을 막기 위해 Table 어노테이션을 활용하여 Mapping시킬 Table 이름을 직접 명시해주는 것이 더 좋다고 생각한다.
사실상 @Table Property도 name만 활용한다.
catalog와 schmea 기능은 웬만하면 (특히 MySQL에서는) 활용하지 않는 것을 추천하며(이유는 아래 Schema와 Catalog에 대해 설명하며 덧붙이겠다.) uniqueConstraints 같은 경우 Schema 자동 생성 시에만 활성화되는 Annotation인데 이전에 말했듯 결국 운영 서버에는 JPA가 자동 생성한 Schema를 활용하지 않는 것이 추천되므로 실제로는 거의 활용되지 않는 기능이다.
@Entity
@Table(uniqueConstraints={@UniqueConstraint(columnNames = {"id_1" , "id_2"})})
public class class_name {
@Id
@GeneratedValue
public Long id;
@NotNull
public Long id_1;
@NotNull
public Long id_2;
}
위 코드에서는 id는 PK(Primary Key)가 될 것이고 id_1과 id_2는 Unique Key가 될 것이다.
물론 Schema 자동 생성 기능을 활용할 때만 적용되는 Property이므로 실제 Table에서 id_1과 id_2를 저장하는 Column이 Unique Key가 아니더라도 에러는 발생하지 않는다.
columnNames의 값으로는 Unique Key Data를 저장할 멤버 변수 이름을 쉼표(,)를 통해 한 번에 입력해주면 된다.
Catalog와 Schema는 개발자가 생성한 DDL을 통해 실제로 활용할 Table이라기 보다는 DB Server 측에서 정의하는 "Namespace", 다른 말로 특정한 객체를 구분할 수 있는 공간이다.
DB는 이 Schema나 Catalog를 통해 소속된 DB Table들을 묶어 놓고 다른 DB와 구분하게 된다.
조금 더 쉽게 이해해보자.
DB A에는 "Menu"라는 Table이 존재한다고 가정하자. 그런데 "Menu"라는 이름은 너무 보편적이어서 DB B에서도 "Menu"라는 Table이 존재하는 것이다.
이때 DB A의 "Menu"와 DB B의 "Menu"라는 것이 다르다는 것을 알려주는 것이 바로 Namespace역할을 하는 Schema나 Catalog인 것이다.일부 DB는 Catalog만 활용하고, 일부 DB는 Schema만 활용하며 일부 DB는 Schmea와 Catalog를 동시에 활용한다.
MySQL 같은 경우 @Table의 Schema와 Catalog 기능을 무시하는 것이 좋다.
우리는 설정값에서 JDBC URL을 지정해줬는데 이 URL의 마지막 부분에 DB Name을 지정해줬다.
MySQL은 이 Schema와 DB Name이 동일하기 때문에 괜히 다르게 설정했다가는 DB Schema가 DB를 제대로 찾지 못하여 에러가 발생하는 불상사가 일어날 수 있기 때문이다.
이전에 말했듯 JPA는 Entity를 "식별자"라는 특별한 값으로 영속성 컨텍스트내에서 내가 원하는 Entity를 뽑아낼 수 있었다.
이러한 "식별자"의 특성 때문에 식별자는 곧 PK Column과 매핑되는 경우가 많다는 것도 설명했다.
Entity의 식별자임을 알리는 Annotation이 바로 @Id이다.
@Id 어노테이션이 붙여진 클래스의 멤버 변수는 "식별자 필드"라고 불리며 영속성 컨텍스트 내에서 Entity를 구별하는 용도로 활용된다.(즉 식별자로써 활용된다)
거의 모든 경우에 식별자 필드는 PK Column과 매핑되지만 필수까지는 아니라는 점은 기억해두자
(물론 내가 생각할 때는 그렇게 사용할 필요는 없다고 생각된다. Entity를 식별할 수 있다는 특징 자체가 DB에서 Row Data들을 식별할 수 있다는 것이고, 굳이 Row Data를 식별할 수 있는 Column과 PK Column을 따로 생성할 필요성은 없는 것 같다. 결국 PK Column이 Row Data를 식별할 수 있어야 하는 Column이므로 같은 역할을 해야 하는 Column이 1개 Table에 2개 존재한다는 의미이며, 이는 공간적인 낭비만 발생시키기 때문이다)
@Id 어노테이션은 단순히 특정 변수가 "식별자 필드"임을 나타내는 역할을 하는 어노테이션이다. JPA에서 제일 중요한 과정이라 단순히라는 말을 붙여야 하나 애매하긴 하지만, 일단은 멤버 변수를 식별자 필드로 지정하여 JPA에 알리는 역할만 수행하기 때문에 Property가 딱히 존재하지는 않는다.
하지만 @Id와 같이 활용되는 @GeneratedValue에 대해서는 자세히 뜯어볼 필요성이 있다.
위에서도 말했듯 식별자 필드는 PK와 매핑되는 경우가 대다수이다.
그런데 DB에서는 INSERT 구문을 넣을 때 대부분 PK 값을 지정하여 넣어주지 않는다.
개발자가 직접 PK값을 넣어주면 PK 값이 중복될 수도 있으며 특정 Data가 사라지는 등 많은 문제점이 발생할 수 있다.
그래서 DB는 PK 값을 자동으로 생성해주는 여러 가지 방법들을 제공하고 있다.
가장 대표적인 것이 AUTO_INCREMENT로써, 가장 마지막에 주입된 Row Data의 PK값에 1을 더해준 값을 새로 주입되는 PK값으로 넣어주는 것이다. 이런 방식을 채택할 경우 개발자는 PK가 중복되는 문제에 대해 고민하지 않아도 되며 PK의 독립성을 유지할 방법에 대해서도 고민하지 않고 개발에만 집중할 수 있다.
DB에서 직접 Query를 생성할 때는 Table에 AUTO_INCREMENT 설정을 해두고 NULL값을 넣어주면 된다. 이 경우 DB는 PK값에 Null이 들어올 수 없음을 인지하고 PK 자동 생성 설정을 확인하여 설정에 맞는 PK 값을 생성하여 DB Table에 데이터를 저장하기 때문이다.
그런데 JPA는 이걸 멍청하다고 해야 할지 보안적으로 뛰어나다고 해야할지 모르겠지만 식별자 값에 Null이 들어오면 바로 에러를 발생시킨다. 즉 Insert 구문을 실행할 때 PK값이 Null이여야지만 PK 자동 생성 로직을 활용할 수 있는데 PK와 연동될 식별자 필드 값이 Null이면 에러가 발생하는 골치 아픈 상황이 발생하는 것이다.
이때 활용하는 것이 @GeneratedValue 어노테이션이다. 식별자 필드에 @GeneratedValue 어노테이션을 같이 활용해주면 JPA는 이 식별자 필드가 DB에 의해 자동 생성되는 값이라는 것을 인지한다. 따라서 식별자 필드 값이 Null이더라도 DB에서 이 값을 자동 생성해줄 것임을 인지하고 로직을 통과시키는 것이다. 이후 DB에서 PK 값을 생성시켜 Data를 저장하면 이때 생성된 PK 값을 받아와서 1차 캐시에 엔티티를 저장함과 동시에 얻어온 PK 값을 식별자로 등록시키는 것이다.
이런 특징 때문에 @GeneratedValue 어노테이션을 활용할 경우 해당 Entity에 대한 Insert 구문은 쓰기 지연 SQL 저장소에 저장되는 것이 아닌 INSERT Query문이 생성되는 순간 즉시 Query를 수행하게 된다. 왜냐하면 Query를 수행하여 Entity에 대한 식별자 값을 무조건 얻어와서 해당 Entity를 영속성 컨텍스트에 등록시켜야 하기 때문이다.
먼저 사용 방법은 @GeneratedValue(strategy=X)에서 X 부분에 아래에서 설명할 값들 중 원하는 방법을 가지는 값을 넣어주면 된다.
GenerationType.IDENTITY
GenerationType.AUTO
GenerationType.SEQUENCE
GenerationType.TABLE
객체 필드(멤버 변수)를 Table에 직접 매핑시키는 것이다.
예를 들어 name이라는 멤버 변수가 USERNAME이라는 Column과 매핑되어야 할 경우 @Column을 통해 매핑 관계를 설정해 줄 수 있을 것이다.
식별자이든 일반 멤버 변수이든 Table과 매핑되어야 하는 모든 멤버 변수에 활용할 수 있는 어노테이션이다.
클래스의 Field(멤버 변수)를 Column에 매핑시키는 Annotation이다.
name, nullable만 주로 활용되며 사실상 나머지 Property는 활용하지 않는다.
위에서 nullable, columDefinition 등 DDL에만 적용되는 속성들이 많았다.
그런데 이 중에서 nullable은 생각보다 필요하면서도 쓸모없는 속성이다.
이전에 말했듯 운영 상에는 자동 생성 Schema를 활용하지 않기 때문에 nullable 속성은 사실상 큰 의미가 없다. 물론 Validation으로 Schmea 자동 생성을 적용했다면 활용은 할 수 있겠으나 대부분 auto 기능을 꺼 놓을 것이다.
그런데 만약 내가 원하는 값이 nullable 속성인데 DB Table에서는 Null 값 허용 여부가 제대로 적용되어 있지 않다면 어떻게 될까?
nullable 속성은 결국은 "DDL"에서만 적용되는 속성이기 때문에 실제 필드가 Null이 아니어도 정상적으로 SQL 구문이 실행돼버리고 말 것이다.
그렇다면, 실수로 DB Table에 NULL을 허용했지만 Null 값을 허용하고 싶지 않다면 어떻게 해야 할까?
DB에 의존적이지 않은 개발을 위해선 Table과 관계없이 애초에 Field 값이 Null이라면 Error를 발생시키면 될 것이다.
이때 활용하는 것이 @NotNull이다.
@NotNull은 nullable 속성을 포함함과 동시에 "유효성 검사"를 같이 해준다.
즉, 만약 Field값이 Null이라면 INSERT 구문을 실행하기 전 해당 Field가 Null이라는 것을 인지하고 에러를 발생시킨다.
@NotNull이 DDL에서의 제약 조건 설정과 유효성 검사를 동시에 해주기 때문에 nullable 보다는 @NotNull을 활용하는 것이 더욱 내가 원하는 대로 로직이 구현될 수 있을 것이다.
Mapping하지 않을 Field에 명시한다.
클래스는 꼭 내가 Mapping을 원하는 값만 존재하지 않는다.
예를 들어 Entity를 설명해주기 위한 String 자료형을 저장하고 싶을 수도 있고, 실제 개발 환경에서는 DTO나 VO를 활용하기는 하지만 그냥 Entity 클래스 형태로 AJAX 등을 통해 데이터를 주고받고 싶을 수도 있을 것이다. 실제로 필자는 List 형식을 AJAX로 보내는 대신 List에 저장될 Data를 그냥 구분자 쉼표를 통해 연결한 이후 String형으로 보낸 적도 있다. 이때 이 String형 데이터는 실제 DB에 저장하고 싶은 데이터는 아닐 것이다.
이런 DB에 실제로 저장하고 싶지 않은 데이터에 적용하여 JPA 측에 해당 필드에 대한 Query문은 생성하지 않도록 알리기 위한 것이 @Transient이다.
JPA가 Entity 데이터에 접근하는 방식을 지정한다.
재밌는 점은 @Access의 Default값은 정확히 정해진 것이 아니라는 점이다.
@Access는 식별자 필드를 명시해주는 @Id의 위치를 기준으로 접근 방식이 설정된다.
마약 @Id가 Getter 함수에 달려 있다면 AccesssType.PROPERTY로, 필드에 직접 달려 있으면 AccessType.FIELD로 설정된다.
나중에 Spring Data JPA를 활용하면 Getter나 Setter를 Lombok으로 처리하는 경우가 다수이기 때문에 사실상 Default는 AccessType.FIELD로 보는 것이 맞을 것 같다.
날짜 Type(java.util.Date, java.util.Calendar를 매핑할 때 활용한다.
DB의 데이터 타입은 생각보다 다양하다. 날짜만 저장하는 타입, 시간만 저장하는 타입, 그리고 날짜와 시간을 모두 저장하는 타입이 존재한다.
하지만 Java Type은 이렇게까지 자세히 타입을 분류해 놓지는 않았기 때문에 원하는 매핑 방식으로 DB에 저장되지 않을 수도 있다.
이때 활용하는 것이 @Temporal로 다른 Property와는 다르게 "JDBC Data Type"에 집중하여 설정값을 전달해준다.
@Temporal을 생략할 경우 java.util.Date와 가장 유사한 TemporalType.TIMESTAMP로 정의된다.
DB의 BLOB, CLOB Type과 매핑된다.
@Lob을 알아보기 이전에 먼저 Lob Type에 대해 먼저 알아볼 필요가 있을 것이다.
Lob은 Text, Image, Video, Sound 등 구조화되지 않은 대형 데이터를 저장하는 목적으로 만들어진 DB Type이다.
Lob의 종류는 CLOB과 BLOB, BFILE, NCLOB Type이 존재한다.
대부분의 MultiMedia 데이터는 크기가 크다. 따라서 DB에서는 따로 이 데이터를 처리할 수 없었고, 이런 문제를 해결하기 위해 만든 것이 Lob이다.
하지만 개인적으로는 BLOB, BFILE, NCLOB은 많이 활용하지 않을 것 같다.
(최소한 한국에서는) 이미지 파일이나 비디오 타입을 다루는 방식은 아래와 같다.
먼저 이미지 파일이나 비디오 타입을 특정 디렉터리에 저장한다. 이후 저장한 데이터 경로를 DB에 저장한다.
나중에 해당 데이터가 필요할 때가 된다면 DB는 데이터가 저장된 경로를 반환하고 이 경로를 활용해 a 태그를 달거나 바로 데이터를 보여주는 방식으로 멀티미디어 데이터를 다루기 때문에 큰 데이터에 대한 처리를 위한 Lob Type을 잘 활용하지 않게 되는 것이다.
개인적으로 그나마 활용할만한 Lob은 CLOB인 것 같다
이 CLOB은 너무나 긴 문자형을 저장해야 할 경우 활용하는 Data Type인데, 이전에는 "LONG" 타입으로 처리했다가 최신 버전부터 LOB 데이터 타입을 활용하고 있다.
Long은 최대 2GB까지 저장 가능하며 테이블 당 1개의 칼럼만 생성 가능했다. 또한 Binary Data만 저장할 수 있었다.
하지만 Lob은 4GB까지 저장 가능하며 테이블 당 여러 컬럼이 생성 가능하며 Binary 및 문자 데이터 모두 저장 가능하다는 점에서 큰 발점을 이뤘다고 볼 수 있다.
@Entity
@Table(name="user")
@Data
public class User {
@Id
private Long id;
@Column(name = "name", columnDefinition="VARCHAR(128)")
private String name;
@Lob
@Column(name = "photo", columnDefinition="BLOB")
private byte[] photo;
}
@Lob 어노테이션은 결국 Lob Data와 매핑되기 때문에 Lob Data Type과 매핑되는 자료형의 멤버 변수만 입력되어야 할 것이다.
이전에 말했듯 CLOB은 대용량의 문자형 데이터를 저장할 수 있다.
즉, 만약 자료형이 char나 String이라면 CLOB의 형태로 데이터를 저장할 것이다.
이외의 경우(예를 들어 MultipartFile일 경우) 대부분 BLOB의 형태로 데이터를 저장할 것이다.
자바의 Enum Type을 매핑할 때 사용한다.
ENUM이란 간단히 설명하자면 final static String을 통해 문자열을 지정하지 않고 이런 고정 데이터를 enum에 담아 마치 클래스의 객체 변수를 사용하는 것과 같이 활용하는 것이다.
@Enumerated에 줄 수 있는 설정 값은 2가지가 존재한다.
만약 enum Gender { MALE, FEMALE}로 저장했다고 가정하자
EnumType.ORDINAL로 저장할 경우 MALE은 1, FEMALE은 2로 저장할 것이며 STRING은 "MALE"과 "FEMALE" 문자열이 저장될 것이다.
하지만 이 어노테이션은 활용을 추천하지 않는데, 훗날 Enum 데이터 형식이 변형되거나 순서가 변경될 경우 에러를 발생시키거나 원하는 값을 추출하지 못할 수도 있기 때문에 유지보수성이나 확장성 등을 고려했을 때는 차라리 Converter를 활용하는 것이 좋을 것이다.