앞선 게시글에선 ORM에 대해 알아보았다. 그럼 ORM의 특징인 객체와 DB를 매핑한다고 하였는데 그중 하나인 Raw JPA에 대해 알아보자.
우선 시작하기 앞서 간혹 헷갈리는 도메인(Domain)에 대해 짚고 넘어가보자.
도메인의 정의 : 컴퓨터 프로그래밍 분야에서 문제를 풀기위해 설계된 어떤 소프트웨어 프로그램에 대한 기능성을 정의하는 연구의 한 영역.
사전적 의미는 이렇고 쉽게 생각하면 우리가 서버를 설계할때 ERD와 API명세를 만드는데 이것과 밀접한 연관이있다. 우리가 API명세를 짤때 서버 이용자가 어떤 행위를 요청할것인가에 대해 미리 정의하고 명세를 작성한다. 예를 들어 회원게시판의 게시글과 댓글을 작성한다고하면
라는 프로세스로 진행을 하게된다. 이때 우리는 로그인을 해야하기 때문에 아이디, 비밀번호의 정보를 가진 User가 필요하고 게시글에 제목, 내용 등을 정의한 Post와 댓글을 정의한 Comment 등의 Entity가 필요함을 알게 되고 해당 엔티티와 해당 엔티티가 가지고있는 아이디, 비밀번호 등과 같은 필드을 작성함으로서 ERD 설계를 하게된다. 이때 도메인의 중요 포인트는 바로 "기능"인데 User가 하는 기능, Post가 하는 기능 등 Entity가 하는 기능의 영역이 각각의 Domain이라 할 수 있다.
본격적으로 Raw JPA에 대해 얘기해보자면 Raw JPA는 DB에서 정의하고 있는 테이블을 자바로 매핑하는 기능을 제공한다. 우리가 개발하면서 Entity클래스에서 사용해왔던 여러 에노테이션(@Entity
, @Table
, @Id
등..)이다.
좀더 자세히 알아보자면
@Entity
: 객체 관점에서의 이름. 디폴트로 클래스명으로 설정. 엔티티의 이름은 JQL에서 쓰임
(JQL : Entity 명으로 쿼리짤때 쓰이는 언어 (ex. JPQL, QueryDsl))
@Table
: RDB의 테이블 이름. 디폴트는 @Entity의 이름. 테이블의 이름은 SQL에서 쓰임
(SQL : Table 명으로 쿼리짤때 쓰이는 언어 (ex. JDBC, SQL Mapper))
@Id
: 엔티티의 주키를 맵핑할때 사용. RDB에서 PK로 설정가능
@Colum
: 엔티티의 필드를 매핑할때 사용. 여러가지 속성사용가능(uniqe, nullable, length...)
@Transient
: 컬럼으로 맵핑하고 싶지않은 멤버 변수에 사용
필드 타입 매핑에 대해 좀더 자세히 들어가보면
@Colum
: String, Date, Boolean 과 같은 타입들에 사이즈를 제한할 용도로 사용
@Enumerated
: Enum의 매핑용도로 사용 주로 @Enumerated(EnumType.STRING)
으로 사용권장
객체지향적 프로그래밍을 좀더 잘 사용하기위해 Composite Value타입에 필드를 정의하는데 사용하는 에노테이션들이 있다. 예를 들어 Account라는 엔티티에 필드로 id, street, city, state, zip code 들이 있다고 하자. 여기서 id를 제외한 필드들은 주소라는 address 객체로 묶을 수 가 있다. 만약 id를 제외한 나머지들을 address 클래스로 묶은후 Account엔티티에 필드로서 그냥 Account를 넣는다면? RDB상으로는 하나의 클래스가 들어가있는거처럼보이는데 이는 묶기전과는 다른 결과를 보인다. 같은 결과를 보이기 위해 사용하는것이 @Embedded
와 @Embeddable
이다.
@Entity
public class Account {
@Id @GeneratedValue
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "street", column = @Column(name = "home_street"))
})
private Address address;
}
@Embeddable
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
}
@Embedded
는 해당 필드가 객체자체가아닌 필드의 집합체라고 선언하는 에노테이션이라고 볼수있고 이에 짝지어있는 Embeddable
은 집합체의 클래스를 매핑해주는 에노테이션이다. @AttributeOverride
은 해당 필드를 override해 이름을 바꿀수있는 에노테이션이다.
우리가 알고있는 OneToOne
, OneToMany
, ManyTOMany
에 대해 좀더 깊이 알아보자.
우선 OneToOne
관계에 경우 관계 지정에 앞서 꼭 물리적으로 테이블이 분리되어있어야 하는지 대해 생각해봐야 한다. 1대1관계를 구성한다는 것은 결국 하나의 목적에 부합되는 공통된 데이터를 관리한다고 볼 수 있으며 이것은 하나의 테이블에서 관리 할 수 있는 데이터일 가능성이 높다는 의미이기 때문이다. 즉, 의도적 중복이 아니면 사용할일이 없다.
다음으로 OneToMany
는 단방향 설정시 문제가 발생 할 수 있다. 예를 들어 객체 저장 및 삭제시 저장 및 삭제 대상이 되는 테이블이 외래키의 주인이 아니라면은 다른 테이블에 update쿼리가 한번씩 더날라가는 문제가 발생하게된다.
속도를 위해 기본적으로 탐색범위 제한을 위해 FetchType 설정을 LAZY로 설정되어 있다.
ManyToOne
에 경우 FetchType이 EGEAR로 되어있으나 기본적으로 LAZY로 설정하는것을 추천한다. 주로 @JoinColumn
을 외래킹 매핑을 위해 사용하며 생략해도 외래키가 생성된다.
ManyToMany
이 설정될 경우 중간 매핑테이블(JoinTable)이 자동으로 생성되서 JPA상에서 숨겨서(Entity 정의없이) 관리된다. 하지만 이경우 해당 테이블 관리가 불가능하여 해당 테이블을 직접정의(Entity 정의)하여 관리하는 방법을 추천한다.
TableA(@OneToMany) > MappingTable(@ManyToOne, @ManyToOne) > TableB(@OneToMany)
이때 매핑테이블은 직접 id를 생성해도되지만 Join된 두테이블의 id(외래키)로 복합키를 만들어 사용하기도 한다. 해당 복합키를 만드는 방법은 두가지있는데
우선 첫번째로 @IdClass
를 사용하여 해당 id 클래스를 따로 만들어주는 방법이다.(필드정의 x)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserChannelId implements Serializable {
private Long user; // UserChannel 의 user 필드명과 동일해야함
private Long channel; // UserChannel 의 channel 필드명과 동일해야함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
}
@Entity
@IdClass(UserChannelId.class)
public class UserChannel {
....
@Id
@ManyToOne
@JoinColumn(name = "user_id")
User user;
@Id
@ManyToOne
@JoinColumn(name = "channel_id")
Channel channel;
...
}
다음은 @EmbeddedId
을 사용하여 위와 마찬가지로 별도의 클래스로 정의해준다.(필드 정의 O)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class UserChannelId implements Serializable {
@Column(name = "user_id")
private Long userId;
@Column(name = "channel_id")
private Long channelId;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
}
@Entity
public class UserChannel {
@EmbeddedId
private UserChannelId userChannelId;
...
@ManyToOne
@MapsId("user_id")
User user;
@ManyToOne
@MapsId("channel_id")
Channel channel;
...
}