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

환경: springboot3.3

url의 path variable로 enum을 받을 경우 아래와 같이 한 번에 받을 수 있지만, enum이 대문자로 관리될 경우, url에 대소문자가 혼용되게 된다.

이 경우 localhost:8080/AGAME/member-no 이렇게 된다(이는 StringToEnumConverterFactory 때문이다).

대소문자가 혼용되는 게 싫어서 아래와 같이 변환을 해서 검증을 하곤 하는데, 이게 또 계속 반복되는 문제점이 생긴다.

path variable의 값을 string의 코드로 보내도 자동으로 enum으로 변환할 수 없을까?!

 

HandlerMethodArgumentResolver 사용해 보기

resolver? Spring MVC에서 HTTP 요청 데이터를 Controller의 메서드 매개변수로 바인딩함

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class GamePathVariableResolver implements HandlerMethodArgumentResolver {

 @Override
  public boolean supportsParameter(MethodParameter parameter) {
    boolean hasAnnotation = parameter.hasParameterAnnotation(PathVariable.class);
    boolean usedEnum = GameType.class.isAssignableFrom(parameter.getParameterType());
    return hasAnnotation && usedEnum;
  }

  @Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
      
    String value = (String) delegate.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    return GameType.findByPath(value);
  }
}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new GamePathVariableResolver());
    }
}

근데? Resolver를 타지 않음

resolver가 스프링에 등록된 것은 확인했다.

그런데 url을 String game으로 호출하면 올바른 enum값이 아니라는 에러가 난다.

org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type

GamePathResolver에 가보지도 못한 건가? 그럼 호출 순서는 StringToEnumConverterFactory -> GamePathResolver??

 

우선 방식을 파악하기 위해 enum값으로 호출하고 resolver에 디버그를 걸어 호출 시 resolver를 타는지 확인해 본다.

그런데 resolver를 아애 타지 않는다..? 디버깅이 안 걸린다.

조금 더 다양한 방법으로 테스트해 본다.

  • 아래와 같이 PathVariable도 아니고 RequestParam도 아닌 일반 객체가 있을 때는 탄다.
  • 근데 잘 타다가도 아래 memberNo에 어노테이션(RequestParam 나 ModelAttribute)을 걸면 또 안탄다.
  • 디버깅을 걸어 MethodParam의 값을 확인해보면 어노테이션 PathVariable로 선언된 GameType이 없다....
@GetMapping("/{game}")
public ResponseEntity<?> checkGameFallbackToInGame(@PathVariable GameType game, String memberNo) {

왜?

먼저 등록된 기본 PathVariableMethodArgumentResolver가 처리할 수 있는 파라미터를 모두 소화하기 때문에 커스텀 리졸버가 호출되지 않을 수 있다... 흐름은 아래 참고..

  • 어노테이션이 있는 매개변수는 기본적으로 Spring의 내장 리졸버에 의해 처리됨
    • RequestParamMethodArgumentResolver는 @RequestParam을 처리.
    • PathVariableMethodArgumentResolver는 @PathVariable을 처리.
    • ModelAttributeMethodProcessor는 @ModelAttribute
  • 어노테이션이 없는 경우 아래의 룰로 기본 매핑된다. 
    • 단순 타입(String, int, boolean 등): 단순한 데이터 타입이면 @RequestParam으로 간주
      • RequestParamMethodArgumentResolver
    • 객체 타입 (사용자 정의 객체): 객체 타입이면 @ModelAttribute로 간주
      • ModelAttributeMethodProcessor
    • 그 외의 룰은 커스텀 어노테이션을 개발하여 매핑할 수 있음

궁금한 사항..

근데! WebMvcConfigurer에 커스텀 리졸버가 등록되면, Spring의 기본 리졸버보다 우선 적용되어 커스텀 리졸버에서 기존 동작을 완전히 덮어쓴다고 한다(그래서 확장하거나 위임해야 한다). 근데 아래와 같이 해보니 그냥 실행 자체가 안된다.. 왜?!

우선 PathVariableMethodArgumentResolver 자체를 override 할 수 있는지 궁금해서 임시로라도 해보려고 했는데 그저 안된다.. 

public class GamePathResolver implements HandlerMethodArgumentResolver {

  PathVariableMethodArgumentResolver resolver;

  public GamePathResolver() {
    this.resolver = new PathVariableMethodArgumentResolver(); // 기본 구현 생성
  }

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    if (resolver.supportsParameter(parameter)) {
      PathVariable path = parameter.getParameterAnnotation(PathVariable.class);
      return "game".equals(path.value());
    }
    return false;
  }

  @Override
  public Object resolveArgument(MethodParameter parameter,
      ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest,
      WebDataBinderFactory binderFactory) throws Exception {
    // 기본 처리 로직 호출
    Object resolvedValue = resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    return GameType.findByPath((String) resolvedValue);
  }
}

그럼 어떻게 해결? ConverterFactory 구현

우선 시간이 없어서 위 resolver 방법은 잠시 중단하고, 커스텀 어노테이션을 이용하여 컨버터와 연결하였다.

public class GamePathConverter implements ConditionalGenericConverter {

  @Override
  public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    return targetType.hasAnnotation(ConvertGamePath.class);
  }

  @Override
  public Set<ConvertiblePair> getConvertibleTypes() {
    return Set.of(new ConvertiblePair(String.class, GameType.class));
  }

  @Override
  public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    return GameType.findByPath((String) source);
  }
}
 
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

...

  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new GamePathConverter());
  }
}

그러면 enum의 코드(string)로 보내도 enum으로 바로 가져올 수 있다.


참고로 포매터와 컨버터는 비슷하지만 아래와 같은 용어적 차이가 있다.

리졸버랑 컨버터는.. 여기서는 사실 두 개의 짬뽕을 하고 싶었고 그걸 리졸버에서 구현하고 싶었는데 안돼서 컨버터를 달아버린..


 

why not AOP?

1. AOP로 파라미터 값을 직접 변경 불가

  • Java의 AOP는 메서드 실행 전에 메서드 파라미터 값을 직접 변경할 수 없음

2. string으로 game argument관리하여 실수 잦음 + 순서도 중요..

@Before("@annotation(org.springframework.web.bind.annotation.PathVariable) && args(game,..)")
  • @PathVariable로 선언된 메서드 파라미터를 가지는 메서드 중, 첫 번째 파라미터 이름이 game인 경우를 대상으로 함..
728x90
반응형
반응형

환경: springboot 2.6.2, spring-web

 

아래와 같은 컨트롤러가 있다.

@RequestMapping("/api/member")
@RestController
@RequiredArgsConstructor
public class MemberController {

  private final UserService userService;

 
  @GetMapping("/loss-game-money")
  public BaseResponse<MemberLossGameMoneyResponse> getMemberLossGameMoney(@ModelAttribute MemberLossGameMoneyRequest request) {
    return userService.getMemberLossGameMoney(request);
  }

  
  @GetMapping("/{id}")
  public BaseResponse<MemberInfoResponse> getMember(@PathVariable String id) {
    return new BaseResponse<>(userService.getMemberInfo(id));
  }
}

 

이 상황에서 아래 api를 요청한다면? endpoint가 정의되어 있지 않아 404가 날 것이라 기대했다.

http://localhost:8600/api/member

하지만 200 ok 가 떨어졌다.

 

분명 컨트롤러에 정의되어 있지 않고, 그렇다고 에러 내용이 controller advice에 정의되어 있지도 않은데.. 

그리고 과거에는 404로 떨어졌던 기억도 있던 터라 구글링을 해본다..

 

의심 1. 스프링 버전 이슈?

구글링 하다가 스프링 버전 2.3 이후부터 바뀐 스펙이라고 적힌 것을 봐서.. 스프링 버전의 문제인가 의심했다.(개소리로 판명)

The change in behavior where Spring Boot started returning a 200 OK response with an empty body for unmatched endpoints instead of a 404 response happened around Spring Boot version 2.3.x.
In versions prior to 2.3.x, the default behavior was to return a 404 Not Found response for unmatched endpoints. However, starting from version 2.3.x, the default behavior was changed to return a 200 OK response with an empty body.

공식 문서를 찾다가 실패하여 신규 프로젝트에 버전을 아래와 같이 중간 버전만 하나씩 올려서 테스트해 봤는데

  • 테스트해 본 버전: 2.2.6.RELEASE, 2.3.4.RELEASE, 2.4.5, 2.5.6, 2.6.3, 2.7.3, 2.7.10, 2.7.18, 3.0.0, 3.2.4(현재 최신)

절대 재현되지 않는다.. 버전 문제는 아니고 소스 문제라고 판명..

아래와 같이 모두 동일한 결과를 return 한다. 404 잘만 나는구먼..

없는 주소로 요청할 경우 404

 

의심 2. controllerAdvice...?

혹시 exception handler가 잡아서 200으로 반환하나 싶어 소스를 뒤져봐도 그런 건 없었다.

 

의심 3. 그럼 필터? 인터셉터?

혹시나 싶어 필터나 인터셉터를 하나씩 주석해 보고 실행해 본다.

로그를 살펴보니 필터에 해당하는 로그는 잘 찍히고 있어 필터 이후에서 200을 반환하는 것이라 판단했다.

그리고 인터셉터를 하나씩 보는데.. 잡았다 요놈..

@Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    if (handler instanceof HandlerMethod) {
    	...
      return HandlerInterceptor.super.preHandle(request, response, handler);
    }
    return false; ///
  }

위 함수에서 컨트롤러에 정의된 api는 if (handler instanceof HandlerMethod) 구문에 true를 반환하여 작업을 하고 마지막에 true를 반환하지만 정의되지 않은 api는 false를 타게 된다. false를 타면 상태코드 200에 empty body로 리턴된다..! false를 반환한다는 의미가 controller를 타지 않고 작업을 종료한다는 의미고 그게 잘 되었으니 어쩌면 맞을 수도..

혹시나 싶어서 신규 프로젝트로 기본 세팅만 한 후 재현해 본다.

1. @EnableWebMvc 없고 + return false

@Component
public class TestInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return false;
  }
}
@Component
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

  private final TestInterceptor interceptor;
  
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
  //TestInterceptor가 이미 빈으로 등록되어 있으므로 주입하여 사용하기위해 아래 주석;
  //TestInterceptor에 @Component가 없으면 빈이 아니므로 아래 주석이 작동함
//    registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
    registry.addInterceptor(interceptor);
  }
}
@RequestMapping("/api")
@RestController
public class TestController {

  @GetMapping("/test")
  public String test( ) {
    return "hello";
  }
}
  • interceptor 설정 전
    • http://localhost:8080/api/test 요청 시
      • 200 hello
    • http://localhost:8080/api/test3 요청 시
      • 404 기본 404 not found 메시지
  • interceptor 설정 후
    • http://localhost:8080/api/test 요청 시
      • 200 empty body
    • http://localhost:8080/api/test3 요청 시
      • 200 empty body

 

200이 아닌 다른 상태 값으로 반환하려면 아래와 같이 수정하면 된다.

empty body에 404로 떨어진다.

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  if (! (handler instanceof HandlerMethod)) {
    response.setStatus(404); //
    return false;
  }
  return true;
}

 

이를 수정해 본다면? 에러를 던져보자.

2. @EnableWebMvc 없고 + throw exception

아래와 같이 false 반환대신 에러를 던지면 throw 절을 만나게 되고 BasicErrorController를 타고 500으로 떨어진다.

(어떤 종류의 exception이건 500으로 떨어진다. 그 이유는 Exception이 처리가 이뤄지지 않은 상태로 WAS에게 전달되었기 때문이다. WAS 입장에서는 처리되지 않은 Exception을 받으면 이를 예상치 못한 문제로 인해 발생했다고 간주하고 status코드 500에 Internal Server Error를 메시지에 설정하게 된다.)

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  if (! (handler instanceof HandlerMethod)) {
    throw new RuntimeException("nono");
  }
  return true;
}

 

@EnableWebMvc 어노테이션의 유무

1. @EnableWebMvc 있고 + 인터셉터에서 return true로 진행시키면

여기서 @EnableWebMvc 어노테이션을 추가하면 (어떤 에러를 던지건) 404로 에러가 바뀐다....

spring-web dependency가 있으면 @EnableWebMvc를 선언하지 않아도 된다고 알고 있는데 결과가 다르다니..(아래에서 계속)

더 확인해 보니 @EnableWebMvc를 선언하면 아래 부분이 true로 내려와서

handler instanceof HandlerMethod == true

결국 preHandler가 true로 return 되어 에러를 만나지 않고, controller 단에 가서 url을 찾다가 없어서 404 -> BasicErrorController를 타는 플로우였다.

2. @EnableWebMvc + 인터셉터 내부에서 에러 발생 시키면

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  throw new HttpClientErrorException(HttpStatus.MULTI_STATUS);
}
2024-04-18 09:56:29.978  WARN 12950 --- [nio-8080-exec-8] o.s.web.servlet.PageNotFound             : No mapping for GET /api/test3
2024-04-18 09:56:29.980 ERROR 12950 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] threw exception

org.springframework.web.client.HttpClientErrorException: 207 MULTI_STATUS
	at com.example.demo.interceptor.TestInterceptor.preHandle(TestInterceptor.java:16) ~[main/:na]
	...

2024-04-18 09:56:29.982 ERROR 12950 --- [nio-8080-exec-8] o.a.c.c.C.[Tomcat].[localhost]           : Exception Processing ErrorPage[errorCode=0, location=/error]

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 207 MULTI_STATUS
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.22.jar:5.3.22]
    ...

 

해당 부분에서도 항상 에러를 뱉게 하면 에러가 발생해도 404로 떨어진다.

그 의미는 에러가 발생해도 controller 쪽으로 요청이 들어온다는 것인데, 그 이유는 WAS의 에러페이지를 위한 요청 때문이다.

https://cs-ssupport.tistory.com/494

 

interceptor에 에러를 반환하고 exception handler를 통해 에러를 반환하려고 하면 어떻게 해야 할까?

interceptor는 dispatcher servlet 이후 스프링 컨텍스트 안에 있기 때문에 controller advice를 통한 처리가 가능하다.

1. @EnableWebMvc + controllerAdvice

@EnableWebMvc를 선언한 상태에서 interceptor에서 에러를 뱉게 아래와 같이 수정하고

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  throw new RuntimeException("yesyes");
}

controllerAdvice를 통해 exception handler를 작성하면 의도한 대로 상태코드 400에 에러 메시지가 나온다!

@RestControllerAdvice
public class InterceptorExceptionHandler {

  @ExceptionHandler({RuntimeException.class})
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public String error(RuntimeException exception){
    return "Exception : " + exception.getMessage();
  }
}

WARN 23846 --- [nio-8080-exec-1] o.s.web.servlet.PageNotFound             : No mapping for GET /api/test3

로그에는 page not found 가 적힌다.

 

2. @EnableWebMvc 제거 + controllerAdvice

여기서 @EnableWebMvc를 선언을 제거한다면?

그래도 api는 동일한 결과가 반환된다.

그런데 로그는 다르게 찍힌다. 반환한 에러에 대한 값이 찍힌다.

ERROR 24122 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: yesyes] with root cause

java.lang.RuntimeException: yesyes
	at com.example.demo.interceptor.TestInterceptor.preHandle(TestInterceptor.java:14) ~[main/:na]
	at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:148) ~[spring-webmvc-5.3.22.jar:5.3.22]
	...

 

@EnableWebMvc 어노테이션 관련..

spring-web dependency가 있으면 @EnableWebMvc를 선언하지 않아도 된다고 알고 있는데 결과가 다르다니..

에 대해 좀 더 알아본다.

우선 맞다. springboot에 spring boot starter web 디펜덴시가 있으면 @SpringBootApplicaion 어노테이션에 의해 @EnableAutoConfiguration 어노테이션이 작동하고 웹의 기능(DispatcherServlet 등)을 사용하기 위한 기본 세팅을 자동으로 해준다.

@EnableWebMvc 어노테이션이 사용되면 스프링은 웹 애플리케이션을 위한 준비를 하게 되는데, 기본 설정값으로 아래 클래스를 읽어온다. 

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
The @EnableWebMvc annotation is used in Spring to enable the Spring MVC framework for a web application. When this annotation is used, it imports the Spring MVC configuration from WebMvcConfigurationSupport
 and helps in setting up the necessary components for handling web requests, such as controllers, views, and more. This annotation is often used in conjunction with the @Configuration annotation to enable Spring MVC configuration in a Java-based configuration approach.

 

우리가 기본 세팅을 커스터마이징 하려면 WebMvcConfigurerAdapter라는 클래스를 extend 해야 하는데, 이 클래스는 spring5.0 이래로 deprecated 되었고, WebMvcConfigurer interface를 implement 해야 한다.

그런데 여기서 주의해야 할 점은 커스텀 세팅을 사용하려면 @EnableWebMvc 어노테이션이 없어야 한다는 것이다.

만약 @EnableWebMvc 어노테이션과 커스텀 세팅을 사용하려면 빈으로 등록된 DelegatingWebMvcConfiguration 클래스를 확장하여 재정의해야 한다. 기본 값을 사용하지 않기에 모든 함수를 재정의해야 함에 주의하자.

https://docs.spring.io/spring-boot/docs/current/reference/html/web.html

https://dev.to/xterm/be-careful-when-using-configuration-classes-with-enablewebmvc-in-spring-boot-2n32

 

Be careful when using @Configuration classes with @EnableWebMvc in Spring Boot

Situation Recently, we have been faced with a strange issue after adding a configuration...

dev.to

위에서 @EnableWebMvc 어노테이션 유무에 따라 결과가 다르게 나온 이유는 아마도,

@EnableWebMvc 어노테이션 + 커스텀 세팅(인터셉터)인데 DelegatingWebMvcConfiguration 클래스의 재정의가 없어 기본 세팅값으로 override 되어 다른 결과가 나온 것이 아닌가 추측된다.

 


위 내용을 학습하다 스프링 버전 3부터 루트 매핑(@RequestMapping("")과 @RequestMapping("/"))에 대한 이슈가 있다는 글을 보아서 참고로 링크 남긴다.. 관련 글 때문에 더 헷갈렸다.

https://github.com/spring-projects/spring-boot/issues/33499

 

404 error occurs with RequestMapping(path="") · Issue #33499 · spring-projects/spring-boot

Used version SpringBoot3.0.0 GA Occurrence event When I specify an empty string in Controller's RequestMapping (@RequestMappint(path="")) and access the root path, The mapping doesn't work properly...

github.com


interceptor in spring context: was 흐름까지 정리 https://cs-ssupport.tistory.com/494

 

[Spring] 스프링 인터셉터

[Spring] Servlet "Filter" 현재 Controller에 의해서 매핑되는 Page가 다음 종류가 있다고 하자 1. 로그인 하지 않고 접근 가능 2. 로그인 해야 접근 가능 >> 여기서 과연 "로그인 해야 접근 가능한 페이지"가

cs-ssupport.tistory.com

https://velog.io/@monkeydugi/Spring-Interceptor%EC%97%90%EC%84%9C-%EC%98%88%EC%99%B8%EB%A5%BC-%EC%9D%91%EB%8B%B5-%ED%95%B4%EC%A3%BC%EB%8A%94-%EB%B0%A9%EB%B2%95

 

Spring Interceptor에서 예외를 응답 해주는 방법

상황이 어떤가 살펴보자.소셜 로그인을 시도한다.authorization code가 유효하지 않으면, 예외를 발생 시킨다.500으로 응답한다.위의 코드는 인터셉터에서 예외를 발생 시킨다.하지만 이렇게 끝내면 5

velog.io

 

728x90
반응형
반응형

기업에서 서버팜을 구축하게 되면 어플리케이션 서버 전에 여러 단계의 라우터나 스위치, 로드발랜서 등등이 구축되어 실제 요청한 사람의 ip를 알기 어려워지게 된다.

소스로 말하자면 아래 값이 로컬이 나오거나 내부 장비의 ip가 찍히는 상황이 생기게 된다.

HttpServletRequest request;
request.getRemoteAddr();

 

그리하여 실제로 요청한 사람/장비의 ip를 알기 어려워지는데.. 아래와 같이 설정하면 해결할 수 있다.

 

1. nginx 설정

1-1. nginx map 설정(생략가능)

map은 다음과 같이 $key라는 변수를 받아 $value라는 결과값을 매핑해준다.
아래 코드에서 $key 값이 a라면, $value 값은 1이다.

map $key $value {
  a 1;
  b 2;
  default 0;
}

map 규칙은 위처럼 쓸 수도 있지만, 다른 파일에 분리해 둔 뒤 받아올 수도 있다.
가령 위의 규칙을 map-rule이라는 파일에 아래와 같이 분리하면:

a 1;
b 2;

다음과 같이 파일의 상대 include할 수 있다:

map $key $value {
  include PATH/TO/map-rule; # map-rule 파일의 절대 경로
  default 0;
}

 

위 map을 사용하여 아래처럼 clientip 라는 변수에 값을 담는다.

    map $http_x_forwarded_for $clientip {
        "" $remote_addr;
        default $http_x_forwarded_for;
    }

 

1-2. server.location 세팅

clientip를 아래와 같이 세팅한다. 

  proxy_set_header   X-Forwarded-For  $clientip;

대략적인 큰 그림은 아래와 같다.

 server {
        listen       80;
        server_name  server.abc.com;
        server_tokens off;
        
        ...
        
        location / {
        	//여기부터
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $clientip;
            여기까지//
            proxy_set_header Connection "";
            proxy_http_version 1.1;
            proxy_pass http://localhost:8200;
        }
    }

 

1-3. nginx reload

nginx -s reload

 

2. 소스 변경

애플리케이션에서 X-Forwarded-For 헤더를 사용하도록 아래 두 가지 방법 중 하나를 적용해야 한다.
  1. HttpServletRequestWrapper 를 사용해서 getRemoteAddr 를 상속 -> 별도로 필터 개발 필요
  2. server.tomcat.remoteip.remote-ip-header 프로퍼티 설정

2번 방식이 더 쉽고 빠르다고 생각하여 적용해본다.

적용하기 전에, 버전 별로 키값이 달라지니 스프링 공식 문서는 꼭 확인해보자.

https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.webserver.use-behind-a-proxy-server

 

“How-to” Guides

Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework’s spring-jcl module. To use Logback, you need to include it and spring-jcl on the classpath. The recommended way to do th

docs.spring.io

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.server

 

Common Application Properties

 

docs.spring.io

지금 상황에서 연관있는 값은 아래 값인 것 같다. 기본 값은 없다.

# springboot2.2 버전 이상이면
server.tomcat.remoteip.remote-ip-header=X-FORWARDED-FOR

# 구버전
#server.tomcat.remote-ip-header=X-FORWARDED-FOR

경우에 따라서는 아래 값도 조정해야할 수 있다.

server.tomcat.remoteip.internal-proxies

이 값은 신뢰할 내부 프록시를 매칭하는 값이다. 정규식으로 작성해야하며 기본값은 아래와 같다. 일반적으로는 기본값으로도 사용가능하지만 프로덕션 환경에서 내부 프록시 ip들이 해당 범위를 넘어갈 수도 있으니 확인이 필요하다. 참고로 공백으로 설정하면 모든 프록시를 신뢰한다는 뜻으로 프로덕션 환경에서는 이렇게 설정하면 위험할 수 있으니 주의해야 한다.

  • 10/8
  • 192.168/16
  • 169.254/16
  • 127/8
  • 등등(공식 문서 확인 필요)

 

728x90
반응형

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

라이브환경 인증서 교체  (1) 2024.01.08
[이슈해결][apache] 304 NOT_MODIFIED  (0) 2023.10.12
[nginx] WAF  (0) 2022.03.30
[nginx] API gateway  (0) 2022.03.14
[nginx] 실전 cors 해결하기  (0) 2022.03.14
반응형

환경: springboot2.7.6

 

로컬에서 개발할 때, 그리고 운영 환경으로 배포할 때 내용물에 따라 설정파일을 분리할 수 있으며, 환경에 따라 다르게 가져가야 한다.

-- 소스 실행
mvn spring-boot:run -Dspring-boot.run.arguments=--spring.profiles.active=production

-- jar 로 빌드해서 배포
java -Dspring.profiles.active=production service.jar

 

설정파일은 파일명 자체를 여러가지로 둘 수도 있고, 그 안에서 환경(profile)을 줄 수도 있다.

 

자바 소스 안에서 프로파일을 분기할 수도 있다.

728x90
반응형
반응형

환경: windows11, springboot2.7.6, java17

 

springboot 프로젝트인 catalog service 를 도커에 올려본다.

 

1. pom.xml 경로에 Dockerfile 생성

FROM openjdk:17-ea-slim-buster
VOLUME /tmp
COPY target/catalog-service-1.0.jar catalog-service.jar
ENTRYPOINT ["java", "-jar", "catalog-service.jar"]

pom.xml 파일 확인하여 jar가 위 이름으로 빌드되는지 확인 필요

 

2. 도커 이미지 생성

mvn clean compile package -DskipTests=true
docker build -t haileyjhbang/catalog-service:1.0 .  //도커이미지생성

 

3. 도커 이미지 -> repository 푸시

docker push haileyjhbang/catalog-service:1.0

 

4. 도커 실행

실행 시 사용 중인 외부 접속 정보가 있으면 아래처럼 전달하는 방법 사용

소스&application.yml 파일 내/외부 꼼곰히 확인 필요

해당 부분 수정 필

docker run -d --network ecommerce-network --name catalog-service -e "eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/" -e "logging.file=/api-logs/catalog-test.log" haileyjhbang/catalog-service:1.0

 

docker network insepect ecommerce-network
localhost:8761

성공적..

728x90
반응형
반응형

환경: windows11, springboot2.7.6, java17

 

springboot 프로젝트인 order service 를 도커에 올려본다.

 

1. pom.xml 경로에 Dockerfile 생성

FROM openjdk:17-ea-slim-buster
VOLUME /tmp
COPY target/order-service-1.0.jar order-service.jar
ENTRYPOINT ["java", "-jar", "order-service.jar"]

pom.xml 파일 확인하여 jar가 위 이름으로 빌드되는지 확인 필요

 

2. 도커 이미지 생성

mvn clean compile package -DskipTests=true
docker build -t haileyjhbang/order-service:1.0 .  //도커이미지생성

 

3. 도커 이미지 -> repository 푸시

docker push haileyjhbang/order-service:1.0

 

4. 도커 실행

실행 시 사용 중인 외부 접속 정보가 있으면 아래처럼 전달하는 방법 사용

소스&application.yml 파일 내/외부 꼼곰히 확인 필요

docker run -d --network ecommerce-network --name order-service -e "spring.zipkin.base-url=http://zipkin:9411" -e "eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/" -e "spring.datasource.url=jdbc:mariadb://mariadb:3306/mydb" -e "logging.file=/api-logs/orders-test.log" haileyjhbang/order-service:1.0

 

띄우고 로그를 보는데 아래와 같이 실패가 났음

패스워드 이상하다는 에러여서 설정 파일 확인

알고보니 로컬에서는 1234로 설정해서 설정파일에 1234로 되어 있었는데, 도커에 마리아 세팅할 때는 다른 패스워드로 하는 바람에 실패난 것. 패스워드를 외부에서 주입받거나 하는 방식도 있겠지만, 우선 수정하고 다시 컴파일하고 도커 이미지 지우고 다시 런하니 성공하였다..

유레카에 등록도 성

성공적..

728x90
반응형
반응형

환경: windows11, springboot2.7.6, java17

 

springboot 프로젝트인 user service 를 도커에 올려본다.

 

1. pom.xml 경로에 Dockerfile 생성

FROM openjdk:17-ea-slim-buster
VOLUME /tmp
COPY target/user-service-1.0.jar user-service.jar
ENTRYPOINT ["java", "-jar", "user-service.jar"]

pom.xml 파일 확인하여 jar가 위 이름으로 빌드되는지 확인 필요

 

2. 도커 이미지 생성

mvn clean compile package -DskipTests=true
docker build -tag haileyjhbang/user-service:1.0 .  //도커이미지생성

 

3. 도커 이미지 -> repository 푸시

docker push haileyjhbang/user-service:1.0

4. 도커 실행

실행 시 사용 중인 외부 접속 정보가 있으면 아래처럼 전달하는 방법 사용

소스&application.yml 파일 내/외부 꼼곰히 확인 필요

docker run -d --network ecommerce-network --name user-service -e "spring.cloud.config.uri=http://config-service:8888" -e "spring.rabbitmq.host=rabbitmq" -e "spring.zipkin.base-url=http://zipkin:9411" -e "eureka.client.serviceUrl.defaultZone=http://discovery-service:8761/eureka/" -e "logging.file=/api-logs/users-ws.log" haileyjhbang/user-service:1.0

docker network inspect ecommerce-network
docker logs user-service

성공적..

728x90
반응형
반응형

환경: springboot2.7.6, java11

 

circuit breaker

  • 장애가 발생하는 서비스에 반복적인 호출이 되지 못하게 차단
  • 특정 서비스가 정상적으로 동작하지 않을 경우 다른 기능으로 대체수행하여 장애를 회피함
  • open이 되었을 때 우회수행
  • spring cloud netflix hystrix(19년 deprecated)
  • resilience 4j ; 경량/자바 8 이상 지원

 

1. pom.xml 에 의존성 추가

<!-- resilience4j -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

2. open feign 을 통해 다른 msa component로 통신하던 부분을 circuit breaker로 변경

우선 기본적으로 제공하는 circuitBreakerFactory을 사용해본다.

//클래스 상단에 주입
    private final CircuitBreakerFactory circuitBreakerFactory;
    
    ...

//함수 안
		//open-feign with error decoder
        //orders = orderServiceClient.getOrders(userId);

        CircuitBreaker circuitBreaker =circuitBreakerFactory.create("circuitbreaker");
        orders = circuitBreaker.run(() -> orderServiceClient.getOrders(userId), throwable -> new ArrayList<>());

3. 이렇게 해두고 해당 msa component를 down 시킨상태에서 api 요청.

로그에는 접속 불가가 뜨지만 결과는 빈 array 반환됨

4. 기본 세팅말고 설정을 상세하게 바꾸고 싶다면 아래처럼 custom하여 빈으로 등록하면 됨

각 설정 의미는 공식 문서 참고

@Configuration
public class Resilience4Config {

    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfig(){
        TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build();
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(4)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) //기본값임
                .slidingWindowSize(2)
                .build();
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                                                            .timeLimiterConfig(timeLimiterConfig)
                                                            .circuitBreakerConfig(circuitBreakerConfig)
                                                            .build());
    }
}
728x90
반응형
반응형

환경: springboot2.7.6, java 17, springcloud 2021.0.8

 

사용 이유:

분산 시스템의 노드(서비스들)를 경량 메시지 브로커(rabbit mq)와 연결하여 상태 및 구성에 대한 변경 사항을 연결된 노드에게 전달(broadcast)한다.

즉 중간에 메세지를 전달하는 미들웨어(AMQP; 메시지 지향 프로토)를 둠으로써 안정적으로 변경사항을 적용하도록 함

 

사용 법:

우선 기존에 spring cloud config 서버가 있어야 하고 거기서 설정파일을 가져오게끔 bootstrap도 추가되어 있어야 한다.

아래 글 참고.

2024.01.29 - [개발/spring] - [cloud] 파일을 동적으로 관리하는 config server

버스로 연결하려는 모든 프로젝트에 아래와 같이 디펜덴시 추가한다. 수정 시 확산을 위해 actuator도 필요하다.

<!-- 설정파일 외부  -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

<!-- actuator -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator </artifactId>
</dependency>
<!-- spring cloud bus -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp </artifactId>
</dependency>
management:
  endpoints:
    web:
      exposure:
        include: busrefresh

actuator 설정에 busrefresh 추가

연결된 서비스 중 아무데서나 위 api를 호출하면 204 성공이 떨어지고 spring cloud config 서버를 통해 받고 있던 데이터들이 전 서비스에 걸쳐 모두 한 번에 refresh 되는 것을 볼 수 있다. (이전에는 각각 /actuator/refresh를 호출했어야 했음)

728x90
반응형
반응형

환경: springboot2.7, java 17, maven

 

h2를 내장하는 테스트 프로젝트 생성

아래와 같이 설정했다고 가정

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
    <scope>runtime</scope>  # <-- 기본이 test 로 되어 있고, 이러면 프로젝트에서 사용할 수 없음
</dependency>
spring:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console   // H2 콘솔 사용한다는 뜻
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test  //db 연결 후 테이블 자동 생성

하지만 서버가 뜨고 db 연결 시 아래와 같은 에러 발생

testdb 데이터베이스를 springboot실행 시 자동 생성해주어야 하는데 그렇지 못함.

H2 1.4.198 이후 버전부터는 보안 문제로 자동으로 디비를 생성하지 않음.

h2 버전의 문제로 1.4 -> 1.3.176으로 낮추면 해결됨

((최신 버전으로 해도 그러는지 확인 필요))

 


 

h2를 메인으로 쓰면서, 서비스 시작 시 entity 테이블을 자동으로 생성하고자 하는 경우

application.yml에 아래처럼 작

spring:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
  jpa:
    defer-datasource-initialization: true   ###
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    generate-ddl: true

springboot 2.5 이상에서는 위처럼 defer-datasource-initialization 값을 추가해야 한다.

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.5-Release-Notes#hibernate-and-datasql

 

Spring Boot 2.5 Release Notes

Spring Boot. Contribute to spring-projects/spring-boot development by creating an account on GitHub.

github.com

 

+ 초기 데이터까지 넣어야 한다면

/resources 경로에 data.sql 파일로 쿼리를 넣으면, entity 생성 후 자동 실행한다.

728x90
반응형

+ Recent posts