2024년 8월 23일 금요일

Caused by: org.hibernate.AssertionFailure: AttributeConverter class [xxxConverter] registered multiple times 문제 해결

로컬 테스트 환경 만드느라 test에 entityManagerFactory 만들 기회가 있었음.

    @Bean
    @Primary
    fun entityManagerFactory(): LocalContainerEntityManagerFactoryBean {
        return LocalContainerEntityManagerFactoryBean().apply {
            dataSource = masterDataSource(masterHikariConfig())
            setPackagesToScan("com.entity")
            jpaVendorAdapter = HibernateJpaVendorAdapter()

            setJpaProperties(object : Properties() {
                init {
                    put("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
                    put("hibernate.hbm2ddl.auto", "create")
                    put("hibernate.show_sql", "true")
                }
            })
            afterPropertiesSet()
        }
    }
근데 계속 에러남.

Caused by: org.hibernate.AssertionFailure: AttributeConverter class [xxxConverter] registered multiple times
AttributeConverter가 이미 등록되어 있다는 오류임 Hibernate 코드를 보면

org.hibernate.boot.model.convert.internal.AttributeConverterManager.java
...
		if ( old != null ) {
			throw new AssertionFailure(
					String.format(
							Locale.ENGLISH,
							"AttributeConverter class [%s] registered multiple times",
							descriptor.getAttributeConverterClass()
					)
			);
		}
...        
요기에서 에러를 내는 것임. 왜 이게 나나 봤더니

org.hibernate.boot.model.process.spi.MetadataBuildingProcess.java 

	public static MetadataImplementor complete(
			final ManagedResources managedResources,
			final BootstrapContext bootstrapContext,
			final MetadataBuildingOptions options) {
            ...
            managedResources.getAttributeConverterDescriptors().forEach( metadataCollector::addAttributeConverter );
            ...
            
	final MetadataSourceProcessor processor = new MetadataSourceProcessor() {
			private final MetadataSourceProcessor hbmProcessor =
						options.isXmlMappingEnabled()
							? new HbmMetadataSourceProcessorImpl( managedResources, rootMetadataBuildingContext )
							: new NoOpMetadataSourceProcessorImpl();

			private final AnnotationMetadataSourceProcessorImpl annotationProcessor = new AnnotationMetadataSourceProcessorImpl(
					managedResources,
					rootMetadataBuildingContext,
					jandexView
			);  
     ...       
            
}            

metadataCollector::addAttributeConverter
첫번째는 package scan을 하여 Converter를 수집하고

org.hibernate.boot.model.source.internal.annotations.AnnotationMetadataSourceProcessorImpl.java

private void categorizeAnnotatedClass(Class annotatedClass, AttributeConverterManager attributeConverterManager) {
		final XClass xClass = reflectionManager.toXClass( annotatedClass );
		// categorize it, based on assumption it does not fall into multiple categories
		if ( xClass.isAnnotationPresent( Converter.class ) ) {
			//noinspection unchecked
			attributeConverterManager.addAttributeConverter( (Class>) annotatedClass );
		}
		else if ( xClass.isAnnotationPresent( Entity.class )
				|| xClass.isAnnotationPresent( MappedSuperclass.class ) ) {
			xClasses.add( xClass );
		}
		else if ( xClass.isAnnotationPresent( Embeddable.class ) ) {
			xClasses.add( xClass );
		}
		else {
			log.debugf( "Encountered a non-categorized annotated class [%s]; ignoring", annotatedClass.getName() );
		}
	}

여기서 Annotation을 스캔해서 한번 찾아 넣음. 두번째는 @Converter 요걸 찾아 넣는 것임. 그래서 설정을 잘못하면 2번 AttributeConverterManager에 들어가고 에러가 나는 것임. 어떻게 찾았냐면. org.hibernate.boot.model.process.spi.MetadataBuildingProcess.class가 데이터를 만들기 위해 사전작업을 하는데

org.hibernate.boot.model.process.spi.MetadataBuildingProcess.java
...
	public static ManagedResources prepare(
			final MetadataSources sources,
			final BootstrapContext bootstrapContext) {
		final ManagedResourcesImpl managedResources = ManagedResourcesImpl.baseline( sources, bootstrapContext );
		final ConfigurationService configService = bootstrapContext.getServiceRegistry().getService( ConfigurationService.class );
		final boolean xmlMappingEnabled = configService.getSetting(
				AvailableSettings.XML_MAPPING_ENABLED,
				StandardConverters.BOOLEAN,
				true
		);
		ScanningCoordinator.INSTANCE.coordinateScan(
				managedResources,
				bootstrapContext,
				xmlMappingEnabled ? sources.getXmlMappingBinderAccess() : null
		);
		return managedResources;
	}
ScanningCoordinator.INSTANCE.coordinateScan을 하게 됨. 여기를 자세히 보면

org.hibernate.boot.model.process.internal.ScanningCoordinator.java

		for ( ClassDescriptor classDescriptor : scanResult.getLocatedClasses() ) {
			if ( classDescriptor.getCategorization() == ClassDescriptor.Categorization.CONVERTER ) {
				// converter classes are safe to load because we never enhance them,
				// and notice we use the ClassLoaderService specifically, not the temp ClassLoader (if any)
				managedResources.addAttributeConverterDefinition(
						new ClassBasedConverterDescriptor(
								classLoaderService.classForName( classDescriptor.getName() ),
								bootstrapContext.getClassmateContext()
						)
				);
			}
			else if ( classDescriptor.getCategorization() == ClassDescriptor.Categorization.MODEL ) {
				managedResources.addAnnotatedClassName( classDescriptor.getName() );
			}
			unresolvedListedClassNames.remove( classDescriptor.getName() );
		}
이렇게 scanResult로 대상이 있으면 CONVERTER를 addAttributeConverterDefinition에 등록하게 됨. 즉 2번 들어가면 저 scanResult에서 제외를 시켜야 하는 것임. 그럼 저 scanResult는 어디서 가져오냐면

		final ScanResult scanResult = scanner.scan(
				bootstrapContext.getScanEnvironment(),
				bootstrapContext.getScanOptions(),
				StandardScanParameters.INSTANCE
		);
스캐너가 도는데 저 스캐너의 범위는 처음 entityManagerFactory가 설정한 setPackagesToScan() 안의 3가지 선언된 Categorization Class를 스캔함

public interface ClassDescriptor {
	enum Categorization {
		MODEL,
		CONVERTER,
		OTHER
	}
    ...

}
즉 스캔을 한 entity package 하위에 @Converter Annotation을 두게 되면 스캔을 할 때 같이 잡혀서 자동으로 등록되고 Springboot의 Component Scan에도 @Converter를 잡게되면 2개가 등록됨. Converter는 entity를 scan하는 곳에 두지 않는 것이 정신건간에 이로움

2024년 1월 8일 월요일

SK에너지 플랫폼개발실이 일하는 방법

 

이끌거나, 따르거나, 떠나거나. 

  • 우리가 업무를 하는데 정말 중요한 것은 리더십과 팔로워십의 적절한 균형입니다. 야구팀이 가장 좋은 성적을 낼 때는 각자 가진 능력을 자신의 특성에 맞게 활용할 때 입니다.

  • 리더십이 있다면 본인의 능력을 발휘하여 우리가 가야 할 방향으로 사람들을 이끄세요.

  • 팔로워십이 있다면 이끄는 사람들과 함께 우리가 가야 할 방향으로 같이 가세요.

  • 단. 이끌기도 따르기도 싫다면 그때는 야구팀을 떠나야 할 때입니다.

실행은 수직적! 문화는 수평적.

  • 어떤 문제에 대해서 의사결정이 나기 전까지는 우리는 동등하며 이에 대한 다양한 의견을 나눌 수 있습니다. 의사 결정자가 미쳐 고민하지 못한 부분은 언제든 이야기 할 수 있으며 할 말을 하는 것 또한 우리의 문화입니다.

  • 하지만 의사결정이 일어난 그 순간 부터는 즉시 실행을 시작해야 합니다. 대부분의 의사결정은 충분한 검토가 된 후에 결정이 된 것입니다. 결정이 되었다면 그 다음은 빠른 실행 과 피드백을 이용해 문제를 고쳐 나가는 것이 더 낫습니다.

잡담을 많이 나누는 것이 경쟁력이다.

  • 개인의 생각은 개인의 경험에서 우러나옵니다. 경험은 올바른 길로 우리를 인도하지만 때론 관성으로 인해 더 나은 방법을 찾는데 방해가 되기도 합니다. 이런 방해를 가장 쉽게 해결하는 것은 다른 사람과 방법에 대해서 가볍게 이야기 하는 것입니다. 따라서 사람들과 많은 대화를 하세요. 많은 잡담은 새로운 아이디어가 되기도 하고 고민하던 문제를 해결하는 단초가 되기도 합니다.

개선 사항은 이슈화 한다. 설령 해결되지 않더라도.

  • 문제를 해결하기 위해선 문제를 인식하는 것이 중요합니다. 문제가 인식 되었다면 어떻게 개선할지 고민하고 이슈화 하세요.

  • 우리가 개선을 하려는 것은 때때로 지금 이 시점에는 불가 한 것들이 있습니다. 모든 것은 알맞은 시기가 있기 때문에 이슈화 하더라도 항상 해결되지 않을 것입니다. 하지만 계속 이슈화를 하세요. 사람은 망각을 하기에 이슈화 하지 않으면 익숙함에 빠져서 나중에는 문제가 인식되지 않습니다.

보고는 팩트에 기반한다.

  • 너무나 당연하지만 그만큼 너무나 어려운 일입니다. 회사는 성과를 만들어내는 곳이기에 어떤 이유로 성과가 나지 않으면 이를 숨기고 싶은 마음이 듭니다. 그럼에도 불구하고 팩트 기반으로 보고를 하지 않으면 우리는 엉뚱한 데에서 시간을 더 낭비하거나 옳지 않은 의사결정을 하게 됩니다. 잠깐의 거짓 보고는 점점 눈덩이 처럼 불어납니다. 그러니 반드시 보고는 팩트 기반으로 해야 합니다.

일의 목적, 기간, 결과, 공유자를 고민하며 일한다.

  • 우리가 하는 일은 단순하지 않습니다. 어떤 업무가 주어진다면 누가, 언제, 어떻게, 왜 해야 하는지 고민하세요. 그리고 그 일이 완료된다면 영향을 받는 사람들을 생각하세요. 그래야만 엉뚱한 결과가 나오지 않습니다. 짧은 생각은 더 깊고 난이도 있는 일을 할 수 없게 만듭니다. 문제를 해결하기 위해 어떤 것이 최선인지 고민하세요.

  • 문제를 해결하는 것은 생각보다 간단할 수 있습니다. 컴퓨터가 없던 세상에서도 모든 업무는 돌아갔습니다. 즉 모든 것을 기술적으로 해결하는 것은 절대 답이 아닙니다. 일의 본질을 보는 눈을 키워야 합니다.

책임은 실행한 사람이 아닌 결정한 사람이 진다.

  • 말 그대로 책임이란 것은 그 일을 행하는 사람이 지는 것이 아닌 의사 결정을 한 사람이 지는 것입니다.

  • 책임의 뜻은 그 일을 '다시 정상적인 궤도에 올려 놓는 것'을 의미합니다. 퇴사가 아닙니다. 만약 결정이 잘못되었다면 빠르게 다시 궤도에 올려 놓아야 합니다. 그것을 빠르게 하기 위해 유연하게 팀과 시스템을 구성하는 것입니다.

고민은 반나절만!, 주변에 항상 질문한다.

  • 어떤 문제를 어떻게 해결할지 고민하는 것은 매우 좋은 습관입니다. 다만 고민하는데 시간을 많이 빼앗겨서는 일이 정상적으로 기간 안에 끝날 수 없습니다. 우리 구성원들은 각자 다양한 경험을 가지고 있습니다. 그렇기에 고민은 길게 하기 보단 자신이 해결하려는 문제적 상황을 공유하여 주변의 도움을 받으세요. 아무도 도움을 줄 수 없거나 문제가 깔끔하게 해결되지 않을 때 고민을 해도 늦지 않습니다.

나의 성장은 조직의 성장이다.

  • Product Owner는 제품의 방향과 가치를 만들어나가는데 매우 중요하면서 전선이 매우 넓습니다. 그리고 직관과 경험이 제품에 끼치는 영향이 매우 높습니다. 세상은 우리가 생각하는 것보다 훨씬 복잡합니다. 이런 환경에서 PO가 어떤 의사결정과 정책을 세우냐에 따라 이후 제품의 성장속도가 직접적으로 영향을 받게 됩니다. PO가 가진 시각을 단위 기능에 초점을 맞추기보다 현 상황에서 제품 전체에 어떤 Feature가 들어가야 가장 효과적인지 선택하고 조율해야 합니다. 그렇기에 PO가 성장할 수록 제품도 성장하게 됩니다.

  • 디자인의 영역은 제품에서 가장 복잡하게 얽혀 있는 영역입니다. 왜냐하면 직접적으로 눈에 보이기 때문입니다. 그리고 개개인의 경험이 가장 잘 드러나기 때문에 충돌 또한 많습니다. 그렇기에 제품 디자이너는 Dog Fooding과 사용자 경험을 이해하는 수준을 넘어서 논리적으로 설명할 수 있어야 하고 수치화에 익숙해져야 합니다. 자신이 디자인한 제품에 들어가는 각종 요소에 대한 '중요도' 판별이 가능해야 합니다. 모두가 Full Spec을 만들고 싶어하지만 우리가 처해있는 시장 상황은 우리를 기다려주지 않습니다. 따라서 제품 디자이너는 사용자 경험 뿐만 아니라 제품이 시장에서 갖는 비즈니스적 상황도 이해할 필요가 있습니다. 이런 일을 하기 위해서는 디자인 뿐만 아니라 제품 관점, 비즈니스적 관점, 사용자의 관점을 보는 시각을 키우는 것이 필요합니다. 이것은 우리가 만드는 제품에도 좋은 영향을 끼치지만 스스로 하는 역할에도 더 넓은 기회를 부여할 밑거름이 됩니다.

  • 개발자는 소설가와 비슷합니다. 자기 자신이 만들어내는 글(코드)가 얼마나 깔끔하고 이해하기 쉽고 유연한지 본인 스스로에게 달렸습니다. IT는 인력 집약적 산업이고 개인의 능력에 따라 결과물의 편차가 매우 크기 때문에 본인 스스로를 성장시켜야 합니다. 뛰어난 개발자 1명이 만들어내는 output은 10명의 평범한 개발자의 코드보다 더 낫습니다. 좋은 코드는 비즈니스적 유연함을 갖고 있습니다. 그러니 본인의 기술력을 향상시켜야 합니다. 좋은 코드는 이를 보는 다른 개발자에게도 좋은 영향을 끼칩니다. 이는 마치 좋은 소설은 아마추어 소설가에게 좋은 밑거름이 되는 것과 같습니다.

개개인의 능력보다 조직력으로 승부한다.

  1. 사람은 누구나 강점과 약점이 존재합니다. 우리는 각자가 갖는 자신의 강점을 최대한 발휘하고 자신의 약점은 다른 구성원이 보완하도록 합니다. 잘 만들어진 조직은 더 좋은 퍼포먼스를 보여 주며 더 나은 결과를 만들어 냅니다.

  2. 야구팀에 이승엽은 1명이면 됩니다. 모두가 투수를 하겠다고 한다면 제대로 야구팀이 돌아가지 않습니다.

  3. 우리가 만드는 'Product'란 이름을 갖는 것들은 이미 시장에 수많은 경쟁자가 있는 것들입니다. 그렇기에 더 디테일하고 치열하게 만들어야 합니다. 그러기 위해서는 모든 분야에 대한 고민이 필요하고 이를 개인이 모두 해쳐나갈 수 없기 때문에 조직이 필요한 것입니다.


우리는 일을 할 때 최선(最善)을 선택하기 보다 대부분 차선(次善)을 선택하게 됩니다.

우리가 갖는 대부분의 환경은 리소스, 계약, 시장 상황, 조직등의 여러가지 이슈로 인해 누구나 다 아는 최선을 선택한다는 것은 어려운 경우가 대부분입니다.
해서 우리는 차선 정도를 선택할 수 있을 때 그것이 우리가 생각한 차선 중 최고인지, 혹은 최악이 아닌 차악의 수준인지를 검토할 필요가 있습니다. 결국 이런 작은 차선들, 차악들이 모여서 제품이 성장하기 때문입니다.


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가 진행되면 해당 문제도 사라질 것이지만 혹시나해서 글로 남긴다.