2021년 9월 2일 목요일

카카오 모빌리티에서 카오너 개발팀이 일하는 방식

개요

개발팀이 일하는 방식을 정리합니다.

개발팀에서 일을 더 잘하는 10가지 방법.

  1. 이끌거나, 따르거나, 떠나거나. 
  2. 실행은 수직적! 문화는 수평적.
  3. 잡담을 많이 나누는 것이 경쟁력이다.
  4. 개선 사항은 이슈화 한다. 설령 해결되지 않더라도.
  5. 보고는 팩트에 기반한다.
  6. 일의 목적, 기간, 결과, 공유자를 고민하며 일한다.
  7. 책임은 실행한 사람이 아닌 결정한 사람이 진다.

  8. 고민은 반나절만!, 주변에 항상 질문한다.
  9. 나의 성장은 조직의 성장이다.
  10. 개개인의 능력보다 조직력으로 승부한다.


일하는 방식

코드리뷰

코드리뷰는 필수다.

  • Approved는 최소 1인이다.
  • 코드리뷰어가 존재하지 않는 경우 상위 조직장, 그 상위 조직장이 없으면 팀장까지 리뷰어로 포함 시킨다.
  • 코드리뷰를 하지 않는 경우는 다음과 같다.
    1. 도메인에 대해 혼자 개발하는 경우.
    2. 코드 리뷰 요청 후 36시간 이상이 지나도록 아무도 보지 않는 경우. (주말 제외)
  • 모든 조직장은 API의 Request, Response, URI 스펙은 필수로 점검한다.
    1. 한번 나간 API는 회수가 불가능하기 때문.
  • PR에 대한 코드는 PR을 올린 사람이 리뷰어를 귀찮게 해서라도 코드리뷰를 하도록 한다.

PR Reject는 1회만 한다.

  • 어떠한 이유로 변경을 요청하기 위해 Reject를 하는 경우 수정 후 다시 Reject 하지 않는다. 
    • 코드 리뷰는 100% 완벽을 요구하는 것이 아니다. 
    • 다시 Reject을 해야 할 것 같은 상황이라면 페어코딩해라. PR에서 댓글로 논쟁을 벌이는건 얻는게 없다.
  • 코드의 스타일(conventions)으로 Reject을 하지 않는다.
    • 코드 스타일은 Linter를 이용해라.
    • 컨벤션을 맞추고 싶으면 PR건에서 이야기 할 것이 아니라 해당 프로젝트의 규칙을 잡는다.

Slack

  • Slack의 멘션은 필요한 경우 상대방을 고려하지 않고 맨션한다. (휴가 무관)
    • 맨션을 받고 싶지 않으면 Slack의 "알림 일시 중지"를 이용한다.
    • 메일로 일하지 않기 때문에 맨션을 하지 않으면 해당 업무에 대해 놓칠 수 있다. 
    • 멘션을 걸 때 멘션의 대상자가 업무를 파악 할 수도 못할 수 도 있다는 것을 기억 해야 한다. 
      • 휴가 or 휴식 or 알림 중지 등등 
  • 휴가를 가는 경우 Slack의 "휴가 중"을 꼭 설정한다.
    • 본인이 휴가를 간다는 것을 알리는 필수 수단이다.
    • 휴가에 방해를 받고 싶지 않으면 '알림 일시 중지'를 꼭 활용하자. 그리고 보지 말자.
      • 정말 크리티컬 한 경우 전화를 하게 되어 있다. (그러니 휴가를 간 사람에게 왠만한 크리티컬 한 이슈가 아닌 이상 휴가 복귀 후 업무를 F/U 하도록 멘션은 걸되 연락하진 말자.)

Release

지향점 > 빠른 배포, 빠른 롤백

  • 릴리즈 주기는 짧게(1주일에 최소 1번) 간다. 
    • QA가 있는 것 아닌 이상 짧게 간다. 
  • 릴리즈는 퇴근 시간 2시간 전, 그리고 금요일에 하지 않는다.

Hotfix

  • Hotfix를 할 때는 무조건적인 페어코딩 or PR 리뷰를 한다.
    • hotfix 때 side effect로 인한 장애가 더 많이 난다.
    • 그러니 일단 침착하고 그 누구든 옆에 본인의 코드를 검증 해줄 사람을 둔다.
    • 아무도 없으면 fennec에게 전화를 해서라도 hotfix 전에 코드를 검증 받는다.


마지막으로

탄탄한 조직은 굳이 Rule이 필요 없다. 서로의 신뢰로 모든게 동작하기 때문이다.


2021년 8월 12일 목요일

springboot 2.5 x & spring-cloud-starter-vault-config 오류 해결하기

 Springboot 2.5.x 버전은 spring-cloud-starter-vault-config 2.2.X 버전과 현재(2021.08.12)는 되지 않는다.


이런 오류가 난다.

Caused by: java.lang.IllegalStateException: Failed to introspect Class [org.springframework.cloud.context.properties.ConfigurationPropertiesBeans] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:481) ~[spring-core-5.3.8.jar:5.3.8]
at org.springframework.util.ReflectionUtils.doWithLocalMethods(ReflectionUtils.java:321) ~[spring-core-5.3.8.jar:5.3.8]
at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.buildPersistenceMetadata(PersistenceAnnotationBeanPostProcessor.java:417) ~[spring-orm-5.3.8.jar:5.3.8]
at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.findPersistenceMetadata(PersistenceAnnotationBeanPostProcessor.java:388) ~[spring-orm-5.3.8.jar:5.3.8]
at org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor.postProcessMergedBeanDefinition(PersistenceAnnotationBeanPostProcessor.java:335) ~[spring-orm-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors(AbstractAutowireCapableBeanFactory.java:1098) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:576) ~[spring-beans-5.3.8.jar:5.3.8]
... 30 common frames omitted
Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata
at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.8.0_252]
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[na:1.8.0_252]
at java.lang.Class.getDeclaredMethods(Class.java:1975) ~[na:1.8.0_252]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:463) ~[spring-core-5.3.8.jar:5.3.8]
... 36 common frames omitted
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata
at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_252]
at java.lang.ClassLoader.loadClass(ClassLoader.java:418) ~[na:1.8.0_252]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) ~[na:1.8.0_252]
at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[na:1.8.0_252]
... 40 common frames omitted

대충 보면 ConfigurationBeanFactoryMetadata class를 못찾는다는 뜻이다.

이유는 Springboot와 Springcloud의 Release Train이 다르기 때문에 아직 의존성이 깔끔하게 해결되지 않아서다.

참조: https://spring.io/projects/spring-cloud#overview 

Adding Spring Cloud To An Existing Spring Boot Application

If you an existing Spring Boot app you want to add Spring Cloud to that app, the first step is to determine the version of Spring Cloud you should use. The version you use in your app will depend on the version of Spring Boot you are using.

The table below outlines which version of Spring Cloud maps to which version of Spring Boot.

Table 1. Release train Spring Boot compatibility
Release TrainBoot Version

2020.0.x aka Ilford

2.4.x, 2.5.x (Starting with 2020.0.3)

Hoxton

2.2.x, 2.3.x (Starting with SR5)

Greenwich

2.1.x

Finchley

2.0.x

Edgware

1.5.x

Dalston

1.5.x

Spring Cloud Dalston, Edgware, Finchley, and Greenwich have all reached end of life status and are no longer supported.

해서 이 둘을 연결하려면 gradle 설정이 좀 필요하다.

gradle 설정은 대충 다음처럼 하면 된다. (참고로 아래코드는 kotlin dsl을 이용했따)


ext {
set("springCloudVersion", "2020.0.3")
}
configure<io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension> {
imports(delegateClosureOf<io.spring.gradle.dependencymanagement.dsl.ImportsHandler> {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:2020.0.3")
})
}


그리고 한가지 더 

원래 vault config는 bootstrap.yml을 읽어서 vault config를 하게 되었는데 

springcloud bootstrap 2020.X 버전 부터는 기본적인 application.yml 안에서 vault 설정을 읽게 바뀌었다.



2019년 9월 16일 월요일

@SpringBootTest를 쓰면서 JPA 테스트 같이 하기

@SpringBootTest를 이용해서 integration test를 할 때 data jpa test도 같이 겸하게 되면 대충 다음과 같은 에러가 난다.

java.lang.IllegalStateException: Failed to load ApplicationContext

 at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:125)
 at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:108)
 at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
 at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
 at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:246)
 at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:91)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$invokeTestInstancePostProcessors$5(ClassTestDescriptor.java:349)
 at org.junit.jupiter.engine.descriptor.JupiterTestDescriptor.executeAndMaskThrowable(JupiterTestDescriptor.java:215)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$invokeTestInstancePostProcessors$6(ClassTestDescriptor.java:349)
 at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
 at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
 at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
 at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
 at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
 at java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)
 at java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:743)
 at java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:742)
 at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.invokeTestInstancePostProcessors(ClassTestDescriptor.java:348)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateAndPostProcessTestInstance(ClassTestDescriptor.java:270)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$2(ClassTestDescriptor.java:259)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$3(ClassTestDescriptor.java:263)
 at java.util.Optional.orElseGet(Optional.java:267)
 at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$testInstanceProvider$4(ClassTestDescriptor.java:262)
 at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:82)
 at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:59)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$0(NodeTestTask.java:80)
 at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:80)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:66)
 at java.util.ArrayList.forEach(ArrayList.java:1257)
 at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:110)
 at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:95)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:71)
 at java.util.ArrayList.forEach(ArrayList.java:1257)
 at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:110)
 at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:95)
 at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:71)
 at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
 at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
 at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
 at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:220)
 at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:188)
 at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:202)
 at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:181)
 at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128)
 at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
 at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
 at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
 at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'SampleRepository': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.kakaomobility.airport.vender.domain.industry.infrastructure.SampleRepository]: Specified class is an interface
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1287)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
 at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:849)
 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
 at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
 at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
 at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
 at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:127)
 at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
 at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117)
 ... 53 more
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.kakaomobility.airport.vender.domain.industry.infrastructure.SampleRepository]: Specified class is an interface
 at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:70)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1279)
 ... 69 more



이유는 여러 블로그에 설명이 되어있으니 그걸 보면 되고 이걸 트릭을 이용해 우회해 보자.


@EntityScan(basePackages = ["com.5dolstory.sample"])
@EnableJpaRepositories(basePackages = ["com.5dolstory.sample"])
@Configuration
class SampleConfiguration


이런식으로 일단 jpaRepository를 scan 할 수 있도록 configuration을 만들고 이걸 SpringBootTest가 load해서 applicationContext에 올리도록 한다.
그리고 jpa test를 위해 몇가지 annotation을 추가해서 테스트용 DB를 사용할 수 있게 하자.

@EnableConfigurationProperties(AppEnvironment::class)
@SpringBootTest(classes = [
    SampleService::class,
    SampleConfiguration::class
])
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
internal class SampleControllerTest {
}


이런식으로 하면 우회해서 테스트 가능하다.
이게 동작하는 이유를 간단히 설명하면 test 코드 동작 시 JPA를 이용 할 때 필요한 요소들을 다 등록하는 것이다.


2019년 7월 15일 월요일

Embedded Mysql for springboot

나는 개인적으로 local 작업시 db, redis 같은 걸 별도로 인스톨해서 쓰지 않고 embedded하는 편이다.

일단 시간이 없으니 볼론으로 들어가서 mysql을 embedded하는 건 다음과 같다. (참고로 코드는 kotlin이다.)

일단 라이브러리 추가.


compile("com.wix:wix-embedded-mysql:4.2.0")

그리고 다음 configration을 추가


@Profile(value = ["default"])
@Component
@EnableConfigurationProperties(value = [DataSourceProperties::class])
class DefaultEmbeddedMysql(
    private val properties: DataSourceProperties
) {
    private val log = LoggerFactory.getLogger(javaClass)
    private lateinit var mysqlServer: EmbeddedMysql

    @PostConstruct
    @Throws(IOException::class)
    fun start() {
        log.info("Initializing embedded Mysql instance")

        val uri = URI.create(properties.url.substring(5))
        val config = MysqldConfig.aMysqldConfig(Version.v5_7_19)
            .withCharset(Charset.UTF8)
            .withPort(uri.port)
            .withTimeZone(TimeZone.getTimeZone("Asia/Seoul"))
            .withUser(properties.username, properties.password)
            .build()

        val path = uri.path.removePrefix("/")

        mysqlServer = EmbeddedMysql.anEmbeddedMysql(config)
            .addSchema(SchemaConfig.aSchemaConfig(path).withCharset(Charset.UTF8).build())
            .start()
    }

    @PreDestroy
    @Throws(InterruptedException::class)
    fun stop() {
        mysqlServer.stop()
    }

    @Bean
    fun dataSource(): DataSource {
        return properties.initializeDataSourceBuilder().build()
    }
}


이러면 springboot가 뜰 때 mysql을 다운받고 mysqld로 mysql을 기동하는 걸 로그로 볼 수 있다.
local에서만 사용하도록 꼭 @Profile을 추가해주자.

2018년 11월 12일 월요일

Kotlin Spring Ambiguous mapping. Cannot map * method 문제

혹시나 kotlin + spring1.5.x 버전으로 개발을 하다가 intellij의 kotlin plugin을 update 하고 나서
Ambiguous mapping. Cannot map '*' method 에러가 난다면 이 이슈도 확인해보자.
(특히 console build는 되는데 intellij에서 build가 안된다면 이 문제일 가능성이 높다.)

증상
intellij에 있는 kotlin plugin을 "1.3.0-release-IJ2018.2-1"으로 업데이트 했더니 잘 뜨던 spring에 문제가 생겨서 확인해봤다.
문제의 위치는 refundController의 expire 메소드였는데 이상하니 httpMethod가 EXPIRE인 function만 문제가 되었다.


Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'refundController' method 
public static org.springframework.hateoas.Resource com.titicacacorp.billing.domain.management.refund.controller.RefundController.expire$default(com.titicacacorp.billing.domain.management.refund.controller.RefundController,long,java.lang.String,int,java.lang.Object)
to {[/management/refunds/{refundId}],methods=[DELETE]}: There is already 'refundController' bean method
public org.springframework.hateoas.Resource com.titicacacorp.billing.domain.management.refund.controller.RefundController.expire(long,java.lang.String) mapped.
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1631)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481)
 at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312)
 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
 at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:208)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.addCandidateEntry(DefaultListableBeanFactory.java:1314)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1280)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeans(DefaultListableBeanFactory.java:1178)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1094)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1064)
 at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
 at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
 ... 36 common frames omitted
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'refundController' method 
 

에러를 보면 Ambiguous mapping으로 이미 어딘가에서 refundController의 expire 메소드를 이미 mapping 했다고 에러가 난 것이다.

처음에는 이것 때문에 매핑룰이 문제인가 해서 룰을 고쳐보았으나 해결되진 않았고 원인은 뜻하지 않는 곳에 있었다.


    @DeleteMapping("/refunds/{refundId}")
    fun expire(
        @PathVariable refundId: Long,
        @RequestParam(required = false) operation: String? = null
    ): Resource {
        refundService.expire(refundId, operation)

        return Resource(refundId)
    }
 
expire 메소드에 대한 코드는 위처럼 생겼는데 문제가 생긴 부분은 뜬금없이 RequestParam이었다.
http delete method의 경우 requestbody에 데이터를 보낼 수 없기 때문에 queryString으로 간단한 데이터를 전송하려고 만들었는데.. kotlin plugin 버전이 올라가면서 여기부분에 처리 문제가 생긴 듯 하다.

        @RequestParam(required = false) operation: String? = null)

위 코드에서 null일 때 default 변수를 세팅하는 로직을 지우면 해결된다.


    fun expire(
        @PathVariable refundId: Long,
        @RequestParam(required = false) operation: String?)


이렇게 말이다.

아마 plugin의 path가 진행되면 해당 문제도 사라질 것이지만 혹시나해서 글로 남긴다.

2018년 7월 3일 화요일

Gson DateTimeFormat Deserialize

LocalDateTime이나 LocalDateTime을 Json에서 Class로 Deserialize 할 때 별도 Deserializer를 구현하지 않으면 이런 에러가 난다.

표면적으로는 이런에러가 나고

com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column XXX path $.XXXX
Gson 안을 보면

Method threw 'java.lang.NullPointerException' exception. Cannot evaluate java.time.LocalDateTime.toString()
에러를 볼 수 있다.

해결하는 방법은 Deserializer를 구현해주면 된다.

    private val gson = GsonBuilder()
            .registerTypeAdapter(LocalDateTime::class.java, JsonDeserializer { json, _, _ ->
                val time = json.asJsonPrimitive.asString
                LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME)
            })
            .registerTypeAdapter(LocalDate::class.java, JsonDeserializer { json, _, _ ->
                val date = json.asJsonPrimitive.asString
                LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE)
            })
            .create()



2018년 6월 19일 화요일

Spring security API Key authenticate

Filter단에서 Id, ApiKey로 API 사용에 대한 인증하고 싶은 경우 다음과 같이 간단하게 하면 됨.

막상 짜고 나니 Interceptor에서 처리해야 해서 기록용으로 남긴다.


class CpApiAuthFilter : AbstractPreAuthenticatedProcessingFilter() {

    val log = LoggerFactory.getLogger(javaClass)!!

    override fun getPreAuthenticatedCredentials(request: HttpServletRequest): CpCredential {
        val apiKey:String? = request.getHeader("x-sample-api-key")
        val cpId:String? = request.getHeader("x-sample-cp-id")
        log.debug("api key auth: cpId=$cpId,apiKey=$apiKey")

        return CpCredential(apiKey, cpId)
    }

    override fun getPreAuthenticatedPrincipal(request: HttpServletRequest): Any {
        return ""
    }
}

data class CpCredential(
        val apiKey:String? = null,
        val cpId:String? = null
){
    fun isValid(): Boolean {
        if(apiKey.isNullOrBlank() || cpId == null || apiKey?.length!! < 5 )
            return false

        try {
            cpId.toLong()
        } catch (e:Exception){
            return false
        }
        return true
    }
    fun getCpId():Long = cpId!!.toLong()
}

class CpApiAuthManager
@Autowired constructor(private val repository: CpRepository)
    : AuthenticationManager {
    override fun authenticate(authentication: Authentication): Authentication {
        val credential = authentication.principal as CpCredential

        if(credential.isValid().not())
            throw BadCredentialsException("apiKey, cpId not found or not expected value.")
        val cpId = credential.getCpId()
        val cp = repository.findOne(cpId)

        if (cp == null || !cp.status)
            throw BadCredentialsException("cp not found or cp expired.")

        if(cp.apiKey != credential.apiKey)
            throw BadCredentialsException("cpId and apiKey do not matched.")

        authentication.isAuthenticated = true
        return authentication
    }
}

인증용 Filter, Manager는 위처럼 생성.

security에 적용하는 건 아래 처럼 하면 됨.


    @Bean
    @Order(1)
    fun webSecurityConfigurerAdapter(): WebSecurityConfigurerAdapter {
        return object : WebSecurityConfigurerAdapter() {
            override fun configure(http: HttpSecurity) {

                http.addFilter(CpApiAuthFilter(CpApiAuthManager(cpRepository)))

                http.headers().frameOptions().disable().xssProtection().xssProtectionEnabled(true)
                        .and().contentTypeOptions()
                        .and().cacheControl().disable()
                        .and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
            }
        }
    }


일부 코드만 발췌한거라 동작을 안할 수도 있다.ㅎㅎㅎ