728x90
반응형
728x90
반응형
반응형

이전 글: 2022.02.10 - [개발/spring] - [개념] interceptor vs filter 그리고 ContentCachingRequestWrapper

 

[개념] interceptor vs filter 그리고 ContentCachingRequestWrapper

매번 보고 또 봐도 까먹고 또 까먹고 헷갈리고 당황하는 것이 이 쪽 친구들인 것 같다. interceptor, filter, aop, servlet, servlet container, dispatcher servlet... 반복학습을 하면 할수록 점점 마스터할 수..

bangpurin.tistory.com

 

이전에 filter와 interceptor의 차이를 보았는데, 오늘은 filter와 oncePerRequestFilter의 차이를 보려고 한다. 그런데 이게 필턴데 스프링 컨텍스트 안에서 작용한다고? 그럼 또 interceptor를 놓칠 수 없다..

OncePerRequestFilter

OncePerRequestFilter는 Spring Framework에서 제공하는 필터로, Spring의 서블릿 컨테이너에서 동작한다. 이는 Spring의 일반적인 필터 체인에서 동작하는데, Spring이 제공하는 기능이기 때문에 Spring 이외의 서블릿 컨테이너에서는 자동으로 동작하지 않는다. 또한 extend로 필터를 구현하며 @Component로 등록되어 있어야 한다.

A Filter can be called either before or after servlet execution. When a request is dispatched to a servlet, the RequestDispatcher may forward it to another servlet. There's a possibility that the other servlet also has the same filter. In such scenarios, the same filter gets invoked multiple times.

Spring guarantees that the OncePerRequestFilter is executed only once for a given request.

spring은 filter에서 spring config 설정 정보를 쉽게 처리하기 위한 GenericFilterBean을 제공한다.

사용자는 요청을 한 번 보냈지만 redirect나 내부적인 이슈로 한 요청이 여러 번의 요청을 낳게 될 수 있다. 이 경우 인증, 로깅 등 필터가 (의도치 않게) 중첩적으로 호출되는데 이를 방지하고 하는 필터가 OncePerRequestFilter이다. OncePerRequestFilter 역시 GenericFilterBean상속받아 구현되어 있다. 즉 스프링이 제어한다는 것이고, 스프링은 OncePerRequestFilter가 주어진 요청에 대해 단 한 번만 수행되는 것을 보장한다.

OncePerRequestFilter는 다음과 같이 동작합니다:

  • 서블릿 컨테이너에서 Spring 필터 체인이 실행될 때, OncePerRequestFilter는 요청을 가로채서 처리
  • 필터 체인을 통해 요청을 전달할 수 있으며, 필터 체인 내에서 다른 필터들이 중복 실행되지 않도록 보장
  • 필터 체인에서 중복 실행 방지가 필요한 경우에 사용. 일반적인 javax.servlet.Filter는 요청이 FORWARD 또는 INCLUDE로 서블릿 내부에서 재처리될 때 여러 번 호출될 수 있지만, OncePerRequestFilter는 각 요청당 한 번만 실행됩니다.

 

실제 동작 흐름:

아무 설정을 하지 않아도 Spring Boot는 Spring 필터(Specifically, Spring Security 등)를 서블릿 컨테이너 필터보다 먼저 실행되도록 자동으로 설정합니다. 이는 Spring Boot의 자동 구성(Autoconfiguration) 기능과 내부 필터 체인 관리 방식 덕분입니다. Spring Boot는 애플리케이션 시작 시 자동으로 Spring 필터들을 서블릿 컨테이너에서 관리되는 필터보다 우선적으로 실행되도록 설정합니다.

  1. 서블릿 컨테이너가 요청을 받습니다.
  2. Spring Boot는 Spring 필터 체인을 먼저 실행하도록 설정합니다.
  3. Spring에서 관리하는 필터(예: OncePerRequestFilter)가 먼저 실행됩니다.
  4. 그 후, 서블릿 컨테이너에 등록된 일반 필터(javax.servlet.Filter)가 실행됩니다.

once-filter -> filter -> interceptor -> controller -> interceptor -> filter -> once-filter

2024-10-08 15:35:41 INFO  [c.n.a.f.CustomServletWrappingFilter     .doFilterInternal    :  24] [once-filter][REQUEST] [GET] /api/asdf
2024-10-08 15:35:41 INFO  [c.n.aapoker.filter.RequestLogFilter     .doFilter            :  34] [filter][REQUEST] [GET] /api/asdf
2024-10-08 15:35:41 INFO  [c.n.aapoker.filter.RequestInterceptor   .preHandle           :  21] [interceptor][REQUEST] [GET] /api/asdf
2024-10-08 15:35:41 INFO  [c.n.aapoker.filter.RequestInterceptor   .postHandle          :  39] [interceptor][RESPONSE] [GET] /api/asdf 404 - 0.003 ms
2024-10-08 15:35:41 INFO  [c.n.aapoker.filter.RequestLogFilter     .doFilter            :  43] [filter][RESPONSE] [GET] /api/asdf 404 - 0.01ms
2024-10-08 15:35:41 INFO  [c.n.a.f.CustomServletWrappingFilter     .doFilterInternal    :  32] [once-filter][RESPONSE] [GET] /api/asdf 404 - 0.011ms
2024-10-08 15:35:41 INFO  [c.n.aapoker.filter.RequestInterceptor   .preHandle           :  21] [interceptor][REQUEST] [GET] /error
2024-10-08 15:35:41 INFO  [c.n.aapoker.filter.RequestInterceptor   .postHandle          :  39] [interceptor][RESPONSE] [GET] /error 404 - 0.013 ms

에러 화면을 요청할 경우 필터와 onceRequestFilter는 한번씩만 실행되고, 인터셉터는 스프링 진영에서 자동으로 route되는 error화면까지 잡힌다.

onceRequestFilter도 스프링에서 관리되는 필터지만 error를 잡지 않는다는 것을 확인!


참고) ContentCachingRequestWrapper

ContentCachingRequestWrapper는 요청(Request) 본문을 캐싱할 수 있도록 도와주는 래퍼 클래스입니다. 기본적으로, 서블릿 요청의 본문은 한 번만 읽을 수 있는 스트림 형태로 제공됩니다. 하지만 특정 상황에서는 요청 본문을 여러 번 읽어야 하거나, 로깅 또는 분석 목적으로 요청 본문을 캐싱하고자 할 때가 있습니다.

ContentCachingRequestWrapper의 역할:

  • 요청 본문을 메모리에 캐싱하여, 한 번 이상 읽을 수 있도록 지원.
  • 요청 본문을 나중에 로그로 남기거나, 요청 데이터를 분석할 때 유용.
728x90
반응형
반응형

이번 글은 이전에 다뤘던 chaining test와 bulk request test의 연장선이다.

2022.02.08 - [서버 세팅 & tool/postman] - [postman] chaining sample test

 

[postman] chaining sample test

이전 글: 2022.02.08 - [서버 세팅, tool 사용법/postman] - [test] environment variable setting & snippets [test] environment variable setting & snippets 이전 글: 2022.02.08 - [서버 세팅, tool 사용법..

bangpurin.tistory.com

2022.02.23 - [서버 세팅 & tool/postman] - [postman] 파일로 bulk request를 불러와서 api test하기

 

[postman] 파일로 bulk request를 불러와서 api test하기

이전 글: 2022.02.08 - [서버 세팅 & tool/postman] - [postman] chaining sample test [postman] chaining sample test 이전 글: 2022.02.08 - [서버 세팅, tool 사용법/postman] - [test] environment variabl..

bangpurin.tistory.com

 

api 두 개를 순차적으로 요청할 때, 첫 번째 api 결과로 얻은 list안의 어떤 값을 두 번째 api의 요청 값에 넣어 순차적으로 테스트하는 방법을 알아본다. 이전 글에서 다룬 chaining test랑도 비슷하지만, chaining test에서는 object안의 항목을 variable로 set 해서 전달했다면, 여기서는 마치 for loop을 도는 것처럼 list안의 항목을 하나씩 돌아 테스트한다.

이전에 다룬 bulk test와도 비슷하지만 별도의 csv파일을 활용하지 않고 진행하는 것에 차이가 있다.

 

예를 들어 첫 번째 api의 결과가 다음과 같다고 가정하자.

{
    "playerInfos": [
        {
            "createdTime": "20171130144206",
            "updatedTime": "20190221153455",
            "playerId": "000120000000000",
            "bizLicenseNo": "1128131171",
            "name": " test",
            "status":"02"
        },
        {
            "createdTime": "20170825181332",
            "updatedTime": "20190221184742",
            "playerId": "000090000000000",
            "bizLicenseNo": "6068637012",
            "name": "미진테크",
            "status":"02"
        },
        {
            "createdTime": "20171106090524",
            "updatedTime": "20190221184837",
            "playerId": "000113000000000",
            "bizLicenseNo": "4558700713",
            "name": "맛있는우동",
            "status":"02"
        },
        {
            "createdTime": "20170728144046",
            "updatedTime": "20190221152314",
            "playerId": "000079000000000",
            "bizLicenseNo": "2140125811",
            "status":"02"
            "name": "공룡핸드폰",
        }
    ...
}

여기서 status = '01' 이면 active, '01'이 아니면 inactive이다. 원하는 것은 active 한 object의 playerId를 꺼내서 다음 api의 request항목(여기서는 path param)에 넣어 순차적으로 요청하는 것이라면, 테스트 코드를 어떻게 작성해야 할까.

전체 흐름을 다음과 같이 생각해볼 수 있다.

  1. 첫 번째 api결과를 json으로 바꾼다.
  2. playerInfos의 object를 순차적으로 돌면서 status = '01' 이면 playerId를 playerIds라는 array에 담는다.
  3. playerIds를 env-variable이나 collection-variable로 설정한다(여기서는 collection-variable로 지정한다).
  4. 두 번째 api에서 collection-variable로 설정된 playerIds를 꺼내서 맨 앞을 꺼내서 변수(playerId)로 설정하고 array에서 지운다.
  5. 수정된 array(playerIds)를 다시 collection-variable로 지정한다.
  6. 두 번째 api의 status code가 200인지 확인하다.
  7. playerIds array의 길이가 0보다 크면 두 번째 api를 요청한다. 그렇지 않으면 종료한다.

 

위 흐름은 아래와 같이 정리된다.

  • 첫 번째 api 요청 후에 세팅해야 하는 것: 1, 2, 3
  • 두 번째 api 요청 전에 세팅해야 하는 것: 4, 5
  • 두 번째api 요청 후에 세팅해야하는 것: 6, 7

 

1. 첫 번째 api 테스트 작성

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

if(pm.response.to.have.status(200)){  //정상요청했다면
    var json = pm.response.json();   //1. 결과를 json으로 담고
    //console.log(json.playerInfos)

    //2. json 중 status가 01인 아이들인 object를 모으고, object 중 playerId만 모아 playerIds에 넣는다.
    let playerIds = json.playerInfos.filter((i) => i.status == '01').map(e => e.playerId);
    //console.log(playerIds);
    pm.collectionVariables.set("playerIds", playerIds);  //3. collection-var로 세팅
}

 

두 번째 api url은 다음 형식이다.

[get] www.abc.com/asps/{{aspId}}  

aspId를 collection-var로 세팅해서 자동으로 가져가게 할 생각이다.

 

2. 두번째 api 테스트 작성 - pre-request script

let playerIds = pm.collectionVariables.get("playerIds"); //4. collection-var에서 꺼냄
let aspId = playerIds.shift(); //5. playerIds array에서 맨 처음 항목 꺼내서 aspId라는 변수로 담고 playerIds에서 지움
pm.collectionVariables.set("aspId", aspId); //5. 변수세팅
pm.collectionVariables.set("playerIds", playerIds); //6. 수정된 array 재세팅

postman test에서는 javascript가 사용 가능하기 때문에 여기서는 shift 함수를 이용했다. 상황에 따라 다양한 함수를 활용 가능하다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift

 

Array.prototype.shift() - JavaScript | MDN

The shift() method removes the first element from an array and returns that removed element. This method changes the length of the array.

developer.mozilla.org

 

3. 두 번째 api 테스트 작성 - test

const playerIds = pm.collectionVariables.get("playerIds");

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);  //6. 현재 요청이 성공인지 우선 확인
});

if(playerIds && playerIds.length > 0){ //collection-var의 array에 데이터가 있는지 확인하고, 있으면
    postman.setNextRequest("get affiliate by asp"); //7. 자기자신을 다시 실행
}else{
    postman.setNextRequest(null); //7. 없으면 끝
}

 

이렇게 두고 테스트하니 아래처럼 첫 번째 api는 1번 두 번째 api는 playerIds array의 크기(여기서는 29)만큼의 루프를 모두 돌아 정상 실행됨을 확인할 수 있었다.

728x90
반응형
반응형

오늘은 두 번째 강의이다.

 

operator라고 publisher와 subscriber사이에서 데이터 변형을 도와주는 것을 만들어본다.

publisher -> [data1] -> operator1 -> [data2] -> oper2 -> [data3] -> subscriber

이렇게 operator1은 publisher에게는 subscriber, oper2에게는 publisher의 역할을 하게 된다. 

 

1. map (1-1 mapping) 작성

public static void main(String[] args){
    List<Integer> list = Stream.iterate(1, i -> i+1) //무한으로 계속 만듦
            .limit(10) //제한을 줘야함
            .collect(Collectors.toList());

    Flow.Publisher<Integer> pub = iterPub(list);

    Flow.Publisher<Integer> mapPub = mapPub(pub, (Function<Integer, Integer>) s -> s * 10); //d2로 만든다
    Flow.Publisher<Integer> map2Pub = mapPub(mapPub, (Function<Integer, Integer>) s -> -s); //d3로 만든다
    map2Pub.subscribe(logSub());
}

private static Flow.Publisher<Integer> mapPub(Flow.Publisher<Integer> pub, Function<Integer, Integer> f) {
    return new Flow.Publisher<Integer>() {
        @Override
        public void subscribe(Flow.Subscriber<? super Integer> subscriber) { //logSub
            pub.subscribe(new DelegateSub(subscriber) {
                @Override
                public void onNext(Integer item) {
                    subscriber.onNext(f.apply(item));
                }
            });
        }
    };
}

private static Flow.Subscriber<Integer> logSub() {
    return new Flow.Subscriber<Integer>() {
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            log.debug("onSubscribe!");
            subscription.request(Long.MAX_VALUE); //무제한으로 다 줘
            //subscription.cancel(); //어떠한 이유로 받는 사람이 데이터가 필요 없어질 때
        }

        @Override
        public void onNext(Integer item) { //정상 데이터
            log.debug("onNext:{}", item);
        }

        @Override
        public void onError(Throwable throwable) { //익셉션을 던지는게 아니라 받아서 처리
            log.debug("onError:{}", throwable);
        }

        @Override
        public void onComplete() { //완료 시그널
            log.debug("onComplete!");
        }
    };
}

private static Flow.Publisher<Integer> iterPub(List<Integer> iter) {
    return new Flow.Publisher<Integer>() {
        @Override
        public void subscribe(Flow.Subscriber<? super Integer> subscriber) {
            //반드시 호출해줘야 함, subscription = 구독이 담고 있는 액션
            subscriber.onSubscribe(new Flow.Subscription() {
                @Override
                public void request(long n) { //갯수는 신경쓰지 말자
                    try{
                        iter.forEach(s -> subscriber.onNext(s));
                        subscriber.onComplete(); //다 보내면 완료를 알려줘야함
                    }catch (Throwable t){
                        subscriber.onError(t);
                    }
                }

                @Override
                public void cancel() {

                }
            });
        }
    };
}

위 코드에서 흐름은 아래와 같다. pub -> logSub로 데이터가 흘러가는걸 downstream이라고 하고 반대 방향으로 올라오는 것을 upstream이라고 부른다.

pub -> [data1] -> mapPub -> [data2] -> logSub
                        <- subscribe(logSub)
                        -> onSubscribe(s)
                        -> onNext
                        -> onNext
                        -> onComplete

일대일 매핑 방식이라 (1-> 10; 2-> 20; ...) mapSub의 onNext에서 매번 logSub의 onNext를 불러 데이터를 전송하는 것을 알 수 있다.

 

2. reduce 작성

public static void main(String[] args){
    List<Integer> list = Stream.iterate(1, i -> i+1) //무한으로 계속 만듦
            .limit(10) //제한을 줘야함
            .collect(Collectors.toList());

    Flow.Publisher<Integer> pub = iterPub(list);

    //Flow.Publisher<Integer> sumPub = sumPub(pub); //합계
    //sumPub.subscribe(logSub());

    Flow.Publisher<Integer> reducePub = reducePub(pub, 0, (BiFunction<Integer, Integer, Integer>)(a, b) -> a+b ); //합계
    reducePub.subscribe(logSub());
}

//1, 2, 3, 4, 5
// 0 -> (0, 1) -> 1
// 1 -> (1, 2) -> 3
// 3 -> (3, 3) -> 6
// ...
private static Flow.Publisher<Integer> reducePub(Flow.Publisher<Integer> pub, int init, BiFunction<Integer, Integer, Integer> bf) {
    return new Flow.Publisher<Integer>() {
        @Override
        public void subscribe(Flow.Subscriber<? super Integer> subscriber) {
            pub.subscribe(new DelegateSub(subscriber){
                int result = init;
                @Override
                public void onNext(Integer item) {
                    result = bf.apply(result, item);
                }

                @Override
                public void onComplete() {
                    subscriber.onNext(result);
                    subscriber.onComplete();
                }
            });
        }
    };
}

private static Flow.Publisher<Integer> sumPub(Flow.Publisher<Integer> pub) {
    return new Flow.Publisher<Integer>() {
        @Override
        public void subscribe(Flow.Subscriber<? super Integer> subscriber) {
            pub.subscribe(new DelegateSub(subscriber){
                int sum = 0;
                @Override
                public void onNext(Integer item) {
                    sum += item;
                }

                @Override
                public void onComplete() {
                    subscriber.onNext(sum);
                    subscriber.onComplete();
                }
            });
        }
    };
}

private static Flow.Subscriber<Integer> logSub() {
    return new Flow.Subscriber<Integer>() {
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            log.debug("onSubscribe!");
            subscription.request(Long.MAX_VALUE); //무제한으로 다 줘
            //subscription.cancel(); //어떠한 이유로 받는 사람이 데이터가 필요 없어질 때
        }

        @Override
        public void onNext(Integer item) { //정상 데이터
            log.debug("onNext:{}", item);
        }

        @Override
        public void onError(Throwable throwable) { //익셉션을 던지는게 아니라 받아서 처리
            log.debug("onError:{}", throwable);
        }

        @Override
        public void onComplete() { //완료 시그널
            log.debug("onComplete!");
        }
    };
}

private static Flow.Publisher<Integer> iterPub(List<Integer> iter) {
    return new Flow.Publisher<Integer>() {
        @Override
        public void subscribe(Flow.Subscriber<? super Integer> subscriber) {
            //반드시 호출해줘야 함, subscription = 구독이 담고 있는 액션
            subscriber.onSubscribe(new Flow.Subscription() {
                @Override
                public void request(long n) { //갯수는 신경쓰지 말자
                    try{
                        iter.forEach(s -> subscriber.onNext(s));
                        subscriber.onComplete(); //다 보내면 완료를 알려줘야함
                    }catch (Throwable t){
                        subscriber.onError(t);
                    }
                }

                @Override
                public void cancel() {

                }
            });
        }
    };
}

합계와 같이 n개의 데이터가 하나의 데이터로 흡수되어 내려갈 때에는 onNext에서는 필요한 계산을 하고 onComplete에서 부모의 onNext를 불러 데이터를 한 번만 전송해주고, onComplete를 불러 더 이상 데이터가 없음을 알려준다.

 

3. generic으로 변환

public static void main(String[] args){
    List<Integer> list = Stream.iterate(1, i -> i+1) //무한으로 계속 만듦
            .limit(10) //제한을 줘야함
            .collect(Collectors.toList());

    Flow.Publisher<Integer> pub = iterPub(list);

    Flow.Publisher<String> mapPub = mapPub(pub, (Function<Integer, String>) s -> "[" +s+ "]"); //d2로 만든다
    mapPub.subscribe(logSub());

    //Flow.Publisher<String> reducePub = reducePub2(pub, "", (BiFunction<String, Integer, String>)(a, b) -> a + "-" + b );
    Flow.Publisher<StringBuilder> reducePub = reducePub2(pub, new StringBuilder(), (a, b) -> a .append(b+","));
    reducePub.subscribe(logSub());

}

//generic으로 전환
//publisher가 발행한 T(int) -> R(string)으로 전환해주는 매개체(pub이자 sub)
private static <T, R> Flow.Publisher<R> mapPub(Flow.Publisher<T> pub, Function<T, R> f) {
    return new Flow.Publisher<R>() {
        @Override
        public void subscribe(Flow.Subscriber<? super R> subscriber) { //logSub
            pub.subscribe(new DelegateSub<T, R>(subscriber) {
                @Override
                public void onNext(T item) {
                    subscriber.onNext(f.apply(item));
                }
            });
        }
    };
}

private static <T, R> Flow.Publisher<R> reducePub2(Flow.Publisher<T> pub, R init, BiFunction<R, T, R> bf) {
    return new Flow.Publisher<R>() {
        @Override
        public void subscribe(Flow.Subscriber<? super R> subscriber) {
            pub.subscribe(new DelegateSub<T, R>(subscriber){
                R result = init;

                @Override
                public void onNext(T item) {
                    result = bf.apply(result, item);
                }

                @Override
                public void onComplete() {
                    subscriber.onNext(result);
                    subscriber.onComplete();
                }
            });
        }
    };
}

//publisher가 발행한 숫자를 변환한 최종 값(string)을 찍기만 하는 subscriber
private static <T> Flow.Subscriber<T> logSub() {
    return new Flow.Subscriber<T>() {
        @Override
        public void onSubscribe(Flow.Subscription subscription) {
            log.debug("onSubscribe!");
            subscription.request(Long.MAX_VALUE); //무제한으로 다 줘
            //subscription.cancel(); //어떠한 이유로 받는 사람이 데이터가 필요 없어질 때
        }

        @Override
        public void onNext(T item) { //정상 데이터
            log.debug("onNext:{}", item);
        }

        @Override
        public void onError(Throwable throwable) { //익셉션을 던지는게 아니라 받아서 처리
            log.debug("onError:{}", throwable);
        }

        @Override
        public void onComplete() { //완료 시그널
            log.debug("onComplete!");
        }
    };
}

 

4. reactor 라이브러리 사용

위에서 손으로 짠 코드는 사실 reactor lib를 사용하면 손쉽게 작성할 수 있다.

implementation 'io.projectreactor:reactor-core:3.4.15'

위 dependency를 추가하고 아래와 같이 진행한다.

 

public static void main(String[] args){
    Flux.create(e -> {
        e.next(1);
        e.next(2);
        e.next(3);
        e.complete();
    })
    .log()
    .subscribe(System.out::println);
}
//로그
0    [main] DEBUG reactor.util.Loggers  - Using Slf4j logging framework
15   [main] INFO  reactor.Flux.Create.1  - onSubscribe(FluxCreate.BufferAsyncSink)
16   [main] INFO  reactor.Flux.Create.1  - request(unbounded)
18   [main] INFO  reactor.Flux.Create.1  - onNext(1)
1    //sysout 결과
18   [main] INFO  reactor.Flux.Create.1  - onNext(2)
2    //sysout 결과
18   [main] INFO  reactor.Flux.Create.1  - onNext(3)
3    //sysout 결과
19   [main] INFO  reactor.Flux.Create.1  - onComplete()

로그(log())를 같이 살펴보면 

  1. flux의 .subscribe를 만나면 타고타고 올라가서 onSubscribe를 먼저 실행하고
  2. request(Long.MAX) -> unbounded로 표현되어 있는 것이고
  3. onNext -> -> onComplete를 실행하는 것을 알 수 있다.

 

<map>

public static void main(String[] args){
    Flux.<Integer>create(e -> {
        e.next(1);
        e.next(2);
        e.next(3);
        e.complete();
    })
    .log()
    .map(s -> s*10)
    .log()
    .subscribe(System.out::println);
}
14   [main] INFO  reactor.Flux.Create.1  - onSubscribe(FluxCreate.BufferAsyncSink)
15   [main] INFO  reactor.Flux.Map.2  - onSubscribe(FluxMap.MapSubscriber)
15   [main] INFO  reactor.Flux.Map.2  - request(unbounded)
15   [main] INFO  reactor.Flux.Create.1  - request(unbounded)
17   [main] INFO  reactor.Flux.Create.1  - onNext(1)
17   [main] INFO  reactor.Flux.Map.2  - onNext(10)
10   // sysout 결과
17   [main] INFO  reactor.Flux.Create.1  - onNext(2)
17   [main] INFO  reactor.Flux.Map.2  - onNext(20)
20   //sysout 결과
17   [main] INFO  reactor.Flux.Create.1  - onNext(3)
17   [main] INFO  reactor.Flux.Map.2  - onNext(30)
30   //sysout 결과
18   [main] INFO  reactor.Flux.Create.1  - onComplete()
18   [main] INFO  reactor.Flux.Map.2  - onComplete()

로그를 위아래 걸어줬더니 위와 같은 로그가 찍혔다. Flux.Create. 로그는 위 log(), Flux.Map. 로그는 아래 log()라고 보면 된다. 위에서는 1이 넘어갔지만 map operator를 건너서 10이 되어 넘어가는 것을 볼 수 있다.

 

<reduce>

public static void main(String[] args){
    Flux.<Integer>create(e -> {
        e.next(1);
        e.next(2);
        e.next(3);
        e.complete();
    })
    .log()
    .map(s -> s*10)
    .log()
    .reduce(0, (a, b) -> a+b)
    .log()
    .subscribe(System.out::println);
}
45   [main] INFO  reactor.Flux.Create.1  - onSubscribe(FluxCreate.BufferAsyncSink)
46   [main] INFO  reactor.Flux.Map.2  - onSubscribe(FluxMap.MapSubscriber)
46   [main] INFO  reactor.Mono.ReduceSeed.3  - | onSubscribe([Fuseable] MonoReduceSeed.ReduceSeedSubscriber)
46   [main] INFO  reactor.Mono.ReduceSeed.3  - | request(unbounded)
46   [main] INFO  reactor.Flux.Map.2  - request(unbounded)
47   [main] INFO  reactor.Flux.Create.1  - request(unbounded)
48   [main] INFO  reactor.Flux.Create.1  - onNext(1)
48   [main] INFO  reactor.Flux.Map.2  - onNext(10)
48   [main] INFO  reactor.Flux.Create.1  - onNext(2)
48   [main] INFO  reactor.Flux.Map.2  - onNext(20)
48   [main] INFO  reactor.Flux.Create.1  - onNext(3)
49   [main] INFO  reactor.Flux.Map.2  - onNext(30)
49   [main] INFO  reactor.Flux.Create.1  - onComplete()
49   [main] INFO  reactor.Flux.Map.2  - onComplete()
49   [main] INFO  reactor.Mono.ReduceSeed.3  - | onNext(60)
60   //sysout 결과
49   [main] INFO  reactor.Mono.ReduceSeed.3  - | onComplete()

reduce operator를 마지막에 추가하고 보면 매 숫자를 받아서(create) 10을 곱하고(map) onNext를 계속하다가 마지막에 오면(onComplete) reduce를 실행해서 최종적으로 60을 받는 것을 볼 수 있다.


위 1~3 과정을 거치고 reactor lib를 사용한 후 로그를 보니 왜 그렇게 로그가 지나가는지 이해하기가 쉬웠다. 왜 굳이 예전 방식으로 코딩을 하시는걸까 궁금했는데 기본적인 원리를 손으로 짜보고 나니까 흐름을 파악하는데 큰 도움이 되는 것 같다!

728x90
반응형
반응형

reactive programming과 관련하여 토비의 라이브 10강을 시청하고 요약정리하려고 한다.

 

1. Iterable vs Observable (duality)

  • Iterable(pull) 방식:   사용하고자 하는 사람이 가져다가 쓴다.
  • Iterable#next
public static void main(String[] args){
    Iterable<Integer> iter = () ->
            new Iterator<>() {
                int i = 0;
                final static int MAX = 10;

                public boolean hasNext() {
                    return i < MAX;
                }

                public Integer next() {
                    return ++i;
                }
            };

    for(Integer i : iter){ //for each
        System.out.println(i);
    }

    for(Iterator<Integer> it = iter.iterator(); it.hasNext();){
        System.out.println(it.next());  //pull 데이터를 꺼내자
    }
}
  • Observable(push) 방식: 데이터를 주면 그걸 받아서 처리하는 방법
  • Observable#notifyObservers
@SuppressWarnings("deprecation")
static class IntObservable extends Observable implements Runnable{
    @Override
    public void run() {
        for(int i=1; i<=10; i++){
            setChanged(); //데이터 바뀌었으니 가져가라고 설정
            notifyObservers(i); //push방식으로 옵저버에게 데이터를 주면
        }
    }
}

public static void main(String[] args){
    Observer ob = new Observer() {
        @Override
        public void update(Observable o, Object arg) {
            //받아
            System.out.println(Thread.currentThread().getName() + " " + arg);
        }
    };
    IntObservable io = new IntObservable();
    io.addObserver(ob); //리스너 등록

    //io.run(); //실행

    //옵저버 패턴은 별도의 스레드에서 작업하기 수월
    ExecutorService es = Executors.newSingleThreadExecutor(); //따로 할당받은 스레드에서
    es.execute(io); //run이 실행됨
    System.out.println(Thread.currentThread().getName() + " DONE"); //메인 스레드에서 실행
    es.shutdown(); //스레드 종료
}

추가) 위 내용에 대해 이해하기 위해서는 옵저버 패턴(Observer pattern)을 필수로 알아야 한다.

옵저버 패턴? 
객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고, 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.

위 내용에 맞게 스스로 관련 내용을 구현할 수 있지만 자바에서 제공하는 Observer 인터페이스와 Observable 클래스를 가지고 구현할 수도 있다.

Observable 클래스를 상속받은 게 "데이터 제공자" Observer 인터페이스를 구현한 게 "데이터 받는 자"이다.

참고로 Observable클래스는 Java9부터 deprecated라서 관련 경고를 없애기 위해 아래 어노테이션을 넣는다.

@SuppressWarnings("deprecation")

 

옵저버 패턴에서는 다음과 같은 이슈가 있다.

  1. complete? 마지막이 언제인지 알기 힘들다.
  2. error? 진행되다 익셉션이 나면? 전파되는 방식이나 에러는 어떻게 처리? fallback?  관련 아이디어가 패턴에 녹아져 있지 않다.

이에 나오게 된 사상이 reactive stream이다.

 

2. reactive stream(스펙)

reactive stream이란 non-blocking(논 블럭킹) backPressure(역압)을 이용하여 비동기 서비스를 할 때 기본이 되는 스펙으로 아래와 같은 특징이 있다.

  1. 잠재적으로 무한한 양의 데이터 처리
  2. 순서대로 처리
  3. 데이터를 비동기적으로 전달
  4. backpressure를 이용한 데이터 흐름 제어

또한 4가지 컴포넌트로 구성되어 있는데 다음과 같다.

  1. publisher: 데이터 제공자(옵저버 패턴의observable)
    • Publisher.subscribe(Subscriber)
  2. subscriber: 데이터를 받아서 사용하는 쪽(옵저버 패턴의 observer)
    • onSubscribe, onNext ...
  3. subscription
  4. proessor

참고로 Java9에 추가된 Flow는 reactvie stream 스펙을 채택하여 사용하고 있다. reative stream 스펙에 대한 내용은 여기에서 확인하기로 하고 이제 소스를 살펴보자.

public static void main(String[] args) throws InterruptedException {
    Iterable<Integer> itr = Arrays.asList(1, 2, 3, 4, 5);
    ExecutorService es = Executors.newSingleThreadExecutor();

    Flow.Publisher p = new Flow.Publisher() {
        @Override
        public void subscribe(Flow.Subscriber subscriber) {
            Iterator<Integer> it = itr.iterator();

            subscriber.onSubscribe(new Flow.Subscription() {
                @Override
                public void request(long n) { //몇 개줄까; 갯수만큼 받고 싶어
                    es.execute(() -> {
                    //스레스 한정 위반이라 n을 안에서 수정 못해, i를 별도로 설정
                        int i = 0;
                        try{
                            while(i++ < n){
                                if(it.hasNext()){
                                    subscriber.onNext(it.next());
                                }else {
                                    subscriber.onComplete();
                                    break;
                                }
                            }
                        }catch (RuntimeException e){
                            subscriber.onError(e);
                        }
                    });
                }

                @Override
                public void cancel() { //중간에 작업 취소 시킬 수 있음

                }
            });
        }
    };

    Flow.Subscriber<Integer> s = new Flow.Subscriber<Integer>() {
        Flow.Subscription subscription; //잠시 저장하자
        @Override
        public void onSubscribe(Flow.Subscription subscription) { //구독이 시작되었어
            System.out.println(Thread.currentThread().getName() + " > onSubscribe ");
            //request
            this.subscription = subscription;
            this.subscription.request(1); //Long.MAX = 다 내놔
        }

        @Override
        public void onNext(Integer item) { //update; 데이터 주면 받아서 처리해; 끌어와 데이터를
            System.out.println(Thread.currentThread().getName() + " > onNext " + item); //받을 준비 되었어
            //버퍼사이즈, cpu 등에 따라 별도로 조절가능
            this.subscription.request(1);//하나 줘
        }

        @Override
        public void onError(Throwable throwable) { //에러가 나면 에러를 나 줘; 재시도 등 처리가능; 구독 끝
            System.out.println(Thread.currentThread().getName() + " > onError " + throwable.getMessage());
        }

        @Override
        public void onComplete() { //다 완료되었어; 구독 끝
            System.out.println(Thread.currentThread().getName() + " > onComplete ");
        }
    };

    p.subscribe(s); //s가 p를 리슨
   // es.shutdown();
    es.awaitTermination(10, TimeUnit.SECONDS);
    es.shutdown();
}

소스 각 라인에 주석으로 설명을 달았으며 큰 흐름은 아래 그림을 보고 이해하면 된다.

reactive stream from line

여기서의 특징은 publisher와 subscriber 사이에 subscription이라는 데이터이자 중계자를 거쳐서 구독과 관련된 설정을 할 수 있다는 점이다.

 


reactive stream 표준 및 명세: https://www.reactive-streams.org/

 

https://www.reactive-streams.org/

Reactive Streams Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure. This encompasses efforts aimed at runtime environments (JVM and JavaScript) as well as network protocols. JDK9 java

www.reactive-streams.org

reactive stream 설명: https://engineering.linecorp.com/en/blog/reactive-streams-armeria-1/

 

Let's Play with Reactive Streams on Armeria - Part 1 - LINE ENGINEERING

What is Reactive Streams? In this post, I'd like to introduce the basic concept of Reactive Streams, and how to use Reactive Streams with Armeria, the

engineering.linecorp.com

https://sabarada.tistory.com/98

 

[Java] Reactive Stream 이란?

reactive stream이란 non-blocking(넌블럭킹) backPressure(역압)을 이용하여 비동기 서비스를 할 때 기본이 되는 스펙입니다. java의 RxJava, Spring5 Webflux의 Core에 있는 ProjectReactor 프로젝트 모두 해당..

sabarada.tistory.com

 

728x90
반응형
반응형

JVM (Java Virtual Machine)

  • 자바 가상 머신으로 자바 바이트 코드(.class 파일)를 OS에 특화된 코드로 변환(인터프리터와 JIT 컴파일러)하여 실행
  • 바이트 코드를 실행하는 표준(JVM 자체는 표준)이자 구현체(특정 밴더가 구현한 JVM)
  • 특정 플랫폼에 종속적

JRE (Java Runtime Environment): JVM + 라이브러리

  • 자바 애플리케이션을 실행할 수 있도록 구성된 배포판
  • JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있음
  • 개발 관련 도구(JDK)는 포함하지 않음

JDK (Java Development Kit): JRE + 개발 툴

  • JRE + 개발에 필요할 툴
  • 소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적
  • 오라클은 자바 11부터는 JDK만 제공하며 JRE를 따로 제공하지 않음
  • Write Once Run Anywhere

Java

  • 프로그래밍 언어
  • JDK에 들어있는 자바 컴파일러(javac)를 사용하여 바이트코드(.class 파일)로 컴파일
  • 자바 유료화? 오라클에서 만든 Oracle JDK 11 버전부터/ 상용으로 사용할 때 유료이며 나머지는 무료

JVM 구조

jvm

Class Loader

  • .class에 있는 바이트코드를 읽고 메모리에 저장
  • loading: 클래스 읽어오는 과정
  • linking: 레퍼런스를 연결하는 과정
  • initialization: static 값들 초기화 및 변수에 할당

메모리

  • 메소드 영역에는 클래스 수준의 정보(클래스 이름, 부모 클래스 이름, 메소드, 변수) 저장
  • 힙 영역에는 객체를 저장
  • 스택 영역에는 스레드 마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택 프레임이라 부르는 블록으로 쌓음. 스레드 종료하면 런타임 스택도 삭제됨
  • PC(Program Counter) 레지스터: 스레드 마다 스레드 내 현재 실행할 instruction의 위치를 가리키는 포인터가 생성됨
  • 네이티브 메소드 스택: Native Method를 호출하는 코드를 수행하기 위한 스택

Execution engine

  • interpreter: 바이트 코드를 한 줄씩 실행
  • JIT compiler: 인터프리터 효율을 높이기 위해, 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러는 반복되는 코드를 모두 네이티브 코드로 바꿈. 그다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용할 수 있도록 지원
  • GC(Garbage Collector): 더이상 참조되지 않는 객체를 모아서 주기적으로 정리하는 프로그램

JNI(Java Native Interface)

  • 자바 애플리케이션에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있게 함
  • native 키워드를 사용한 메소드 호출

Native Method Library

  • C, C++로 작성된 라이브러리

 

garbage?

  • 주소를 잃어버려서 사용할 수 없는 메모리, 정리되지 않은 메모리, dangling object

garbage collection(GC)?

  • garbage의 메모리 해제; JVM이 한가할 때(idle) 혹은 메모리가 부족해져 OS에게 추가로 메모리를 할당해달라고 요청할 때 실행

 

JVM의 Heap영역의 대전제

  • 대부분의 객체는 대부분 일회성이며 금방 접근 불가능한 상태(Unreachable)가 된다.
  • 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
  • 생존 기간에 따라 heap을 두 가지 영역으로 쪼갬(Young, Old 영역) - 초기에는 Perm 영역이 존재하였지만 Java8부터 제거됨

 

minor gc?

  • 새롭게 생성된 객체가 할당(Allocation)되는 Young 영역에 대한 가비지 컬렉션(Garbage Collection)
  • 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young영역에 생성되었다가 사라짐
    • The Young Generation is further divided into three spaces: Eden space and two Survivor spaces (S0 and S1).
  • eden 영역이 꽉 찬 경우 발생, 빠름
    • eden 영역에서 사용되지 않아진 객체의 메모리 해제
    • eden 영역에서 살아남은 객체는 s1/s2로 이동(s1이 꽉차면 s2, vice versa)
       

major gc/full gc?

  • minor gc속에서 살아남은 객체가 복사되는 Old 영역에 대한 가비지 컬렉션(Garbage Collection)
  • 복사되는 과정에서 대부분 Young 영역보다 크게 할당되며, 크기가 큰 만큼 가비지는 적게 발생함
  • old 영역이 꽉 찬 경우 발생, 느림

 

구체적으로 어떻게?

  • Stop the world(STW): GC를 실행하기 위해 JVM이 어플리케이션의 실행을 멈추는 작업
    • GC를 실행하는 스레드를 제외한 모든 스레드의 작업이 중단되고, GC가 완료되면 작업 재개됨
    • GC옵션이나 알고리즘으로 STW시간을 단축할 수 있음
  • Mark and Sweep: 
    • Mark: 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
    • Sweep: Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
    • Stop The World를 통해 모든 작업을 중단시키면, GC는 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 각각이 어떤 객체를 참고하고 있는지를 탐색하며 mark and sweep 작업을 진행함

algorithm

java11 default gc

-XX:+UseG1GC

힙을 격자/작은 지역으로 쪼개서 특정 지역별로 gc가 일어남.

그래서 예측가능하고 정지시간이 짧음,

성능이 좋고 메모리 효율이 좋음,

gc처리가 병렬처리 가능

튜닝 가능

 

java8 default gc

-XX:+UseParallelGC

G1GC가 자바7부터 나왔기 때문에 사용 가능 다만 기본값은 아니라는 점

 

주로 사용되는 GC

  1. G1 garbage collector(G1GC)
    1. java 7+ 사용가능
  2. Z garbage collector(ZGC)
    1. java 11+ 사용가능
  3. Parallel garbage collector
    1. java 5+ 사용가능
728x90
반응형

'개발 > java' 카테고리의 다른 글

[jmh] benchmark test  (0) 2022.08.04
[powermock] mock static method  (0) 2022.07.21
[keyword] transient in serialization  (0) 2022.02.16
[junit5] no test found / checked exception is invalid  (0) 2022.02.07
[junit] test runner  (0) 2022.01.03
반응형

2022.01.03 - [서버 세팅 & tool/vm on mac] - [vm] virtual box centos7 세팅

 

[vm] virtual box centos7 세팅

1. oracle virtual box for mac 설치 2. centos7 iso 받기 https://ftp-srv2.kddilabs.jp/Linux/packages/CentOS/7.9.2009/isos/x86_64/ 참고로 gui 리눅스를 사용하기 위해 풀버전으로 받았음 ​ 3. https://mine..

bangpurin.tistory.com

최소설치 /32기가 /2기가램 으로 진행

vm 을 마련해주고 nginx 설치를 진행한다.

 

참고로 cli로 리눅스를 깔으니 마우스포인터가 없어 복붙이 힘들어.. 맥 터미널로 vm 에 붙어서 작업했다..

vm 서버의 ip를 알기 위해서는 아래 명령어를 입력하면 된다.

ip addr

 

1. nginx repo 추가

sudo vi /etc/yum.repos.d/nginx.repo

//열린 파일에 아래 작성
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1

 

2. nginx 설치

sudo yum install -y nginx

 

3. (옵션)centos에 설치된 모든 라이브러리 업데이트, 자바 설치

sudo yum update -y
sudo yum install java-1.11 ~~

 

4. 방화벽 포트 열기

# 포트 개방
sudo firewall-cmd --permanent --zone=public --add-port=8080/tcp
# 방화벽 재시작
sudo firewall-cmd --reload
# 개방된 포트 목록 확인
sudo firewall-cmd --list-ports

 

5. 서비스 등록 및 시작

sudo systemctl status nginx
sudo systemctl enable nginx
sudo systemctl start nginx
728x90
반응형
반응형

 

아래는 문제가 되는 화면이다..

 

구글링을 하다보니 아래 사이트 처럼 세팅하라고 한다. 윈도우를 아애 종료시키고 설정하는게 핵심이다..!!

https://kb.parallels.com/116239

 

KB Parallels: Nested Hyper-V support in Parallels Desktop virtual machines

: this feature is currently not supported in Parallels Desktop on Mac computers with Apple M1 chip.

kb.parallels.com

처음에 이렇게 나와서 당황했지만.. 윈도우 끄고 설정창 다시 켜면 된다..

 

맥을 재기동하니까 녹스 정상 작동 된다..!

728x90
반응형

'서버 세팅 & tool > vm on mac' 카테고리의 다른 글

[parallels] local server not working on parallels  (0) 2022.04.11
[vm] nginx 설치  (0) 2022.02.24
[vm] axon server 설치  (0) 2022.01.12
[vm] virtual box 브릿지 네트워크 설정  (0) 2022.01.12
[vm] jenkins 설치  (0) 2022.01.10
반응형

이전 글: 2022.02.08 - [서버 세팅 & tool/postman] - [postman] chaining sample test

 

[postman] chaining sample test

이전 글: 2022.02.08 - [서버 세팅, tool 사용법/postman] - [test] environment variable setting & snippets [test] environment variable setting & snippets 이전 글: 2022.02.08 - [서버 세팅, tool 사용법..

bangpurin.tistory.com

 

 

포스트맨으로 파일에 든 데이터를 순차적으로 넣어가며 테스트하고 싶을 때, json과 csv 방법 두 가지 방법이 있는데 각각에 대해서 알아본다.


1. csv 방식

당연하게도 csv파일을 만들어야 포스트맨에서 불러올 수 있는데, 이 방법은 아래와 같다.

https://bangpurin.tistory.com/56

 

csv 파일만들기

1. 엑셀로 만들기 엑셀에 필요한 데이터를 아래와 같이 만든다. 0으로 시작하는 숫자나 길이가 긴 숫자는 엑셀이 알아서 숫자 포맷을 바꿔버리는데, 열을 선택하고 텍스트 형태로 전환해주면 아

bangpurin.tistory.com

 

request로 사용할 데이터가 든 파일을 준비한다. csv 확장자로 저장된 파일이어야 하며, 최상단에는 지칭하는 이름이 들어있어야 한다(노트패드나 엑셀에서 csv파일을 만들 수 있다).

포스트맨에서 collection -> runner 클릭하고 콜랙션의 api를 옆으로 드래그한다. 필요시 순서를 맞추고, 테스트할 api를 선택한다.

오른쪽 영역에서 위 csv파일을 데이터 부분에서 csv 파일을 불러온다.

preview를 눌렀을 때 아래와 같이 팝업이 뜨면 정상적으로 읽힌 것이다.

자 이제 파일을 준비했으니, 저 변수를 가져올 부분을 세팅해보자. 나는 저 변수를 POST 요청의 항목으로 사용할 예정이라 아래와 같이 수정해주었다.

 

가. pre-request script에서 파일에서 읽어 온 값을 변수 처리한다. 어떤 변수로 세팅해야 할지는 상황에 따라 다르겠다. 이전 글을 보면 변수의 범위에 대해 언급한 적이 있는데, 동일하게 생각해서 설정하면 된다.

var cardNo = pm.iterationData.get("card") //csv파일의 각 줄의 card부분을 가져와 변수 세팅
var zipcode = pm.iterationData.get("zipcode") //csv파일의 각 줄의 zipcode부분을 가져와 변수 세팅
console.log(cardNo) //로그
pm.collectionVariables.set("card", cardNo); //위 cardNo를 콜랙션 변수로 세팅
pm.collectionVariables.set("zipcode", zipcode); //위 zipcode를 콜랙션 변수로 세팅

 

나. body 부분을 수정한다. 변수가 들어갈 부분을 수정해준다.

 

다. 잘 들어갔는지 확인. 테스트 코드로 확인한다.

pm.test("request 변수 확인", function () {
    var req = JSON.parse(pm.request.body.raw); //request body를 가져와서
    
    console.log(req) //로그찍고
    console.log(req[0].card) //string "9460777788890000"
    console.log(pm.iterationData.get("card"))//number 9460777788890000
    
    //parseInt(req[0].card)로 number로 바꿔서 비교해야 같게 나옴
    pm.expect(parseInt(req[0].card)).to.eql(pm.iterationData.get("card"));
    pm.expect(parseInt(req[0].address.zipcode)).to.eql(pm.iterationData.get("zipcode"));
});

 

라. 위 가~다 과정을 하고 나서 꼭 ctrl+s 를 눌러서 저장을 해주고, 다시 collection의 runner로 돌아와서 Run api-testing을 클릭한다.

테스트가 성공하는 것을 볼 수 있다. 만약 실패해도 뭐가 틀렸는지 알려주기 때문에 바로 수정하면 된다.

참고로 각 요청을 클릭하면 실제로 어떤 json이 요청 나가고 들어왔는지 볼 수 있다.

 

참고

1. csv파일의 특성 상 숫자 형의 맨 처음 오는 0은 잘리니까 조심해야 한다(사진에서도 파일에는 0이 있지만 포스트맨에서 불러왔을 때에는 0이 없어지는 것이 확인된다). 해결법이 있을 법도 한데,, 아직 없는 것 같다. csv파일을 json포맷으로 바꿔서 했다는 글만 있다.

https://community.postman.com/t/how-to-pass-a-csv-in-collection-runner-which-starts-with-0s/14422/9

 

How to pass a csv in collection runner which starts with 0's

This is a very common scenario to have leading zeroes in imported data variables. I’m surprised why postman didn’t resolve this yet. As a temporary solution, I am converting CSV to JSON and running all my data driven tests (yes, this is cumbersome way

community.postman.com

 

2. 그리고 csv 안의 숫자의 길이가 16자리 이상이면 포스트맨이 값을 알아서 변환해버리는 이슈가 있다. 변수를 불러올 때 자바스크립트의 Number를 사용해서 생기는 이슈라고는 하는데 어쨌든 request와 다른 값으로 쏘는 것은 명백한 이슈기 때문에 숫자를 문자로 바꿔서 요청할 수 있게끔 지원해달라는 문의글이 최신으로도 올라오고 있다. 이를 방지하려면 아래와 같이 csv안의 숫자 앞뒤로 "(큰따옴표)를 넣어주면 되긴 하지만.. 데이터가 많을수록 귀찮아질 것이기에.. 아래의 json변환 방법을 사용하려 쏘는 게 더 나을 것 같다.

https://github.com/postmanlabs/postman-app-support/issues/8771

 

Collection runner modifying large numbers · Issue #8771 · postmanlabs/postman-app-support

Describe the bug For CSVs, numbers over 16 digits are being rounded/modified when used in the Collection Runner. See example below: raw value value used in request 5720687706200772658.153 572068770...

github.com


 

2. json 방식

request json이 이미 준비되었을 경우 유용하다. 하지만 위에서 언급한 csv방식에서 지원하지 않는 leading zero이슈를 해결하기 위해 사용되기도 한다.

여기서는 csv -> json으로 바꾼 후 테스트를 해보도록 한다.

https://www.convertcsv.com/csv-to-json.htm

 

CSV To JSON Converter

 

www.convertcsv.com

 

위 사이트에서 미리 준비된 csv를 선택하면 아래에 결과를 볼 수 있다. 여러 포맷으로 변환해주는데 포스트맨에서는 맨 처음 양식을 지원하기 때문에 csv to json으로 변환해주고 download를 눌러주면. json파일이 받아진다.

만들어진 파일을 열어본다(위 사진의 result data에서도 확인가능하다).

자동으로 타입을 판별해서 그런지 zipcode가 숫자 타입으로 json이 만들어졌는데, 이를 문자열로 바꿔서 다시 만들도록 한다(api request에서 문자로 보내야 하는지 숫자로 보내야 하는지에 따라 다르게 설정하면 된다).

사이트 output 옵션 중에 강제로 stringify하는 곳이 있는데, 비워두면 전체를 문자열화, 위치를 콤마로 구분해서 넣어주면 그 곳만 문자열화 해준다. 우리는 두번째에 위치한 zipcode를 문자열화 해야 하니 2를 넣어주고 다시 csv to json을 눌러준다. 아래에 문자열로 바뀐 json을 확인한다.

 

포스트맨에서 json파일을 불러오고 preview버튼을 누르면 아까 csv에서는 사라졌던 0이 잘 들어와 있는 것을 볼 수 있다.

 

csv로 테스트했을 때는 다 숫자로 읽어와서 테스트 코드도 숫자로 변환하는 부분이 있었는데, json에서는 이를 유동적으로 바꿀 수 있어서 더 좋은 것 같다. 우리는 지금 모든 값을 문자화 했으니 테스트 코드도 문자 비교로 바꿔야 통과한다.

pm.test("request 변수 확인", function () {
	var req = JSON.parse(pm.request.body.raw);
	console.log(req)
	pm.expect(req[0].card).to.eql(pm.iterationData.get("card"));
	pm.expect(req[0].address.zipcode).to.eql(pm.iterationData.get("zipcode"));
});

마치며..

csv는 숫자 값이면 (숫자가 아니어도 숫자 꼴이기만 하면; ex. 카드번호, 폰번호) 숫자로 인식해서 예상하지 못한 문제가 생길 수 있는데, json으로 변환하면 이를 조절할 수 있으니 json으로 테스트하는 게 더 정확하고 예측 가능한 것 같다!

728x90
반응형
반응형

1. 엑셀로 만들기

엑셀에 필요한 데이터를 아래와 같이 만든다. 0으로 시작하는 숫자나 길이가 긴 숫자는 엑셀이 알아서 숫자 포맷을 바꿔버리는데, 열을 선택하고 텍스트 형태로 전환해주면 아래와 같이 왼쪽 상단에 삼각형이 마킹되면서 텍스트화 해주어 깨지지 않는다.

저장 시 csv로 저장하면 끝.

만들어진 csv파일을 노트패드로 열어보면 아래와 같이 잘 들어가 있다.

 

2. notepad로 처음부터 만들기

별거 없고 위 사진 처럼 처음부터 저 양식으로 만들면 된다. 다만 위험하다. csv는 사람이 조작하면 바이트가 이상하게 조작되어 읽을 때 깨질 수 있으니 (특히 한글!!) 숫자나 영어만 있을 때 간단히 하기에 좋다.

728x90
반응형
반응형

들어가며..

마이크로 서비스 아키텍처(MSA) 프로젝트를 개발 및 운영을 하다 보면 도메인 모델은 복잡해지고 점점 설계 시점의 의도와는 다른 방향으로 변질되는 일이 빈번히 발생한다. 특히 요즘처럼 고차원적인 UX, 급변하는 IT 시장의 흐름으로 인해 시도 때도 없이 달라지는 기획팀/사업부의 요구사항을 충족하는 모델을 만드는 건 더욱 어려운 일이 되었다. 게다가 이렇게 복잡한 내용을 하나의 화면에서 다 보여달라고 하니.. 아무리 인덱스를 추가하고 쿼리를 튜닝하더라도 조회 속도가 나지 않고, n번의 api를 결과를 합치면서 생기는 실수, 더러운 소스코드 등은 결국 서비스의 질을 낮추기에 충분해진다.

(내가 경험한) MSA

이게 왜 어려워졌을까? 데이터의 변경과 조회 시 필요한 데이터가 관점에 따라 명백히 다른데, 이걸 하나의 모델/애플리케이션/디비에서 해결하려다 보니 (각 영역에서 필요하지 않은 속성들로 인해) 복잡도가 증가하고 변질되는 것은 아닐까?

그럼 어떻게 이 문제를 해결 할 수 있을까? 데이터의 변경과 조회를 나누면 되지 않을까? 해서 나온 게 CQRS이다.

 

CQRS란?

마이크로 서비스의 패턴이 무려 44가지나 있다고 하는데, 간단하게 관련 용어 몇 가지만 살펴본다.

  •  DDD - Domain Driven Design 도메인 주도 개발(방법론)
    • 비즈니스를 도메인 별로 나누어 설계하는 방식/접근법
  • EDA - Event Driven Architecture
    • 분산된 시스템 간에 이벤트를 생성, 발행(publishing)하고 발행된 이벤트를 필요로 하는 수신자(subscriber)에게 전송되는 흐름으로 구성된 아키텍처로 이벤트를 수신한 수신자가 이벤트를 처리하는 형태임
    • Architectural Patterns
  • CQRS - Command Query Responsibility Segregation 패턴 
    • "CQRS는 DDD 기반의 Object Model 방법론 적용 시 나타났던 문제점들을 해결하기 위해 등장했다"
  • Aggregate Pattern
  • Saga Pattern
    • MSA 기반의 분산 시스템에서 분산된 DB의 정합성을 보장하는 방법, 트랜젝션 관리주체는 애플리케이션(not DB)
    • sequence diagram을 따라가다(transaction flow 속에서) rollback이 필요할 때 처리하는 방법
    • Choreography-Based Saga(각자 알아서 처리)
    • Orchestration-Based Saga(중앙 컨트롤 타워가 정리)
DDD is well suited with some architectural patterns like CQRS, Event Driven Architecture, Event sourcing, etc but they are not required to use DDD.

 

CQRS를 구현할 수 있게 하는 axon framework

  • axon framework는 DDD 패러다임 하에서 event sourcing과 CQRS 패턴을 이용해 애플리케이션을 작성할 수 있도록 도와주는 framework
  • 작성 글: [cqrs] axon framework란

실습

환경: springboot2.6.2 / axon framework 4.2.1 + axon server / postgresql14

1. axon server를 설치하고

2022.01.12 - [서버 세팅 & tool/vm on mac] - [vm] axon server 설치

 

[vm] axon server 설치

2022.01.03 - [세팅/vm on mac] - [vm] virtual box centos7 세팅 [vm] virtual box centos7 세팅 1. oracle virtual box for mac 설치 2. centos7 iso 받기 https://ftp-srv2.kddilabs.jp/Linux/packages/CentOS/..

bangpurin.tistory.com

2. springboot project에 axon dependencies 올려서 개발

axonVersion = "4.2.1"

implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: "$axonVersion"
implementation group: 'org.axonframework', name: 'axon-configuration', version: "$axonVersion"

3. 블로그 흐름 따라가며 개발하였고

https://cla9.tistory.com/2?category=814447 

 

1. Axon Framework 개요

1. 개요 앞으로 진행될 포스팅은 MSA에 관심 많은 분을 대상으로 DDD, CQRS 및 Event Sourcing 내용을 알고 있다고 가정하고, Spring 환경에서 AxonFramework를 활용해 개념 구현하는 방법에 대해 소개하고자

cla9.tistory.com

4. 개발하면서 막히는 부분/ 깨달은 부분은 따로 정리

2022.01.12 - [개발/axon framework] - [axon] command/query project 생성 - clone coding

5. 깃에 등록

https://github.com/haileyjhbang/cqrs-clone.git


axon framework 구조

axon framework

Command Applcation

  • Event-Sourced Aggregate: EventStore로부터 Event를 재생하면서 모델을 최신 상태로
  • State-Stored Aggregate: EventStore에 Event를 적재와 별개로 모델 자체에 최신 상태를 DB에 저장

Query Application

  • Point to Point Query: 하나의 QueryHandler를 찾아 진행
  • Scatter-Gather Query: 동일한 Query를 처리하는 Handler가 여러 App에 등록되어있을 때, 이를 처리하는 방법
  • Subscription Query: Point to Point Query를 요청하였을 때, 만약 Query를 수행하는 Read Model이 바뀌었다면, 화면에 출력되는 결과와 Read Model 사이 데이터 정합성 불일치 문제가 발생한다. 따라서 이를 해결하기 위해 주기적으로 Query를 재요청하는 방식

 

추가) Saga pattern in axon

axon framework에서도 분산 트랜젝션을 위해 saga 패턴을 지원한다(saga event 정보를 db에 저장).

//어노테이션
@StartSaga
@SagaEventHandler(associationProperty = "transferID")
@EndSaga


//함수 호출방식
SagaLifecycle.associateWith("accountID", event.getDstAccountID());
SagaLifecycle.end();

[axon - saga; orchestration] https://cla9.tistory.com/22?category=814447

 

활용방안?

초기 그림의 MSA 구조의 시스템 & 대용량 처리가 필요한 시스템에 적용하면 효과적인 대안이 될 수 있을 것이란 생각이 들었다. ES 전파만 확실하면 쿼리 성능도 좋아질 것이고 두 애플리케이션의 결합도도 낮아 독립적인 스캐일 업 또한 가능할 것이다.

하지만 러닝 커브의 압박, 분산된 구조로 인한 트러블 포인트의 증가, 서비스 복잡도 증가 그리고 DB를 많이 쓰기 때문에 유능한 DBA나 넉넉한 DB공간 확보 필요 등의 비용이 많이 들 수 있어 비즈니스 로직이 간단한 서비스면 굳이 도입할 필요는 없을 것 같다.


참고

https://docs.axoniq.io/reference-guide/

 

Introduction - Axon Reference Guide

The standard version, called "Axon Server", is open source and free to download and use. It is provided under an AxonIQ-specific open source license. While this license allows you to run the software freely in any environment, it is less permissive than th

docs.axoniq.io

 

책: 마이크로 서비스 패턴(Chris Richardson 저)

http://www.yes24.com/Product/Goods/86542732

 

마이크로서비스 패턴 - YES24

모놀리식 애플리케이션을 마이크로서비스 아키텍처로 성공적으로 전환하는 방법마이크로서비스 아키텍처 기반의 애플리케이션을 성공적으로 구축하려면 새로운 아키텍처의 개념을 이해하는

www.yes24.com

요약본: https://microservices.io/patterns/index.html

 

Microservices Pattern: A pattern language for microservices

Microservices.io is brought to you by Chris Richardson. Experienced software architect, author of POJOs in Action, the creator of the original CloudFoundry.com, and the author of Microservices patterns. Chris helps clients around the world adopt the micros

microservices.io

 

https://app.mural.co/t/cloudingegration6924/m/cloudingegration6924/1598872302455/cb40356de0e1fcc36618a25f5f5e2ed18761f3ca

 

mSVC Patterns

 

app.mural.co

 

728x90
반응형

'개발 > axon framework' 카테고리의 다른 글

[axon] saga (2) - clone coding  (0) 2022.02.18
[axon] saga (1) - clone coding  (0) 2022.02.18
[axon] query handler(2) - clone coding  (0) 2022.02.14
[axon] query handler(1) - clone coding  (0) 2022.02.11
[axon] event upcasting - clone coding  (0) 2022.02.04

+ Recent posts