Web/Spring 2020. 5. 15. 14:12

 

MongoDbConfig를 작성할때, 몽고디비 서버 호스트관련하여 ClusterSettings.Builder를 작성해줘야하는데, mongo host에 모든 클러스터 서버 호스트를 명시하지 않고, 하나의 DNS(여러 서버를 하나로 묶은) 혹은 여러 서버 리스트 중 하나의 primary 호스트(ex. primary host를 명시하면 밑에 예외는 발생하지 않지만, 읽기 부하분산이 안된다.)만 명시한경우에는 반드시 multiple mode를 명시해주어야 한다. 내부적으로 host의 갯수를 보고 single mode인지 multiple mode인지 판단하기 때문이다. 해당 코드는 아래와 같다.

 

private ClusterSettings(final Builder builder) {
        // TODO: Unit test this
        if (builder.srvHost != null) {
            if (builder.srvHost.contains(":")) {
                throw new IllegalArgumentException("The srvHost can not contain a host name that specifies a port");
            }

            if (builder.hosts.get(0).getHost().split("\\.").length < 3) {
                throw new MongoClientException(format("An SRV host name '%s' was provided that does not contain at least three parts. "
                        + "It must contain a hostname, domain name and a top level domain.", builder.hosts.get(0).getHost()));
            }
        }

        if (builder.hosts.size() > 1 && builder.requiredClusterType == ClusterType.STANDALONE) {
            throw new IllegalArgumentException("Multiple hosts cannot be specified when using ClusterType.STANDALONE.");
        }

        if (builder.mode != null && builder.mode == ClusterConnectionMode.SINGLE && builder.hosts.size() > 1) {
            throw new IllegalArgumentException("Can not directly connect to more than one server");
        }

        if (builder.requiredReplicaSetName != null) {
            if (builder.requiredClusterType == ClusterType.UNKNOWN) {
                builder.requiredClusterType = ClusterType.REPLICA_SET;
            } else if (builder.requiredClusterType != ClusterType.REPLICA_SET) {
                throw new IllegalArgumentException("When specifying a replica set name, only ClusterType.UNKNOWN and "
                                                   + "ClusterType.REPLICA_SET are valid.");
            }
        }

        description = builder.description;
        srvHost = builder.srvHost;
        hosts = builder.hosts;
        mode = builder.mode != null ? builder.mode : hosts.size() == 1 ? ClusterConnectionMode.SINGLE : ClusterConnectionMode.MULTIPLE;
        requiredReplicaSetName = builder.requiredReplicaSetName;
        requiredClusterType = builder.requiredClusterType;
        localThresholdMS = builder.localThresholdMS;
        serverSelector = builder.packServerSelector();
        serverSelectionTimeoutMS = builder.serverSelectionTimeoutMS;
        maxWaitQueueSize = builder.maxWaitQueueSize;
        clusterListeners = unmodifiableList(builder.clusterListeners);
    }

 

ClusterSettings.Builder.build 메서드의 일부인데, mode를 set하는 부분에 mode를 명시적으로 넣지 않았다면 작성된 호스트 갯수를 보고 클러스터 모드를 결정한다. 만약 MonoDb 서버 여러개를 하나의 도메인으로 묶어 놓았다면, 보통 DNS하나만 설정에 넣기 마련인데, 이러면 write 요청이 secondary에 들어가게 되면 아래와 같은 에러가 발생하게 된다.(먄약 실수로 secondary host를 넣었다면 쓰기요청에 당연히 아래 예외가 계속 발생한다.)

 

MongoNotPrimaryException: Command failed with error 10107 (NotMaster): 'not master' on server bot-meta01-mongo1.dakao.io:27017. 

 

왠지, SINGLE모드일때는 secondary로 write요청이 들어왔을때 primary로 위임이 안되는듯하다.(이건 조사해봐야할듯함, 왠지 싱글모드이면 당연히 프라이머리라고 판단해서 그럴듯?..) 그렇기 때문에 클러스터는 걸려있고, 서버 리스트를 여러 서버를 묶은 DNS 하나만 작성한다면 반드시 ClusterSetting에 "MULTIPLE"을 명시해서 넣야야한다 !

posted by 여성게
:
Web/Spring 2020. 4. 29. 20:50

 

오늘 다루어볼 내용은 spring application.yaml(properties)파일들의 로드 규칙 및 순서이다. 기본적인 내용일수는 있겠지만 필자는 이번에 해당 순서의 중요성을 다시 한번 알게되서 한번더 정리해보려고 한다.

 

 

Spring Boot Features

If you need to call remote REST services from your application, you can use the Spring Framework’s RestTemplate class. Since RestTemplate instances often need to be customized before being used, Spring Boot does not provide any single auto-configured RestT

docs.spring.io

 

위 링크를 보면 PropertySource의 적용 순서가 나와있다. 우리가 신경 쓸 것은 application.properties와 application-{profiles}.properties의 순서이다. 위 링크에서는 application-{profiles}.properties가 더 우선 순위를 갖는다고 이야기하고 있다. 그 말은 무엇일까? 아래 간단히 application.properties 파일을 확인해보자.

 

#application.yaml
spring:
  application:
    name: name-1
    
#application-dev.yaml
spring:
  application:
    name: name-2

 

과연 위 yaml파일들을 모두 적용하고 나서 아래 코드를 실행한다면 어떤 값이 출력될까? 실행할때 VM option에

 

"-Dspring.profiles.active=dev"

 

을 넣어서 run 해야한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestController
@SpringBootApplication
public class JenkinsSampleApplication implements CommandLineRunner {
 
    @Value("${spring.application.name}")
    private String appName;
 
    public static void main(String[] args) {
        SpringApplication.run(JenkinsSampleApplication.class, args);
    }
 
    @Override
    public void run(String... args) throws Exception {
        log.info(appName);
    }
 
}
cs

 

결과는 "name-2"가 출력된다. 그 말은 application-{profiles}.yaml이 우선 적용이 된다는 뜻이다. 그렇다면 아래와 같은 config가 있다면 어떨까?

 

#application.yaml
spring:
  application:
    name: name-1
    
#application-dev.yaml
spring:
  profiles:
    include: dev-common
  application:
    name: name-2
    
#application-dev-common.yaml
spring:
  application:
    name: name-3

 

출력결과는 "name-3"이다. 그 말은 application-dev.yaml에서 include한 application-dev-common.yaml이 가장 큰 우선순위를 갖는 것이다. 

 

#application.yaml
spring:
  profiles:
    include: dev-common
  application:
    name: name-1
    
#application-dev.yaml
spring:
  application:
    name: name-2
    
#application-dev-common.yaml
spring:
  application:
    name: name-3

 

위 파일은 application-dev-common.yaml파일을 application.yaml에 include하였다. 이때 결과는 어떻게 될것인가? "name-2"를 출력할 것이다. 그 이유는 application.yaml에서 include했지만 application-dev.yaml이 더 우선순위를 갖기 때문이다. 즉, include된 설정값이 가장 우선순위를 갖기 위해서는 application-{profiles}.yaml에 include해야한다.

(만약 application-{profiles}.yaml이 없었다면 application-dev-common.yaml의 설정인 "name-3" 설정이 적용이 될것이다.)

 

해당 규칙들을 잘 활용해서 관리하기 쉽게 설정값들을 유지하면 좋을 것 같다. 여기까지 간단하게 spring application 설정의 적용 규칙 및 순서에 대해 다루어보았다.

posted by 여성게
:
Web/Spring 2020. 3. 20. 21:44

 

이전 시간에 DB관련 테스트 작성하는 법을 다루어 봤는데, 이번 시간에는 Webflux handler 테스트 코드를 한번 작성해보려고 한다. 이전까지는 service 단까지만 테스트를 모듈별로 작성하였지만, 핸들러로 인입하여 한번에 모든 로직을 돌려보는 테스트는 직접 넣어보지 않았던 것 같다.(사실 핸들러, 컨트롤러 테스트가 이것저것 설정해야할 것들이 많아서..)

 

간단히 바로 예제를 다루어본다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class RouteConfig {
 
    @Bean
    public RouterFunction<ServerResponse> routeFunction(TestHandler testHandler) {
        return route()
                .nest(path("/"), builder -> builder
                        .GET("/test", accept(APPLICATION_JSON), testHandler::testHandler)
                ).build();
    }
}
 
@Component
public class TestHandler {
 
    @Autowired
    private TestService testService;
 
    public Mono<ServerResponse> testHandler(ServerRequest serverRequest) {
        return ServerResponse.ok().body(BodyInserters.fromProducer(testService.testService(), String.class));
    }
}
 
@Service
public class TestService {
    public Mono<String> testService() {
        return Mono.just("testResponse");
    }
}
cs

 

간단하게 handler와 service 클래스를 구현하였다. 단순히 String을 반환하고 있다. 해당 핸들러를 테스트하는 코드는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@SpringBootTest(classes = {
        TestHandler.class,
        RouteConfig.class,
        TestService.class
})
@MockBeans({
})
class HandlerTest {
 
    private WebTestClient client;
    @Autowired
    private RouteConfig routeConfig;
    @Autowired
    private TestHandler testHandler;
 
    private static final String ENDPOINT = "/test";
 
    @BeforeEach
    public void beforeTest() {
        client = WebTestClient
                .bindToRouterFunction(routeConfig.routeFunction(testHandler))
                .build();
    }
 
    @Test
    public void handlerTest() {
        client.get()
                .uri(ENDPOINT)
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class)
                .value(value -> {
                    assertEquals(value, "testResponse");
                });
    }
 
}
cs

 

spring MVC와는 조금 다를 수 있다. 해당 테스트 코드를 간단하게 설명하면 우리가 작성한 routeConfig를 WebTestClient에 바인딩해준다. 그 이후에 해당 클라이언트를 이용하여 routeConfig 내에 존재하는 라우팅 엔드포인트에 요청을 날리면 핸들러 로직을 수행후 응답을 내려준다. 그리고 해당 응답을 이용하여 원하는 응답이 나왔는지 assert 해볼 수 있다. 위는 정상적인 경우만 다루어보았고, 한번 없는 요청을 보내보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@SpringBootTest(classes = {
        TestHandler.class,
        RouteConfig.class,
        TestService.class
})
@MockBeans({
})
class HandlerTest {
 
    private WebTestClient client;
    @Autowired
    private RouteConfig routeConfig;
    @Autowired
    private TestHandler testHandler;
 
    private static final String ENDPOINT = "/test";
    private static final String INVALID_ENDPOINT = "/invalid";
 
    @BeforeEach
    public void beforeTest() {
        client = WebTestClient
                .bindToRouterFunction(routeConfig.routeFunction(testHandler))
                .build();
    }
 
    @Test
    public void handlerTest() {
        client.get()
                .uri(ENDPOINT)
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class)
                .value(value -> {
                    assertEquals(value, "testResponse");
                });
    }
 
    @Test
    public void notFoundHandlerMappingTest() {
        client.get()
                .uri(INVALID_ENDPOINT)
                .exchange()
                .expectStatus().is4xxClientError();
    }
 
}
cs

 

테스트를 하나더 추가하였다. 이 요청 url은 존재하지 않는 handler mapping이므로 정상적이라면 404 not found를 응답코드로 내려줄 것이라고 기대하고 테스트코드를 작성하였다.

 

실행해보면 테스트가 정상적으로 통과한다.

 

여기까지 간단하게 웹플럭스 핸들러 테스트를 작성해보았다. 아주 간단하게만 작성하였지만 테스트에 고려할만한 TC(Test Case)는 아주 많은 것같다. 400 예외를 고려한 테스트, 혹은 특정 예외를 고려한 테스트 혹은 모든 로직이 잘 동작하는 테스트 마지막으로 파라미터마다 동작이 다른것을 테스트 해보는 경우등 아주 많은 경우가 있을 것이다. 모든 테스트케이스를 잘 고려해 테스트를 짜보자.

 

마지막으로 만약 애플케이션에 ErrorHandler가 적용되어있다면 위와 같이 짠 테스트 코드는 ErrorHandler가 내뱉는 응답을 받지 못한다. 왜냐 테스트 컨텍스트 빈으로 등록하지 않았기 때문이다. 만약 파라미터가 잘못들어와 로직상에서 예외를 던졌고, 만약 그 예외를 ErrorHandler가 받아서 400이라는 HttpStatusCode로 리턴하고 있는데 그것을 기대하고 파라미터가 잘못들어온 경우를 is4xxClientError()로 잡으면 테스트는 통과하지 못한다. 그 이유는 WebTestClient는 500에러를 내뿜고 있기 때문이다. 그 이유는 위에 말한 것과 같이 ErrorHandler를 테스트 컨텍스트 빈으로 등록하지 않아서 4xx로 변환되지 못하고 애플리케이션 로직상에서 나는 예외(500)로 인식하기 때문이다. 

posted by 여성게
:
Web/Spring 2020. 3. 3. 16:30

 

오늘 다루어볼 내용은 spring cache를 이용할때, 쉽게 놓쳐 실수 할 수 있는 @CacheEvict이다. @CacheEvict는 캐시 되어 있는 내용을 refresh, 정확히는 삭제하는 어노테이션인데 명시적으로 캐시 키값을 명시해주지 않으면 발생할 수 있는 실수 있다. 바로 예제를 살펴본다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.cache;
 
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
 
@EnableCaching
@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class CacheApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
 
    private final CacheService cacheService;
 
    @GetMapping("/")
    public Mono<String> cacheTest(@RequestParam("id"String id) {
        return Mono.just(cacheService.cacheTest(id));
    }
 
    @GetMapping("/refresh")
    @CacheEvict(value = "testCache")
    public Mono<String> cacheRefresh() {
        return Mono.just("refresh");
    }
 
    @Component
    public static class CacheService{
        @Cacheable("testCache")
        public String cacheTest(String id) {
 
            return id+(Math.random()*100);
 
        }
    }
 
}
 
cs

 

  • 15Line - spring cache를 사용하겠다라는 어노테이션
  • 40Line - testCache라는 value로 cacheTest 메서드의 결과를 캐시한다. 하지만 여기서 쉽게 지나칠수 있는 것은 cacheTest 메서드의 String id 값이 "cacheTest"라는 캐시의 키값이 되어 들어간다. 이말은 같은 "cacheTest" value이지만 키값에 따라 데이터가 여러건 캐시될 수 있다라는 뜻이다.
  • 33Line - value가 "testCache"인 캐시를 삭제한다. 여기서 또한 쉽게 놓칠 수 있는 부분이 있다. 해당 메서드는 파라미터가 없는데, 이때 value = "testCache"이며 key가 0인 캐시 데이터를 지우게 된다. 

 

바로 요청을 날려서 결과를 보자.

 

 

계속 날려도 같은 값이 온다. 그리고 refresh 요청을 보내보자.

 

 

이후에 다시 기존 api를 호출해보자.

 

 

똑같다. 이 말은 캐시가 삭제되지 않았다는 것이다. 왜그럴까? 이유는 "testCache"인 캐시에 key가 ab인 캐시가 생성되어 있는데, 캐시는 "testCache"인 key가 ab가 아닌 캐시를 삭제하려고 하고 있기 때문이다. 해결방법은 무엇일까? 그것은 캐시를 삭제하는 메서드에도 똑같이 key값을 가지는 매개변수를 받던가 혹은 @CacheEvict(value="testCache",key="ab")를 넣어준다. 하지만 수동으로 넣는 것은 유지보수 측면으로 좋지 않으니, 캐시를 삭제할때 key값을 메서드 매개변수로 받도록 한다.

 

그리고 한가지더 방법은 @CacheEvict(value="testCache", allEntries=true)로 지워주는 것도 방법이다. 이것은 key값 상관없이 해당 value로 들어간 모든 캐시데이터를 지우는 것이다.

 

방법1)

캐시를 지우는 메서드에 캐시를 생성하는 메서드와 같이 key값이 되는 매개변수를 넣어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.cache;
 
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
 
@EnableCaching
@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class CacheApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
 
    private final CacheService cacheService;
 
    @GetMapping("/")
    public Mono<String> cacheTest(@RequestParam("id"String id) {
        return Mono.just(cacheService.cacheTest(id));
    }
 
    @GetMapping("/refresh")
    @CacheEvict(value = "testCache")
    public Mono<String> cacheRefresh(@RequestParam("id"String id) {
        return Mono.just("refresh");
    }
 
    @Component
    public static class CacheService{
        @Cacheable("testCache")
        public String cacheTest(String id) {
 
            return id+(Math.random()*100);
 
        }
    }
 
}
 
cs

 

방법2)

해당 value로 생성된 모든 캐시 엔트리를 지운다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.cache;
 
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
 
@EnableCaching
@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class CacheApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
 
    private final CacheService cacheService;
 
    @GetMapping("/")
    public Mono<String> cacheTest(@RequestParam("id"String id) {
        return Mono.just(cacheService.cacheTest(id));
    }
 
    @GetMapping("/refresh")
    @CacheEvict(value = "testCache", allEntries = true)
    public Mono<String> cacheRefresh() {
        return Mono.just("refresh");
    }
 
    @Component
    public static class CacheService{
        @Cacheable("testCache")
        public String cacheTest(String id) {
 
            return id+(Math.random()*100);
 
        }
    }
 
}
 
cs

 

여기까지 @CacheEvict를 사용할때 유의해야할 점이었다. 마지막으로 무거운 로직에 캐시를 무조건 해야한다는 생각은 버려야한다. 아무리 무거운 로직이라도 자주 변경되는 데이터라면 캐시하면 오히려 성능 저하로 이어진다. 올바르게 용도에 맞게 캐시를 사용하도록 하자!

posted by 여성게
:
Web/Spring 2020. 2. 20. 00:46

 

오늘 다루어볼 내용은 Webflux에서 사용되는 WebClient가 onCancel이 호출되는 시점이다.  이번에 애플리케이션을 개발하면서 많은 서비스가 통신을 하는데, 필자가 개발한 애플리케이션의 jaeger 로그를 보니 WebClient가 cancelled("The subscription was cancelled") 되었다는 로그가 찍혀있었다. 무슨 이유로 이러한 로그가 남는지 확인했더니, 아래와 같은 이유였다.

 

"AServer -> BServer ->CServer"

 

이렇게 3개의 서버가 통신하는 상황인데, AServer의 Client는 ReadTimeOut이 3초이고, BServer는 ReadTimeOut이 5초이다. 그런데 CServer가 응답을 주는데 4초가 걸렸다면? 

 

이런 상황에서 발생하는 것이 cancelled 상황이다. AServer는 이미 Readtimeout이 나버려서 500 에러가 발생하였고, 이제 더이상 BServer는 응답을 받아올 필요가 없어졌으므로 subscribe를 cancel을 해버리는 것이다. 간단히 위와 같은 상황을 샘플 앱으로 작성해 보았다.

 

[AServer:9090]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient.Builder webClientFactory(){
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(3000, TimeUnit.MILLISECONDS)));
 
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)));
    }
 
    @Bean
    public WebClient webClient() {
        return webClientFactory().build();
    }
 
}
 
@Slf4j
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class ExamApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ExamApplication.class, args);
    }
 
    private final WebClient client;
 
    @GetMapping("/")
    public Mono<String> hello(){
        log.info("exam server accept request");
        return client.get("http://localhost:8080/")
                .retrieveBodyToMono(String.class);
}
 
 
 
cs

 

[BServer:8080]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient.Builder webClientFactory(){
        TcpClient tcpClient = TcpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
                .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
 
        return WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)));
    }
 
    @Bean
    public WebClient webClient() {
        return webClientFactory().build();
    }
 
}
 
@Slf4j
@RestController
@SpringBootApplication
@RequiredArgsConstructor
public class ClientApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
 
    private final WebServiceClient client;
 
    @GetMapping("/")
    public Mono<String> hello(){
        log.info("client server accept request");
        return client.get("http://localhost:7070/")
                .retrieveBodyToMono(String.class);
    }
 
}
 
 
cs

 

[CServer:7070]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@SpringBootApplication
public class ServerApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
 
    @GetMapping("/")
    public String hello() throws InterruptedException {
        Thread.sleep(10000);
        return "hello";
    }
 
}
cs

 

이제 "http://localhost:9090/" 으로 요청을 보내보자!

 

 

보면 AServer(exam)은 ReadTimeOut Exception이 발생하였고, subscription was cancelled라는 메시지를 남기고 있다. 혹시나 이러한 문제가 발생한 사람이 있다면.. 적절히 서버들의 WebClient 옵션을 조정해줄 필요가 있거나 혹은 비정상적으로 응답이 느린 구간이 있을 수 있다. 하지만 때때로 클라이언트 애플리케이션에서 커낵션 풀을 사용하지 않아 과도하게 로컬 포트를 많이 개방한다거나 등의 문제로 cancelled되는 경우가 있으니, 여러가지 상황을 고려해보아야 한다.

 

마지막으로 ConnectionTimeOut, ReadTimeOut, SocketTimeOut의 차이점을 모른다면 아래 링크를 확인하자.

 

2019/02/12 - [프로그래밍언어/Java&Servlet] - Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점?

 

Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점?

Java - ConnectionTimeout,ReadTimeout,SocketTimeout 차이점? 사실 지금까지 웹개발을 해오면서 ConnectionTimeout,ReadTimeout,SocketTimeout에 대해 대략적으로만 알고있었지 사실 정확히 설명해봐라 혹은 차이..

coding-start.tistory.com

 

posted by 여성게
:
Web/Spring 2019. 10. 23. 21:33

응답으로 나가는 Object에 getter가 존재하지 않아서 Response Content-type을 추론하지 못했다. 그래서 필터 단에서 임의로 DefaultMediaType으로 */*? * 로 넣어버리지만 사실 Content-Type에 wildCard는 들어가지 못한다. 즉, 예외발생!

posted by 여성게
:
Web/Spring 2019. 10. 18. 00:33

 

오늘 다루어볼 포스팅은 ResponseBodyAdvice를 이용하여 컨트롤러 응답값을 가공해보는 예제이다. 사실 HandlerInterceptor의 postHandler 같은 곳에서 응답값을 가공할 수 있을 듯하지만, 사실 인터셉터 단에서 응답 가공은 불가능하다. 하지만 우리는 적절한 값으로 응답을 가공하고 싶을 때가 있는데, 그럴때 사용하는 것이 ResponseBodyAdvice이다.

 

예제 상황은 다음과 같다.

 

"Controller에서 응답값을 Enum class로 둔다. 하지만 실제 클라이언트에는 특정 응답용 객체로 컨버팅 후 내려준다."

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RestControllerAdvice
public class EnumResponseCtrlAdvice implements ResponseBodyAdvice<Object> {
 
    @Override
    public boolean supports(MethodParameter returnType, Class<extends HttpMessageConverter<?>> converterType) {
        return true;
    }
 
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return body instanceof EnumController.ResponseEnum ? new Response((EnumController.ResponseEnum) body) : body;
    }
 
    @Data
    @AllArgsConstructor
    class Response{
        EnumController.ResponseEnum status;
    }
}
 
@RestController
@RequestMapping
public class EnumController {
 
    @GetMapping
    public ResponseEnum enumResponse(){
        return ResponseEnum.SUCCESS;
    }
 
    @Getter
    enum ResponseEnum{
        SUCCESS("success"),FAIL("fail");
 
        String value;
        ResponseEnum(String value){this.value=value;}
 
    }
 
}
 
=>최종응답
{
"learnableStatus""LEARNABLE"
}
cs

 

어렵지 않다. 응답값은 분명 Enum class 인데, 클라이언트에 반환된 응답값은 ResponseEnum 타입의 오브젝트로 리턴되었다. 공통적인 응답 가공이 필요할때 사용하면 좋을 듯하다. 

posted by 여성게
:
Web/Spring 2019. 10. 12. 12:49

 

이번에 다루어볼 포스팅은 커스텀 어노테이션과 인터셉터를 이용하여 특정 컨트롤러의 매핑 메서드에 전처리(인증 등)를 하는 예제이다. 즉, 커스텀하게 어노테이션을 정의하고 컨트롤러 혹은 컨트롤러 메서드에 어노테이션을 붙여(마치 @RequestMapping과 같은) 해당 컨트롤러 진입전에 전처리(인증)을 하는 예제이다. 물론, Allow Ip 같은 것은 필터에서 처리하는 것이 더 맞을 수 있지만, 어떠한 API는 인증이 필요하고 어떠한 것은 필요없고 등의 인증,인가 처리는 인터셉터가 더 적당한 것 같다. 이번 포스팅을 이해하기 위해서는 Spring MVC의 구동 방식을 개념정도는 알고 하는 것이 좋을 듯하다. 아래 포스팅을 확인하자.

 

 

spring의 다양한 intercept 방법 (Servlet Filter, HandlerInterceptor, PreHandle, AOP)

1. 다양한 intercept 방법들과 주 사용처 Servlet Filter : 인코딩, 인증, 압축, 변환 등 HandlerInterceptor : 세션, 쿠키, 검증 등 AOP : 비즈니스단 로깅, 트랜잭션, 에러처리 등 2. Servlet Filter 와 Handler..

sjh836.tistory.com

 

개발 방향은 컨트롤러에 인증을 뜻하는 어노테이션을 커스텀하게 붙이고, 인터셉터에서 어노테이션의 유무를 판단하여 인증을 시도한다. 특별히 어려운 코드는 없어 간략한 설명만 하고 넘어가겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public enum Auth {
 
    NONE,AUTH
 
}
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsAuth {
 
    Auth isAuth() default Auth.NONE;
 
}
 
@Slf4j
@Component
public class CommonInterceptor implements HandlerInterceptor {
 
    private Map<String,User> userMap = new HashMap<>();
 
    @PostConstruct
    public void setup() {
        User user = new User("","yeoseong_gae",28);
        userMap.put("yeoseong-gae",user);
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("interceptor");
        String userId = request.getParameter("userId");
 
        IsAuth annotation = getAnnotation((HandlerMethod)handler, IsAuth.class);
 
        Auth auth = null;
 
        if(!ObjectUtils.isEmpty(annotation)){
            auth = annotation.isAuth();
            //NONE이면 PASS
            if(auth == Auth.AUTH){
                if(ObjectUtils.isEmpty(userMap.get(userId))){
                    log.info("auth fail");
                    throw new AuthenticationException("유효한 사용자가 아닙니다.");
                }
            }
        }
        return true;
    }
 
    private <extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
        return Optional.ofNullable(handlerMethod.getMethodAnnotation(annotationType))
                .orElse(handlerMethod.getBeanType().getAnnotation(annotationType));
    }
 
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User{
        private String id;
        private String name;
        private int age;
    }
}
 
@Slf4j
@RestControllerAdvice
public class CtrlAdvice {
 
    @ExceptionHandler(value = {Exception.class})
    protected ResponseEntity<String> example(Exception exception,
                                             Object body,
                                             WebRequest request) throws JsonProcessingException {
        log.debug("RestCtrlAdvice");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("message:"+exception.getMessage());
    }
 
}
 
@RestController
public class AuthController {
 
    @IsAuth(isAuth = Auth.AUTH)
    @GetMapping("/auth")
    public boolean acceptUser(@RequestParam String userId){
        return true;
    }
 
    @IsAuth
    @GetMapping("/nonauth")
    public boolean acceptAll(@RequestParam String userId){
        return true;
    }
}
cs

 

마지막으로 인터셉터를 등록하기 위한 Config 클래스이다.

 

1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
 
    private final CommonInterceptor commonInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(commonInterceptor);
    }
}
cs

 

커스텀한 어노테이션을 정의하고, 컨트롤러 매핑 메서드중 인증을 하고 싶은 곳에 어노테이션을 등록해준다. 그리고 인터셉터에서 현재 찾은 핸들러의 핸들러메서드에 우리가 등록한 커스텀한 어노테이션이 있는지 혹은 해당 핸들러메서드의 클래스에 붙어있는지 등을 리플렉션을 이용하여 확인한다. 만약 있다면 유효한 사용자인지 판단한다. 참고로 인터셉터에서는 디스패처 서블릿 더 뒷단에서 구동하기 때문에 현재 매핑된 핸들러 메서드의 접근이 가능하다.

 

마지막으로 인증이 실패한다면 예외를 발생시키고, 컨트롤러 어드바이스를 이용하여 예외를 처리한다. 참고로 인터셉터는 앞에서 말한 것과 같이 디스패처 서블릿 뒷단에서 구동되기때문에 ControllerAdvice 사용이 가능하다.

 

현재 다루고 있는 예제는 하나의 예시일 뿐이다. 사실 스프링 시큐리티를 사용한다면 굳이 필요없는 예제일수 있다. 하지만 인증뿐아니라 여러가지로 사용될수 있기에 응용하여 사용하면 될듯하다. 해당 코드는 절대 실무에선 사용될 수없는 로직을 갖고 있다.(예제를 위해 간단히 작성한 코드이기에) 컨셉을 갖고 더 완벽한 코드로 완성해야 한다. 만약 해당 코드가 필요하다면 밑의 깃헙에 소스를 올려놓았다.

 

 

 

yoonyeoseong/useAnnotationAndInterceptorAuth

Contribute to yoonyeoseong/useAnnotationAndInterceptorAuth development by creating an account on GitHub.

github.com

 

posted by 여성게
: