veluxer62 / veluxer62.github.io

veluxer's blog
http://veluxer62.github.io
MIT License
2 stars 0 forks source link

3월 개발일지 #661

Closed veluxer62 closed 1 year ago

veluxer62 commented 1 year ago

Motivation

Suggestion

1

veluxer62 commented 1 year ago

@Async로 처리한 비동기 코드를 테스트할 때, 테스트에서는 EnableAsync 설정을 빼줌으로써 비동기로 동작하지 않게 해서 동기적으로 테스트를 수행하도록 하는 방법이 있다.

기능적인부분을 테스트함에 있어 동기냐 비동기냐가 중요한게 아니기에 이러한 테스트 설정은 좋은듯하다.

https://stackoverflow.com/questions/42438862/junit-testing-a-spring-async-void-service-method

다만 스텍오버플로우에서 적혀있듯이 profile 설정을 통해 하는 것은 운영코드를 테스트를 위해 설정하는 격이 되므로 별로인것으로 보임 그래서 아래와 같이 excutor를 오버라이딩 해버리면 됨

@TestConfiguration
class TestConfig {
    @Bean
    @Primary
    fun taskExecutor(): TaskExecutor {
        return SyncTaskExecutor()
    }
}
veluxer62 commented 1 year ago

JIRA에서 담당자 할당시 자동으로 라벨링해주는 설정을 적용했다. 진즉에 할껄

veluxer62 commented 1 year ago

소나큐브 뱃지 달기

https://stackoverflow.com/questions/49066014/how-to-add-sonarcloud-badge-on-github

veluxer62 commented 1 year ago

기능테스트 시 데이터베이스 이슈사항

기능테스트와 Respository 테스트가 같은 DB를 바라보는 상황이 생김. 이유는 Postgresql 방언으로 인메모리 디비를 못씀

해결방법

  1. 데이터베이스 분리
  2. Repository 테스트 전 데이터 삭제
  3. 데이터 중복이 생기지 않게 테스트

1번이 가장 쉽고 2, 3번 순서로 어려움. 대신 3번이 어떻게보면 가장 이상적임

veluxer62 commented 1 year ago

circle ci에서 동일한 port 서비스를 올리면 name 설정을 통해 호스트를 바꿀 수 있다.

https://support.circleci.com/hc/en-us/articles/360007186173-Port-conflicts-with-service-containers-on-Docker-executor

veluxer62 commented 1 year ago

mockk에서 relexed 를 매번 주지 않고 setting.properties에서 글로벌설정을 할 수 있다.

https://mockk.io/#settings-file

그나저나 relaxUnitFun는 언제 생겼지....

relexed가 기본값이 false인 이유는 ture는 위험하기 때문이라고... https://stackoverflow.com/questions/73233826/what-does-relaxed-true-do-in-mockk

veluxer62 commented 1 year ago

단위 테스트 SUT 설정 방법 변경 제안

기본의 단위테스트 설정방법 보다 나은 방법이 있어서 단위 테스트 SUT(System under test) 설정 방법을 소개하고자 합니다.

아래 코드의 문제점이 무엇일까요?

class AdminAnnouncementMessageFacadeTest : FunSpec() {
    private val adminAnnouncementMessageHistoryService: AdminAnnouncementMessageHistoryService = mockk()
    private val orderableVendorService: OrderableVendorService = mockk()
    private val storeService: StoreService = mockk()
    private val chatClient: ChatClient = mockk()
    private val adminAnnouncementMessageFacade: AdminAnnouncementMessageFacade = AdminAnnouncementMessageFacade(
        adminAnnouncementMessageHistoryService,
        orderableVendorService,
        storeService,
        chatClient,
    )

    //...테스트
}

하나의 테스트 케이스가 다른 테스트 케이스에 영향을 미친다는 것입니다. 아래 코드와 같이 test2는 every 설정을 하지 않았음에도 값이 반환됨을 볼 수 있습니다.

val histories = AdminAnnouncementMessageHistoryFactory().produceMany().take(3).toList()

test("test") {
    every { adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged()) } returns PageImpl(histories)

    val actual = adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged())
    println(actual.totalElements) // 3
}

test("test2") {
    val actual = adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged())
    println(actual.totalElements) // 3
}

물론 공통설정을 목적으로 의도적으로 위와 같이 작성할 순 있습니다. 하지만 그렇게 하고 싶다면 beforeEach에서 정의를 하는게 맞다고 생각합니다.

그래서 위 문제를 해결하기 위해 우리는 아래와 같이 SUT를 정의했었는데요.

class AdminAnnouncementMessageFacadeTest : FunSpec() {
    private lateinit var adminAnnouncementMessageHistoryService: AdminAnnouncementMessageHistoryService
    private lateinit var orderableVendorService: OrderableVendorService
    private lateinit var storeService: StoreService
    private lateinit var chatClient: ChatClient
    private lateinit var adminAnnouncementMessageFacade: AdminAnnouncementMessageFacade

    override suspend fun beforeEach(testCase: TestCase) {
        adminAnnouncementMessageHistoryService = mockk()
        orderableVendorService = mockk()
        storeService = mockk()
        storeService = mockk()
        chatClient = mockk()
        adminAnnouncementMessageFacade = AdminAnnouncementMessageFacade(
            adminAnnouncementMessageHistoryService,
            orderableVendorService,
            storeService,
            chatClient,
        )
    }
}

이렇게 하면 매 테스트마다 mocking을 초기화 하기 때문에 위에서 정의한 테스트 코드는 실패하게 됩니다. image

val histories = AdminAnnouncementMessageHistoryFactory().produceMany().take(3).toList()

test("test") {
    every { adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged()) } returns PageImpl(histories)

    val actual = adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged())
    println(actual.totalElements) // 3
}

test("test2") {
    val actual = adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged())
    println(actual.totalElements) // error
}

no answer found for: AdminAnnouncementMessageHistoryService(#7).getHistories(INSTANCE) 즉, mocking이 되지 않았으니 응답값을 알 수 없는 것입니다.

하지만 위 방법에는 몇가지 문제점이 있습니다.

lateinit var를 사용한다.SUT는 한번 정의하면 재할당할 필요가 없습니다. 그럼에도 불구하고 var를 사용하기 때문에 아래와 같이 테스트 코드에서 재할당할 가능성이 존재하게 됩니다. 물론 누군가 테스트를 망가뜨릴 목적으로 하지 않는한 거의 발생하지 않겠지만 lateinit var는 좋지 못한 패턴임이 분명합니다.

매 테스트 코드마다 객체를 새로 생성한다.단위 테스트를 수행할 SUT 객체는 그렇게 무겁지는 않습니다. 하지만 이런 코드가 우리는 엄청 많죠. 매 단위테스트에서 객체를 테스트 케이스마다 재생성한다면 분명 메모리 사용량이 그렇지 않은 코드보다는 많아질 것입니다.라고 예상했지만 클래스 하나만 테스트 했을땐 차이가 거의 없네요 ㅠ

적용전 image

적용후 image

아무튼 위와같은 이유로 아래와 같이 단위테스트를 위한 SUT 정의 방법을 변경하는 것을 제안해보고자 합니다.

class AdminAnnouncementMessageFacadeTest : FunSpec() {
    private val adminAnnouncementMessageHistoryService: AdminAnnouncementMessageHistoryService = mockk()
    private val orderableVendorService: OrderableVendorService = mockk()
    private val storeService: StoreService = mockk()
    private val chatClient: ChatClient = mockk()
    private val adminAnnouncementMessageFacade: AdminAnnouncementMessageFacade = AdminAnnouncementMessageFacade(
        adminAnnouncementMessageHistoryService,
        orderableVendorService,
        storeService,
        chatClient,
    )

    override suspend fun beforeEach(testCase: TestCase) {
        clearAllMocks()
    }

    init {
        val histories = AdminAnnouncementMessageHistoryFactory().produceMany().take(3).toList()

        test("test") {
            every { adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged()) } returns PageImpl(histories)

            val actual = adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged())
            println(actual.totalElements) // 3
        }

        test("test1") {
            val actual = adminAnnouncementMessageHistoryService.getHistories(Pageable.unpaged())
            println(actual.totalElements) // error
        }
    }
}
veluxer62 commented 1 year ago

https://www.mimul.com/blog/cognitive-biases-in-programming/?utm_medium=social&utm_source=gaerae.com&utm_campaign=%EA%B0%9C%EB%B0%9C%EC%9E%90%EC%8A%A4%EB%9F%BD%EB%8B%A4

veluxer62 commented 1 year ago

피처 토글 오픈소스

https://github.com/Unleash/unleash

veluxer62 commented 1 year ago

기능테스트에서 fixture로 facade나 service를 이용하여 entity를 조회후 사용할 때 lazy loading 에러를 만날 수 있다.

TX 밖에서 lazy loading을 쓰고 싶은경우 아래와 같이 application.yml에 설정하면된다.

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

다만 안티패턴이니 운영코드에서는 사용하지말것

https://www.baeldung.com/hibernate-lazy-loading-workaround

하지만 이렇게 기능테스트를 하면 문제가 또 생기는데 바로 운영코드에서 잘못된 사용으로 lateinit error가 발생하는 것을 테스트코드에서 못잡는다는 것이다.... 결국 해당 옵션은 끄고 Facade 내에서는 lazy loading이 되도록 하고 테스트에서는 조회시 오류가 나지 않도록 반환 전에 선언해서 loading을 강제로 시키는 형식으로 하기로함

veluxer62 commented 1 year ago

테스트를 위해 DGS CustomGraphQLClient를 사용하는 경우 아래와 같이 input 값에 날짜형식값을 넣으면 에러가 발생한다.

val input = CreateOrderSheetInput(
      orderableVendorId = orderableVendor.id!!,
      requestedDeliveryDate = LocalDate.of(2023, 1, 1),
      additionalRequests = "요청 사항",
      products = listOf(
          CreateOrderSheetProductInput(
              orderableVendorProductId = product.id,
              count = 10.toDouble(),
          ),
      ),
  )
  val variables = mapOf("input" to input)

  // When
  val actual = clientBuilder.token(fixture.점주_토큰_생성(manager.id!!)).build()
      .executeQuery(mutation, variables)
      .extractValueAsObject("createOrderSheet", typeRef<CreateOrderSheet>())
Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.netflix.graphql.dgs.client.Request["variables"]->java.util.Collections$SingletonMap["input"]->com.spoqa.cart.generated.types.CreateOrderSheetInput["requestedDeliveryDate"])
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.netflix.graphql.dgs.client.Request["variables"]->java.util.Collections$SingletonMap["input"]->com.spoqa.cart.generated.types.CreateOrderSheetInput["requestedDeliveryDate"])
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
    at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1300)
    at com.fasterxml.jackson.databind.ser.impl.UnsupportedTypeSerializer.serialize(UnsupportedTypeSerializer.java:35)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFields(MapSerializer.java:808)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeWithoutTypeInfo(MapSerializer.java:764)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:720)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:35)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:774)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
    at com.fasterxml.jackson.databind.ObjectMapper._writeValueAndClose(ObjectMapper.java:4568)
    at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3821)
    at com.netflix.graphql.dgs.client.CustomGraphQLClient.executeQuery(CustomGraphQLClient.kt:34)
    at com.netflix.graphql.dgs.client.CustomGraphQLClient.executeQuery(CustomGraphQLClient.kt:30)
    at com.spoqa.cart.application.presentation.graphql.orderSheet.OrderSheetMutationTest$1$1.invokeSuspend(OrderSheetMutationTest.kt:70)
    at com.spoqa.cart.application.presentation.graphql.orderSheet.OrderSheetMutationTest$1$1.invoke(OrderSheetMutationTest.kt)
    at com.spoqa.cart.application.presentation.graphql.orderSheet.OrderSheetMutationTest$1$1.invoke(OrderSheetMutationTest.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$innerExecute$1.invokeSuspend(TestCaseExecutor.kt:83)
    at io.kotest.engine.test.TestCaseExecutor$execute$innerExecute$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$innerExecute$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineDebugProbeInterceptor.intercept(CoroutineDebugProbeInterceptor.kt:29)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.InvocationTimeoutInterceptor$intercept$3.invokeSuspend(InvocationTimeoutInterceptor.kt:43)
    at io.kotest.engine.test.interceptors.InvocationTimeoutInterceptor$intercept$3.invoke(InvocationTimeoutInterceptor.kt)
    at io.kotest.engine.test.interceptors.InvocationTimeoutInterceptor$intercept$3.invoke(InvocationTimeoutInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturnIgnoreTimeout(Undispatched.kt:100)
    at kotlinx.coroutines.TimeoutKt.setupTimeout(Timeout.kt:146)
    at kotlinx.coroutines.TimeoutKt.withTimeoutOrNull(Timeout.kt:103)
    at io.kotest.engine.test.interceptors.InvocationTimeoutInterceptor.intercept(InvocationTimeoutInterceptor.kt:42)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2$3.invokeSuspend(TestInvocationInterceptor.kt:36)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2$3.invoke(TestInvocationInterceptor.kt)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2$3.invoke(TestInvocationInterceptor.kt)
    at io.kotest.mpp.ReplayKt.replay(replay.kt:18)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2.invokeSuspend(TestInvocationInterceptor.kt:31)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2.invoke(TestInvocationInterceptor.kt)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2.invoke(TestInvocationInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
    at io.kotest.engine.test.TestInvocationInterceptor.intercept(TestInvocationInterceptor.kt:30)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TimeoutInterceptor.intercept(TimeoutInterceptor.kt:33)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.BlockedThreadTimeoutInterceptor.intercept(BlockedThreadTimeoutInterceptor.kt:74)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineLoggingInterceptor.intercept(CoroutineLoggingInterceptor.kt:30)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.SoftAssertInterceptor.intercept(SoftAssertInterceptor.kt:27)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.AssertionModeInterceptor.intercept(AssertionModeInterceptor.kt:25)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.LifecycleInterceptor.intercept(LifecycleInterceptor.kt:51)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.EnabledCheckInterceptor.intercept(EnabledCheckInterceptor.kt:31)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invokeSuspend(TestCaseExtensionInterceptor.kt:24)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invoke(TestCaseExtensionInterceptor.kt)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invoke(TestCaseExtensionInterceptor.kt)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invokeSuspend(TestExtensions.kt:149)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
    at io.kotest.extensions.spring.SpringTestExtension.intercept(SpringTestExtension.kt:73)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invokeSuspend(TestExtensions.kt:146)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
    at io.kotest.engine.test.TestExtensions.intercept(TestExtensions.kt:154)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor.intercept(TestCaseExtensionInterceptor.kt:24)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invokeSuspend(CoroutineErrorCollectorInterceptor.kt:28)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invoke(CoroutineErrorCollectorInterceptor.kt)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invoke(CoroutineErrorCollectorInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor.intercept(CoroutineErrorCollectorInterceptor.kt:27)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invokeSuspend(coroutineDispatcherFactoryInterceptor.kt:57)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invoke(coroutineDispatcherFactoryInterceptor.kt)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invoke(coroutineDispatcherFactoryInterceptor.kt)
    at io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory$withDispatcher$4.invokeSuspend(FixedThreadCoroutineDispatcherFactory.kt:53)
    at io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory$withDispatcher$4.invoke(FixedThreadCoroutineDispatcherFactory.kt)
    at io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory$withDispatcher$4.invoke(FixedThreadCoroutineDispatcherFactory.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
    at io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory.withDispatcher(FixedThreadCoroutineDispatcherFactory.kt:52)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor.intercept(coroutineDispatcherFactoryInterceptor.kt:56)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.SupervisorScopeInterceptor$intercept$2.invokeSuspend(SupervisorScopeInterceptor.kt:23)
    at io.kotest.engine.test.interceptors.SupervisorScopeInterceptor$intercept$2.invoke(SupervisorScopeInterceptor.kt)
    at io.kotest.engine.test.interceptors.SupervisorScopeInterceptor$intercept$2.invoke(SupervisorScopeInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.SupervisorKt.supervisorScope(Supervisor.kt:61)
    at io.kotest.engine.test.interceptors.SupervisorScopeInterceptor.intercept(SupervisorScopeInterceptor.kt:22)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.InvocationCountCheckInterceptor.intercept(InvocationCountCheckInterceptor.kt:24)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TestFinishedInterceptor.intercept(TestFinishedInterceptor.kt:21)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TestNameContextInterceptor$intercept$2.invokeSuspend(TestPathContextInterceptor.kt:35)
    at io.kotest.engine.test.interceptors.TestNameContextInterceptor$intercept$2.invoke(TestPathContextInterceptor.kt)
    at io.kotest.engine.test.interceptors.TestNameContextInterceptor$intercept$2.invoke(TestPathContextInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
    at io.kotest.engine.test.interceptors.TestNameContextInterceptor.intercept(TestPathContextInterceptor.kt:34)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TestPathContextInterceptor$intercept$2.invokeSuspend(TestPathContextInterceptor.kt:20)
    at io.kotest.engine.test.interceptors.TestPathContextInterceptor$intercept$2.invoke(TestPathContextInterceptor.kt)
    at io.kotest.engine.test.interceptors.TestPathContextInterceptor$intercept$2.invoke(TestPathContextInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
    at io.kotest.engine.test.interceptors.TestPathContextInterceptor.intercept(TestPathContextInterceptor.kt:19)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor.execute(TestCaseExecutor.kt:93)
    at io.kotest.engine.spec.runners.SingleInstanceSpecRunner.runTest(SingleInstanceSpecRunner.kt:121)
    at io.kotest.engine.spec.runners.SingleInstanceSpecRunner.access$runTest(SingleInstanceSpecRunner.kt:31)
    at io.kotest.engine.spec.runners.SingleInstanceSpecRunner$SingleInstanceTestScope.registerTestCase(SingleInstanceSpecRunner.kt:92)
    at io.kotest.engine.test.scopes.DuplicateNameHandlingTestScope.registerTestCase(DuplicateNameHandlingTestScope.kt:30)
    at io.kotest.engine.test.scopes.TestScopeWithCoroutineContext.registerTestCase(scopes.kt)
    at io.kotest.core.spec.style.scopes.AbstractContainerScope.registerTestCase$suspendImpl(ContainerScope.kt:213)
    at io.kotest.core.spec.style.scopes.AbstractContainerScope.registerTestCase(ContainerScope.kt)
    at io.kotest.core.spec.style.scopes.AbstractContainerScope.registerTestCase$suspendImpl(ContainerScope.kt:213)
    at io.kotest.core.spec.style.scopes.AbstractContainerScope.registerTestCase(ContainerScope.kt)
    at io.kotest.core.spec.style.scopes.ContainerScope$DefaultImpls.registerTest(ContainerScope.kt:49)
    at io.kotest.core.spec.style.scopes.AbstractContainerScope.registerTest(ContainerScope.kt:204)
    at io.kotest.core.spec.style.scopes.ContainerScope$DefaultImpls.registerTest(ContainerScope.kt:76)
    at io.kotest.core.spec.style.scopes.AbstractContainerScope.registerTest(ContainerScope.kt:204)
    at io.kotest.core.spec.style.scopes.FunSpecContainerScope.test(FunSpecContainerScope.kt:95)
    at com.spoqa.cart.application.presentation.graphql.orderSheet.OrderSheetMutationTest$1.invokeSuspend(OrderSheetMutationTest.kt:51)
    at com.spoqa.cart.application.presentation.graphql.orderSheet.OrderSheetMutationTest$1.invoke(OrderSheetMutationTest.kt)
    at com.spoqa.cart.application.presentation.graphql.orderSheet.OrderSheetMutationTest$1.invoke(OrderSheetMutationTest.kt)
    at io.kotest.core.spec.style.scopes.FunSpecRootScope$context$1.invokeSuspend(FunSpecRootScope.kt:20)
    at io.kotest.core.spec.style.scopes.FunSpecRootScope$context$1.invoke(FunSpecRootScope.kt)
    at io.kotest.core.spec.style.scopes.FunSpecRootScope$context$1.invoke(FunSpecRootScope.kt)
    at io.kotest.core.spec.style.scopes.RootScopeKt$addTest$1.invokeSuspend(RootScope.kt:36)
    at io.kotest.core.spec.style.scopes.RootScopeKt$addTest$1.invoke(RootScope.kt)
    at io.kotest.core.spec.style.scopes.RootScopeKt$addTest$1.invoke(RootScope.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$innerExecute$1.invokeSuspend(TestCaseExecutor.kt:83)
    at io.kotest.engine.test.TestCaseExecutor$execute$innerExecute$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$innerExecute$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineDebugProbeInterceptor.intercept(CoroutineDebugProbeInterceptor.kt:29)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.InvocationTimeoutInterceptor.intercept(InvocationTimeoutInterceptor.kt:28)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2$3.invokeSuspend(TestInvocationInterceptor.kt:36)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2$3.invoke(TestInvocationInterceptor.kt)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2$3.invoke(TestInvocationInterceptor.kt)
    at io.kotest.mpp.ReplayKt.replay(replay.kt:18)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2.invokeSuspend(TestInvocationInterceptor.kt:31)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2.invoke(TestInvocationInterceptor.kt)
    at io.kotest.engine.test.TestInvocationInterceptor$intercept$2.invoke(TestInvocationInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
    at io.kotest.engine.test.TestInvocationInterceptor.intercept(TestInvocationInterceptor.kt:30)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TimeoutInterceptor.intercept(TimeoutInterceptor.kt:33)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.BlockedThreadTimeoutInterceptor.intercept(BlockedThreadTimeoutInterceptor.kt:74)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineLoggingInterceptor.intercept(CoroutineLoggingInterceptor.kt:30)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.SoftAssertInterceptor.intercept(SoftAssertInterceptor.kt:26)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.AssertionModeInterceptor.intercept(AssertionModeInterceptor.kt:24)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.LifecycleInterceptor.intercept(LifecycleInterceptor.kt:51)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.EnabledCheckInterceptor.intercept(EnabledCheckInterceptor.kt:31)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invokeSuspend(TestCaseExtensionInterceptor.kt:24)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invoke(TestCaseExtensionInterceptor.kt)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor$intercept$2.invoke(TestCaseExtensionInterceptor.kt)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invokeSuspend(TestExtensions.kt:149)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1$1.invoke(TestExtensions.kt)
    at io.kotest.extensions.spring.SpringTestExtension.intercept(SpringTestExtension.kt:73)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invokeSuspend(TestExtensions.kt:146)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
    at io.kotest.engine.test.TestExtensions$intercept$execute$1$1.invoke(TestExtensions.kt)
    at io.kotest.engine.test.TestExtensions.intercept(TestExtensions.kt:154)
    at io.kotest.engine.test.interceptors.TestCaseExtensionInterceptor.intercept(TestCaseExtensionInterceptor.kt:24)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invokeSuspend(CoroutineErrorCollectorInterceptor.kt:28)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invoke(CoroutineErrorCollectorInterceptor.kt)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor$intercept$3.invoke(CoroutineErrorCollectorInterceptor.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:166)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
    at io.kotest.engine.test.interceptors.CoroutineErrorCollectorInterceptor.intercept(CoroutineErrorCollectorInterceptor.kt:27)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:92)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invoke(TestCaseExecutor.kt)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invokeSuspend(coroutineDispatcherFactoryInterceptor.kt:57)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invoke(coroutineDispatcherFactoryInterceptor.kt)
    at io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor$intercept$4.invoke(coroutineDispatcherFactoryInterceptor.kt)
    at io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory$withDispatcher$4.invokeSuspend(FixedThreadCoroutineDispatcherFactory.kt:53)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)

해당 오류를 해결하기 위해서는 아래와 같이 dgsObjectMapper Bean 설정을 해주면 된다.

@Bean
fun dgsObjectMapper(): ObjectMapper {
    return ObjectMapper().apply {
        registerModule(JavaTimeModule())
    }
}

https://netflix.github.io/dgs/advanced/custom-object-mapper/

veluxer62 commented 1 year ago

의존 주입의 중요성.... DGS에 이슈 남김

Describe the Feature Request

If you put the time value in the input as shown below.

val client = GraphQLClient.createCustom("http://localhost:8080/graphql") { url, headers, body ->
    val httpHeaders = HttpHeaders()
    headers.forEach { httpHeaders.addAll(it.key, it.value) }
    httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)

    val exchange = RestTemplate().exchange(
        url,
        HttpMethod.POST,
        HttpEntity(body, httpHeaders),
        String::class.java,
    )

    HttpResponse(exchange.statusCodeValue, exchange.body)
}

val mutation = "some mutation"

val input = FooInput(
    name = "test",
    date = LocalDate.of(2023, 1, 1),
)
val variables = mapOf("input" to input)

client.executeQuery(mutation, variables)
    .extractValueAsObject("foo", typeRef<FooField>())

An error will occur.

Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.netflix.graphql.dgs.client.Request["variables"]->java.util.Collections$SingletonMap["input"]->com.spoqa.cart.generated.types.CreateOrderSheetInput["date"])
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.netflix.graphql.dgs.client.Request["variables"]->java.util.Collections$SingletonMap["input"]-
... 

So I had to take action as below.

val input = FooInput(
    name = "test",
    date = LocalDate.of(2023, 1, 1),
)

val objectMapper = jacksonObjectMapper().apply { registerModule(JavaTimeModule()) }
val variables = mapOf("input" to objectMapper.readValue(objectMapper.writeValueAsString(input)))

I found the following settings in the DGS document.

@Bean
@Qualifier("dgsObjectMapper")
public ObjectMapper dgsObjectMapper() {
    ObjectMapper customMapper = new ObjectMapper()
    customMapper.registerModule(JavaTimeModule());
    return customMapper;
}

However, this setting does not work. Because of the code below.

internal object GraphQLClients {

    internal val objectMapper: ObjectMapper =
        if (ClassUtils.isPresent("com.fasterxml.jackson.module.kotlin.KotlinModule\$Builder", this::class.java.classLoader)) {
            ObjectMapper().registerModule(KotlinModule.Builder().nullIsSameAsDefault(true).build())
        } else ObjectMapper().registerKotlinModule()
    // ...
}
class CustomGraphQLClient(private val url: String, private val requestExecutor: RequestExecutor) : GraphQLClient {
    //...
    override fun executeQuery(query: String, variables: Map<String, Any>, operationName: String?): GraphQLResponse {
        val serializedRequest = GraphQLClients.objectMapper.writeValueAsString(
            Request(
                query,
                variables,
                operationName
            )
        )

        val response = requestExecutor.execute(url, GraphQLClients.defaultHeaders, serializedRequest)
        return GraphQLClients.handleResponse(response, serializedRequest, url)
    }
}

The objectMapper cannot be replaced by redefining bean. Because it is generated by GraphQLClients. In my opinion, it seems that the problem can be solved if the object mapper can be injected dependently.

veluxer62 commented 1 year ago

@TransactionalEventListener를 사용하면 예외가 발생해도 예외를 무시해버린다. 예외 발생 방법은 있겠으나.... 별로 좋은 매커니즘은 아닌거 같다. 되도록 쓰지말자....

veluxer62 commented 1 year ago

MockServer 를 사용할 때 MockClient를 부모클래스에 정의해놓고 공통으로 사용하고자 하는 경우 Bean으로 등록하면 안된다. 왜냐하면 Bean은 싱글톤이므로 매 호출 시 재사용된다. 이때 만약 특정 테스트에서 mocking을 한 경우 다른 테스트에서 mocking한 것이 전파되기 때문에 원치않게 mocking이 되는 이슈가 있다. 이를 해결하려면 mocking을 호출 시 마다 새롭게 생성하도록 해야하는데 매 테스트 코드마다 호출하는 코드를 사용하는게 별로라서 부모클래스에 람다로 생성하도록 하였다.

@Bean
    fun mockery(): Mockery {
        return Mockery(
            sendbirdApi = { MockServerClient("localhost", SENDBIRD_API_PORT) },
            slackApi = { MockServerClient("localhost", SENDBIRD_API_PORT) },
        )
    }
veluxer62 commented 1 year ago

TransactionalEventListener를 쓰면 TX가 끝나야 이벤트가 실행된다. 가끔 필요한 상황이 생기면 쓸만하지만....숨은 로직을 만드는 결과를 초래하게되고 코드순서대로 동작하지 않기 때문에 디버깅이 어렵다.

아래 이슈와 관련된 내용

inviteOrderableVendorAccountToOrderableVendorAllChannels 에 숨은 로직이 있었네요. 해당 코드는 TX가 끝나고 동작해야 정상적으로 동작합니다. 이유는 아래 Facade 코드 때문인데요.

@Transactional
fun createAccount(data: CreateOrderableVendorAccountFacadeData): OrderableVendorAccount {
    val orderableVendor = orderableVendorService.getById(data.orderableVendorId)

    val createOrderableVendorAccountData = CreateOrderableVendorAccountData(
        username = data.username,
        password = data.password,
        orderableVendor = orderableVendor,
    )

    // 1
    val createdAccount = orderableVendorAccountService.createAccount(createOrderableVendorAccountData)

    // 2
    val response = chatClient.createOrderableVendorAccountChatUser(createdAccount)

     // 3
    orderableVendorAccountService.updateSendbirdUserId(
        orderableVendorAccountId = createdAccount.id,
        newSendbirdId = response.userId.toString(),
    )

    return createdAccount
}
  1. 유통사 계정을 생성합니다.
  2. 샌드버드 사용자를 생성합니다.
  3. 샌드버드 사용자 ID를 생성한 유통사 계정에 업데이트 합니다.

문제는 1번인데요. inviteOrderableVendorAccountToOrderableVendorAllChannels는 1번에서 발행한 OrderableVendorAccountCreatedEvent 이벤트를 받아 실행됩니다. 다만 Listener가 TransactionalEventListener이기 때문에 Facade의 로직중 3번이 모두 실행되고 난 후 실행됩니다. 즉, 1번이 실행된 후 실행되는게 아니라 3번이 실행된 후 동기적으로 실행되는 것입니다.

inviteOrderableVendorAccountToOrderableVendorAllChannels 코드 내부에 보면 SendbirdID를 이용하여 채팅방에 초대를 하는 로직이 있는데요. TransactionalEventListener를 사용하면 3번이 실행된 후 해당 코드가 실행되기 때문에 문제가 없지만 EventListener로 변경하면 SendbirdID가 null이기 때문에 널포인터 에러가 발생합니다.

일단 구현방향을 고민해야하기 때문에 롤백부터 합니다.

TransactionalEventListener를 사용하지 맙시다 제발.....

ps) 그전에는 왜 몰랏냐? 기능 테스트 시 @Async는 에러 로그만 표시하기 때문에 로그를 놓친것 같습니다....@Async가 main Thread에서 동작하도록 찾아볼께요....ㅠㅠ

veluxer62 commented 1 year ago

그럼 위 facade.createAccount함수를 어떻게 바꾸면 좋을까?

orderableVendorAccountService.createAccount의 매개변수에 sendbirdId를 가지고 오는 것을 람다로 넘기고 facade에서 chatClient.createOrderableVendorAccountChatUser(createdAccount)를 람다에 넣는 식으로 하면 의존성을 역전시킬 수 있다.

그리고 orderableVendorAccountService.updateSendbirdUserIdorderableVendorAccountService.createAccount에 넣는다면 더이상 TransactionalEventListener를 사용하지 않고도 이벤트를 처리할 수 있게 된다.

veluxer62 commented 1 year ago

Mockserver Log Level 바꾸는 법

val configuration = Configuration.configuration().logLevel(Level.WARN)
mockServer = ClientAndServer.startClientAndServer(configuration, listOf(SENDBIRD_API_PORT, SLACK_API_PORT))