JPA는 어떻게 접근해야 할까?
1. What is JPA?
JPA는 요즘 개발자분들이라면 알고 계시는 개념이기도 하지만, 언제나 그렇듯 아직도 많은 회사가 mybatis를 운영하는 환경도 많기 때문에 쉽사리 JPA에 접근하기 쉬운 환경이 아닌 개발자 분들도 많을 것으로 예상됩니다. 이런 분들을 위해서 지난 5년여간 mybatis 환경과 ibatis 환경에서만 살아온 제가 JPA를 공부하고, JPA로 서비스를 구축을 시작하면서 JPA가 무엇인지, 처음 접할때 어떤 관점으로 접근하는게 좋을지 얕은 지식으로 간략하게나마 남겨보려고 합니다.
혹시나 빨리 요점만 알고싶으시다면 Bold글씨나 색이 있는 글씨 위주로만 보시면 좋습니다.
2. JPA와 DBMS
DBMS를 생각하시면 다들 들어보신 단어가 있으실 것입니다. 관계형 DB, 말그대로 DB의 Table간에는 관계를 가지고 있다고 해서 관계형 DB라고 다들 생각 하실것입니다. 그에 반해 JPA는 Java Persitence API의 약자로 JAVA라는 것을 알 수 있습니다. JAVA하면 다들 떠오르시는 단어, 객체지향 언어라는 것입니다. 이런 관점의 차이만 안다면 JPA에 대해서 다 아신 것이라고 생각합니다.
쉬운 예로 아래와 같은 테이블 구조를 가진 DB가 있다고 가정을 해봅시다.
이러면 여러가지 PK때문에 오류가 나겠지만 편의상 이러한 구조의 테이블이 있다고 가정해봅시다. 가족과 식구는 1:N 구조로 가족 테이블의 PK가 식구 테이블의 FK로 사용되면서 두 테이블간의 관계가 있다고 흔히 정의합니다. DB상에서는 이러한 구조를 갖지만 JAVA와 같은 객체지향 프로그래밍 언어를 사용한다면? 이러한 특성을 가지는 관계를 맺기가 힘들기도 하고, 객체지향 언어에서는 없는 개념이기도 합니다. "식구 테이블과 가족테이블간의 관계를 어떻게 객체지향 프로그래밍으로 표현할 수 있을까?" 이러한 물음에 나온 기술이 JPA입니다.
3. 객체지향에서 관계형을 표현하고 관리하는 기술
실제 소스에서는 객체지향이라는 점을 극대화 하여 실제 DB 테이블의 컬럼값을 기준으로 Join하여 Family의 전체 데이터를 접근할 수 있도록 합니다. 아래 kotlin소스를 예제로 들어보겠습니다.
@Entity
class Family {
@Id
val lastName: String
val city: String
}
@Entity
class Person {
@Id
val seq: Long
@ManyToOne
@JoinCloumn(name="lastName")
val family: Family
val firstName: String
val gender: String
}
Person이라는 클래스에서 가족 테이블의 PK인 lastName을 기준으로 하여 Family 클래스와 Person 클래스 사이의 관계를 맺어 서로 접근할 수 있게 제공하고 있습니다. 그렇다면 한가지 궁금증이 생기실 겁니다. "그러면 Family 클래스에서 Person 클래스 접근은 못하는 것인가?" 이러한 질문이 생각나셨다면 JPA 마스터 하시기 일보 직전입니다! 위의 소스를 수정해서 두 클래스를 서로 조회할 수 있도록 해보겠습니다.
@Entity
class Family {
@Id
val lastName: String
val city: String
@OneToMany(mappedBy="family")
val persons: List<Person> = emptyList()
}
@Entity
class Person {
@Id
val seq: Long
@ManyToOne
@JoinCloumn(name="lastName")
val family: Family
val firstName: String
val gender: String
}
Family 클래스에 @OneToMany 어노테이션을 통해 서로 접근 할 수 있도록 편의성을 제공하고 있습니다. 하지만 여기서 중요한 점은 서로 양방향이 아닌 단방향이 2개로 이루어져 있다는 점입니다. JPA에서는 이러한 접근을 통해 DB상의 1:N, N:1, 1:1, N:M 과 같은 관계를 표현하고 있습니다. 그러면 이제 @ManyToOne, @OneToMany와 같은 관계는 어떻게 설정해야 하는지 의문이 드실텐데, 해당 방법은 다음과 같은 방법으로 설정하시면 쉽게 파악 하실 수 있으실 겁니다!
4. @ManyToOne, @OneToMany... 언제 사용해야하는 가?
외래키를 기준으로 생각하시면 쉽습니다! 식구 테이블에 외래키로 가족 테이블의 PK가 있죠? 식구 테이블에는 가족 테이블의 PK가 FK로 지정되어 있어 여러 개가 나올 수 있지만 식구 테이블에는 PK로 무조건 하나입니다. 이러한 관점에서 외래키가 있는 클래스를 기준으로 Many의 위치를 지정해주시면 됩니다. 이것이 장점이라면 굳이 사용해야 하는가?라고 생각하신다면 다음과 같은 장점도 있습니다.
5. 객체지향의 장점을 관계형이 표현한다면?
객체지향 프로그래밍을 공부한다면 가장 먼저 접하는 장점, 바로 상속입니다. 상속의 관점으로 접근해서 관계형을 표현한다면 생기는 장점에 대해서 설명 드리겠습니다.
5-1. 상속을 통해 테이블의 구조를 결정한다!
위의 이미지와 같은 소스는 객체지향 공부를 하시면 자주 접하는 예시 구조입니다. 이 방법을 DB로 표현하는 방법은 없습니다. 이러한 구조를 탈피하기 위해 3가지 방법을 JPA에서 제공을 해주고 있습니다. BIRD, MAMMALS, FISH의 속성을 모두 하나의 테이블로 관리(단일 테이블 전략), 부모클래스와 각각의 자식클래스간의 관계를 만든다(조인 전략), 자식클래스에서 부모클래스의 속성을 관리(구현 클래스 테이블 전략)
5-2. 중복되는 소스를 줄일 수 있다!
@MappedSuperClass를 통해 운영을 하다 보면 자주 생성하고 관리되는 컬럼인 생성시간, 생성자, 수정시간, 수정자 와 같은 공통된 컬럼을 관리할 수 있게 됩니다. 위의 그림에서 해당 테이블에 데이터를 생성한 사람이 동일하게 저장된다고 하면 다음과 같은 코드를 만들어 Animal인 부모클래스와 자식 클래스들에게 공통되게 적용할 수 있습니다. Animal 뿐만 아니라 BIRD와 MAMMALS등의 자식클래스에도 동일하게 적용이 가능합니다.
@MappedSuperClass
abstract class BaseInfoEntity{
... // 필요한 소스들... ex. 생성자 등...
val createdBy: String
val createdAt: LocalDateTime
val modifiedBy: String
val modifiedAt: LocalDateTime
}
@Entity
class Animal: BaseInfoEntity() {
...
}
5-3. 공통으로 관리할 수 있는 속성값을 통해 하나의 클래스로 관리 할 수 있다!
위의 예제를 본다면 모두 생성하고, 수정한다는 속성이 동일한 규격이기 때문에 하나의 클래스로 묶어서 객체지향의 관점으로 접근하고 싶으실 겁니다. JPA를 통해 이러한 고민을 해결할 수 있습니다. 공통되는 속성을 하나로 묶어 클래스로 관리하면서 코드를 재사용 할 수 있도록 하는 객체지향의 장점을 사용할 수 있습니다.
@MappedSuperClass
abstract class BaseInfoEntity{
... // 필요한 소스들... ex. 생성자 등...
@Embedded
val created: DataControl
@Embedded
val modified: DataControl
}
@Embeddable
class DataControl {
val user: String
val at: LocalDateTime
}
... //생략
6. 이 글을 마치면서
이 글은 JPA의 사용법이나 정확한 개념을 공유하다기 보다 처음 JPA를 접하는 개발자 분들에게 JPA라는 객체지향이 관계형 DB를 어떻게 바라보고 활용하는 가에 대해서 알고 가셨으면 하는 바람에 글을 적어봤습니다. 다음에는 JPA의 또 다른 중요 개념인 지연로딩, 즉시로딩이 어떻게 관리되고 처리되며, 사용시 주의해야할 사항에는 어떠한 것들이 있는지 알아보는 글을 포스팅해보도록 하겠습니다.