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

1. restart 

(13)Permission denied: make_sock: could not bind to address 0.0.0.0:80

아파치를 재시작할 때 위와 같이 권한 에러가 나는 경우가 있다.

아파치를 80 포트로 띄우는데, 1024 이하의 well-known 포트는 root 만 접근이 가능한데, root 계정이 아닌 계정으로 띄우니 에러가 난 것이다. 실제로 파일의 소유권을 보면 root가 아니라 www 계정이다.

[irteamsu@wa901 apache]$ cd bin
[irteamsu@wa901 bin]$ ll
total 2796
-rwxr-xr-x 1 www www  144096 Jun 16 10:52 ab
-rwxr-xr-x 1 www www    3455 Jun 16 10:50 apachectl
-rwxr-xr-x 1 www www    7017 Jun 16 10:52 apr-1-config
-rwxr-xr-x 1 www www    6611 Jun 16 10:52 apu-1-config
-rwxr-xr-x 1 www www   22662 Jun 16 10:50 apxs
-rwxr-xr-x 1 www www   13048 Jun 16 10:52 checkgid
-rwxr-xr-x 1 www www    8925 Jun 16 10:50 dbmmanage
-rw-rw-r-- 1 www www     987 Jun 16 10:50 envvars
-rw-rw-r-- 1 www www     987 Jun 16 10:50 envvars-std
-rwxr-xr-x 1 www www   56480 Jun 16 10:52 htcacheclean
-rwxr-xr-x 1 www www   34160 Jun 16 10:52 htdbm
-rwxr-xr-x 1 www www   25280 Jun 16 10:52 htdigest
-rwxr-xr-x 1 www www   37000 Jun 16 10:52 htpasswd
-rwxr-xr-x 1 www www 2384976 Jun 16 10:52 httpd
-rwxr-xr-x 1 www www   22816 Jun 16 10:52 httxt2dbm
-rwxr-xr-x 1 www www   25200 Jun 16 10:52 logresolve
-rwxr-xr-x 1 www www   27360 Jun 16 10:52 rotatelogs

쉘의 소유주를 변경하고 root가 아닌 계정에서도 실행할 수 있게 권한도 변경한다.  

's' = The directory's setgid bit is set, and the execute bit is set.
[irteamsu@wa901 bin]$ sudo chown root:www httpd
[irteamsu@wa901 bin]$ sudo chmod +s httpd
[irteamsu@wa901 bin]$ sudo chown root:www apachectl 
[irteamsu@wa901 bin]$ chmod +s apachectl
-rwxr-xr-x 1 www  www  144096 Jun 16 10:52 ab
-rwsr-sr-x 1 root www    3455 Jun 16 10:50 apachectl
-rwxr-xr-x 1 www  www    7017 Jun 16 10:52 apr-1-config
-rwxr-xr-x 1 www  www    6611 Jun 16 10:52 apu-1-config
-rwxr-xr-x 1 www  www   22662 Jun 16 10:50 apxs
-rwxr-xr-x 1 www  www   13048 Jun 16 10:52 checkgid
-rwxr-xr-x 1 www  www    8925 Jun 16 10:50 dbmmanage
-rw-rw-r-- 1 www  www     987 Jun 16 10:50 envvars
-rw-rw-r-- 1 www  www     987 Jun 16 10:50 envvars-std
-rwxr-xr-x 1 www  www   56480 Jun 16 10:52 htcacheclean
-rwxr-xr-x 1 www  www   34160 Jun 16 10:52 htdbm
-rwxr-xr-x 1 www  www   25280 Jun 16 10:52 htdigest
-rwxr-xr-x 1 www  www   37000 Jun 16 10:52 htpasswd
-rwsr-sr-x 1 root www 2384976 Jun 16 10:52 httpd
-rwxr-xr-x 1 www  www   22816 Jun 16 10:52 httxt2dbm
-rwxr-xr-x 1 www  www   25200 Jun 16 10:52 logresolve
-rwxr-xr-x 1 www  www   27360 Jun 16 10:52 rotatelogs

그럼 권한 문제는 해결된다.

 

2. stop

/usr/local/apache/bin]# apachectl stop
...
httpd (no pid file) not running

아파치를 정지하는데 위와 같은 메시지가 뜨면서 정지가 안 되는 현상이 있다. 아마 1번이 진행되지 않은 상태에서 아파치가 실행되었는데 중간에 권한이 바뀌어서 문제가 되는 것으로 보인다. 실제로 ps로 서비스를 확인해보면 정지가 안 되어 있는 걸 확인할 수 있다.

이 현상은 정지할 때 ../logs 폴더에 httpd.pid 파일을 가지고 정지를 시키는데 어떤 이유로 아파치 실행 시 해당 파일이 없을 경우 나는 에러였다. 

/usr/local/apache/logs]# ll

drwxr-xr-x 15 www www 175  6?? 16 10:52 ..
-rw-r-----  1 www www   0  6?? 16 10:56 modsec_debug_log

위 경로로 가서 httpd.pid 파일을 만들고 pid를 넣어준다.

[wa902 logs]$ ps -ef | grep -E "httpd[[:space:]]-k" | grep -v "grep" 
root     14827     1  0 Jun16 ?        00:00:03 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14843 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14846 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14848 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14850 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14852 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14853 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14854 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14855 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14856 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start
www      14857 14827  0 Jun16 ?        00:00:00 /home/www/apps/httpd-2.2.21/bin/httpd -k start

[wa902 /usr/local/apache/logs]# cat httpd.pid 
14827

그리고 다시 apachectl stop을 하면 잘 되는 것을 확인할 수 있다.

728x90
반응형
반응형

reactor를 공부하다 보면 Mono/Flux 이후에 많이 보게 되는 게 Sinks이다.. 솔직히 아직 mono와 flux도 완벽하게 이해하지 못했는데, 자꾸 새로운 개념이 튀어나오는 게 두렵긴 하지만 ㅋㅋ 어쨌건 계속 봐야 모래알 같은 지식이 쌓여 해변가가 될 것이라 믿기에, 짧은 지식으로 나마 원문을 파보도록 한다.

java doc에 기재되어 있는 Sinks의 개념이다. mono/flux보다 한 단계 더 내부에서 실제 동작하는 시그널의 구조체 같은 느낌인 듯 한데, 저것만 봐서 완전히 와닿지 않는다.

https://projectreactor.io/docs/core/release/reference/#processors

 

Reactor 3 Reference Guide

10:45:20.200 [main] INFO reactor.Flux.Range.1 - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription) (1) 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | request(unbounded) (2) 10:45:20.205 [main] INFO reactor.Flux.Range.1 - | onNext(1) (3) 1

projectreactor.io

기존에 processor라는 개념이 있는데, publisher이면서 subscriber의 역할을 하는 친구이다. 보통 reactive stream의 중간체(데이터를 구독받았다가 다시 방출하는..)를 가리키는데 reactive stream에서는 publisher로 보통 구현한다(이때 subscriber 인터페이스의 함수를 직접 호출하지 않도록 조심해야 한다).

 In Reactor a sink is a class that allows safe manual triggering of signals.

Sinks는 안전하게 event trigger를 할 수 있게 하는 interface며 processor의 대용으로 사용할 수 있다.

 

 

1. 일반 구독(flux.subscribe)

@Test
@DisplayName("해당 구독행위는 array에 대해서 하였기에 뒤에서 변화가 이루어진 부분에 대해서는 이미 끝난 행위라서 아무런 변화가 없음")
public void flux(){
    List<String> array = new ArrayList<String>();
    array.addAll(Arrays.asList(new String[]{"a", "b", "c", "d", "e", "e"}));
    Flux.fromIterable(array).collectList().subscribe( (data)->System.out.println(data)); //구독
    array.addAll(Arrays.asList(new String[]{"1", "2", "3"}));  //데이터 변화
}
[a, b, c, d, e]

구독 이후에 발생한 데이터의 변화는 감지하지 않는다. 데이터가 정해져있고 거기에 구독자를 추가한다.

 

2. subscribe를 계속 감지하게 하려면? Processor 사용

Flux의 Processor에는 FluxProcessor, EmitterProcessor, ReplayProcessor 등 많은 프로세서들이 존재하는데, 그중 EmitterProcessor는 여러 구독자(subscriber)가 사용할 수 있는 구독과 발행이 동시해 일어나는 프로세서이다.

EmitterProcessor: An implementation of a message-passing Processor implementing publish-subscribe with synchronous (thread-stealing and happen-before interactions) drain loops.

또한, 구독 행위가 등록되고 난 이후에 해당 이벤트가 발생하면 구독하는 대상에게 데이터를 동기적으로 전달한다.

 

EmitterProcessor를 사용한 코드

@Test
public void beforeSink(){
    List<String> array = new ArrayList<String>();
    array.addAll(Arrays.asList(new String[]{"a", "b", "c", "d", "e"}));

    //프로세서 시작 구간.
    EmitterProcessor<List<String>> data = EmitterProcessor.create();  //발행인
    data.subscribe(t -> System.out.println("1번 : "+t));  //구독자 추가
    FluxSink<List<String>> sink = data.sink();   //배달부
    sink.next(array); //배달 완료

    array.addAll(Arrays.asList(new String[]{"new", "data", "hello"}));  //내용 추가

    data.subscribe(t -> System.out.println("2번 : "+t));  //구독자 추가
    sink.next(array);  //배달

    array.addAll(Arrays.asList(new String[]{"1", "2", "3"}));  //내용 추가
    sink.next(array);  //배달
}
1번 : [a, b, c, d, e]
1번 : [a, b, c, d, e, new, data, hello]
2번 : [a, b, c, d, e, new, data, hello]
1번 : [a, b, c, d, e, new, data, hello, 1, 2, 3]
2번 : [a, b, c, d, e, new, data, hello, 1, 2, 3]

Processor는 기존에 있거나 새롭게 등장한 구독자(subscriber)에게 데이터(old data)를 전달한다. 구독자 중심이랄까. 

발행하는 기관(processor)을 건설하고 구독자를 모집(subscribe)한 뒤에 계속해서 발행(sink.next)하는 형태이다. 구독 이후에 데이터가 변경되어 발행되었다면 추가 구독없이 변경된 데이터를 받아볼 수 있다. 

작업 할 내용이 데이터가 중심이면 1번의 일반 구독형태를 사용하고, 구독자가 중심이면 프로세스를 사용하면 될 것 같다.

 

3. EmitterProcessor 부분이 reactor 3.5에서 deprecated 돼서 Sinks로 바꿔본다.

@Test
@DisplayName("flux: hot 테스트")
public void sinkTest_multicast(){
    List<String> array = new ArrayList<String>();
    array.addAll(Arrays.asList(new String[]{"a", "b", "c", "d", "e"})); //내용

    Sinks.Many<List<String>> sink = Sinks.many().multicast().directBestEffort(); //발행인
    sink.asFlux().subscribe(data -> System.out.println("1번 : " + data)); //구독자 추가
    sink.tryEmitNext(array); //발행함
    sink.asFlux().subscribe(data -> System.out.println("2번 : " + data)); //구독자 추가
    array.addAll(Arrays.asList(new String[]{"1", "2", "3", "4", "5"})); //내용 추가
    sink.tryEmitNext(array); //발행함
    sink.asFlux().subscribe(data -> System.out.println("3번 : " + data)); //구독자 추가
}
1번 : [a, b, c, d, e]
1번 : [a, b, c, d, e, 1, 2, 3, 4, 5]
2번 : [a, b, c, d, e, 1, 2, 3, 4, 5]

1번은 한번 구독으로 변경된 데이터도 다시 받았고, 2번은 구독 이후 데이터를 받았다. 3번은 구독(subscribe) 이후에 아무 데이터 변경이 없어서 로그에 남지 않는다.

 


 

아래 예제를 따라하면 Sink의 multicast/unicast의 차이, sinks.one/sinks.many를 공부할 수 있다. hot/cold publisher에 대한 개념은 덤!

https://prateek-ashtikar512.medium.com/projectreactor-sinks-bac6c88e5e69

 

ProjectReactor — Sinks

public static <T> Sinks.One<T> one() A Sinks.One that works like a conceptual promise: it can be completed with or without a value at any…

prateek-ashtikar512.medium.com

 

@Test
@DisplayName("n subscribers :: 1 message")
public void sinkOne(){
    Sinks.One<Object> sink = Sinks.one(); //n subscribers :: 1 message
    Mono<Object> mono = sink.asMono();

//        mono.subscribe(d -> System.out.println("Sam: " + d));
    mono.subscribe(new Subscriber<Object>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(Long.MAX_VALUE); //많이 받겠다고 해고 sinks.one 자체가 1개만 방출해서 하나만 받음
            System.out.println(">>>> onSubscribed " + s); // 이거 호출되고..
        }

        @Override
        public void onNext(Object o) {
            System.out.println(">>>> onNext " + o);
        }

        @Override
        public void onError(Throwable t) {
            System.out.println(">>>> onError " + t.getMessage());
        }

        @Override
        public void onComplete() {
            System.out.println(">>>> onComplete");
        }
    });
    mono.subscribe(d -> System.out.println("Sam: " + d));

    sink.tryEmitValue("Hollo");
    sink.tryEmitValue("hi~"); //안받음! one이라서.. 하나만받음
}

@Test
@DisplayName("1 subscriber :: n message")
public void unicast(){
    Sinks.Many<Object> sink = Sinks.many().unicast().onBackpressureBuffer();
    Flux<Object> flux = sink.asFlux();

//        flux.subscribe(d-> System.out.println("Date : "+ d));
    flux.subscribe(new Subscriber<Object>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(Long.MAX_VALUE);
            System.out.println(">>>> onSubscribed " + s); // 이거 호출되고..
        }

        @Override
        public void onNext(Object o) {
            System.out.println(">>>> onNext " + o);
        }

        @Override
        public void onError(Throwable t) {
            System.out.println(">>>> onError " + t.getMessage()); //errrrr
        }

        @Override
        public void onComplete() {
            System.out.println(">>>> onComplete");
        }
    });
    flux.subscribe(d-> System.out.println("Date : "+ d));

    sink.tryEmitNext("hi");
    sink.tryEmitNext("i am hungry");
    sink.tryEmitNext("bye");
}

@Test
@DisplayName("sink.many는 갑자기 많은걸 방출하면 FAIL_NON_SERIALIZED 에러가 나서 이때는 재시도를 해줘야 데이터 누수가 없음")
public void unicast2() throws InterruptedException {
    Sinks.Many<Object> sink = Sinks.many().unicast().onBackpressureBuffer();
    Flux<Object> flux = sink.asFlux();

    List<Object> list = new ArrayList<>();
    flux.subscribe(e -> list.add(e));

    for(int i = 0; i< 1000; i++){
        final int j = i;
        CompletableFuture.runAsync(() -> {
            sink.emitNext(j, new Sinks.EmitFailureHandler() {
                @Override
                public boolean onEmitFailure(SignalType signalType, Sinks.EmitResult emitResult) {
                    System.out.println(emitResult.toString());
                    //return true; // true if the operation should be retried, false otherwise.
                    // Sinks.many() factory methods that fail with EmitResult.FAIL_NON_SERIALIZED when multiple producers emit concurrently
                    //그래서 FAIL_NON_SERIALIZED 에러일 때는 재시도하도록 해줘야 함
                    return emitResult.equals(Sinks.EmitResult.FAIL_NON_SERIALIZED) ? true : false;
                }
            });
        });
    }

    Thread.sleep(5_000);
    System.out.println(list.size());
}

@Test
@DisplayName("n subscribers :: n message / 구독 이후 발행한 메세지부터 받음 / hot")
public void multicast(){
    Sinks.Many<Object> sink = Sinks.many().multicast().onBackpressureBuffer();
    Flux<Object> flux = sink.asFlux();

    sink.tryEmitNext("hi");
    sink.tryEmitNext("hello");

    flux.subscribe(d -> System.out.println("SAM: " + d));
    flux.subscribe(new Subscriber<Object>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(5); //총 가져올 갯수 1이면 sink.one과 같은 효과
            System.out.println(">>>> onSubscribed "); // 이거 호출되고 이제부터 데이터 받음
        }

        @Override
        public void onNext(Object o) {
            System.out.println(">>>> onNext " + o); //실 데이터
        }

        @Override
        public void onError(Throwable t) {
            System.out.println(">>>> onError " + t.getMessage()); //errrrr
        }

        @Override
        public void onComplete() {
            System.out.println(">>>> onComplete");
        }
    });

    sink.tryEmitNext("????");
    flux.subscribe(d-> System.out.println("here: " +d));
    sink.tryEmitNext(" new");
}

@Test
@DisplayName("n subscribers :: n message / 구독 이전 발행 한 메세지도 받음 / cold")
public void manyReplay(){
    Sinks.Many<Object> sink = Sinks.many().replay().all(); //onSubscribe 하고 바로 첨부터하고 그담부터는 리스닝
    Flux<Object> flux = sink.asFlux();

    sink.tryEmitNext("hi");
    sink.tryEmitNext("how are you");

    flux.subscribe(d -> System.out.println("First: " + d));
    flux.subscribe(new Subscriber<Object>() {
        @Override
        public void onSubscribe(Subscription s) {
            s.request(Long.MAX_VALUE); //총 가져올 갯수 1이면 sink.one과 같은 효과
            System.out.println(">>>> onSubscribed "); // 이거 호출되고 이제부터 데이터 받음
        }

        @Override
        public void onNext(Object o) {
            System.out.println(">>>> onNext " + o); //실 데이터
        }

        @Override
        public void onError(Throwable t) {
            System.out.println(">>>> onError " + t.getMessage()); //errrrr
        }

        @Override
        public void onComplete() {
            System.out.println(">>>> onComplete");
        }
    });

    sink.tryEmitNext("???");
    flux.subscribe(d -> System.out.println("Demon: " +d));
    sink.tryEmitNext("new");
}
728x90
반응형

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

[webclient] 비슷한데 뭘 써야할지 모르겠는 것들  (0) 2022.04.01
[webflux] block vs toFuture  (0) 2022.03.31
[webflux] 실무투입고민  (0) 2022.03.30
[spring] spring-web and spring-webflux  (0) 2022.03.25
[reactive] 10. Flux  (0) 2022.03.25
반응형

2022년 4월 시작

서울에서 나고 자라 서울에 있는 대학교/대학원을 다녔고, 서울에 있는 회사에 취업하여 딱히 운전에 대한 필요성이 있지는 않았다. 어차피 지금 따도 장롱이라 생각하여 미루고 미루며 크게 시작하지 않았는데, 정신을 차리고 보니 삼십 대 중반을 향해 달려가고 있는 나를 보았고.. 더 늦기 전에 몸에 익숙해져야 나중에 진짜 필요할 때 잘할 수 있겠다는 생각이 들어 운전면허(2종 보통)를 시작하게 되었다. 

 

학원 정하기

집이 1호선 라인에 가장 가까워서 1호선 라인에 있는 학원 위주로 찾아보았다.

  • 신도림, 광명, 온수

이 세 곳이 나왔다. 근데 신도림은 말이 신도림이지 까치산역에서 더 가까운 것 같았고, 가격도 가장 비쌌으며, 신도림 주변 도로가 복잡하여 어려울 것 같아 과감히 제외하였다.

그러면 광명과 온수가 남았는데.. 지하철 개수도 비슷하고, 가격은 광명이 만원인가? 더 비쌌는데 그렇게 차이 나는 것도 아니고.. 나름 직장인이니까 주말에도 운영하는 곳으로 가야지.. 하다가 온수로 정하게 되었다!

정보를 찾을 때 학원 홈페이지를 들어가면서 자료를 비교했었는데, 나중에 든 생각은.. outdated 된 자료인 것 같다는 것이다.. 진짜 정확하게 비교하려면 전화해서 문의하는 게 좋을 것 같다.

 

학원 방문

그렇게 온수로 가야지~ 하고 탄력근무제 조정하고 4/27일 오후에 학원을 방문을 하였다. 홈페이지에 학과 시간표가 있어서 간 김에 듣고 오려고 나름 시간 계산하고 2시쯤에 도착했다. 가서 수강료를 결제하는데 홈페이지랑 달라서 흠칫하였다.. 내 기억에 홈페이지에선 69만 원인가 그랬던 것 같은데 결제는 82만 원쯤 한 듯하다.

물론 인터넷에 순수하게 수강료만 올려놓고 결제는 이것저것 추가해서 그럴 수도 있는데, 기분은 약간 사기당한 느낌 + 인터넷 정보가 오래된 건가 하는 생각을 지울 수 없었다. 뭐 그래도 여기까지 왔는데 다시 물리기도 싫어서 바로 결제하고 학원 수강카드를 받았다. 마그네틱으로 되어있는데, 자기장에 매우 취약하므로 폰이랑 같이 두다가는 긁히지 않는 수모를 겪게 된다.......ㅎ  수강카드로는 교육 출석 확인은 물론 합격할 때까지 시험 출석체크도 다 하므로 잃어버리지 않는 게 좋겠다.

학원 첫인상은.. 빌라 단지 속에 휑하게 학원이 갑자기 있는 느낌? 게다가 사무실/강의실은 컨테이너 박스? 같은 곳이어서 뭔가 열악한 느낌이 있었는데, 거기서 실제로 지내는 시간은 거의 없으니 신경 쓰지는 않았다.

 

4/27 학과 수업

2:30분부터 3시간을 들었다. 생각보다 재밌었다. 한문철 블박 보는 느낌도 있고,, 그 와중에 필기시험이 급 불안해져 쉬는 시간에는 앱으로 몇 개 풀어보고 그렇게 보냈다. 하나 기억나는 건 학과 선생님이 자기 경찰 출신이라며 경찰로 일하다가 은퇴하고 하는 거라고 말하셨는데.. 그때는 학원의 모든 선생님들이 경찰 출신인 줄 알았다(나중에 아니라는 것을 알게 됨ㅋㅋ). 경찰이니 믿음직 + 말 잘 듣고 잘 배워야지~ 싶었는데 ㅋㅋㅋ 나중에 반전이 있었다.. 뭔가 학원의 첫인상을 좋게 심으려는 음모인 것 같다.

 

5/11 필기시험

그 후 나름의 바쁜 현생을 보내고 5/11일이 돼서야 다음 단계로 나아가게 된다. 또다시 반차를 쓰고 서부 면허장으로 간다. 사실 학원이랑 연계해서 하면(학원에 아침 9시까진가 가면) 학원차 타고 강서 면허장으로 가서 단체로 시험 볼 수 있는데, 직장인은 그렇게 일찍 일어나지 못해요 22333 게다가 강서는 집에서 너무 멀고 서부 면허장이 훨씬 가까워서 따로 하기로 한다.

필기시험도 시간이 어떻게 있나 미리 알아봤는데(허탕 칠 시간도 없는 직장인 2233) 오전 11시까진가 가서 접수를 해야 오전에 시험을 볼 수 있기에 넉넉하게 10시까지 도착할 수 있게 하였다.

등록할 때 여쭤보니 학원과 연계하지 않고 필기시험을 따로 볼 거면 면허장 홈페이지 가서 예약을 하라고 하였다. 그런데 나는 학원에서 학과 3시간을 들었는데, 면허장 홈페이지에서는 나의 학과 내역이 안 잡혀서 자꾸 학과교육을 들으라고 하는게 아닌가.. 계속 시도하다가 화나서 몸빵을 하기로 했고, 실제로 그렇게 한 후기도 있어서 아침 일찍 면허장으로 출발하였다.

서부 면허장 도착은 9시 반쯤 한 것 같다. 그 시간의 면허장은 쾌적하였다. 응시표 작성하고 접수하고 바로 위층 가서 필기시험을 보라고 한다. 시험장에 가니 감독관께서 응시표를 가져가시고 지정해준 컴퓨터에 가서 시험을 치면 된다. 이게 또 뭐라고 살짝 떨리긴 했는데 바로 직전에 본 문제가 나와서 여유롭게 시험을 쳤다. 주변에서 바로바로 시험을 끝내고 나가는데 다들 합격이었어서 나도? 싶었다. 시험은 총 40분을 주는데 20분 정도 보고 제출하였다(심지어 재검도 안 했다). 점수가 바로 모니터에 뜨는데 93점 ㅋㅋㅋ 오예 합격~

 

5/11 장내 기능 교육 1

필기를 본 날 오후에 바로 기능 교육 4시간 중 2시간을 예약해두었다. 학원 가기까지 시간이 남아서 틈틈이 유튜브로 장내기능 교육영상을 찾아서 봤다. 학원에서는 최대한 운전에 대한 감을 익히는 게 맞는 것 같아서 기본적인 조작은 다 보고 외우고 교육을 들으러 갔다.

나는 오늘 처음 운전을 하는데, 선생님께서는 대뜸 나를 운전석에 앉히더니 S자 코스를 3-4번 돌라고 시키셨다. 처음에는 당황스러웠는데 이게 또 되네? 해보니 재밌었고 자신감도 붙어서 좋았다. 그 후 장내 기능 설명을 해주셨는데 기본 조작을 외우고 간 게 확실히 도움이 되었다. 선생님이 1분 만에 후룩 말해주시고 넘어가는데 처음 해보면 여기서 시간 낭비했을 듯.

바로 실전으로 기능 시험장을 N바퀴 돌았다. 처음에는 주차가 어려웠는데 공식대로 하니까 점점 쉬웠고 오히려 맨첨에 있던 경사 코스랑 마지막의 과속 부분이 헷갈렸다. 경사는 선생님이 액셀을 밟았다가 바로 때고 브레이크를 밟고 대기해야 한다고 하는데 왜 그래야 하는지 몰라서 어떨 땐 되고 어떨 땐 안되고 첨에 혼동스러웠고, 과속은 속도 줄이고 깜빡이 까먹는 거랑 처음에 너무 빨리 액셀 밟으면 또 감점이라 그 타이밍이 어려웠다. 뭐 어쨌건 처음 운전한 거 치고 재밌었고 차차 연습하면 되겠지 싶었다.

 

5/14 장내 기능 교육 2

첫 번째 교육을 받고 유튜브로 열심히 익혔다. 경사로도 유튜브 설명을 들으니 이해가 되었다.. 그 선 사이에 서서 기계가 바뀌는 걸 보고 출발해야 한다는 걸.. 유튜브가 알려줬다.ㅎㅎ 그리고 주차와 관련해서는 학원에서 종이를 줬는데 그거 열심히 외워갔다. 두 번째 교육이지만 시험 전 마지막 교육이기도 하니 그래서 이날은 100점 주행을 목표로 하고 갔다.

차에 타자마자 선생님이 기능 시험 음성을 켜주신다. 처음에는 다리가 덜덜 떨렸다. 시험 본다는 느낌이어서 그랬을까. 시험 친다는 생각으로 하긴 했다. 그렇게 첫 바퀴를 돌았는데 합격점수가 나왔다. 선생님도 운전 고수네~라고 해주셔서 기분 좋았다. 그렇게 한 시간에 장내 코스를 4~5바퀴 돈 것 같다. 나는 두 시간 했으니까 9바퀴 정도 돌았겠지. 100점도 4~5번 나와서 자신감이 많이 붙었고 바로 기능 시험을 접수하러 간다.

 

5/17 기능시험

오후 1시에 시험을 예약했고 대기실에 앉아있으면 응시표/신분증 검사하고 순서대로 앉혀주신다. 그 뒤로 한 명씩 시험 보러 가는데 전광판에 몇 점이고 어디서 감점되었고 합/불합인지 나오는데 점점 떨렸다. 7~8번째쯤 내가 호명되었고 가방은 다 맡기고 맨 몸으로 차에 올라탔다. 긴장 덜하려고 중얼중얼거리면서 시험을 쳤다. 맨 처음 경사로 코스에서 정지를 했는데 선 안에 차가 안 들어와 기계의 색이 안 변해서 살짝 당황했지만, 교육 중에도 이런 적이 있었던지라 똑같이 대처했더니 감점 없이 시험을 진행할 수 있었다. 그거 빼고는 나머지는 여유롭게 진행했다. 주차도 한 큐에...

100점으로 시험을 합격하게 된다. 합격증을 받고 도로주행을 예약한다.

 

5/20 도로주행 1

진짜 운전은 도로주행이 아닌가! 기능이야 뭐 장내니까.. 하고 자만심을 누른 상태에서 주행 교육에 간다. 유튜브로 차선 안에서 잘 주행하는 거랑 끼어들기하는 법 이런 일반적인 내용을 보고 갔다. 코스가 온수역 근처라 생소하기도 하고 무작정 코스를 외우는 게 어차피 안 외워질 것 같아서 한번 경험하고 외울 생각이었다.

드디어 운전대를 잡았다. 선생님이 길을 옆에서 알려주시긴 하지만 불안 + 답답하셨는지 운전대를 자꾸 대신해서 돌려주셔서 내가 감을 잡는데 어려움이 있었다. 물론 그 마음도 이해가 가지만 나는 혼자 감을 알아야 하는데.. 자꾸 기회를 잃는 것 같아 불안했다. 어찌어찌 4개의 코스를 완주했고 첫날이니까 운전에 대한 감을 익히는데 만족했다.

 

5/24 도로주행 2

주말 사이에 코스 4개를 열심히 외웠다. 유튜브도 계속 보고 혼자 정리하고 B코스! 이러면 어떻게 가는지, 어디서 뭘 하는지 생각날 수 있게 준비해 갔다. 오늘은 길 몰라서 오는 불안감 없이 온전히 나 혼자 운전하는 게 목표라는 생각으로 갔다.

첫 시간에 만난 선생님과 다른 선생님을 만났는데, 처음에는 무뚝뚝해 보이셨는데, 내가 운전대를 잡으니까 TMT로 바뀌셔서 엄청 수다를 떨었다. 개인 인생사/가족사/학원 비하인드/내 얘기 등등 만난 지 한 시간 만에 별의별 얘기를 하면서 운전했는데 그러니까 진짜 긴장이 하나도 안돼서 운전도 잘되었다. 그리고 위에서 언급한 경찰 선생님들의 비하인드, 학원 내의 불합리한 착취 현장 등등 많은 이야기를 들었는데 놀라우면서도 흥미로웠다. 근데 선생님의 이런 성향이 호불호가 갈릴 수도 있겠다는 생각은 했다. 나는 호응을 잘하고 내 얘기도 스스럼없이 해서 이야기가 계속 이어졌는데, 이게 안 맞으면 처음에 잘 끊어야겠다. 어쨌건 확실히 길을 아니까 여유도 생기고 운전에만 집중할 수 있어서 좋았다. 어디서 조심해야 하고 뭘 봐야 하는지도 알려주셔서 좋았다. 이렇게 운전하면서 나의 실수/단점을 파악했다. 

  • 정지할 때 N으로 바꾸는 거 자꾸 잊음
  • 깜빡이 끌 때 너무 힘껏? 해서 끄다가 다른 거 켜버림(좌회전 끄다가 우회전 키고..ㅎ)
  • N으로 바꾼 거 잊고 브레이크 발 떼었는데 언덕이라 차가 뒤로 밀려 당황한 점.. 꼭 N으로 바꾸면 정신줄 놓지 않고 D로 바꾸고 브레이크 발 떼어야지.. 등등

이렇게 수업 후 바로 복기해서 다음 주행에서는 잊지 않고 잘할 수 있게 공부했다.

 

5/27 도로주행 3

두 번째 수업 후 코스별 포인트도 다시 정리하고, 나의 실수 요소도 다시 리스트업 해서 공부해갔다. 세 번째 시간에 만난 선생님은 두 번째 시간에 만났던 TMT선생님이었는데 나를 기억하고 계셔서 편하게 운전하였다. 이 날은 두 번째 시간보다는 덜 TMT였는데 조용하게 운전하다가 간간히 질문드리고 이런 식으로 진행하였다. 처음에는 도로주행 6 시간 해서 어떻게 시험 보고 합격하나 했는데.. 스스로 공부해가니 점점 자신감도 붙고 운전에도 여유가 생기는 게 느껴졌다.

이 날도 실수한 점 잘 기억했다가 복기해서 따로 공부하고 이런 식으로 진행했다.

 

6/2 도로주행시험

5/31일에 시험을 보고 싶었지만 사람이 몰려서 가장 빠른 날인 6일 뒤에 시험을 치르게 되었다. 거의 일주일 만이라 운전 감이 사라졌을까 봐 살짝 불안했지만 매일매일 유튜브 4개 돌려보고 이런 식으로 계속 공부했다. 그리고 도로주행이라는 게 도로의 사정에 따라 변수가 달라지는 것이라 오늘 나의 운이 좋길 바라며 시험 치러 갔다.

3시 시험이라 2:30까지 가서 대기하고 있었는데, 결론적으로 4:30까지 대기였다..ㅠ 도로주행은 감독관 한 명에 4명의 시험생이 붙는다. 1/2번 시험 보고 오면 3/4번 시험 보러 가는 구조였는데 나는 3번이었다..ㅎ 3시가 되면 주의사항 같은 거 알려주시고 3:20분쯤 1/2번과 시험 보러 가신다. 3/4번은 대기장에서 대기하다가 4시까지 다 같이 어디로 오라고 지령을 받게 된다. 나는 이 말을 듣고 4시가 되면 학원차가 와서 다 같이 픽업해서 거기로 데려다주는 줄 알고 에어컨 쐬면서 앉아있었다. 그런데 50분쯤 되니까 갑자기 사람들이 우르르 나가는 느낌을 받아서 따라갔는데, 다들 각자 알아서 걸어서 거기까지 가는 것이 아닌가..?!!! 이제 맞는 거야? 싶었는데 우선 따로 행동하면 안 될 것 같아서 따라갔다(무엇보다 나랑 같이 시험 보실 4번분이 앞장서서 가셔서 따라갈 수밖에 없었다). 그렇게 도로변의 집합장소에서 서서 다시 대기를 하는데, 4시까지 오랬는데 나의 감독관님의 차량은 4:30쯤 왔다.. (나중에 물어보니 1/2번이 시험 본 코스에 차가 많아서.. 그랬다고..)

이렇게 감독관님을 뵙고 3번이었던 내가 먼저 시험을 본다. 4번 분은 뒤에서 참관을 하시고, 나는 기계로 코스를 랜덤으로 뽑는다. 사실 집합장소가 B/D 코스의 시작점이라 B/D코스 중 하나가 나오는 거 아니야? 싶었는데 진짜 랜덤이었다. C코스 가 나왔다.. D > C > A > B 순으로 어렵다고 생각했는데 C라니..ㅎ 게다가 4:30분이면 차도 많을 시간이고(C코스에 고속화도로가 있어서 안 그래도 차가 많음).. 그래도 완주만 하자! 는 생각으로 차분하게 시험을 본다.

생각보다 별일 없었다!! 교육받을 때 보다 덜 떨리기도 했고, 자신감 있게 시작하니 좋았다. 정석대로 운전하다가 감독관님이 저기 차가 있으니 하나 옆에서 갈까요? 그러면 그 말대로 하면 되는 거였고, 다행히 합류 지점에도 차가 딱 없을 때여서 끼어들기도 무난하게 했다. 다만 우회전을 하기 위해서는 버스정류장 지나서 3차선 가서 우회전을 해야 하는데, 버스가 설지, 안 설지 눈치 보다가 깜빡이를 늦게 넣고 바로 들어가서 거기서 7점 감점되었다. 그래서 93점으로 합격! 휴.. 너무 좋았다. 이제 끝이라니!!

그 후 4번분이랑 교대해서 내가 참관을 하는데, 4번분이 5 수생이셨다.. 4번 떨어지고 오늘이 5번째 시험이라는 것이다?!!. 게다가 추가 교육도 들었어서 학원 선생님이랑 통화도 할 만큼 친해지셨다고..(맨날 오늘은 붙었어?라고 안부 전화가 온 댔다 ㅋㅋ) 참관하는데 그날은 무난하게 운전하신 것 같다. 차 간 거리 유지에서 감점되셔서 그분도 93점으로 합격! 둘 다 합격해서 기쁨의 박수를 치고 행복하게 시험이 마무리되었다. (알고 보니 그날 그 감독관님이 본 4명 다 합격이라고 하셨다. 어쩌면 좋은 감독관님 만나는 것도 운인 듯하다.)

이렇게 응시표에 합격 도장을 딱 받으면!! 그러면 이제 더 이상 학원에 안 나와도 된다는 것이다!! (그리고 학원 수강카드는 그냥 분리수거해서 버리면 된다.)

자랑스러운 나의 응시표, 뭐든지 한 큐에 끝내서 기뻤다.

온수역까지 나름 멀고.. 교통비 내면서 열심히 댕겼는데 시원섭섭했다. 추가로 돈 내가며 시험 보는 건 정말 하고 싶지 않았는데(시간이 없는 직장인 2233 그리고 추가금은 너무 비싸!!) 다행이다.

 

6/3 면허증 받기

기쁜 맘에 다음날 근무 시간을 조정하고 면허증 발급을 받으러 간다. 역시 서부 면허장으로 갔다. 3시 반쯤 간 것 같은데, 사람이 엄청 많았다. 번호표를 뽑는데 내 앞 대기자가 70명이었다.. 그래도 중간에 빨리 빠져서 40분 만에 접수를 했다.

영문/국문/모바일 면허증 받는데 8000원이었다. 인터넷 후기에서 만원으로 보고 갔는데 요즘 프로모션 기간이라 8천 원에 해준다고 하셨다. 심지어 모바일 없이 영문만 받는데 만원이고, 모바일까지 받는 건 8천 원으로 더 싸니 당연히 이걸 해야지! 결제하고 10분 정도 대기하니 면허증을 주셨다. 

모바일 신분증 앱을 받아 운전면허증을 발급받았는데, 처음에는 자꾸 대상자가 아니라고(면허를 안 땄다고) 나와서 이게 행정적으로 아직 처리가 안된 건가 불안했었는데 한 6시간 정도 지나고 다시 해보니 화면이 넘어갔다.. 앱에서 하라는 대로 면허증을 nfc로 읽히고 서부 면허장에서 등록했던 비밀번호를 입력하면 폰에도 면허증이 생긴다. 경찰청장 인증인 찐 면허증이라 나중에 투표를 한다거나 공식적인 자리에서 인정이 된다고 하니 따로 면허증을 들고 다니지 않아도 내가 나라는 것을 인증할 수 있어서 편리한 것 같다.

이렇게 나의 오랜 버킷리스트 중 하나인 면허증 받기 미션 종료!


나중에 알게 되었는데 온수 운전면허학원이 평이 좀 안 좋은 것 같았다. 이걸 미리 알았다면 온수로 갔을까 하는 생각을 하긴 했지만 결론적으로 딱히 나쁜 곳 같지도 않고, 어차피 운전은 내가 하는 것이니까 내가 공부해서 가면 무난하게 졸업할 수 있는 그런 곳인 것 같다. 안 좋다면 여기만 안 좋을까? 다른 곳도 상황에 따라 / 사람에 따라 안 좋을 수도 있다고 생각한다. 너무 주변 말에 흔들리지 마시길~

728x90
반응형

'일상 > 잡담' 카테고리의 다른 글

oracle developer day 베스트드레서  (0) 2022.01.19
반응형

websocket vs RESTful http

webscoket(pubnub.com)

  • 공통점
    1. tcp layer
    2. OSI 7계층 중 최상위 application layer
    3. TLS security(https, wss); 443 port
  • 차이점
    • websocket
      1. bi-directional: 양방향 통신
      2. full-duplex: 서버와 클라이언트가 동시에 독립적으로 상대에게 이야기할 수있음
      3. event based
      4. stateful protocol: 한쪽에서 끊을때까지 계속 연결된 상태
      5. persistent single TCP connection: websocket connection lifecycle동안 하나의 tcp connection에서 통신
      6. ws:// wss://
      7. 리얼타임/게임/채팅과 같이 실시간 업데이트, 지속적인 데이터를 받아야 하는 곳
    • RESTful http
      1. uni-directional: 한번에 한 방향으로 통신
      2. request-response based
      3. stateless protocol
      4. non persistent connection: 요청하고 끊고 응답주고 끊고, 10개의 요청을 보내면 10개의 connection이 생김
      5. http://, https://
      6. 과거 데이터, 데이터를 한번만 받아도 되는 곳

 

앞으로 아래 두 글을 바탕으로 샘플 websocket 프로젝트를 만들고 변형한다. websocket과 webflux는 사상이 비슷(event driven)한 친구라 이번 기회에 두 스택을 같이 사용해보려고 한다.

환경: java17, springboot2.5.6 / webflux / mongo on docker

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-websocket

 

Web on Reactive Stack

The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports

docs.spring.io

https://www.baeldung.com/spring-5-reactive-websockets

 

Reactive WebSockets with Spring 5 | Baeldung

A quick and practical example of Spring Framework 5 Reactive WebSockets API.

www.baeldung.com

 

728x90
반응형
반응형

환경: docker, mysql8.0.29

 

r2dbc:mysql://localhost:3306/webflux

 

스프링 설정 상, DB url이 위와 같더라도 스프링에서 r2dbc를 사용할 뿐, 인텔리제이 등 다른 DB client에서는 아래와 같이 jdbc로 사용하면 된다.

intellij mysql

연결 후 mysql 명령어를 실행하면 다음과 같은 로그가 지나간다.

mysql        | mbind: Operation not permitted

docker-compose.yml에 위와 같이 security_opt 부분을 추가하고 docker-compose up 으로 다시 구동시키면 에러가 안 난다.

 

https://docs.docker.com/engine/security/seccomp/#run-without-the-default-seccomp-profile

도커 쪽 문서를 보니 seccomp profile이 없을 때 나는 에러인 듯 하다.

728x90
반응형
반응형

환경: docker, mongo 5.0.9

 

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.

docker-compose.yml 에 아래와 같이 설정하여 docker-compose up 명령어로 실행 시..

db client tool에서는 아래와 같이 접속을 시도해야한다.

mongodb://nhn:nhn@localhost:3307

유저를 주지 않으면 authentication 에러가 나고, 잘못된 포트를 주면 connection 에러가 난다..

 An error occurred while loading instance info: command hostInfo requires authentication
connect ECONNREFUSED 127.0.0.1:27017

여기서 헷갈렸던 부분이 포트인데, yml에는 3307:27017 이렇게 적혀있어서 왜 저렇게 쓰는지 의문이었는데,

앞 포트(3307)은 외부 노출 포트고 뒤 포트(27017)는 컨테이너 포트인 것 같다.

Expose ports.
Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen).

어쨋건 mongoDB의 내용을 더 편하게 볼 수 있는 클라이언트(mongo compass)까지 확인 완료.

mongo compass

위 내용은 intellij에서도 확인가능하며 콘솔 명령어로도 조회할 수 있어 더 편리한 것 같다.

intellij

 

mongo db 간단 명령어

# mongo 접속
mongo -u nhn -p nhn

# 현재 로그인 정보
db.runCommand({connectionStatus: 1})

# db list
show dbs
# db change
use webflux
# see table
show tables
= show collections

# table을 collection이라고 부름에 주의
# chat table 조회
db.chats.find()

 

아래 명령어가 정리된 문서가 있는데, collection에 실제 콜랙션 이름을 넣어야 한다. collection은 일반 dbms의 테이블과 같은 용도이다.

https://www.mongodb.com/docs/manual/reference/mongo-shell/

 

mongo Shell Quick Reference — MongoDB Manual

Docs Home → MongoDB ManualYou can retrieve previous commands issued in the mongo shell with the up and down arrow keys. Command history is stored in ~/.dbshell file. See .dbshell for more information.The mongo shell can be started with numerous options.

www.mongodb.com

 

그리고 보다보니 find와 aggregate가 비슷해 보였는데(일반 select절의 where문과 같이 필터링하는 역할) 깊이 보면 차이가 있는 듯하다.

https://secretartbook.tistory.com/21

 

[MongoDB] 확장 검색 쿼리 - Aggregation의 목적 및 작동방식

MongoDB의 Find명령으로는 데이터를 그룹핑해서 특정 조건에 일치하는 도큐먼트의 개수를 확인한다거나 하는 복잡한 처리는 수행할 수 없었다. MongoDB의 Aggregation은 FIND 명령으로는 처리할 수 없는

secretartbook.tistory.com

아직 실제로 막 써보지 않아서 와닿지는 않는다.


https://blog.silnex.kr/dockerdocker-compose-%EC%A0%95%EB%A6%AC/

 

[Docker]docker-compose 정리

Docker Compose 여러 docker를 한번에 up(setup)할때 사용하는 방법이다. yml 파일을 사용하며, docker-compose up명령어로 실행하여 사용할 수 있다. TL;DR version: '{버전}' services: {도커 이름}: driver: {네트워크 이

blog.silnex.kr

 

728x90
반응형
반응형

linux의 .bash_profile과 같은 파일은 한 번 만들고 source 명령어를 통해 활성화하면 다음부터는 별다른 실행 없이 바로 적용되는 것을 알 수 있다. 허나 mac 같은 경우에는 그렇지가 않아서 매우 귀찮았는데, 이는 최초에 초기화 시 불러오는 파일명이 달랐기 때문이다.

아마 최초에 zsh관련 라이브러리를 설치했기 때문이 아닌가 싶긴한데, 내 컴퓨터에서는 아래의 이름을 된 파일이 최초에 읽어진다.

.zshrc

따라서 저 파일을 생성하고 그 안에 작성하면 무엇이든 두 번씩 하지 않아도 된다.

나는 이미 .bash_profile에 적어뒀던지라 그냥 그 파일을 읽어오게 했다.

if [ -f ~/.bash_profile ]; then
  . ~/.bash_profile
fi
728x90
반응형

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

[redis] 설치 / redis-cli  (0) 2025.01.09
[parallels] local server not working on parallels  (0) 2022.04.11
[vm] nginx 설치  (0) 2022.02.24
[parallels] nox...... 99%.....  (0) 2022.02.23
[vm] axon server 설치  (0) 2022.01.12
반응형

환경: springboot 2.5.6, springfox-boot-starter 3.0.0

 

어느 날인가 스웨거를 들어가기만 해도 아래와 같은 에러 로그가 지나가는 것이 관찰되었다.

2022-06-03 11:58:36 WARN  [i.s.m.p.AbstractSerializableParameter   .getExample          : 421] Illegal DefaultValue null for parameter type integer
	java.lang.NumberFormatException: For input string: ""
    	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
        ...

 

아니 내가 api를 실행시킨 것도 아니고, 스웨거 화면을 새로고침만 했는데 에러 로그라니.. 이건 필히 설정 문제일 것이라는 생각에 참을 수 없었다.

저 에러는 보편적으로 다른 String들이 null을 허용하는데 반해 int는 null을 허용하지 않기 때문에 발생하는데, 스웨거 실행 시 발생하니까 스웨거 관련 설정에서 int를 사용하는 곳이 의심스러웠다.

스웨거에서 api param 세팅해주는 부분을 보았다. 숫자형으로 받는 부분 위주로 보았다.

주로 아래와 같이 @RequestParam으로 직접 받거나 dto를 사용하여 받고 있었다.

@RequestParam(defaultValue = "0") int page
@RequestParam BigInteger seq

public class HoleCardCollectionWeeklyMissionSummaryReq {
    @ApiModelProperty(required = true)
    @NotBlank
    private Integer week;
    ...

 

관련하여 아래 글을 읽고 힌트를 얻은 후 몇 번 이리저리 테스트해본 결과 특정 상황을 포착했다.

  1. BigInteger는 상관없음
  2. @RequestParam(default value = "0") 으로 설정된 int는 상관없음
  3. 필수 값일 경우(@ApiModelProperty(required = true) / @NotBlank) 상관있음

https://soobindeveloper8.tistory.com/444

 

java.lang.NumberFormatException: For input string: ""

@ApiParam( value = "Paging Limit Num", name = "limitNum", type = "int", example = "") @RequestParam (value="limitNum", defaultValue="0")​ java.lang.NumberFormatException: For input string: "" W..

soobindeveloper8.tistory.com

 

즉 필수 값이면서 default value가 설정되지 않은 request param 중 숫자(int)를 스웨거가 보여주려고 하는데, defaultValue도 없고 example도 없어서(null 이어서) 에러가 나는 것이다.

그래서 문제가 된 부분을 찾았고 defaultValue을 주기보다는 스웨거 용 예시(@ApiParam(example))를 추가하여 해결하였다.

public class HoleCardCollectionWeeklyMissionSummaryReq {
    @ApiModelProperty(required = true)
    @ApiParam(example = "1")
    @NotBlank
    private Integer week;
    ...
728x90
반응형
반응형

이전에 반복적인 작업이 필요한 상황에서 런덱이나 Linux의 crontab을 사용하여 주기적인 api호출을 했었던 적이 있는데, 모든 컴포넌트의 정산, 배치성 업무를 계속 등록하여 1. 관리가 안되고 2. 런덱서버의 OOM 및 과부하로 인해 필요한 job이 제 때 실행되지 않는 문제를 겪었었다.

그 후 spring-batch와 같은 라이브러리를 통해 전문적으로 배치를 관리할 수 있다는 것을 알았지만, 보통 배치성 업무는 한 컴포넌트에 두세 개 정도로 많지 않은 경우가 대부분인데 이를 위해 별도의 spring-batch 프로젝트를 생성하여 관리하는 게 번거롭게 느껴졌다.

한두 개의 심플한 api를 별도의 스케줄러 없이 하나의 프로젝트로 관리하려면? spring-boot에 들어있는 @Schueduled라는 어노테이션을 메서드 위에 붙여서 쉽게 해결할 수 있다.

 

본 글에서는 @Schueduled를 사용하여 인스턴스 별 로컬 캐싱을 하는 예시를 정리한다.


1부 @Schueduled

@Scheduled를 사용하기 위한 조건

  • 메서드의 return type이 void일 경우(return type이 있어도 무시됨) && 메서드의 input parameter가 없을 경우
@Scheduled(cron = "0/3 * * * * *")
public void sumStatisticsResponse() {
	...
}

 

그리고 @EnableScheduling 어노테이션을 프로젝트에 붙여야 한다.

@SpringBootApplication
@EnableScheduling
public class Application {
	...
}

 

@Scheduled 옵션

  • cron : cron표현식 사용 가능 ("초 분 시 일 월 주 (년)")
  • fixedDelay : 이전 작업이 끝난 시점으로부터 얼마나 쉬고 다시 작업할지를 설정(milliseconds 단위)
    • 한 개의 인스턴스가 작업해야 하는 경우 
    •  ex) fixedDelay = 5000
  • fixedDelayString : fixedDelay의 문자열 버전
    • ex) fixedDelay = "5000"
  • fixedRate : 이전 작업이 수행되기 시작한 시점으로부터 언제 다시 시작할지 고정된 시간을 설정(milliseconds 단위)
    • 주의: 기본값은 병렬 작업이 아니므로, 이전 작업이 안 끝났다면 fixedRate로 설정한 시간이 다가와도 작업하지 않음
    • ex) fixedRate = 3000
  • fixedRateString : fixedDelay의 문자열 버전
    • ex) fixedRate = "3000"
  • initialDelay : 스케줄러에서 메서드가 등록되자마자 수행하는 것이 아닌 초기 지연시간을 설정
    • 사전에 작업해야 할 내용이 있을 경우 대기시간 설정 가능
  • initialDelayString : initialDelay의 문자열 버전
  • zone : cron표현식을 사용했을 때 사용할 time zone(기본값: 서버의 타임존)
//파리 시간으로 매윌 15일 10:15:00에 실행
@Scheduled(cron = "0 15 10 15 * ?", zone = "Europe/Paris")

//프로퍼티로 값을 받아서 사용 가능
@Scheduled(fixedRateString = "${myscheduler.period}", initialDelay = 2000)
@Scheduled(cron = "${cron.expression}")

 

병렬 처리

기본값으로 스프링은  local single-threaded scheduler로 처리한다. 즉, 복수개의 @Scheduled가 설정되어 있어도 이전  작업이 다 끝나기를 기다린다는 뜻이다. 스레드 풀을 생성하여 빈으로 등록해주면 알아서 찾아서 해당 스레드 풀로 병렬 처리가 된다(혹은 비동기 처리를 추가할 수도 있다).

1. spring-boot properties(or)

spring.task.scheduling.pool.size=5

2. in code

@Bean
public TaskScheduler  taskScheduler() {
    ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
    threadPoolTaskScheduler.setPoolSize(5);
    threadPoolTaskScheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
    return threadPoolTaskScheduler;
}

2부 인스턴스 별 로컬 캐싱을 3초 주기로 설정하기

@Component
public class CacheEventService {

    @Autowired
    private CommonDataRepository commonDataRepository;

    private Long ival1;
    private Long ival2;
    private Long ival3;

    @Scheduled(cron = "0/3 * * * * *")
    @Transactional
    public void sumStatisticsResponse() {
    	//데이터 조회 준비
        //통계데이터와 같은 무거운(?)쿼리 실행 = 캐싱할 데이터
        //후처리(계산 등)
       
        ival1 = commonDataRepository.sumDailyBrandCampaignIval1(dailyDatePk) + (staticGlobalData != null ? staticGlobalData.getIval1() : 0);
        ival2 = commonDataRepository.sumDailyBrandCampaignIval2(dailyDatePk) + (staticGlobalData != null ? staticGlobalData.getIval2() : 0);
        ival3 = commonDataRepository.sumDailyBrandCampaignIval3(dailyDatePk) + (staticGlobalData != null ? staticGlobalData.getIval3() : 0);
}
////////////////////////

@Service
public class RetrieveEventServiceImpl {

    @Autowired
    private CacheEventService cacheService;
    
    public BaseResponse<StatusResponse> getCurrentInfo(String tno, MobileGameType gameType, EntityGachaConfig gachaConfig) {
    	//기타 로직
		...
    	//원래는 이 안에 함수를 호출할 때마다 디비 조회하는 로직이 있었는데,
        //3초 한번 캐싱된 데이터를 불러와서 세팅함
        Long ival1 = cacheService.getIval1();
        Long ival2 = cacheService.getIval2();
        Long ival3 = cacheService.getIval3();

		...
	}
}

사실  별거 없다. 단지 내게 생소했을 뿐..

class안의 필드를 3초 주기로 세팅해주고 그 값을 가져와 사용하는 방식이다. 메서드를 호출할 때마다 디비를 호출하지 않는 장점이 있다. 

위 방식의 단점은, 로컬 인스턴스 별로 캐싱이 이루어지기 때문에 디비가 받는 부하(조회)는 3초에 한 번씩 인스턴스 수만큼 생긴다. 8개의 인스턴스가 돌고 있다면 3초마다 저 쿼리다 8번 호출되겠지. 인스턴스와 무관하게 같은 결과가 나온다면 한 번의 조회로 관리되게 수정할 필요가 있어 보인다. 레디스나 별도 디비의 값으로 글로벌하게 관리하면 될 듯하다(그래서 내가 과거에 이 방식을 안 써본 듯, 어쨌든 다양한 방법을 알고 있는 건 좋은 것이니까).


3부 스프링 캐시

위와 같은 방법으로 캐싱을 할 수도 있지만, 스프링이 제공하는 캐싱도 있다.

별도의 항목이고 내용이 길어지므로 링크로 첨부한다.

2022.05.30 - [개발/spring] - [spring-cache] 스프링 캐시

 

[spring-cache] 스프링 캐시

스프링이 제공하는 캐싱이 있다. implementation 'org.springframework.boot:spring-boot-starter-cache' 위 의존성을 추가하고 프로젝트에 @EnableCaching 어노테이션을 추가한다. spring-boot를 사용하면 @Enabl..

bangpurin.tistory.com

 

 


원문: https://www.baeldung.com/spring-scheduled-tasks

 

The @Scheduled Annotation in Spring | Baeldung

How to use the @Scheduled annotation in Spring, to run tasks after a fixed delay, at a fixed rate or according to a cron expression.

www.baeldung.com

 

728x90
반응형
반응형

Ehcache의 주요 특징

  1. 효율적인 캐싱: 메모리와 디스크를 사용한 하이브리드 캐싱을 지원하여, 대용량 데이터의 캐싱도 효율적으로 처리할 수 있습니다.
    1. 메모리 캐시: 자주 사용되는 데이터를 메모리에 저장하여 빠른 응답 시간을 제공합니다. 메모리 캐시는 가장 빠른 캐싱 옵션입니다.
    2. 디스크 캐시: 메모리 캐시의 데이터를 디스크에 저장하여, 메모리 용량 초과 시에도 데이터를 유지할 수 있습니다. 디스크 캐시는 메모리보다 느리지만 더 많은 데이터를 저장할 수 있습니다.
  2. 분산 캐싱: 클러스터링과 분산 캐싱을 지원하여, 여러 애플리케이션 인스턴스 간의 캐시 일관성을 유지할 수 있습니다.
    1. Ehcache는 여러 노드 간에 캐시 데이터를 공유하고 동기화할 수 있는 클러스터링 기능을 제공합니다. 이를 통해 분산 환경에서 데이터 일관성을 유지할 수 있습니다.
    2. Terracotta와 통합하여 확장 가능한 분산 캐시 클러스터를 구축할 수 있습니다.
    3. Ehcache + Terracotta의 장점:
      1. 분산 캐시 처리: 여러 서버에서 동일한 캐시 데이터를 공유하고 동기화할 수 있습니다.
      2. 확장성: 여러 서버에 걸쳐 캐시를 분산 처리하므로, 대규모 애플리케이션에서도 효율적으로 캐시를 관리할 수 있습니다.
      3. 고가용성: Terracotta는 분산 환경에서 장애 복구 기능을 제공하여 데이터의 고가용성을 보장합니다.
      4. 데이터 일관성: 동기화 또는 비동기화 모드를 통해 캐시 데이터의 일관성을 유지할 수 있습니다.
  3. 플러그인 방식: Spring Framework, Hibernate, JPA 등과 쉽게 통합되어, 개발자가 캐싱을 손쉽게 구현할 수 있습니다.
  4. 캐시 구성: 다양한 캐시 정책(예: LRU, LFU, FIFO)을 통해 캐시의 만료, 용량 제어, 데이터 일관성 관리가 가능합니다.
    1. TTL(Time To Live): 캐시 항목의 유효 시간을 설정하여, 지정된 시간이 지나면 캐시에서 자동으로 제거됩니다.
    2. TTI(Time To Idle): 캐시 항목이 사용되지 않은 시간 기준으로 만료되는 설정입니다. 일정 시간 동안 접근되지 않은 캐시 항목은 제거됩니다.
    3. LRU(Least Recently Used): 최근에 사용되지 않은 항목을 먼저 제거하여 캐시의 크기를 관리하는 정책입니다.
    4. LFU(Least Frequently Used): 사용 빈도가 적은 항목을 먼저 제거하여 캐시의 크기를 관리합니다.
  5. 기본 및 확장 기능: 기본적인 캐시 기능 외에도, 캐시 이벤트 청취자, 트랜잭션 캐시, 캐시 복제 등 다양한 기능을 제공합니다.
  6. JCache 표준 지원 & Spring과의 통합:
    1. Ehcache는 Spring 프레임워크와의 통합을 지원하며, Spring의 캐시 추상화를 통해 쉽게 설정하고 사용할 수 있습니다.
    2. 애노테이션 기반 캐시 관리(@Cacheable, @CacheEvict 등)를 지원하여 개발 생산성을 높일 수 있습니다.
  • 3.1 캐시 일관성 전략
    1. Write-through 캐시
      • 개념: 애플리케이션이 데이터베이스에 데이터를 쓰는 동시에 캐시에 데이터를 갱신하는 방식입니다. 쓰기 작업이 캐시와 데이터베이스에 동시 반영됩니다.
      • 장점: 데이터베이스와 캐시 간의 일관성이 보장됩니다.
      • 단점: 쓰기 작업의 지연 시간이 증가할 수 있습니다.
    2. Write-behind 캐시
      • 개념: 애플리케이션이 데이터를 캐시에 먼저 쓰고, 비동기적으로 데이터베이스에 반영하는 방식입니다. 캐시에 데이터를 저장한 후, 일정 시간 후에 데이터베이스에 기록됩니다.
      • 장점: 쓰기 작업의 성능이 향상됩니다.
      • 단점: 캐시와 데이터베이스 간의 일관성 문제가 발생할 수 있으며, 데이터 유실 가능성도 있습니다.
    3. Cache-aside 패턴 / 읽을때
      • 개념: 애플리케이션이 먼저 캐시에서 데이터를 조회하고, 캐시된 데이터가 없으면 데이터베이스에서 데이터를 읽은 후 캐시에 저장하는 방식입니다.
      • 장점: 읽기 작업 성능이 매우 높으며, 캐시된 데이터는 자주 읽히는 데이터로 효율적으로 관리됩니다.
      • 단점: 캐시와 데이터베이스 간 일관성을 보장하지 않으므로, 캐시 무효화 전략이 필요합니다.
    4. TTL(Time To Live) 기반 캐시
      • 개념: 캐시에 저장된 데이터에 **유효 기간(Time To Live, TTL)**을 설정하여, 데이터가 일정 시간이 지나면 자동으로 무효화됩니다.
      • 장점: 오래된 데이터가 자동으로 갱신되므로 일관성을 유지할 수 있습니다.
      • 단점: TTL을 적절하게 설정하지 않으면, 캐시 히트율이 떨어지거나 불필요한 갱신이 발생할 수 있습니다.
    5. 캐시 무효화(Cache Invalidation)
      • 개념: 데이터베이스에서 데이터가 변경되면 해당 데이터를 캐시에서 무효화하여, 다음 읽기 작업 시 새로운 데이터를 캐시에 다시 로드하는 방식입니다.
      • 장점: 데이터베이스와 캐시 간의 일관성을 보장합니다.
      • 단점: 데이터베이스와 캐시 간 동기화 지연으로 인해 일시적인 일관성 문제가 발생할 수 있습니다.

    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'javax.cache:cache-api'
    implementation 'org.ehcache:ehcache:3.6.2'

application.properties

# 캐시 구성 파일 위치 설정
spring.cache.jcache.config=classpath:ehcache.xml
spring.cache.type=ehcache

ehcache.xml(자바로도 가능)

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
         updateCheck="false">

    <!-- 기본 캐시 설정 -->
    <defaultCache
            maxEntriesLocalHeap="10000"
            eternal="false"
            timeToIdleSeconds="300"
            timeToLiveSeconds="600"
            overflowToDisk="false"/>

    <!-- 사용자 정의 캐시 설정 -->
    <cache name="exampleCache"
           maxEntriesLocalHeap="1000"
           eternal="false"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"
           overflowToDisk="true"
           diskSpoolBufferSizeMB="20"
           maxEntriesLocalDisk="10000"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU"/>
</ehcache>

캐시 구성 세부 사항

  • 캐시 만료 정책
    • timeToIdleSeconds: 캐시에 저장된 데이터가 얼마나 오랫동안 사용되지 않았을 때 만료될지 설정합니다.
    • timeToLiveSeconds: 캐시에 저장된 데이터가 얼마나 오랫동안 존재할 수 있는지 설정합니다.
    • eternal: 데이터가 영원히 만료되지 않도록 설정합니다. true일 경우 만료 시간 설정이 무시됩니다.
  • 캐시 용량 관리
    • maxEntriesLocalHeap: 캐시에서 허용할 최대 엔트리 수를 설정합니다.
    • overflowToDisk: 메모리 캐시가 가득 차면 디스크로 데이터를 내보낼지 설정합니다.
    • maxEntriesLocalDisk: 디스크 캐시에 허용할 최대 엔트리 수를 설정합니다.
  • 데이터 유지 정책
    • memoryStoreEvictionPolicy: 메모리 캐시에서 데이터를 제거할 정책을 설정합니다. LRU (Least Recently Used), LFU (Least Frequently Used), FIFO (First In, First Out) 등이 있습니다.

 

위 의존성/설정을 추가하고 프로젝트에 @EnableCaching 어노테이션을 추가한다.

spring-boot를 사용하면 @EnableCaching 어노테이션만으로도 ConcurrentMapCacheManager를 기본으로 등록해주기 때문에 따로 등록할 것은 없지만 커스텀을 할 경우 아래와 같이 빈을 등록하여 override 해야 한다.

@Configuration
@EnableCaching
public class CachingConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
          new ConcurrentMapCache("directory"), 
          new ConcurrentMapCache("addresses")));
        return cacheManager;
    }
}

참고) 캐시 매니저 종류

  • ConcurrentMapCacheManager: Java의 ConcurrentHashMap을 사용해 구현한 캐시를 사용하는 캐시 매니저
  • SimpleCacheManager: 기본적으로 제공하는 캐시가 없어 사용할 캐시를 직접 등록하여 사용하기 위한 캐시 매니저
  • EhCacheCacheManager: 자바에서 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시 매니저
  • CompositeCacheManager: 1개 이상의 캐시 매니저를 사용하도록 지원해주는 혼합 캐시 매니저
  • CaffeineCacheManager: Java 8로 Guava 캐시를 재작성한 Caffeine 캐시를 사용하는 캐시 매니저
  • JCacheCacheManager: JSR-107 기반의 캐시를 사용하는 캐시 매니저

 

ConcurrentMapCache 생성자를 통해서 캐시 이름과 null값을 캐싱할지 여부, 사용할 concurrentMap을 지정할 수 있다. 참고로 ConcurrentMapCache는 만료시간에 의한 자동 만료 기능이 없기 때문에, 수동으로 체크 혹은 @Scheduled를 활용하여 구현해야 한다.

참고로 캐시 흐름을 로그로 보고 싶다면 별도 이벤트 리스너를 구현해야한다.

@Slf4j
public class CacheEventConfig implements CacheEventListener<Object, Object> {

  @Override
  public void onEvent(CacheEvent<?, ?> event) {
    log.info(">>>[Caching event] working {} :: {}", event.getType(), event.getKey());
  }
}

캐시 조회/저장 @Cacheable

캐시 해야 하는 데이터가 return 되는 메서드 위에 설정한다.

@Cacheable("addresses")
public String getAddress(Customer customer) {...}

getAddress가 호출될 때 실제 로직이 시행되기 전 input parameter(key = customer)에 해당하는 캐시를 먼저 확인하고, 없으면 실제 로직을 실행하고 캐싱을 한다. 위 예시는 Customer클래스가 key 값이므로(별도의 key값을 설정하지 않았으므로) hashcode와 equals 메서드를 오버라이드 해 어떤 데이터일 때 같다고 판별할지 명백히 해야 한다. 아래 별도의 링크를 통해 키 판별에 대해 확인하자.

@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) {...}

이렇게 여러 개의 캐시를 줄 수도 있다. 이때 둘 중 하나라도 값이 있으면 실제 로직을 실행하지 않는다.

@Cacheable(value = "bestSeller", key = "#book.bookNo")
public Book getBestSeller(Book book, User user, Date dateTime) {...}

Key값의 지정에는 SpEL이 사용된다. 그렇기 때문에 만약 파라미터가 객체라면 위와 같이 하위 속성에 접근할 수 있다.

@Cacheable(value = "bestSeller", key = "#book.bookNo", condition = "#user.type == 'ADMIN'")
public Book getBestSeller(Book book, User user, Date dateTime) {...}

만약 파라미터 값이 특정 조건인 경우에만 캐시를 적용하기를 원한다면 condition을 이용하면 된다.

 

캐시 삭제 @CacheEvict

@CacheEvict(value = "bestSeller")
public void clearBestSeller() {...}

@CacheEvict(value="directory", key=customer.name)
public String getAddress(Customer customer) {...}

기본적으로 메서드의 키에 해당하는 캐시만 제거한다. 위 예시는 이름이 같은 캐시만 제거할 것이다.

@Caching(evict = { 
  @CacheEvict("addresses"), 
  @CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}

여러 이름의 캐시를 지울 때는 위와 같이 복수개 설정이 가능하다.

@CacheEvict(value="addresses", allEntries=true)
public String getAddress(Customer customer) {...}

key와 상관없이 모든 객체를 지우려면 allEntries = true로 지정하면 된다.

 

캐시 수정 @CachePut

캐시 값이 바뀌었다면 바로 수정해주어야 한다. @CachePut은 @Cacheable과 유사하게 실행 결과를 캐시에 저장하지만, 캐시의 저장 유무와 상관없이 항상 메서드의 로직을 실행한다는 점에서 다르다.

@CachePut(value="addresses")
public String getAddress(Customer customer) {...}

 

@CacheConfig

하나의 캐시에 대해 일괄 관리를 하고자 하면 아래와 같이 클래스 레벨에서 처리할 수도 있다. 이때 이름을 중복으로 적을 필요 없이 클래스 선언으로 해결된다.

@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {

    @Cacheable
    public String getAddress(Customer customer) {...}
}

 

조건절

위에서 잠깐 나왔지만 spEL을 활용하여 condition / unless의 조건절을 줄 수 있다.

@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}

@CachePut(value="addresses", unless="#result.length()<64")
public String getAddress(Customer customer) {...}

 

캐시 key 설정에 대한 내용

https://jistol.github.io/spring/2017/02/09/springboot-cache-key/

 

(Spring Cache) @Cacheable key값 정하기

 

jistol.github.io

https://sunghs.tistory.com/132

 

[SPRING] 스프링 캐시 사용하기

spring에서 cache 관련 된 기능을 지원한다. 기존 cache 처리라고 하면, Redis, memcached 등의 추가적인 memoryDB를 이용하거나, application 레벨에서 사용 가능한 EhCache 등이 많이 쓰이는데, 이 중 applicati..

sunghs.tistory.com

 

728x90
반응형

+ Recent posts