반응형

공식문서 : link

getting started : link

 

1. 개요

Spring Cloud Gateway는 API Gateway 기능을 구현할 수 있는 라이브러리로 대표적인 기능으로는 routing, filtering 있다.

@SpringBootApplication
public class DemogatewayApplication {
	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
			.route("path_route", r -> r.path("/get")
				.uri("http://httpbin.org"))
			.route("host_route", r -> r.host("*.myhost.org")
				.uri("http://httpbin.org"))
			.route("rewrite_route", r -> r.host("*.rewrite.org")
				.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
				.uri("http://httpbin.org"))
			.route("hystrix_route", r -> r.host("*.hystrix.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
				.uri("http://httpbin.org"))
			.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
				.uri("http://httpbin.org"))
			.route("limit_route", r -> r
				.host("*.limited.org").and().path("/anything/**")
				.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
				.uri("http://httpbin.org"))
			.build();
	}
}

 

 

2. 설정 클래스

- org.springframework.cloud.gateway.route.RouteDefinition  : 라우터 IO

- org.springframework.cloud.gateway.route.Route : RouteDefinitionRouteLocator에서 RouteDefinition을 Route로 변경

- org.springframework.cloud.gateway.route.Route.AsyncBuilder : Route Builder

- org.springframework.cloud.gateway.route.Route.Builder : Route Builder

 

3. 중요 클래스

org.springframework.cloud.gateway.route.CachingRouteLocator

- Route를 캐싱하는 클래스

- RefreshRoutesEvent 이벤트를 받아 RouteLocator에서 라우터를 다시 읽어 캐시 갱신

- 요청이 들어올 경우 RoutePredicateHandlerMapping.lookupRoute에서 CachingRouteLocator.getRoutes를 호출하여 캐싱된 Route 리스트 Get

 

org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping

- 클라이언트 요청에 대해 알맞은 라우팅을 lookup하는 클래스

 

org.springframework.cloud.gateway.handler.FilteringWebHandler

- 글로벌 필터와 라우터에 등록되어 있는 필터를 합쳐 필터를 수행한다.

 

org.springframework.cloud.gateway.filter.NettyRoutingFilter

- 실제 라우팅 URI에 요청하는 필터

 

org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint, GatewayControllerEndpoint

- 실제 actuate 요청을 받는 컨트롤러

-  5. actuator 설정 참조

 

4. 요청 흐름도

0. Client Request

 

1. reactor.netty.http.server.HttpServer.HttpServerHandle.onStateChange

 

2. org.springframework.http.server.reactive.ReactorHttpHandlerAdapter.apply

 

3. org.springframework.web.server.adapter.HttpWebHandlerAdapter.handle

 

4. org.springframework.web.reactive.DispatcherHandler.handle

 

5. org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping.getHandlerInternal

 

6. org.springframework.cloud.gateway.handler.FilteringWebHandler.handle

 

7. org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain.filter

 

 

5. actuator 설정

- pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

 

- application.yml

management:
  endpoints:
    web:
      exposure:
        include: 
          - gateway
  endpoint:
    gateway:
      enabled: true

 

- endpoints

  • GET /actuator/gateway/globalfilters: Displays the list of global filters applied to the routes.
  • GET /actuator/gateway/routefilters: Displays the list of GatewayFilter factories applied to a particular route.
  • POST /actuator/gateway/refresh: Clears the routes cache.
  • GET /actuator/gateway/routes: Displays the list of routes defined in the gateway.GET /actuator/gateway/routes/{id}: 특정 route 설정 정보
  • GET /actuator/gateway/routes/{id} : Displays information about a particular route.
  • POST /actuator/gateway/routes/{id}: Add a new route to the gateway.
  • DELETE /actuator/gateway/routes/{id}: Remove an existing route from the gateway.

 

 

참고

https://blog.jungbin.kim/spring/2021/02/27/spring-cloud-gateway-route-locator.html

https://dlsrb6342.github.io/2019/05/14/spring-cloud-gateway-%EA%B5%AC%EC%A1%B0/

DB를 이용한 동적반영

https://medium.com/bliblidotcom-techblog/spring-cloud-gateway-dynamic-routes-from-database-dc938c6665de

반응형
반응형

@RestController는 @ResponseBody를 디폴트로 포함하므로 생략 가능.

반응형

'Spring' 카테고리의 다른 글

[Spring] SQLException to Spring DataAccessException 변환  (0) 2022.05.24
반응형

이슈

rewriteBatchedStatements=true

고객사에서 위와 같은 mysql 옵션을 추가하자 다음과 같은 오류가 발생하였다.

java.lang.NullPointerException
at com.mysql.cj.ClientPreparedQuery.computeMaxParameterSetSizeAndBatchSize(ClientPreparedQuery.java:66)

 

오류가 발생한 코드는 Batch JDBC 코드를 작성할 때 자주 사용하는 보일러 플레이트 코드로 executeBatch()가 실행될 때 오류가 발생하였다.

Blob blob = ...
try (Connection connection = DBUtil.getInstance().getConnection();
         PreparedStatement preparedStatement = connection.prepareStatement(query)) {
        connection.setAutoCommit(false);
        for (TransactionBatch batch : batches) {
            try {                   
                preparedStatement.setString(1, batch.getDeviceID());
                preparedStatement.setBlob(2, blob);
                preparedStatement.addBatch();
            } catch (Exception e) {
               e.printStackTrace();
            }
        }

        preparedStatement.executeBatch();
        blob.free();

 

해결

연구소에서 재현을 위해 rewriteBatchedStatements=true 옵션을 추가했는데 동일한 현상이 발생하지 않았고 추후 확인해보니 고객사와 mysql-connector-java 버전에 차이가 있었다.

연구소는 버전이 5.1.38, 고객사는 8.0.19였다. 버전을 고객사와 동일하게 맞춘 후 동일한 현상이 재현되었다.

구글링을 해보니 setBlob() 또는 setBinaryStream()을 사용할 때 문제가 발생하며 해당 메서드 대신 setBytes()를 사용해야 했다. setBytes()를 사용하면 오류가 발생하지 않는다.

이유

이유를 좀 더 찾아보자.

 

2015년 6월에 스택오버플로우에서 해당 관련 글이 올라왔다. (computeMaxParameterSetSizeAndBatchSize가 PreparedStatement에 있는 것으로 보아 버전이 달라보인다.). 해당 답변에서는 rewrite를 하기 위해 사이즈를 재 계산해야 하지만 input stream에서는 할 수 없다라는 의미처럼 보이지만, 정확히 이해가 되지 않는다.

 

 

2017년 3월에 mysql 커뮤니티에 버그 리포트(#85317)로 해당 관련 글이 올라왔다. mysql 개발자가 해당 글을 당일 확인하고 버그로 판단하고 해결되기까지...........무려 5년이 걸렸다.

 

5년 뒤인 8.0.29 버전에 이슈가 패치되었다고 한다. => 링크

  • When the connection property rewriteBatchedStatements was set to true, inserting a BLOB using a prepared statement and executeBatch() resulted in a NullPointerException. (Bug #85317, Bug #25672958)

setBlob()을 사용한 기존 코드를 유지한 채 8.0.29 버전으로 수행하니 더 이상 오류가 발생하지 않았다.

 

8.0.19와 8.0.29로 코드를 비교해서 어떠한 점이 패치되었는지 판단하려 하였으나, 많은 부분이 변경 되어 확인하기가 어려웠다.

 

8.0.19 버전에서 NPE가 발생한 소스는 다음과 같다.

   @Override
    protected long[] computeMaxParameterSetSizeAndBatchSize(int numBatchedArgs) {
        long sizeOfEntireBatch = 0;
        long maxSizeOfParameterSet = 0;

        for (int i = 0; i < numBatchedArgs; i++) {
            ClientPreparedQueryBindings qBindings = (ClientPreparedQueryBindings) this.batchedArgs.get(i);

            BindValue[] bindValues = qBindings.getBindValues();

            long sizeOfParameterSet = 0;

            for (int j = 0; j < bindValues.length; j++) {
                if (!bindValues[j].isNull()) {

                    if (bindValues[j].isStream()) {
                        long streamLength = bindValues[j].getStreamLength();

                        if (streamLength != -1) {
                            sizeOfParameterSet += streamLength * 2; 
                        } else {
                            int paramLength = qBindings.getBindValues()[j].getByteValue().length; // NPE 발생
                            sizeOfParameterSet += paramLength;
                        }
                        
                        ....

 

getByteValue()를 하는 중 배열에 들어 있는 값이 null이라 발생하지 않았을 까 싶다..

 

 

 

반응형
반응형

swagger-ui는 아래 예제처럼 url 이라는 쿼리 파라미터로 외부 리소스의 API 스펙을 임포트하여 사용할 수 있다.

https://petstore.swagger.io/?url=https://darosh.github.io/openapi-directory-lite/specs/github.com/v3.json

 

3.3.8 이후 버전부터는 XSS 보안 이슈로 인하여 해당 옵션을 일반적으로 사용할 수 없도록 변경하였다.

해당 보안 이슈는 다음을 참고한다.

 

사용해야 한다면 재량적으로 쿼리 파라미터에 queryConfigEnabled=true을 주어 사용할 수 있다.

해당 패치 노트는 다음을 참고한다.

 

필자는 swagger-ui를 v3.36.1을 사용하고 있었으며 이후 springdoc 1.6.9 버전으로 마이그레이션을 진행했다.

springdoc 1.6.9 버전은 swagger-ui 4.11.1 버전을 포함하고 있었기 때문에 위와 같은 문제를 직면했다.

 

필자는 특이하게도 springdoc을 이용하여 기본 url인 localhost:8080/swagger-ui/index.html을 사용하지 않고, 특별한 방식으로 swagger-ui를 import해서 사용했다.

관련하여 다음을 참고한다.

@Configuration
@SpringBootApplication
@EnableTransactionManagement
public class App implements WebMvcConfigurer {

  ...

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/tester/**")
        .addResourceLocations("classpath:/META-INF/resources/static/",
            "classpath:/META-INF/resources/webjars/swagger-ui/4.11.1/");
  }

 

따라서 swagger를 사용할 때는 localhost:8080/tester/index.html를 사용했고 url 쿼리 파라미터를 위해 queryConfigEnabled를 사용하였으나 동작하지 않았다.

 

혹시나 하여 기본적으로 제공하는 springdoc의 localhost:8080/swagger-ui/index.html에서도 동작하지 않았다.

 

이후, springdoc에서 제공하는 설정값을 아래와 같이 세팅하여 기동하니 queryConfigEnabled를 주지 않아도 url 옵션이 동작하였다.

springdoc:
  swagger-ui:
    query-config-enabled: true

 

 

반응형
반응형

디자인 패턴 중 일반적으로 가장 처음에 배우는 전략 패턴을 자바로 어떻게 작성해야 하는지에 대한 내용이다.

전략 패턴에 대한 설명은 생략한다.

 

문자열 비교를 해주는 Util이 있다고 가정해보자.

public class StringCompareUtil {

	/**
	 * 일반 문자열 비교
	 */
	public static int compare(String s1, String s2) {
		return s1.compareTo(s2);
	}

	/**
	 * 문자열 길이를 이용한 비교
	 */
	public static int compareLength(String s1, String s2) {
		return s1.length() - s2.length();
	}

}

 

만약 새로운 문자열 비교 기능이 필요하다면 위 클래스를 수정해야 한다.  OCP를 위반하기 때문에 인터페이스를 이용하여 클래스를 변경해보자.

public interface Comparator {
	public int compare(String s1, String s2);

}

public class NormalStringCompare implements Comparator {

	public int compare(String s1, String s2) {
		return s1.compareTo(s2);
	}

}

public class LengthStringCompare implements Comparator {

	public int compare(String s1, String s2) {
		return s1.length() - s2.length();
	}

}

 

인터페이스를 이용하였고 각 기능을 제공하는 클래스를 각각 구현하였다. 이런 식으로 작성한다면 해당 기능을 사용하는 클라이언트 입장에서는 어떠한 구현체여도 인터페이스를 사용하기만 하면 된다.

 

문자열만 비교하는 인터페이스로 사용하기에 아쉽기 때문에 제네릭을 추가하자.

public interface Comparator<T> {
	public int compare(T s1, T s2);

}


public class NormalStringCompare implements Comparator<String> {

	public int compare(String s1, String s2) {
		return s1.compareTo(s2);
	}

}

public class LengthStringCompare implements Comparator<String> {

	public int compare(String s1, String s2) {
		return s1.length() - s2.length();
	}

}

 

새로운 비교 기능을 위해 클래스를 새롭게 구현해도 되지만, 기능이 추가될 때마다 클래스의 개수가 계속 늘어나게 된다. 만약 해당 비교 기능을 특정한 클라이언트 구현체에서만 사용한다면 익명 클래스를 이용해서 해당 위치에서 작성해주자.

		new Comparator<String>() {
			public int compare(String s1, String s2) {
				// TODO
			}
		};

 

위와 같이 익명 클래스를 생성하면 해당 위치에서 새로운 객체가 계속 만들어지므로 해당 익명 클래스를 객체로 갖는 private static final 필드에 저장하고 재사용하는 것을 고려하자.

 

여기까지 책에 대한 설명이다. 참고로 블로그 저자는 2판을 보고 작성하고 있는데, 3판에서는 람다에 대한 챕터로 해당 내용을 새롭게 구성하여 설명하고 있다.

 

이제 익명 클래스 대신에 람다를 사용하는 것을 추천한다. 람다에 대한 설명은 다른 블로그 글을 참조하기 바란다.

 

반응형
반응형

이 챕터는 자바를 처음 배울 때 계층형(추상) 클래스를 사용하는 이유를 설명하는 내용이라고 생각해도 될 것 같다. 그만큼 내용이 어렵지 않으므로 간단하게 설명한다.

 

1. 태그 클래스란?

태그 클래스란 특정한 변수를 기준으로 분기하여 두 가지 이상의 기능을 제공하는 클래스를 의미한다.

public class Figure {
	enum Shape {
		RECTANGLE, CIRCLE
	};

	final Shape shape;

	double length;
	double width;

	double redius;

	Figure(double length, double width) {
		this.shape = Shape.RECTANGLE;
		this.length = length;
		this.width = width;
	}

	Figure(double redius) {
		this.shape = Shape.CIRCLE;
		this.redius = redius;
	}

	double area() {
		switch (shape) {
		case RECTANGLE:
			return length * width;
		case CIRCLE:
			return Math.PI * (redius * redius);
		default:
			throw new AssertionError();
		}
	}
}

 

A 기능을 위한 생성자와 인스턴스 필드들이 존재하며, B 기능을 위한 생성자와 인스턴스 필드들이 각각 존재한다.

 

이러한 경우 단점은 다음과 같다.

 

1. 가독성이 떨어진다.

2. 필요 없는 기능을 위한 필드도 함께 생성되므로 메모리 요구량이 늘어난다.

3. 또 다른 기능이 생긴다면 해당 클래스를 수정해야 하며, swtich문도 모두 수정해야 한다.

 

2. 해결 방안

공통 기능은 추상(abstract) 클래스로 옮기고 서로 다른 기능은 하위 클래스로 구현한다.

 

반응형
반응형

이전에 작성한 글로 대신한다.

 

[JAVA] Constant Interface : 상수를 정의한 인터페이스

 

[JAVA] Constant Interface : 상수를 정의한 인터페이스

이전 블로그에서 2017년 7월 20일에 작성한 글 프로젝트 소스를 보다가 상수(Constant)를 인터페이스(Interface)에 정의한 것을 발견했다. 학교 수업과 책으로 자바를 배웠을 때, 항상 상수는 클래스에

camel-context.tistory.com

 

반응형
반응형

토비의 스프링 3장. 예외 챕터를 보면 DB 벤더마다 SQL 에러코드가 다르고, 관리하기 어려워 스프링은 자바에서 다루는 SQLException을 변환하여 DataAccessException을 사용하도록 한다.

 

DB 벤더마다 에러코드를 매핑 값은 sql-error-codes.xml 이다.

 

업무 중 SQLException을 직접 Spring 예외로 변환해야 하는 기능이 필요하여 찾아보니 아래와 같이 작성하면 된다.

 

SQLErrorCodeSQLExceptionTranslator sqlTranslator = new SQLErrorCodeSQLExceptionTranslator();
DataAccessException ex = sqlTranslator.translate(null, null, sqlException);

 

SQLErrorCodeSQLExceptionTranslator.translate() 메서드에서 마지막 인자로 전달하는 SQLException 객체에는 SQLState 값이 채워져 있어야지 변환할 수 있다.

 

 

 

반응형

'Spring' 카테고리의 다른 글

[Spring] @ResponseBody는 @RestController으로 대체 가능  (0) 2022.08.16

+ Recent posts