728x90
Unable to load credentials from any of the providers in the chain 
AwsCredentialsProviderChain(credentialsProviders=
SystemPropertyCredentialsProvider(), EnvironmentVariableCredentialsProvider(), WebIdentityTokenCredentialsProvider(), ProfileCredentialsProvider(), ContainerCredentialsProvider(), InstanceProfileCredentialsProvider()]) : SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set., ProfileCredentialsProvider(): Profile file contained no credentials for profile default: ProfileFile(profiles=[]), ContainerCredentialsProvider(): Cannot fetch credentials from container - neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set., InstanceProfileCredentialsProvider(): Unable to load credentials from service endpoint.

라는 에러가 발생했다.
앱푸시를 보내려는 토큰을 받아오는 과정인데
회사의 사수분께서 도커 안쪽에서 aws의 accessKey, secretKey 설정에 대해 들은게 없다고 하셨었는데
이 에러가 나서 운영중인 서비스 애플리케이션의 컨테이너로 들어가보니까

경로는 ~/.aws 이었다.

안에 보니까 config, credentials 파일이 있었는데

config

credentials

이 두개가 존재했다. 이게 없어서 상단의 오류 메세지를 뿜어냈던 것이다.

728x90

'디버깅' 카테고리의 다른 글

@Async 사용시 에러 해결  (0) 2022.11.04
Jenkins 에러  (0) 2022.08.11
YAML 파일을 읽어보자  (0) 2022.08.09
AbstractMessageConverter  (0) 2022.08.09
728x90

전역 값

회사 프로젝트에서 resources 디렉토리에 있는 설정파일은 application.properties를 사용한다.

그래서 여기에 값을 할당해주고 @Value를 가져다가 사용한다.

properties의 값을 가져올때는 @Value("${properties키 값}")으로 불러오기 때문에

이번에 내가 진행하게된 프로젝트 설정파일 application.yaml에도 똑같이 @Value가 적용될줄 알았다.

근데 적용하려고 보니까 에러가 나는것이다.

에러메세지부터 바로 보자.

에러메세지

Could not resolve placeholder 'application.open-api.adminKey' in value "${application.open-api.adminKey}"

라는 에러가 발생했다.

설정을 전부 그대로 두고 다시 구현해보니까 그냥 성공하게 되는 이유는 무엇일까 🤔

추후에 다시 알아봐야 할 것 같다.

멍청하기는 아래의 해결법에서 yaml의 키를 보면 admin-key인데

위에 오류메세지에서는 adminKey로 매핑을 시켰었다.

정말 한심하기 짝이없다. 👿👿👿👿👿👿

아무튼 이런 설정 방법도 있구나 라는것을 알아두면 되겠다.

해결법

Baeldung 설정

벨덩 문서에 가보면 yaml파일 설정에 대한 것을 설명해주었다.

이걸보고 나는 키값을 할당해주려고 아래와 같이 구현하였다.

application.yaml

open-api:
  admin-key: 키값
  authorization: 인증코드

YamlConfiguration.java

@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "open-api") // 기본이 되는 뿌리 키값 여기서는 open-api이다.
@Getter
@Setter //setter가 없으면 Setter가 없다고 에러를 뱉어준다.
public class YamlConfiguration {
    private String adminKey;
    private String authorization;
}

여기서 @ConfigurationProperties 라는 어노테이션을 사용하려면 gradle에서

annotationProcessor에 다음을 추가해주어야한다.

annotationProcessor (
    // ...
            'org.springframework.boot:spring-boot-configuration-processor'
    )

그리고 사용하는 객체에서는 의존성 주입을 받아서 사용하면 되겠다.

public class Foo {

    private final YamlConfiguration yamlConfiguration;

    public Foo(YamlConfiguration yamlConfiguration) {
        this.yamlConfiguration = yamlConfiguration;
    }

    public void test() {
        yamlConfiguration.getAdminKey();
        yamlConfiguration.getAuthorization();
    }

}

@ValueConfiguration을 설정하는방법으로 어떤게 더 편한지 가독성이 좋은지 잘 모르겠다.

상황에 따라 편한걸 사용하는게 맞는지 이거는 조금 더 협업을 해보면 답이 찾아지지 않을까 싶다.

728x90

'디버깅' 카테고리의 다른 글

Jenkins 에러  (0) 2022.08.11
AWS SNS 토큰 에러  (0) 2022.08.10
AbstractMessageConverter  (0) 2022.08.09
RequestParamMethodArgumentResolver  (0) 2022.08.09
728x90

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver에 정의되어있는 자바독을 읽으면

주어진 요청의 컨텍스트에서 메소드 매개변수를 인수 값으로 해석하기 위한 전략 인터페이스라고 설명되어 있다.

HandlerMethodArgumentResolver에는

스크린샷 2021-11-29 오후 10 09 59

이렇게 두개의 메소드가 있는데 supportsParameter()로 메소드의 매개변수를 처리할 수 있는지 여부를 판단한다.

@RequestBody

@RequestBody 어노테이션을 읽으려면

HandlerMethodArgumentResolver
AbstractMessageConverterMethodArgumentResolver
AbstractMessageConverterMethodProcessor
RequestResponseBodyMethodProcessor

이순서로 확장되어있는 Resolver를 찾아보면 된다.

저번 포스팅에서 봤던 ArgumentResolver들을 포함한 RequestMappingHandlerAdapter 에서는

HandlerMethodArgumentResolverComposite를 주입받게 된다.

HandlerMethodArgumentResolverComposite에서도

맞는 ArgumentResolver를 찾아서 동작하게 하려고 하는데 27개의 리졸버중에

RequestResponseBodyMethodProcessor@RequestBody를 처리해준다.

스크린샷 2021-11-29 오후 10 24 32
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }
}

그리고 이제 이 객체를 바인딩 해야한다.

MessageConverter

HandlerMethodArgumentResolverComposite에서 메세지 컨버터를 10개중에 또 찾아내야 한다.

그거는 아래에서 설명하겠다.

스크린샷 2021-11-29 오후 10 31 17

HttpInputMessage에서 들어온 키값과 @RequestBody가 붙은 객체 인자들 값을 서로 비교해서

맞으면 매핑을 시켜주는 것 같다.

AbstractMessageConverterMethodArgumentResolver

AbstractMessageConverterMethodArgumentResolver에서 MediaType

메세지 컨버터를 맞는걸 찾는데 걸리는 컨버터는 바로 MappingJackson2HttpMessageConverter이다.

예상은 하고 있었지만 왜 저 클래스가 GenericHttpMessageConverter 인지는 아직 잘 모르겠다.

메소드중 위 기능을 하는 일부를 가져와보았다.

스크린샷 2021-11-29 오후 10 46 47

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
  @Nullable
  protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
      Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

    MediaType contentType;
    boolean noContentType = false;
    try {
      contentType = inputMessage.getHeaders().getContentType();
    }
    catch (InvalidMediaTypeException ex) {
      throw new HttpMediaTypeNotSupportedException(ex.getMessage());
    }
    if (contentType == null) {
      noContentType = true;
      contentType = MediaType.APPLICATION_OCTET_STREAM;
    }

    Class<?> contextClass = parameter.getContainingClass();
    Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
    if (targetClass == null) {
      ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
      targetClass = (Class<T>) resolvableType.resolve();
    }

    HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
    Object body = NO_VALUE;

    EmptyBodyCheckingHttpInputMessage message;
    try {
      message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

      for (HttpMessageConverter<?> converter : this.messageConverters) { //여기에 아까 말했던 Converter가 10개 들어오게 된다.
        Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
        GenericHttpMessageConverter<?> genericConverter =
            (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
        if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
            (targetClass != null && converter.canRead(targetClass, contentType))) {
          if (message.hasBody()) { //여기가 참 조건이 성립하려면 위에서 GenericHttpMessageConverter 클래스 유형이어야 한다.
            HttpInputMessage msgToUse = //여기는 메소드 이름을 보면 body를 읽기 전,후 로 나뉘어 있어 aop동작을 하는것으로 짐작된다.
                getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
            body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
            body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
          }
          else {
            body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
          }
          break;
        }
      }
    }
    catch (IOException ex) {
      throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
    }

    if (body == NO_VALUE) {
      if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
          (noContentType && !message.hasBody())) {
        return null;
      }
      throw new HttpMediaTypeNotSupportedException(contentType,
          getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
    }

    MediaType selectedContentType = contentType;
    Object theBody = body;
    LogFormatUtils.traceDebug(logger, traceOn -> {
      String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
      return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
    });

    return body;
  }
}

이렇게 해서 커맨드 객체 @RequestBody가 붙은 HelloForm유형에 맞는

데이터들을 파싱해주고 처리를 해줄 수 있게 된다.

번외로 AbstractMessageConverterMethodProcessor의 구현체는

RequestResponseBodyMethodProcessor, HttpEntityMethodProcessor 이므로

새롭게 알게된 사실인데 @RequestBody를 붙이고 싶지 않다면 HttpEntity<T>RequestEntity<T>로 매개변수를 받아주면

HttpEntityMethodProcessor가 동작하고, 가공이 조금 덜 되었지만 그래도 그안에 커맨드 객체까지 담아오는 body 데이터를 받을 수 있다.

정리

내 수준으로는 아직 이것밖에 이해 못했지만 그래도 얼추 디버깅으로 바짝 쫓아갈 수는 있다고 생각한다.

예전엔 이걸 봐도 어떻게 돌아가는지 무지성으로 넘기기만 했지, 자세하게 들여다 볼 실력도 안됐었다.

근데 지금은 천천히 늦지만서도 찍어보면서 어떻게 흐름이 진행되는지는 감을 익히는 것 같다.

이 부분도 자바 공부를 더 하다보면 깊게 알 수 있게 되지 않을까 싶다.

HelloController.java

@RestController
@RequestMapping("/api/v1")
public class HelloController {

    @PostMapping("/hello")
    public ResponseEntity<?> hello(@RequestBody HelloForm helloForm) {
        return ResponseEntity.ok(Map.of("message", "success","data", helloForm));
    }
}

HelloControllerTest.java

@WebMvcTest(HelloController.class)
class HelloControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void test() throws Exception {
        String ss = "{\"message\": \"hello\", \"name\": \"lsj\"}";
        mockMvc.perform(post("/api/v1/hello")
                .content(ss)
                .contentType(MediaType.APPLICATION_JSON_VALUE))
            .andDo(print())
            .andExpect(status().isOk());
    }
}

HelloForm.java

@Getter //없으면 406에러를 발생한다. 깂을 못읽어 주입을 못해주는것 같다.
@NoArgsConstructor
public class HelloForm {

    private String message;
    private String name;

    //없으면 커맨드 객체 주입 안됨
    public HelloForm(String message, String name) {
        this.message = message;
        this.name = name;
    }

}
728x90

'디버깅' 카테고리의 다른 글

AWS SNS 토큰 에러  (0) 2022.08.10
YAML 파일을 읽어보자  (0) 2022.08.09
RequestParamMethodArgumentResolver  (0) 2022.08.09
RequestMapping 동작  (0) 2022.08.09
728x90

@RequestParam을 처리해주는 아규먼트 리졸버 = RequestParamMethodArgumentResolver

RequestParamMethodArgumentResolver는 AbstractNamedValueMethodArgumentResolver를 상속한 콘크리트 클래스고, resolveArgument를 오버라이딩 하지 않았기 때문에 AbstractNamedValueMethodArgumentResolver.resolveArgument가 호출된다.

흐름도

스크린샷 2021-11-25 오후 9 14 30

일단 흐름도는 이러하다.

어제 살펴봤던 내용은 DispatcherServlet 이전의 처리내용이었다.

스프링 MVC에 대해서 교환대라고 할 수 있는 DispatcherServlet 이 클래스가

HandlerAdapter 등.. 조건에 부합하는 객체들을 찾아서 전달을 다 해준다.

@RequestParam 이 있을 때는 당연히 매핑이 되어서 가져올것은 알았지만

없을 때 생략해도 가져오는 이것은 동작이 어떻게 되는지 궁금했다.

코드부터 보자.

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(String id, String name) {
        return id + name;
    }

    @GetMapping("/hello2")
    public String hello2(@RequestParam String id, String name) {
        return id + name;
    }

}

이러한 두개의 예시 hello, hello2를 만들었고 테스트 코드로 디버깅하는게 더 좋을것 같아서

테스트 코드로 디버깅을 진행했다.

@WebMvcTest(HelloController.class)
@AutoConfigureMockMvc
class HelloControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void hello() throws Exception {
        mockMvc.perform(get("/hello?id=lsj&name=홍길동"))
            .andDo(print())
            .andExpect(status().isOk());
    }

}

일단 돌리게 되었을 때 DispatcherServletRequestMappingHandlerAdapter를 호출하게 된다.

여기서 요청 객체를 처리하는줄 알았었는데

ArgumentResolver들 중에 해당 객체를 처리할 수 있는 Resolver 클래스를 찾는다.

스크린샷 2021-11-25 오후 9 25 03

RequestMappingHandlerAdapter의 일부 메소드인데 이 클래스가

부름을 받으면 바로 Resolver들을 추가해주는데

여기서 유심히 봐야하는 부분이 디버그로 파란줄 쳐진 부분과

//catch-all아래 2번째줄 이 두줄을 유심히 봐야하는데

여기서 내가 느낀것은 useDefaultResolution이 옵션이라고 생각하고 넘어갔다.

스크린샷 2021-11-25 오후 9 28 58

HandlerMethodArgumentResolverComposite

다음은 저 어댑터들을 가지고서 넘어온 파라미터를 처리해줄 수 있는 Resolver들을 찾는데

스크린샷 2021-11-25 오후 9 41 16

그것은 이 HandlerMethodArgumentResolverComposite에서 for문으로 찾아주게 되어있다 ❗️❗️❗️❗️

RequestParamMethodArgumentResolver

여기서 체크하는 로직이 위 이미지 RequestParamMethodArgumentResolver 이다.

보면 파라미터 어노테이션을 갖고 있는지 여부, 또는 @RequestPart를 갖고있는지 등등

분기로 판단해서 객체를 처리하려고 한다.

여기서 아까 보고 넘어갔다했던 useDefaultResolution 애가 분기문에 this.useDefaultResolution이 보이는데

이게 true라면 현재 가지고있는 매개변수의 타입대로 따라가서 값을 매핑시켜준다.

스크린샷 2021-11-25 오후 9 35 30

0번째 인자는 false, 25번째 인자는 true를 갖고있다.

정리를 해보자면

resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
상태값이 false이면 @RequestParam이 있는 경우 매개변수 생성

resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
상태값이 true이면 @RequestParam이 없는 경우 매개변수 생성을 하는 것

그래서 둘다 있으나 없으나 생성을 해준다.

근데 이제는 성능은 어떤게 좋냐고 묻는다면 명시적으로 @RequestParam을 붙인 객체는 0번째에서 바로

찾아서 매핑이 될것이다.

반대로 명시적으로 붙이지 않았다면?

매번 25번째에 있는 Resolver를 통해서 매핑시키게 될 것이다. 이게 단순 한개라면 모르겠지만,

여러 사용자 + 여러 스레드 + 파라미터의 갯수 세개의 조건이 셋중에 하나 또는 전부가 많아진다면

성능은 안좋아질게 훤히 보인다.

그래서 안붙여도 되지만 성능을 최적화 하려면 명시적으로 @RequestParam을 붙여주는것이 좋다.

정리

테코톡에서 디버그를 본 후에 깊게 한번 들어와서 공부를 해보려고 어제부터 탐색을 했다.

깊게 이렇게 들어와서 하나씩 보는것이 소스분석에도 도움이 되고 더 나아가서는

회사코드를 인계받을 때에도 이렇게 분석을 하면 핵심 로직을 빠르게 파악할 수 있을 것 같다.

그러면서 동시에 이렇게 정리까지 하니 머릿속에 많이 남아서 지식으로 가져가는 것도 좋은것 같다.

공부를 이렇게 했어야 됐는데 너무 늦은건가 싶기도 하지만 꾸준한게 답인것 같다. 🔥🔥🔥

728x90

'디버깅' 카테고리의 다른 글

AWS SNS 토큰 에러  (0) 2022.08.10
YAML 파일을 읽어보자  (0) 2022.08.09
AbstractMessageConverter  (0) 2022.08.09
RequestMapping 동작  (0) 2022.08.09
728x90

얼마만의 포스팅인지 모르겠다.

일단 바로 스타트 ❗❗❗

깃허브를 원래 올리는게 맞지만 회사 코드라서 따로 올리지는 못한다. 😥😥😥

테스트

@GetMapping에 대해서 어떻게 돌아가는지 궁금해서 무작정 실행을 시켜봤다.

image

지금 보이는 이미지는

톰캣에서 HTTP 메세지를 받아오는 구간이다.

그러니까 DispatcherServlet전에 수행되는 구간이다.

어떻게 저걸 담고있냐는

localhost:8081/swagger-ui.html 이라는 곳에서

크롬브라우저를 이용한 HTTP 통신을 하려고한다 라는 헤더를 추출한것이다.

image

여기서 api를 호출하고 GET방식으로 조회하는것 까지 확인했다.

오해했던 부분

처음에 나는 @GetMapping이 GET메소드를 만들어준다?

라고 생각했다.

HTTP 완벽 가이드를 읽으면서도 멍청하게 생각을 했다.

나는 바보다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

그런게 아니라 HTTP 메소드에서 헤더를 보고

이러이러한 요청이 있다. 부터 시작하는 것이 웹의 동작일텐데 간과하고 있었다.

과정

이렇게 쭉 지나오면 이제 FrameworkServlet을 만나게 된다.

image

이 클래스는 추상클래스로 되어있고

이걸 구현한게 바로 DispatcherServlet이다.

image

프레임워크 서블릿의 이 메소드를 지나면서

디스패처 서블릿이 이제 해당 요청에 대한 메소드를 탐색한다.

이 때 탐색하라고 핸들러에게 넘기는데

image

그 핸들러가 바로 RequestMappingHandlerAdapter 클래스이다.

얘가 @RequestMapping에 대한 모든 요청을 받아서 매칭을 해주고

핸들러 메소드들을 쭉 지나서 마지막에는 내가 구현한 API쪽으로 넘어오게 된다.

그렇게해서 해당하는 데이터들을 가져와서 클라이언트쪽으로 가공된 알맞는 데이터들을 보내주게 된다.

정리

이렇게 깊게 찾아보는게 기억에도 훨씬 잘남고 개념 확립이 착착 되는것 같아 좋다.

문득 낮에 보던 우아한 테코톡 디버깅을 보고 그리고 순간 궁금했어서 이렇게 찾아보게 된것 같다.

암만 주저리주저리 읊고 해봤자 모르겠고

그냥 코드를 보는게 답이라고 생각한다.

728x90

'디버깅' 카테고리의 다른 글

AWS SNS 토큰 에러  (0) 2022.08.10
YAML 파일을 읽어보자  (0) 2022.08.09
AbstractMessageConverter  (0) 2022.08.09
RequestParamMethodArgumentResolver  (0) 2022.08.09

+ Recent posts