[4] CUD를 담당하는 Command Service

안상철·2022년 8월 16일
0

Kotlin Spring Boot

목록 보기
5/14
post-thumbnail

Command Service는 새로운 레코드를 DB에 삽입하거나 Query Service에서 조회 한 데이터를 조작하는 서비스 입니다. 이 페이지 에서는 멤버의 생성(Create) → 비밀번호 변경(Update) → 회원삭제(Delete) 순으로 진행 해 봅시다.

1. Create

레코드의 생성은 JPA의 기본 기능인 save()를 이용합니다. Front에서 전달해 준 값을 IN DTO 객체로 받아 그대로 DB에 삽입합니다.

1. 컨트롤러

    @Operation(summary = "회원가입")
    @PostMapping("/sign-up")
    fun createMember(@RequestBody signUpIn: SignUpIn): ResponseEntity<SignUpOut> {
        val memberOut = memberCommandService.createMember(signUpIn)
        return ResponseEntity.ok(memberOut)
    }

SignUpIn DTO라는 형태로 화면에서 값을 받아 memberCommandService에서 레코드를 삽입한 뒤, 다시 화면에 SignUpOut객체를 전달 합니다.

2. SignUpIn

data class SignUpIn(
    val memberName: String,
    val memberId: String,
    val password: String,
) {
    fun toEntity(passwordEncoder: PasswordEncoder): Member {
        return Member(
            memberRole = Member.MemberRole.MEMBER.toString(),
            memberName = memberName,
            memberId = memberId,
            initPasswordChange = false,
            password = passwordEncoder.encode(password),
            deleted = false
        )
    }
}

화면에서는 멤버의 이름, 아이디, 비밀번호를 입력해 API에 전달하면 data class내에 정의된 toEntity 메서드를 통해 추가적인 값을 고정으로 넣어주게 합니다.

3. memberCommandService

@Service
@Transactional
class MemberCommandService(

    private val memberRepository: MemberRepository,
    private val passwordEncoder: PasswordEncoder

) {
    fun createMember(signUpIn: SignUpIn): SignUpOut {
        val member: Member = try {
            memberRepository.save(signUpIn.toEntity(passwordEncoder))
        } catch (
            e: DataIntegrityViolationException
        ) {
            val defaultMsg = "계정을 생성할 수 없습니다. 관리자에게 문의 해 주세요"
            val msg = e.message ?: throw RuntimeException(defaultMsg)
            when {
                msg.contains("USER_ID") -> throw IllegalArgumentException("이미 사용중인 아이디 입니다.")
                else -> throw RuntimeException(defaultMsg)
            }
        }
        return SignUpOut.fromEntity(member)
    }
)

Query Service와 달리 Transactional 어노테이션에 readOnly 조건이 붙지 않습니다.

memberRepository.save()에 SignUpIn.toEntity 메서드로 리턴 된 사용자 한 명을 DB에 삽입하고 catch구문에서 예외처리를 해 줍니다. 이 예시에서는 리턴되는 에러 메세지에 constraint ~ USER_ID 구문이 있으면 에러메세지를 전달하게 작성 한 예시입니다.

원래는 memberRepository에 레코드를 삽입하기 전 예외처리 로직을 추가하지만, 중요한 비즈니스 로직이 아닌 경우 위처럼 작성하면 쿼리를 두 번 날릴 필요가 없다는 장점이 있습니다.

우리는 try 구문에서 삽입 한 member 레코드를 다시 SignUpOut DTO 객체로 변환해 화면에 리턴 해 줍니다.

4. signUpOut DTO

data class SignUpOut(

    val name: String,
    val memberId: String
) {
    companion object {
        fun fromEntity(e: Member): SignUpOut {
            return SignUpOut(
                name = e.memberName(),
                memberId = e.memberId()
            )
        }
    }
}

signUpIn 객체와 달리 fromEntity 메서드를 통해 전달받은 member인자에 해당하는 이름와 아이디를 화면에 넘겨줄 수 있도록 합니다.

2. Update

1. controller

    @Operation(summary = "비밀번호 변경")
    @PatchMapping("/password/{oid}")
    fun updateMemberPassword(
        @PathVariable("oid") oid: Long,
        @RequestBody updateMemberPasswordIn: UpdateMemberPasswordIn
    ): ResponseEntity<Nothing> {
        // SecurityUtil.checkMemberOid(oid)
        ResponseEntity.ok(memberCommandService.updateMemberPassword(updateMemberPasswordIn))
        return ResponseEntity.noContent().build()
    }

비밀번호 변경을 통해 업데이트를 알아봅시다. 컨트롤러 작성법은 👉🏻 여기

컨트롤러의 PatchMapping은 보통 하나의 값을 업데이트 할 때 사용하고, 두 가지 이상의 값을 변경 할 때는 PutMapping 어노테이션을 붙여줍니다.

pathVariable은 클라이언트에게 받는 데이터가 특정 리소스의 식별자인 경우 사용하고, 필드별 조회조건만 있을 경우는 RequsetParam을 사용 해 줍니다.

또한 이번에는 비밀번호를 변경하고 API의 리턴타입을 ResponsEntity으로 두었습니다. ResponseEntity는 화면에 Http 상태코드와 함께 서버 통신여부를 알려주는 클래스이고 Nothing은 204상태코드와 함께 리턴 할 값이 없음을 나타냅니다. 변경은 성공적으로 되었으나 딱히 클라이언트에게 전달 해 줄 것은 없다 라는 뜻입니다. return 구문에는 ResponseEntity.noContent().build()를 써줍니다.

2. memberCommandService

      fun updateMemberPassword(updateMemberPasswordIn: UpdateMemberPasswordIn) {
        val dbMember = memberRepository.getByOid(updateMemberPasswordIn.oid)
        dbMember.updatePassword(
            Member.UpdatePasswordForm(
                password = passwordEncoder.encode(updateMemberPasswordIn.password),
                initPasswordChange = true
            )
        )
    }
  

회원가입 예시와는 다르게 memberRepository에서 비밀번호를 변경하고자 하는 사용자를 먼저 찾아줍니다.

3. 엔티티 클래스 메서드

엔티티에 정의된 멤버 프로퍼티가 private이 아니라면 service 클래스에서도 변경할 수 있지만 좋은 방법이 아닙니다. 가능하면 해당 필드의 값 변경이 필요할 때, 목적이나 의도에 맞는 메서드를 엔티티 클래스에 작성하고 서비스에서는 불러와 사용하는 방법을 권장합니다.

  data class UpdatePasswordForm(
        val password: String? = null,
        val initPasswordChange: Boolean
    )
    
fun updatePassword(f: UpdatePasswordForm) {
        if (!f.password.isNullOrBlank()) this.password = f.password
        this.initPasswordChange = true
    }

member Entity 클래스 내에 작성한 data class와 비밀번호 변경 메서드 입니다. data class로 DTO처럼 레코드 업데이트에 필요한 값을 객체로 정의 해 주고, 메서드에서는 이 객체를 받아 값을 변경 해 줍니다.

    fun updateMemberPassword(updateMemberPasswordIn: UpdateMemberPasswordIn) {
        val dbMember = memberRepository.getByOid(updateMemberPasswordIn.oid)
        dbMember.updatePassword(
            Member.UpdatePasswordForm(
                password = passwordEncoder.encode(updateMemberPasswordIn.password),
                initPasswordChange = true
            )
        )
    }  

다시 서비스 단으로 돌아와서 memberOid를 통해 조회 한 사용자 한 명은 DTO형태가 아니라 엔티티 그 자체이므로, 우리가 위에서 작성 한 업데이트 메서드를 불러와 사용할 수 있습니다.

updatePassword에는 클라이언트가 전달 해 준 비밀번호를 passwordEncoder를 통해 암호화하고, 이 사용자가 비밀번호를 변경 했다는 initPasswordChange라는 값을 true로 함께 전달 해 주면 업데이트가 진행됩니다.

3. Delete

    @Operation(summary = "회원 삭제")
    @DeleteMapping("/{memberOid}")
    fun deleteMember(
        @PathVariable("memberOid") memberOid: Long
    ): ResponseEntity<Nothing> {
        memberCommandService.deleteMember(memberOid)
        return ResponseEntity.noContent().build()
    }

delete는 Delete 어노테이션을 붙여서 이 API를 레코드를 삭제하는 API임을 알려줍니다.

1. JPA의 delete(), deleteAll().. 등등을 통해 레코드 자체를 삭제

JPA의 delete ~는 실제 레코드를 db에서 삭제하는 메서드 입니다. delete()의 인자로는 엔티티 자체를 넣어주면되는데

  val member = memberRepository.findById(memberId)
memberRepository.delete(member)

위처럼 delete안에 특정 엔티티를 넣으면 DB에서 삭제됩니다. deleteAll은 엔티티 리스트를 넣어 한번에 다건의 레코드를 삭제합니다.

참고

  • deleteAll() : for문을 돌면서 다건의 데이터를 하나하나 delete쿼리를 날려 삭제한다.
  • deleteAllInBatch(): for문을 돌지 않고 한번에 다건의 데이터를 삭제한다.
  • deleteInBatch(): delete와 deleteAllInBatch와는 다르게 엔티티에 값이 있던지 없던지 확인하지 않고 한번에 다건의 데이터를 삭제한다.

2. flag 컬럼만 업데이트

flag컬럼은 여부컬럼이라고 하는 사용/미사용 등의 의미를 가진 속성을 말합니다. 회원의 삭제여부 등을 말합니다. 사용자 엔티티에 deleted등의 컬럼으로 현재 활동중인지 아닌지를 판단한다면 deleted를 true로 변경하는 업데이트 메서드를 만들고, 다른 모든 비즈니스 로직에서 사용자를 조회할 때는 deleted가 false인 사용자만 조회하면 됩니다. 값 업데이트는 2번 Update와 같기 때문에 따로 예시는 없습니다.

delete방법은 flag 컬럼 업데이트 방법을 선호하는데 그 이유는 1번처럼 실제 데이터를 삭제 할 경우 서로 참조관계나 제약조건에 의해 삭제가 되지 않거나, 에러나 나거나 값 한 개를 삭제하기 위한 추가 로직이 필요할 수 있기 때문입니다. 업데이트 방식은 상대적으로 제약조건이나 참조관계를 덜 고려해도 되기 때문에 편합니다.

대신 참조 관계가 별로 없는 기본 엔터티나 실제로 db에서 삭제해야 될 경우는 delete메서드를 사용하고, 그렇지 않다면 업데이트 방식 + 사용 여부가 true인 레코드만 조회하는 방법을 선택하면 됩니다.

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

0개의 댓글