어노테이션 반복정의를 위한 @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에 접근하여 넣었느냐에 따라 위치가 뒤 섞여있는 것을 확인할 수 있습니다.

Observer 패턴과 Publisher/Subscriber(Pub-Sub) 패턴의 차이점

|

본 글은 토비의 봄 TV 5회 스프링 리액티브 프로그래밍 (1) - Reactive Streams 영상을 보던 중 “Observer패턴과 Pub-Sub패턴의 차이”에 대한 얘기가 나와 궁금해 찾아 본 자료를 정리한 문서입니다.
“Head First Design Patterns” 책에는 Obaserver Pattern == Pub-Sub Pattern으로 나와 있지만 실제 찾아보면 비슷한 개념 사이에 확연한 차이점이 존재합니다.

가장 큰 차이점은 중간에 Message Broker 또는 Event Bus가 존재하는지 여부입니다.

Pattern Notification

Observer패턴은 Observer와 Subject가 서로를 인지하지만 Pub-Sub패턴의 경우 서로를 전혀 몰라도 상관없습니다.

Observer패턴의 경우 Subject에 Observer를 등록하고 Subject가 직접 Observer에 직접 알려주어야 합니다.

Pub-Sub패턴의 경우 Publisher가 Subscriber의 위치나 존재를 알 필요없이 Message Queue와 같은 Broker역활을 하는 중간지점에 메시지를 던져 놓기만 하면 됩니다.
반대로 Subscriber 역시 Publisher의 위치나 존재를 알 필요없이 Broker에 할당된 작업만 모니터링하다 할당 받아 작업하면 되기 때문에 Publisher와 Subscriber가 서로 알 필요가 없습니다.

Observer패턴에 비해 Pub-Sub패턴이 더 결합도가 낮습니다.(Loose Coupling)##

Publisher와 Subscriber가 서로의 존재를 알 필요가 없기 때문에 당연히 소스코드 역시 겹치거나 의존할 일이 없습니다.
만약 결합도가 높다면 의도하거나 잘못된 코딩일 가능성이 큽니다.

Observer패턴은 대부분 동기(synchronous) 방식으로 동작하나 Pub-Sub패턴은 대부분 비동기(asynchronous) 방식으로 동작합니다.##

이유는 Broker로 MessageQueue를 많이 사용하기 때문입니다.

Observer패턴은 단일 도메인 하에서 구현되어야 하나 Pub-Sub패턴은 크로스 도메인 상황에서도 구현 가능합니다.

이 역시 Broker라는 중간 매개체가 있기 때문인데 어플리케이션의 도메인이 다르더라도 MessageQueue(Broker)에 접근만 가능하다면 처리가 가능하기 때문입니다.

참고

Observer vs Pub-Sub pattern : https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c

HTML5(video) + Spring Boot(Tomcat)로 동영상 재생하기

|

HTML5의 video태그를 이용하여 파일 재생시 SpringBoot 에서 어떻게 설정해야하는지 간단한 방법이 있어 정리합니다.

샘플소스

기존 다른 방식들은 response에 파일을 직접 쓰도록 로직에 모두 구현을 했어야 하는데 StreamingResponseBody를 이용하여 아래와 같이 간단해집니다.

...
    private final String DIR = "${FILE_DIR}/";

    @GetMapping("/download")
    public StreamingResponseBody stream(HttpServletRequest req, @RequestParam("fileName") String fileName) throws Exception {
        File file = new File(DIR + fileName);
        final InputStream is = new FileInputStream(file);
        return os -> {
            readAndWrite(is, os);
        };
    }

    private void readAndWrite(final InputStream is, OutputStream os) throws IOException {
        byte[] data = new byte[2048];
        int read = 0;
        while ((read = is.read(data)) > 0) {
            os.write(data, 0, read);
        }
        os.flush();
    }
...

HTML 소스에서는 아래와 같이 호출합니다.

...
    <video controls src="/download?fileName=test.mp4">
        not use video
    </video>
...

원리

  1. 위 방식은 Progressive Download방식으로 서버에서는 요청시마다 전체 파일을 보내주고 video 태그에서는 점진적으로 필요한 만큼씩 OutputStream에서 읽어가게 됩니다. 실제로 readAndWrite 메소드의 while구문에서 로그를 찍어보면 동영상을 재생하지 않을 경우 write를 중간에 멈춰 있는 것을 볼 수 있습니다.

  2. StreamingResponseBody 클래스는 TaskExecutor을 이용하여 비동기 서블릿 실행을 지원해줍니다. Spring API 문서를 보면 아래와 같이 설명이 되어 있습니다.

A controller method return value type for asynchronous request processing where the application can write directly to the response OutputStream without holding up the Servlet container thread.
Note: when using this option it is highly recommended to configure explicitly the TaskExecutor used in Spring MVC for executing asynchronous requests. Both the MVC Java config and the MVC namespaces provide options to configure asynchronous handling. If not using those, an application can set the taskExecutor property of RequestMappingHandlerAdapter.
  1. 비동기이긴 하나 논블락킹은 아니기 때문에 별도 Thread를 계속 점유하고 있는 문제가 있습니다.
  2. 동영상 플레이를 하지 않고 대기시 async timeout 설정을 별도로 하지 않으면 중간에 연결이 끊겨버리며 DISPATCH_PENDING에러를 발생시킵니다. 또한 video태그는 다시 동영상을 재생하기 위해 같은 URL을 또 호출하게 되고 서버는 처음부터 다시 파일을 보내게 됩니다.

참고

Streaming 기술 이해 : http://linuxism.tistory.com/1267 Spring API (StreamingResponseBody): https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.html Itube - Spring Boot Streaming Response Body : https://github.com/shazin/itube

거버넌스(Governance) 모델 정리

|

조대협님의 “대용량 아키텍처와 성능 튜닝”중 거버넌스 모델을 공부하고 생각을 정리하는 차원에서 요약해봅니다.
정확한 정보및 개념은 “대용량 웹서비스를 위한 마이크로 서비스 아키텍쳐의 이해:http://bcho.tistory.com/948” 를 참고하세요.

거버넌스(Governance)란?

  • 시스템을 개발하는 조직구조 / 프로세스를 정의한 것.

중앙 집중형 거버넌스 모델

  • 중앙에서 표준 프로세스 및 규약, 정책을 내려줌.
  • 모두 동일하게 개발하기 때문에 유지보수 편함.

분산형 거버넌스 모델

  • 각 서비스에서 자체 규약으로 개발.
  • 표준 API만 외부로 노출.

분산형 거버넌스 모델을 수행하기 위한 팀구조의 특징은 아래와 같습니다.

Cross functional team

  • 필요한 모든 역활의 인원을 한 팀으로 묶음.
  • 타팀에 대한 의존성이 낮아지기 때문에 빠름 개발 가능.

DevOps

  • 개발 + 운영
  • 피드백에 따른 서비스 개선
  • 인프라까지 조절할 수 있어 개선에 따른 저항은 줄어들었으나 난이도는 높아짐

Project(X), Product(O)

  • 프로젝트별로 팀원 투입이 아닌 프로덕트별 구성.
  • 팀원이 해당 프로덕트에 영속됨으로써 지속적인 서비스 개선과 재교육에 대한 비용을 줄임.

위 모든 조건이 가능해지면 자체적으로 기획/개발/운영이 가능해지는 Self-oranized team 모델이 됨.

주의점 : Alignment

  • 팀 간의 수준 격차를 맞춰야 함 : 특정 서비스는 개발속도를 못따라옴
  • 최소한의 표준이 필요

Conclusion

MSA를 실현하기 위한 조직 구조로 분산형 거버넌스 모델운영에 대해 가이드를 하신것 같습니다.
빠른 서비스 개발및 런칭을 위한 것이 cross functional team인 것 같고, 그 이후 지속적인 개선과 운영을 위한 조직으로 devops를,
이를 효과적으로 운영하기 위해 팀원을 Project가 아닌 Product별로 배치하도록 하며, 주의점으로 팀간 수준을 맞추거나 최소한의 표준정책등을 지정하여 효율적인 운영을 할 수 있다고 요약하면 될 것 같습니다.