본문 바로가기

Spring + Kotlin API Server 만들기 (4) : Service , TDD

Service 설계

Device API가 제공해야 할 Service를 분석하면 다음과 같다.

  • Device list를 조회 할 수 있어야 한다.
  • 특정 Device를 조회 할 수 있어야 한다.
  • 특정 Device의 value를 수정 할 수 있어야 한다.
  • 특정 Device의 command를 수정 할 수 있어야 한다.

이를 토대로 Service를 작성하면 다음과 같다.

package com.studuy.study.device

import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class DeviceService(
    private val deviceRepository: DeviceRepository
) {
    fun getAllDevice(): Iterable<Device>{
        return  deviceRepository.findAll();
    }

    @Transactional
    fun createDevice(): Device{
        val device = Device()
        return deviceRepository.save(device)
    }

    fun getDevice(deviceId: Long): Device{
        return deviceRepository.findById(deviceId).orElseThrow()
    }

    @Transactional
    fun updateDeviceValue(deviceId: Long, value: String): Device{
        val device = deviceRepository.findById(deviceId).orElseThrow()
        device.value = value
        return  deviceRepository.save(device)
    }

    @Transactional
    fun updateDeviceCommand(deviceId: Long, command: String): Device{
        val device = deviceRepository.findById(deviceId).orElseThrow()
        device.command = command
        return deviceRepository.save(device)
    }
}
  • 설명
    • private val deviceRepository: DeviceRepository
      • 생성자에 type을 적어 놓으면 Spring이 auto wired해서 dependency injection을 해준다.
    • @Service
      • Spring에서 해당 class가 Service에 해당하는 걸 알려준다.
    • @Transactional
      • Spring에서 현재 스코프가 영속성 컨텍스트에 해당 한다는 것을 알려준다.
      • readOnly가 true로 설정 돼 있으면 컨텍스트가 끝났을 때 dirty checking을 하지 않아 데이터를 수정하더라도 반영이 안된다.
      • class에 @Transactional(readOnly = true)를 하면 class내부의 모든 함수들이 읽기 전용으로 데이터를 불러온다고 설정된다.
      • 위 상태에서 특정 함수에 @Transactional을 붙여주면 readOnly의 defalut값이 false이기 때문에 dirty checking을 하며, 데이터 수정이 일어난다는 걸 의미한다.

 

TDD(Test Driven Development)

TDD란 한국어로 하면 기능 주도 개발이란 뜻으로 한개의 기능을 개발 할 때마다 테스트를 하고, 검증하면서 개발하는 방식이다. Spring에서는 프로젝트를 생성하면 TDD를 염두에 두고 생성된다.

Service Test Code 작성

지금까지는 프로젝트의 main/kotlin/com.study.study 에서 코드가 작성되어 있다면 test 코드는 test/kotlin/com.study.study 에서 작성된다. DeviceService test코드를 다음과 같이 작성한다.

package com.studuy.study.device

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.Mockito.any
import org.mockito.Mockito.`when`
import org.assertj.core.api.Assertions.assertThat
import org.mockito.InjectMocks
import org.springframework.data.repository.findByIdOrNull
import java.util.*

@ExtendWith(MockitoExtension::class)
internal class DeviceServiceTest {

    @Mock
    private lateinit var deviceRepository: DeviceRepository

    @InjectMocks
    private lateinit var deviceService: DeviceService

    @Test
    fun `test create device`() {
        //given
        val device = Device()
        device.id = 1

        //when
        `when`(deviceRepository.save(any(Device::class.java))).thenReturn(device)

        //do
        val createdDevice = deviceService.createDevice()

        //then
        assertThat(createdDevice).isNotNull
        assertThat(createdDevice.id).isNotNull
    }

    @Test
    fun `test update value`(){
        //given
        val value = "223142"
        val device = Device()
        device.id = 1

        //when
        `when`(deviceRepository.findById(device.id!!)).thenReturn(Optional.of(device))
        `when`(deviceRepository.save(device)).thenReturn(device)

        //do
        val updatedDevice = deviceService.updateDeviceValue(device.id!!, value)

        //then
        assertThat(updatedDevice.value).isEqualTo(value)
    }

    @Test
    fun `test update command`(){
        //given
        val command = "test command"
        val device = Device()
        device.id = 1

        //when
        `when`(deviceRepository.findById(device.id!!)).thenReturn(Optional.of(device))
        `when`(deviceRepository.save(device)).thenReturn(device)

        //do
        val updatedDevice = deviceService.updateDeviceCommand(device.id!!, command)

        //then
        assertThat(updatedDevice.command).isEqualTo(command)

    }
}
  • 설명
    • @Mock
      • class를 가짜로 dependency injection 해준다. injection class는 interface만 동일하며, 내부 적으로는 when으로 어떤 동작을 할지 알려주지 않으면 작동하지 않는다.
    • @InjectMocks
      • 실제로 테스트를 진행할 class이다.
    • @Test
      • 현재 함수가 test를 위한 함수라는 걸 명시(사실 많은 기능이 wrapper되는 것이다.)
    • when
      • mocking된 class가 특정 행위를 할때 반환해야 하는 값을 알려준다.
    • assertThat
      • 실행된 결과가 의도한 결과와 맞는지 확인해준다. 다르면 error를 throw하고, Test는 fail 된다.

위와 같이 작성한뒤 test를 다음과 같이 실행하면 잘 통과하는걸 확인할 수 있다.

test 성공

다음에는 Controller와 Rest API에 대해서 포스팅 할 예정이다.