[2] Domain - 엔티티 작성과 RepositoryCustom

안상철·2022년 7월 15일
0

Kotlin Spring Boot

목록 보기
3/14
post-thumbnail

이번 페이지에서는 db설정과 엔티티 정의, repository에서 필요한 레코드를 select하는 방법 까지를 기술합니다.

1. yml에 DB정보 기재 ~ Entity 정의

yml에 DB정보를 기입하고 엔티티를 정의하는 방법은 구글에도 사용 예시가 많이 나와있을 뿐더러, 어디던 예외 없이 공통 사항이기 때문에 생략하겠습니다. 코틀린에서의 사용법을 주로 기술하겠습니다.

1. 모든 엔티티가 공통 컬럼을 가지고 있어야 할 때

@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
    }
}

공통 컬럼을 생성 할 때 자동으로 생성되게 할 수도 있습니다.

2. 코틀린에서의 엔터티 정의

1번과 이어지는 내용입니다. Java와 기본구조는 동일하지만 코틀린에서는 게터/세터를 어노테이션 없이 함수형으로 정의합니다.

  • 게터: 코틀린에서 function을 뜻하는 fun으로 엔티티의 멤버프로퍼티(자바의 Field)를 가져올 수 있습니다.
  • 세터: 세터는 게터처럼 함수를 사용하지 않고 var과 val로 구분합니다.
  • var: 읽기 / 쓰기 모두 가능한 타입
  • val: 읽기만 가능한 타입

따라서 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("일반 사용자"),
    }
}

3. repository를 커스텀해서 사용하기

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( ): 결과의 레코드 수를 반환

4. Service단에서 엔터티의 세터를 사용하는 사례

앞서 var과 val의 차이를 설명하고 var타입의 멤버 프로퍼티는 어디던지 바로 꺼내와 사용할 수 있다고 했습니다.

val dbMember = memberRepository.getByMemberNo(memberNo)
val name = dbMember.name

하지만 위처럼 바로 꺼내오는 것은 엔터티에 private로 선언 한 경우 public(코틀린에서는 생략)으로 변경해야 하기 때문에 권장되지 않고, dataClass와 함수를 이용합니다. Service단을 기술하는 문서에서 더 자세히 다루고, 여기서는 사용사례만 보겠습니다.

1. dbMember를 한명 꺼내 온 다음, 멤버 엔터티에 정의한 convertDeletedStatus()함수를 불러다 씁니다.

예: 사용자 한 명의 삭제여부를 변경하고 싶을 때 함수를 사용하는 경우

fun deleteMember(memberNo: Long) {
        val dbMember = memberRepository.getByMemberNo(memberNo)
        dbMember.convertDeletedStatus()
    }

2. 코틀린에서 data class는 말 그대로 data를 담는 클래스이고 생성자, 게터세터, equals, hashCode까지 모두 가진 만능 클래스입니다.

단! 데이터 클래스 자체는

  • abstract(자바의 Abstract)
  • open(자바의 Interface와 비슷)
  • sealed(enum과 비슷하지만 enum과 달리 다른 클래스가 상속받아 값을 수정할 수 있다.)
  • inner(하나의 클래스 안에 포함된 클래스, 바깥 클래스의 지역변수를 사용할 수 있다.)

일 수 없습니다.

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단을 기술하도록 하겠습니다.

profile
웹 개발자(FE / BE) anna입니다.

0개의 댓글