(React) PropTypes 사용방법과 종류

|

React Component의 prop값을 검증하기 위해 PropTypes를 이용하여 값을 지정할 수 있습니다.
React v15.5부터 다른 패키지로 변경되었는데 ‘prop-types’라이브러리를 사용하라고 권고하고 있습니다.

사용방법

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Foo extends Component {
  ...
  
  static propTypes = {
    strArg : PropTypes.string,            
    numArg : PropTypes.number.isRequired   
  }
}

위와 같이 설정할 경우 Foo 컴포넌트의 strArg값은 string 타입이여야 하며, numArg값은 number 타입이여야 합니다.
그리고 strArg값은 지정하지 않아도 되나 isRequired를 지정한 numArg값은 반드시 설정해야 합니다.

또한 propTypes은 아래와 같이 class 밖에서도 설정 가능합니다.

class Foo extends Component {
  ...
}

Foo.propTypes = {
  strArg : PropTypes.string
}

PropTypes의 종류

PropTypes으로 설정할 수 있는 종류는 아래와 같습니다.

kind description
array 배열
bool true/false
func 함수
number 숫자
object 객체
string 문자열
symbol 심벌 개체(ES6)
node 렌더링 가능한 모든것(number, string, element, 또는 그것들이 포함된 array/fragment)
element React element
instanceOf(ClassName) JS에서 instanceof로 정의 가능한 클래스 인스턴스
oneOf([…Value]) 포함된 값들중 하나.(ex: oneOf([‘남자’,’여자’]))
oneOfType([…PropTypes]) 포함된 PropTypes들중 하나. (ex: oneOfType([PropTypes.string, PropTypes.instanceOf(MyClass)]))
arrayOf(PropTypes) 해당 PropTypes으로 구성된 배열
objectOf(PropTypes) 주어진 종류의 값을 가진 객체
shape({key:PropTypes}) 해당 스키마를 가진 객체.(ex:shape({name:PropTypes.string,age:PropTypes.number}))
exact({key:PropTypes}) 명확하게 해당 스키마만 존재해야함.

Reference

Typechecking With PropTypes:https://reactjs.org/docs/typechecking-with-proptypes.html

SSM(simple-spring-memcached) MultiCache 사용하기 (@ReadThroughMultiCache, @UpdateMultiCache, @InvalidateMultiCache)

|

“Spring + Memcached” 조합일때 simple-spring-memcached(이하 ssm)이 많이 사용되는데 인터넷에 보면 대부분 @ReadThroughSingleCache@ReadThroughAssignCache에 대한 설명이나 예제는 많은데 @ReadThroughMultiCache관련된 예제는 유독 찾아보기 힘들었습니다.
심지어 공식 가이드에도 간략하게만 써있어서 실제 동작 방식에 대해 알아보기 위해 xmemcached기반으로 직접 테스트 프로젝트를 만들어보고 테스트 해본 내용에 대한 포스팅입니다.

Basic

아래 코드는 기본적인 사용 방법입니다.

@ReadThroughMultiCache
public List<Integer> randomMulti(@ParameterValueKeyProvider List<Integer> nums) {
    ......
}

하나의 java.util.List인자값을 포함하고 java.util.List 또는 java.util.List를 상속한 클래스를 리턴하는 메소드에만 적용할 수 있습니다.
위와 같이 적용시 ssm은 자동으로 인자값의 List와 결과값의 List를 매핑하여 분산하여 캐시키를 저장하게 됩니다.
위 Java코드를 기반으로 실행시 인자값과 반환값이 아래와 같을 경우 실제 캐시에 저장되는 예시입니다.

// argument : List(1,2,3,4,5)
// returnValue : List(11,22,33,44,55)

stats cachedump 5 100
ITEM local:TMON::COMMON:randomMulti:1 [135 b; 1543323568 s]    // value is 11
ITEM local:TMON::COMMON:randomMulti:2 [145 b; 1543323568 s]    // value is 22
ITEM local:TMON::COMMON:randomMulti:3 [155 b; 1543323568 s]    // value is 33
ITEM local:TMON::COMMON:randomMulti:4 [165 b; 1543323568 s]    // value is 44
ITEM local:TMON::COMMON:randomMulti:5 [175 b; 1543323568 s]    // value is 55
END

@ReadThroughSingleCache의 경우 “1,2,3,4,5” 전체를 키 값으로 사용하지만 @ReadThroughMultiCache의 경우 List의 각 키 값을 분산 저장하고 재활용 합니다.
위와 같이 캐시가 저장된 상태에서 다시 아래와 같이 실행할 경우 기존 캐시된 값은 그대로 사용하고 캐시가 없는 부분만 다시 캐시에 저장합니다.

// argument : List(2,4,6,8)
// returnValue : List(22,44,66,88)

stats cachedump 5 100
ITEM local:TMON::COMMON:randomMulti:1 [135 b; 1543323568 s]
ITEM local:TMON::COMMON:randomMulti:2 [145 b; 1543323568 s]   // use existing cache
ITEM local:TMON::COMMON:randomMulti:3 [155 b; 1543323568 s]   
ITEM local:TMON::COMMON:randomMulti:4 [165 b; 1543323568 s]   // use existing cache
ITEM local:TMON::COMMON:randomMulti:5 [175 b; 1543323568 s]
ITEM local:TMON::COMMON:randomMulti:6 [185 b; 1543324016 s]   // new cache
ITEM local:TMON::COMMON:randomMulti:8 [205 b; 1543324016 s]   // new cache
END

새로 캐시를 저장하는 것이 아니기 때문에 expiration 설정을 했을 경우 1~5번 캐시는 동시에 삭제되고 6,8번 캐시는 이후 삭제됩니다.

위 메서드를 디버깅해보면 애초에 인자값에 캐시 데이터가 없는 값만 추려 전달하는것을 볼 수 있습니다.

// execute 1 => argument = List(1,2,3,4,5)
@ReadThroughMultiCache
public List<Integer> randomMulti(@ParameterValueKeyProvider List<Integer> nums) {
    // nums = List(1,2,3,4,5)
    ......
}

// execute 2 => argument = List(2,4,6,8)
@ReadThroughMultiCache
public List<Integer> randomMulti(@ParameterValueKeyProvider List<Integer> nums) {
    // nums = List(6,8)
    ......
}

이미 캐싱 되 있는 값을 Update 하기 위해서는 @UpdateMultiCache를 이용 할 수 있습니다.

@UpdateMultiCache
public void updateMulti(@ParameterValueKeyProvider List<Integer> multi, @ParameterDataUpdateContent List<List<Integer>> content) {
    ......
}

위 예제와 같이 @ParameterDataUpdateContent어노테이션을 사용하여 저장할 값을 직접 주입할 수 있으며,

@ReturnDataUpdateContent
@UpdateMultiCache
public List<Integer> updateMulti(@ParameterValueKeyProvider List<Integer> multi) {
    ......
}

@ReturnDataUpdateContent어노테이션을 사용하여 반환값을 저장할 수도 있습니다.

캐시를 만료 시킬때는 @InvalidateMultiCache어노테이션을 사용합니다.

@InvalidateMultiCache
public void invalidateMulti(@ParameterValueKeyProvider List<Integer> multi) {
    ......
}

Caution - Argument

@ReadThroughMultiCache는 반드시 하나의 java.util.List 인자값을 포함해야합니다.

@ReadThroughMultiCache
public List<Integer> randomMulti(@ParameterValueKeyProvider Integer multi) {
    ......
}

위와 같이 인자값이 잘못됬을 경우 정상실행 되나 캐싱되지 않으며 아래와 같은 오류 메시지가 발생합니다.

[2018-11-27 22:13:46] [WARN ] c.g.c.s.a.CacheAdvice.warn[55] Caching on execution(XXXService.randomMulti(..)) aborted due to an error.
com.google.code.ssm.aop.support.InvalidAnnotationException: No one parameter objects found at dataIndexes [[0]] is not a [java.util.List]. [public java.util.List test.service.XXXService.randomMulti(java.lang.Integer)] does not fulfill the requirements.

java.util.List타입 대신 Array를 사용하더라도 동일한 오류를 발생시킵니다.

@ReadThroughMultiCache
public List<Integer> randomMulti(@ParameterValueKeyProvider Integer[] multi) {
    ......
}
[2018-11-27 22:16:23] [WARN ] c.g.c.s.a.CacheAdvice.warn[55] Caching on execution(XXXService.randomMulti(..)) aborted due to an error.
com.google.code.ssm.aop.support.InvalidAnnotationException: No one parameter objects found at dataIndexes [[0]] is not a [java.util.List]. [public java.util.List test.service.XXXService.randomMulti(java.lang.Integer[])] does not fulfill the requirements.

java.util.List타입 인자값이 2개 이상일 경우에도 오류를 발생시킵니다.

@ReadThroughMultiCache
public List<Integer> randomMulti(@ParameterValueKeyProvider List<Integer> multi, @ParameterValueKeyProvider(order = 1) List<Integer> multi2) {
    ......
}
[2018-11-27 22:19:18] [WARN ] c.g.c.s.a.CacheAdvice.warn[55] Caching on execution(XXXService.randomMulti(..)) aborted due to an error.
com.google.code.ssm.aop.support.InvalidAnnotationException: There are more than one method's parameter annotated by @ParameterValueKeyProvider that is list public java.util.List test.service.XXXService.randomMulti(java.util.List,java.util.List)

하지만 java.util.List타입 인자값이 1개라면 아래와 같이 다른 키 값이 추가 되더라도 정상적으로 캐시를 저장합니다.

@ReadThroughMultiCache
public List<List<Integer>> randomMulti(@ParameterValueKeyProvider Integer fixSize, @ParameterValueKeyProvider(order = 1) List<Integer> multi) {
    ......
}
// argument : fixSize = 2 , multi = List(1,2,3)

stats cachedump 6 100
ITEM local:TMON::COMMON:randomMulti:2,3 [145 b; 1543325621 s]
ITEM local:TMON::COMMON:randomMulti:2,2 [145 b; 1543325621 s]
ITEM local:TMON::COMMON:randomMulti:2,1 [145 b; 1543325621 s]
END

순서를 반대로 해도 정상적으로 저장합니다.

@ReadThroughMultiCache
public List<List<Integer>> randomMulti(@ParameterValueKeyProvider List<Integer> multi, @ParameterValueKeyProvider(order = 1) Integer fixSize) {
    ......
}
// argument : multi = List(1,2,3), fixSize = 2 

stats cachedump 6 100
ITEM local:TMON::COMMON:randomMulti:3,2 [145 b; 1543325799 s]
ITEM local:TMON::COMMON:randomMulti:2,2 [145 b; 1543325799 s]
ITEM local:TMON::COMMON:randomMulti:1,2 [145 b; 1543325799 s]
END

Caution - Return Data

@ReadThroughMultiCache는 반드시 java.util.List타입의 반환값을 가져야합니다.

반환값이 java.util.List이 아닐 경우 메소드 자체는 정상 동작하지만 아래와 같이 오류 메시지를 반환하며 캐시는 저장되지 않습니다.

@ReadThroughMultiCache
public Integer randomMultiFindOne(@ParameterValueKeyProvider List<Integer> multi) {
    ......
}
[2018-11-27 22:30:32] [WARN ] c.g.c.s.a.CacheAdvice.warn[55] Caching on execution(XXXService.randomMultiFindOne(..)) aborted due to an error.
com.google.code.ssm.aop.support.InvalidAnnotationException: The annotation [com.google.code.ssm.api.ReadThroughMultiCache] is only valid on a method that returns a [java.util.List] or its subclass. [public java.lang.Integer test.service.XXXService.randomMultiFindOne(java.util.List)] does not fulfill this requirement.

인자값과 반환값을 쌍으로 캐시에 저장하기 때문에 인자값의 size와 반환값의 size는 동일해야 합니다.
인자값보다 반환값의 size가 더 많거나 적을 경우 아래와 같은 오류 메시지를 남기고 캐시는 저장되지 않습니다.

[2018-11-27 22:38:24] [WARN ] c.g.c.s.a.ReadThroughMultiCacheAdvice.generateByKeysProviders[166] Did not receive a correlated amount of data from the target method: %s. Result list will be unsorted and won't respect the order of the keys passed in argument.

인자값과 반환값의 size가 같을 경우 캐시를 분할하여 저장하게 되는데 인자값과 반환값의 같은 index끼리 저장하게 되기 때문에 반환값의 순서가 중요합니다.
순서가 다를 경우 오류도 없이 캐시가 엉망으로 저장될 수 있습니다.

Conclusion

SSM은 분명 편하게 “Spring + Memcached” 조합을 사용 할 수 있게 해주지만 간단한 만큼 인적오류로 인한 실수를 범할 수 있으며 오류 로그 역시 warn 레벨로 남기기 때문에 잘못을 인지하지 못하고 사용하는 경우가 많습니다.
특히 @ReadThroughMultiCache의 경우 위에서 알아본 바와 같이 개발자가 실수할 수 있는 여지가 많기 때문에 더더욱 신중하게 사용해야 하지만 자동으로 분할하여 캐시를 저장하며 실행시 알아서 캐시되 있지 않은 값만 따로 실행해주기 때문에 분명 매력적인 부분이 존재합니다.

(Spring) ImportAware is not work

|

ImportAware 구현체가 동작하지 않는 이슈가 발생해 구글링해보다가 해결이 되지 않아 직접 스프링 코어 소스를 까서 원인을 확인한 부분에 대해 기록한 글입니다.

Issue

개발중인 모듈을 @EnableXXX 방식으로 어노테이션 지정시 별도 설정없이 동작하기 위해 아래와 같은 Configuration을 Import 하도록 하였습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LocalCacheConfig.class)
public @interface EnableLocalCache {
    ……
}

public class LocalCacheConfig implements ImportAware, BeanFactoryPostProcessor {
    ……

@Override
    public void setImportMetadata(AnnotationMetadata importMetadata) {
        enabled = importMetadata.hasAnnotation(EnableLocalCache.class.getName());
        if (! enabled) {
            return;
        }
……
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if (! enabled) {
            return;
        }

        beanFactory.registerSingleton("localCacheManager", localCacheManager());
    }
}

ImportAware.setImportMetadata 메소드에서 @EnableLocalCache 여부를 파악하고 다른 bean이 생성되기전 BeanFactoryPostProcessor.postProcessBeanFactory 메소드를 통해 localCacheManager를 주입하여 다른 Bean 생성시 주입되는 localCacheManager Bean이 null이 되지 않게하는 처리 부분입니다. 간단하게 테스트용 프로젝트를 만들고 위와 같이 실행시

  1. ImportAware.setImportMetadata
  2. BeanFactoryPostProcessor.postProcessBeanFactory

위 순서대로 동작함을 확인하고 개발을 진행하였습니다.

모듈 개발을 완료하고 실 서비스에 적용하였는데 POC를 진행한 Spring 5.x 버전에서는 이상이 없었으나 Spring 4.x 버전에서는 ImportAware가 실행되지 않아 localCacheManager Bean을 등록하지 못하는 이슈가 발생하였습니다. (사실 이슈가 처음 발생했을때는 버전 문제라는 것 조차 인식하지 못한 상태이긴 합니다.)

Spring Context refresh 주요 단계

// AbstractApplicationContext.java
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();

Spring 기동시 context를 초기화 하는 과정의 일부입니다. BeanFactoryPostProcessor 구현시 두번째 단계인 invokeBeanFactoryPostProcessors메소드를 실행하게 되는데 이 과정에서 bean 생성에 필요한 BeanDefinition을 추가하거나 BeanFactory 생성 후 작업을 진행하게 됩니다.

Spring 4.x

invokeBeanFactoryPostProcessors 메소드 실행과정에서 bean이 Singleton으로 생성되며 beanFactory에 저장되게 됩니다. PriorityOrdered를 구현하거나 @Order어노테이션이 추가되 있지 않다면 위 코드에서 beanFactory.getBean시 생성되어 beanFactory에 저장되게 됩니다.

// PostProcessorRegistrationDelegate.java
// Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : nonOrderedPostProcessorNames) {
   nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

Bean 생성이후 BeanFactoryPostProcessor.postProcessBeanFactory 메소드를 실행한 후 invokeBeanFactoryPostProcessors 메소드 실행과정이 끝나며 이 때 ImportAware.setImportMetadata는 실행되지 않습니다. 이 후 ImportAware.setImportMetadata 메소드가 실행되는 시점은 finishBeanFactoryInitialization 메소드에서 BeanDefinition중 생성되지 않은 bean에 대해 생성하면서 실행되게 되는데 이 때 BeanFactoryPostProcessor를 구현한 구현체는 이미 singleton으로 생성되었기 때문에 더 이상 실행되지 않고 끝나게 됩니다.

Spring 5.x

5.x 버전의 경우 똑같이 invokeBeanFactoryPostProcessors메소드 실행과정을 거치나 BeanDefinitionRegistryPostProcessor중 ConfigurationClassPostProcessor의 postProcessBeanFactory 동작시 차이가 생깁니다.

// PostProcessorRegistrationDelegate.java
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);

BeanFactoryPostProcessor만 구현한 구현체를 처리하기전 BeanDefinitionRegistryPostProcessor를 구현한 구현체를 먼저 처리하는 과정이 있는데 Spring에서 기본적으로 실행하는 구현체로 ConfigurationClassPostProcessor가 존재합니다.

//ConfigurationClassPostProcessor.java
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
   int factoryId = System.identityHashCode(beanFactory);
   if (this.factoriesPostProcessed.contains(factoryId)) {
      throw new IllegalStateException(
            "postProcessBeanFactory already called on this post-processor against " + beanFactory);
   }
   this.factoriesPostProcessed.add(factoryId);
   if (!this.registriesPostProcessed.contains(factoryId)) {
      // BeanDefinitionRegistryPostProcessor hook apparently not supported...
      // Simply call processConfigurationClasses lazily at this point then.
      processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
   }

   enhanceConfigurationClasses(beanFactory);
   beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

위 구현체의 먼저 실행하는 postProcessBeanFactory 메소드를 보면 마지막줄에 beanFactory에 ImportAwareBeanPostProcessor를 추가하는 부분이 있습니다. 이로 인해 bean을 생성할때 ImportAwareBeanPostProcessor.postProcessBeforeInitialization이 실행되면서 ImportAware.setImportMetadata 메소드가 먼저 실행하게 되고 그 이후 BeanFactoryPostProcessor.postProcessBeanFactory 실행되게 됩니다. 참고로 ConfigurationClassPostProcessor는 PriorityOrdered를 구현한 구현체로 실행순서는 최하위 입니다.

@Override
public int getOrder() {
   return Ordered.LOWEST_PRECEDENCE;  // within PriorityOrdered
}

그래서 BeanFactoryPostProcessor가 아닌 BeanDefinitionRegistryPostProcessor를 구현한 구현체와 함께 ImportAware를 사용할 경우 5.x 버전에서도 똑같이 동작하지 않게 됩니다.

Conclusion

  • BeanFactoryPostProcessor는 bean을 생성하기 전 BeanFactory 초기화 이후 실행되며 ImportAware는 bean이 생성되는 시점에 실행되므로 일반적으로 실행시점은 ImportAware가 더 늦게 실행됩니다.
  • bean생성 이전 (또는 BeanFactoryPostProcessor실행 이전)에 로직을 실행하고 싶을 경우 ImportBeanDefinitaionRegistrar를 이용할 수 있습니다.
public class LocalCacheConfig implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        if (!importingClassMetadata.hasAnnotation(EnableLocalCache.class.getName())) {
            return;
        }

        try {
            Map<String, Object> metaData = importingClassMetadata.getAnnotationAttributes(EnableLocalCache.class.getName());
……
        } catch (Exception e) {
            log.error("localcache initialize error : {}", e.getMessage());
        }
    }
}

ConfigurationClassPostProcessor가 postProcessBeanDefinitionRegistry를 실행하면서 ImportBeanDefinitaionRegistrar구현체를 모두 실행해줍니다.

  • POC진행은 꼭 같은버전에서 하세요 ㅠㅠㅠㅠㅠ

어노테이션 반복정의를 위한 @Repeatable 작성법과 주의점

|

개발시 어노테이션을 많이 이용하는 편인데 종종 하나의 클래스, 또는 메소드에 여러 속성을 정의하고 싶을때가 있습니다.
JDK ~1.7 에서는 아래와 같은 방식으로 정의가 가능했습니다.

// case 1
@GreenColor
@BlueColor
@RedColor
public class RGBColor { ... }

// case 2
@Color(colors={"green", "blue", "red"}
public class RGBColor { ... }

JDK 1.8부터는 같은 어노테이션을 중복정의 가능한 @Repeatable 어노테이션을 제공합니다.

@Repeatable Tutorial

JavaDoc에 정의된 정식 설명은 아래와 같습니다.

The annotation type java.lang.annotation.Repeatable is used to indicate that the annotation type whose declaration it (meta-)annotates is repeatable. The value of @Repeatable indicates the containing annotation type for the repeatable annotation type.

@Repeatable 어노테이션을 이용하면 다음과 같이 정의가 가능해집니다.

@Color("green")
@Color("blue")
@Color("red")
public class RGBColor { ... }

어노테이션을 정의하는 방법은 실 사용할 어노테이션과 그 어노테이션 묶음 값을 관리하는 컨테이너 어노테이션을 작성해야 합니다.

@Repeatable(value = Colors.class)
public @interface Color {}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Colors {
    Color[] value();  
}

위와 같이 정의시 아래와 같은 방법으로 해당 클래스에 정의된 어노테이션을 뽑아올 수 있습니다.

Colors colors = this.getClass().getAnnotation(Colors.class);

주의점

위와 같이 설정된 상태에서 아래와 같이 클래스를 작성하게 될 경우 이슈가 있습니다.

public class AnnotationTest {
    @Repeatable(value = Colors.class)
    public @interface Color {
        String value();
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Colors {
        Color[] value();
    }

    @Color("green")
    @Color("blue")
    @Color("red")
    public class RGBColor {}

    @Color("green")
    public class GreenColor {}

    @Test
    public void repeatableAnnotationTest() {
        RGBColor rgbColor = new RGBColor();
        GreenColor greenColor = new GreenColor();

        Colors rgbColors = rgbColor.getClass().getAnnotation(Colors.class);
        Color[] rgbColorArray = rgbColor.getClass().getAnnotationsByType(Color.class);

        Colors greenColors = greenColor.getClass().getAnnotation(Colors.class);
        Color[] greenColorArray = greenColor.getClass().getAnnotationsByType(Color.class);

        System.out.println("rgbColors : " + rgbColors);
        System.out.println("rgbColorArray : " + rgbColorArray);
        System.out.println("rgbColorArray.length : " + (rgbColorArray != null ? rgbColorArray.length : 0));
        System.out.println("greenColors : " + greenColors);
        System.out.println("greenColorArray : " + greenColorArray);
        System.out.println("greenColorArray.length : " + (greenColorArray != null ? greenColorArray.length : 0));
    }
}

테스트 코드를 돌려보면 결과는 아래와 같이 나옵니다.

rgbColors : @com.tmoncorp.module.sduf.test.common.AnnotationTest$Colors(value=[@com.tmoncorp.module.sduf.test.common.AnnotationTest$Color(value=green), @com.tmoncorp.module.sduf.test.common.AnnotationTest$Color(value=blue), @com.tmoncorp.module.sduf.test.common.AnnotationTest$Color(value=red)])
rgbColorArray : [Lcom.tmoncorp.module.sduf.test.common.AnnotationTest$Color;@1996cd68
rgbColorArray.length : 3
greenColors : null
greenColorArray : [Lcom.tmoncorp.module.sduf.test.common.AnnotationTest$Color;@3339ad8e
greenColorArray.length : 0

GreenColor의 클래스는 @Color어노테이션이 1개만 정의되어 있어 @Colors로 묶이지 않고 getAnnotation로 가져올 경우 null을 반환하게 됩니다.
그리고 @Color어노테이션이 1개만 정의되어 있을 경우 @Colors로 묶이지 않아 Retention Policy 정책이 생성되지 않으므로 getAnnotationsByType 메소드 호출시 반환값이 0개가 됩니다.

위와 같은 현상을 해결하기 위해서는 하위 어노테이션 정의시 @Retention을 정의해주고 getAnnotationsByType메소드를 이용하여 값을 찾으면 정확한 반환값을 찾을 수 있습니다.

    @Repeatable(value = Colors.class)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Color {
        String value();
    }

실행하면 아래와 같이 값이 존재하는것을 확인할 수 있습니다.

rgbColors : @com.tmoncorp.module.sduf.test.common.AnnotationTest$Colors(value=[@com.tmoncorp.module.sduf.test.common.AnnotationTest$Color(value=green), @com.tmoncorp.module.sduf.test.common.AnnotationTest$Color(value=blue), @com.tmoncorp.module.sduf.test.common.AnnotationTest$Color(value=red)])
rgbColorArray : [Lcom.tmoncorp.module.sduf.test.common.AnnotationTest$Color;@19e1023e
rgbColorArray.length : 3
greenColors : null
greenColorArray : [Lcom.tmoncorp.module.sduf.test.common.AnnotationTest$Color;@7cef4e59
greenColorArray.length : 1

참고

Oracle JDK Doc - Annotation Type Repeatable
Java 8 Repeating Annotations Tutorial using @Repeatable with examples

Consistent Hashing과 Memecached를 이용한 테스트 샘플

|

Consistent Hashing 이란?

웹 서버의 개수가 수시로 변경될 때 요청에 대해 분산하는 방법으로 Key의 집합을 K, 노드(또는 서버)의 크기를 N라고 했을 때, N의 갯수가 바뀌더라도 대부분의 키들이 노드를 그대로 사용할 수 있습니다.
예를 들어 10000개의 key값을 5개의 노드에 분산할 경우 2000개씩 나뉘는데 개중 하나의 노드가 죽더라도 전체를 재 분배하는 것이 아니라 2000개의 key에 대해서만 재분배 합니다.

Consistent Hashing Node Example

예를 들어 일반 Hash를 이용하여 노드를 분산할 경우 아래 그림과 같은 방식으로 접근 할 수 있습니다.
3으로 나눈 나머지 값을 이용하여 3개 노드중 하나를 접근하도록 설정 할 수 있는데 이런 방식의 경우 하나의 노드가 죽었을 경우 모든 key값에 대해 다시 연산해야하는 문제가 생깁니다.

Compare Hash

Consistent Hashing의 경우 각 노드를 링의 개념으로 연결하여 각 key가 포함되어야 하는 특정 구간을 결정하게 됩니다.
이 과정에서 하나의 노드가 죽는다 하여도 해당 노드에 속한 Key값은 다음 구간을 담당하는 노드에서 처리하게 되므로 나머지 노드의 key를 재분배 하지 않아도 됩니다.

Compare Consistent Hashing

Example: HashRing

위에서 설명한 링의 개념에 대해 실제 key에 따라 어떻게 노드를 결정하는지 아래와 같이 예시를 만들어보았습니다.

분배규칙 : A < 1, B < 3, C < 5, D < 7

다음과 같은 규칙일 때
맨 처음 ‘key=1’이 인입 될 경우 아래와 같이 위치 합니다.

Example 1

그 다음 ‘key=4’가 인입되면 아래와 같이 위치 합니다.

Example 2

그 외 여러 key값이 인입하게 되어 아래와 같은 상황이 되었습니다.

Example 3

이 때 노드 B가 죽고 ‘key=1’이 유실됩니다.

Example 4

노드 B가 죽은 상태에서 ‘key=1’이 다시 인입되면 B의 다음 구간인 C에 포함되게 됩니다.

Example 5

위와 같이 특정 노드가 죽더라도 전체 key를 재분배 하지 않게 됩니다.
하지만 Hashring에도 문제가 있는데 아래와 같이 노드 B가 되살아나고

Example 6

노드 B가 되살아난 상태에서 다시 ‘key=1’가 인입되면 아래 그림과 같이 중복된 key와 value가 존재하게 됩니다.

Example 7

위 상황에서 다시 노드 B가 죽을 경우 노드 C에 있던 이전 값이 노출 될 수 있는 문제가 있는데 memcached의 경우 만료시간을 설정하여 해결 할 수 있습니다.

가상노드

특정 노드가 죽으면 해당 key가 모두 다른 노드에 분배되는데 위 예제에서 보면 노드 B가 죽을 경우 노드 C에 key가 몰리는 것처 보이지만 사실상 아래 그림과 같이 각 노드는 가상의 노드를 여러개 만들어 노드가 죽었을때도 균등하게 분배되도록 처리합니다.

vituralnodes

Memcached 샘플파일

위 이론을 바탕으로 실제 Memcached를 이용하여 Consistent Hashing이 어떻게 동작하는지 확인 할 수 있는 샘플 프로젝트입니다.
Key에 대한 분배및 Proxy 역활은 소스에 포함되어 있는 Simple-Spring-Memcached(ssm)에서 담당하게되며 git을 설치 후 clone 명령을 이용하여 다운받을 수 있습니다.

$ git clone https://github.com/jistol/docker-compose-memcached-multi-test-sample.git    

구성

  • Gradle
  • Spring Boot
  • Google Simple Spring Memcached (SSM)

Memcached 실행

  1. Docker 설치

  2. Memcached Image 다운로드
    • docker pull memcached
  3. docker-compose를 이용하여 실행
    • ${PROJECT_HOME}/docker-compose up -d
    • 위와 같이 실행하면 아래와 같이 3개의 포트로 memcached 서버가 기동됩니다.
$ docker-compose up -d    
Creating network "memcached_default" with the default driver    
Creating memcached_memcached3_1 ... done    
Creating memcached_memcached1_1 ... done    
Creating memcached_memcached2_1 ... done    
$ docker ps -a    
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                      NAMES     
8158e741dacb        memcached           "docker-entrypoint.s…"   13 seconds ago      Up 10 seconds               0.0.0.0:11212->11211/tcp   memcached_memcached2_1    
2627e168914e        memcached           "docker-entrypoint.s…"   13 seconds ago      Up 9 seconds                0.0.0.0:11211->11211/tcp   memcached_memcached1_1    
4c6daf4fc275        memcached           "docker-entrypoint.s…"   13 seconds ago      Up 9 seconds                0.0.0.0:11213->11211/tcp   memcached_memcached3_1    

Test

아래와 같이 2가지 테스트를 할 수 있도록 구성되어 있습니다.

WAS 1 + Memcached 3 : Consistent Hashing 테스트

  • gradle 명령을 이용하여 Tomcat 기동합니다.
$ gradle clean build -x bootRun    
  • 아래 URL을 통해 캐시를 주입합니다.
POST : http://localhost:8080/{key}/{value}
  • telnet으로 Memcached에 접속, 값이 들어갔는지 확인합니다.
$ telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get {key}


$ telnet localhost 11212
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get {key}

$ telnet localhost 11213
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get {key}

위 과정을 반복하면 입력한 KEY값이 동일한 노드의 Memcached에 들어가는것을 볼 수 있습니다.

WAS 3 + Memcached 3 : 서버등록 순서 오류에 의한 키배분 오류 테스트

resource/application.yml 파일을 보면 아래와 같이 각 profiles마다 다르게 서버 순서를 나열해 두었습니다.

server.port: 8080
mem-server: localhost:11211 localhost:11212 localhost:11213

---
spring.profiles: local1
server.port: 8081
mem-server: localhost:11212 localhost:11213 localhost:11211

---
spring.profiles: local2
server.port: 8082
mem-server: localhost:11213 localhost:11211 localhost:11212

이와 같이 설정시 인입된 서버 port에 따라 동일한 key에 대해 ssm이 다르게 분배하는 것을 확인 하는 테스트입니다.

  • gradle 명령을 이용하여 각 profile별로 Tomcat 3대를 기동합니다.
$ gradle clean build -x bootRun    
$ gradle clean build -x bootRun -Dspring.profiles.active=local1   
$ gradle clean build -x bootRun -Dspring.profiles.active=local2   
  • 아래 URL을 통해 캐시를 주입합니다.
POST : http://localhost:8080/{key}/{value}
POST : http://localhost:8081/{key}/{value}
POST : http://localhost:8082/{key}/{value}
  • telnet으로 Memcached에 접속, 값이 들어갔는지 확인합니다.
$ telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get {key}


$ telnet localhost 11212
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get {key}

$ telnet localhost 11213
Trying ::1...
Connected to localhost.
Escape character is '^]'.
get {key}

위 과정을 반복하면 동일한 key를 넣더라도 어느 port에 접근하여 넣었느냐에 따라 위치가 뒤 섞여있는 것을 확인할 수 있습니다.