이번 페이지에서는 db설정과 엔티티 정의, repository에서 필요한 레코드를 select하는 방법 까지를 기술합니다.
yml에 DB정보를 기입하고 엔티티를 정의하는 방법은 구글에도 사용 예시가 많이 나와있을 뿐더러, 어디던 예외 없이 공통 사항이기 때문에 생략하겠습니다. 코틀린에서의 사용법을 주로 기술하겠습니다.
@Configuration
@EntityScan(basePackages = ["kr.co.도메인 클래스"])
@EnableJpaRepositories(basePackages = ["kr.co.도메인 클래스"])
@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider")
class JpaConfig {
@Bean
fun auditingDateTimeProvider() = DateTimeProvider {
Optional.of(LocalDateTime.now())
}
}
JPA 설정 클래스의 일부입니다. EnableJpaAuditing 어노테이션을 사용하면 Spring Audit를 이용해 공통 엔티티를 관리할 수 있게 됩니다. Bean으로 등록한 다음 엔티티 클래스에서 꺼내 사용 해 봅시다.
그리고 AbstractEntity도 하나의 class이므로 일부 테이블들이 대체키를 사용 할 경우에 ID를 변수로 넣어서
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class AbstractEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?
) {
@CreatedDate
@Column(name = "created_at", updatable = false)
protected lateinit var createdTime: LocalDateTime
@Column(name = "created_by", updatable = false)
@CreatedBy
protected var createdBy: String? = null
@LastModifiedDate
@Column(name = "last_modified_at")
protected lateinit var lastModifiedTime: LocalDateTime
@Column(name = "last_modified_by", updatable = true)
@LastModifiedBy
protected var lastModifiedBy: String? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AbstractEntity
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id?.hashCode() ?: 0
}
}
공통 컬럼을 생성 할 때 자동으로 생성되게 할 수도 있습니다.
1번과 이어지는 내용입니다. Java와 기본구조는 동일하지만 코틀린에서는 게터/세터를 어노테이션 없이 함수형으로 정의합니다.
따라서 val로 선언한 멤버프로퍼티는 함수형으로 꺼내올 수 만 있고,
var로 선언한 경우 Service단에서 member.name 등으로 꺼내 값을 직접 입력 할 수도 있습니다.
또한 class의 ( ) 부분에 멤버 프로퍼티(자바의 필드)를 정의하고, 리턴 타입을 뜻하는 { } 부분에 함수, enum, data Class등을 정의하거나 로직을 작성해 사용할 수 있습니다.
통상적으로 이 엔티티에만 해당되는, 혹은 가장 밀접한 로직이나 enum등을 작성합니다.
member와 team 엔터티가 있다고 가정 해 봅시다.
team이름을 변경하는 로직을 member엔터티에 작성하지 않고, team에서 사용하는 enum클래스를 member 엔터티에 작성하지 않습니다!
@Entity
@Table(
name = "member",
indexes = [
Index(name = "MEMBER_USER_ID", columnList = "member_no", unique = true)
]
)
class Member(
@Column(name = "member_no")
id: Long? = null,
@Column(name = "member_id")
private val memberId: String,
private var password: String,
@Convert(converter = BooleanToYNConverter::class)
private var deleted: Boolean,
@Column(name = "member_role")
private var memberRole: String
) : AbstractEntity(id) {
fun memberId() = memberId
fun password() = password
fun deleted() = deleted
fun memberRole() = memberRole
data class UpdatePasswordForm(
val password: String? = null
)
fun updatePassword(f: UpdatePasswordForm) {
if (!f.password.isNullOrBlank()) this.password = f.password
}
fun convertDeletedStatus() {
deleted = true
}
enum class MemberRole(
val memberRoleValue: String
) {
ADMIN("관리자"),
MEMBER("일반 사용자"),
}
}
interface MemberRepositoryCustom {
fun getByMemberNo(memberNo: Long?): Member
}
먼저 메서드를 정의하는 interface를 하나 생성하고, 사용자 번호로 사용자 한 명을 가져오는(리턴하는) fun을 하나 정의 해 줍시다.
interface MemberRepository : JpaRepository<Member, Long>, MemberRepositoryCustom {
@Query(value = "select * from member", nativeQuery = true)
fun selectAllMembers(): List<Any>
override fun getByMemberNo(memberNo: Long?): Member {
TODO("Not yet implemented")
}
}
일반 Repository도 커스텀 Repo를 상속 받아 줍니다. 현재는 네이티브 쿼리를 이 곳에 작성하고, 오버라이드 한 메서드는 implRepo에서 구현합니다. 물론 이 Repo에서 QueryDsl을 상속 받아도 사용이 가능 합니다.
class MemberRepositoryImpl : PrimaryQueryDslSupport(Member::class.java), MemberRepositoryCustom {
private val qMember = QMember.member
override fun getByMemberNo(memberNo: Long?): Member {
return from(qMember)
.where(
qMember.deleted.isFalse,
qMember.memberNo.eq(memberNo)
)
.fetchOne() ?: throw DomainEntityNotFoundException(memberNo!!, Member::class, "요청하신 사용자는 존재하지 않습니다.")
}
}
우리는 멀티 데이터 소스 문서 에서 QueryDsl을 만들었습니다. impl클래스를 일반 클래스로 선언, 리턴 클래스를 커스텀 한 QuertDslSupport 클래스로 하고 위에서 만든 커스텀Repo를 상속받아줍니다.
QueryDsl을 사용하기 위한 qMember를 선언하고, 커스텀 Repo의 메서드를 override해서 from절의 로직을 작성 해 줍니다. 위 예시는 사용자 중에 삭제가 되지 않았고 회원 번호가 일치하는 회원 한 명을 꺼내온다는 뜻입니다.
자주 쓰는 fetch절
fetchOne( ): 한 개의 특정한 레코드를 조회
fetch( ): 결과를 ‘배열’로 한 개씩 반환 → [name][id] [password]…
fetchAll( ): 결과를 배열로, 한 번에 모두 반환 → [0][name] [id], [1][name] [id] …
fetchCount( ): 결과의 레코드 수를 반환
앞서 var과 val의 차이를 설명하고 var타입의 멤버 프로퍼티는 어디던지 바로 꺼내와 사용할 수 있다고 했습니다.
val dbMember = memberRepository.getByMemberNo(memberNo)
val name = dbMember.name
하지만 위처럼 바로 꺼내오는 것은 엔터티에 private로 선언 한 경우 public(코틀린에서는 생략)으로 변경해야 하기 때문에 권장되지 않고, dataClass와 함수를 이용합니다. Service단을 기술하는 문서에서 더 자세히 다루고, 여기서는 사용사례만 보겠습니다.
예: 사용자 한 명의 삭제여부를 변경하고 싶을 때 함수를 사용하는 경우
fun deleteMember(memberNo: Long) {
val dbMember = memberRepository.getByMemberNo(memberNo)
dbMember.convertDeletedStatus()
}
단! 데이터 클래스 자체는
일 수 없습니다.
dbMember 한 명을 불러와서 미리 정의한 함수를 사용 → 함수의 매개변수로 data class를 전달 해 줍니다.
예: dataClass를 이용하는 경우
fun updateMemberPassword(password: String) { val dbMember = memberRepository.getByMemberNo(updateMemberPasswordIn.memberNo) dbMember.updatePassword( Member.UpdatePasswordForm( password = passwordEncoder.encode(password), ) ) }
엔터티 클래스에서 정의한 updatePassword함수로 레코드를 변경합니다.
fun updatePassword(f: UpdatePasswordForm) {
if (!f.password.isNullOrBlank()) this.password = f.password
}
다음 문서에서는 repo에서 꺼내 온 값을 어떻게 CRUD하는지, 어디서 처리하는지에 대해 Service단을 기술하도록 하겠습니다.