728x90

📌 Spring Security

Spring SecurityServlet Filters를 기반으로 동작한다.

Spring Security는 인증, 권한 부여 및 보호를 제공하는 프레임워크이다.

스크린샷 2021-12-03 오후 10 38 42

전체적인 구조는 위와 같다.

Gradle 설정

plugins {
      id 'io.spring.dependency-management' version "1.0.10.RELEASE"
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

📌 Filter

그래서 필터의 역할을 먼저 아는것이 중요한데,

Filter 영어 단어만 봐도 뭔가를 필터로 걸러주는 느낌이 난다.

업무중에 통신 하나하나의 로그를 다 찍는 과정을 Filter를 통해서 구현했던 경험이 있다.

이 필터를 줄줄이 연결하면 FilterChain이 되는 것이다.

다음은 스프링 공식문서에서 FilterChain의 구조를 가져왔다.

구조

스크린샷 2021-12-01 오후 9 19 39

클라이언트는 서버에 요청을 보내고 컨테이너는 요청 URI를 기반으로 HttpServletRequest를 처리하는 필터와

필터체인을 생성한다. Spring MVC에서의 ServletDispatcherServlet이 된다.

Filter 인터페이스를 구현하면 doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

메소드를 구현해주어야 한다.

chain.doFilter()를 호출하면 다음 필터를 차례대로 수행하는 것이다.

이렇게 필터들이 쭉 연결되어 기능을 수행한다고 생각하면 된다.

📌 DelegatingFilterProxy

구조

스크린샷 2021-12-01 오후 9 30 41

설명

SpringServlet 컨테이너의 라이프사이클과

SpringApplicationContext 사이의 연결을 허용하는

DelegatingFilterProxy라는 필터를 제공한다.

이 필터는 Servlet Filter로 애플리케이션 컨텍스트에서 요청한 것을 스프링 컨테이너에 생성된

Bean Filter를 찾고 그 필터를 호출한다.

DelegatingFilterProxy는 컨테이너가 시작되기 전에 필터를 등록해야 하기 때문에 중요하다.

📌 FilterChainProxy

Spring Security의 서블릿 지원은 FilterChainProxy에 포함되어 있다.

FilterChainProxySecurityFilterChain을 통해 여러 필터 인스턴스에 위임할 수 있도록

시큐리티에서 제공하는 특수 필터이다.

FilterChainProxyBean이라서 DelegatingFilterProxy안에 포함된다.

구조

스크린샷 2021-12-01 오후 9 45 51

이제보니 구조가 좀 잡혀가는것 같다.

클린코드와 자바의 정석을 읽었더니 시큐리티 아키텍처가 이해되는건 무엇일까.... 🤔🤔🤔

📌 SecurityFilterChain

SecurityFilterChain은 클라이언트에서 필터를 돌다가 DelegatingFilterProxy에 들어온 요청을

받아서 이 요청에 대해 처리해야 하는 Security Filter를 찾아 수행한다.

구조

스크린샷 2021-12-01 오후 9 57 39

📌 Security Filter

핵심인 시큐리티 필터이다. 이 필터는 SecurityFilterChain API를 사용해

FilterChainProxy에 삽입된다. 이 필터들은 순서가 중요하다.

필터의 순서를 알 필요는 없다.

근데 알아두면 유용하게 사용할 수는 있다.

필터에는 다음과 같은 필터들이 존재하고 위에서 아래로 순서가 매겨져 있다.

  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

블럭이 쳐져있는 필터들을 유의해서 더욱 살펴봐야 하겠다.

📌 Handling Security Exceptions

위에서 ExceptionTranslationFilter를 사용하면 AccessDeniedException이나,

AuthenticationException을 HTTP Response로 변환할 수 있다.

스크린샷 2021-12-01 오후 11 16 45
  1. 일단 들어온 요청에서 FilterChain.doFilter()로 나머지 애플리케이션을 호출한다.
  2. 사용자가 인증이 되지 않았거나, AuthenticationException이 발생한 경우에 인증을 진행한다.

    이 경우 사용자가 성공적으로 인증되면 RequestCache에 사용자를 저장하고 이 캐시를 사용하여 원래 요청을 쭉 진행한다.
  3. 여기서 AccessDeniedException이 발생하게 되면 액세스는 거부되고, AccessDeniedHandler는 이 예외를 처리하기 위해 호출된다.

이 설정이 AccessDeniedExceptionAuthenticationException에 대한 예외처리가 없다면

ExceptionTranslationFilter는 아무것도 수행하지 않는다.

정리

여기까지가 일단 스프링 시큐리티의 기본적인 흐름이다.

다음 포스팅에서는 인증(Authentication) 에 대해서만 쭉 다뤄보는것으로 하겠다.

어렴풋이 알고 있는 기본값들에 대해서만 사용할게 아니라 입맛에 따라 커스텀하며 필터를 작업해주는 것이

개발자의 역량이라고 생각한다. 이게 뒷받침되려면 당연히 이런 공식문서를 읽어보는게 답이다.

전체적인 아키텍처의 흐름을 알아야 잘 설계할 수 있다.

아무튼 이 아키텍처를 공부함으로써 어제의 나보다는 오늘이 더 성장했다.

업무나 프로젝트에 들어간 시큐리티에 대한 코드를 보면 지금보다 더 이해할거라고 자신한다 🔥🔥🔥🔥

728x90

'Spring > Security' 카테고리의 다른 글

로그인 동작 순서  (0) 2022.08.09
Authentication 인증  (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
728x90

OpenSSH

이 글을 쓰게된건 일단 회사에서 여유시간이 생기게 되면 공부하는 시간을 가져도 된다고 하셨었다.

그래서 이참에 사이드 프로젝트 하던것을 좀 ec2 서버에 올려놓으면서 RDS설정도 해야겠다 싶었다.

근데 키가 없으니까 또 접속하기가 매우 불편했다.

왜냐면, 웹으로 접속하는 법을 썼어야 했으니까.

키 파일이 없는데 언제 어디서나 접속하게끔 하고 싶었고

그렇게 적용하게된 OpenSSH이다.

설치

일단 시작은 다음과 같다.

sudo apt update
sudo apt-get install openssh-server

먼저 패키지 시스템을 업데이트 해주고, openssh-server를 설치해준다.

ssh를 설치하게 되면 자동으로 실행된다.

그리고 나서 현재 상태를 조회할 수 있다.

sudo systemctl status ssh
스크린샷 2021-11-11 오후 8 51 12

이렇게 상태를 보고 동작중인지 확인이 되었다.

만약에 동작중이 아니라면

sudo systemctl enable ssh
sudo systemctl stop ssh
sudo systemctl start ssh

sudo systemctl restart ssh

stop, start를 사용하던지

restart를 사용하던지 편한것을 사용하면 되겠다.

방화벽은 나는 ec2 서버의 ubuntu를 사용하였기 때문에

인바운드 규칙에서 22번을 열어주면 방화벽 설정은 따로 해줄 필요가 없었다.

그리고 이제

접속할 쪽에서 openssh-client를 받아준다.

윈도우는 여기서 다루지 않겠다. 다른곳 가면 잘 나온다.

나는 WSL2를 사용해서

sudo apt-get install openssh-client

를 해줘서 패키지를 다운받았다. 그리고 나서 접속하는 명령어는

기존 명령어부터 먼저 보자면,

ssh -i "private key.pem" username@ip_address

방식이었고 pem파일이 없으면 실행이 안됐다.

설정법

일단 설치는 다 진행했고 다시 ec2서버로 가서

vi /etc/ssh/sshd_config 파일을 열어준다.

여기서 openssh-server에 대한 설정을 해줄 수가 있는데

전부 주석으로 처음에 막혀있다.

스크린샷 2021-11-11 오후 8 58 52

이렇게 주석되어 있는 소스들을 풀어주었다.

포트하고 어느 주소만 허용할 것인지

나는 언제 어디서나 사용할 수 있게끔 ip를 다 열어주었다. 사내에서는 이렇게 하면 안될 것이다.

막혔던 부분

아니 나는 당연히 여기서 무조건 되겠지 하고 걍했다.

남들 하는것처럼 비밀번호 나오겠지 뭐

또는 아? 인증화면에서 yes 해서 known_hosts에 등록되겠지 했다.

ㅋㅋㅋㅋㅋㅋㅋㅋ 미친짓이었다.🔥

해결책

🤔 왜 안됐을까?

아니 비밀번호를 설정만 해주면 뭐하나

일단 비밀번호 설정을 모르는 사람들을 위해 명령어 한줄 치고 가겠다.

나는 default username인 ubuntu를 썼다. 그리고 ubuntu 계정으로 들어와있다.

스크린샷 2021-11-11 오후 9 08 45

그렇기때문에

뭐 모르는상태에서 변경해야 될 수도 있을때는 sudo를 붙이면 되고,

물론 root가 아닌 얘기다!!!

sudo passwd username 으로 비밀번호를 설정해주면 된다.

여기까진 당연한것이고...

아까 설정했던 부분인 vi /etc/ssh/sshd_config로 다시 가보자

아래로 내려보면서 찾아보면

스크린샷 2021-11-11 오후 9 13 13

PasswordAuthentication 설정이 있다.

비밀번호 인증 ? yes 를 해야 당연히 적용이되지 바보같은것..

그래서 이제는 이 변경 설정을 적용해줘야 하기 때문에

위에서 봤던 명령어로 다시 껐다가 켜주면 정상적으로 로그인이 된다!!!

스크린샷 2021-11-11 오후 9 14 57

이렇게 ssh 접속으로 비밀번호까지 쳐주게 되면?

스크린샷 2021-11-11 오후 9 17 14

잘 나오게 된다. 아주 편리하고 좋은 것 같다.

이제 여기에 여러가지 설정들을 할것이다❗️❗️❗️❗️❗️ 🔥🔥🔥🔥

728x90

'클라우드' 카테고리의 다른 글

Docker compose  (0) 2022.08.07
[Docker] 도커란  (0) 2022.08.05
728x90

모든 코드는 깃허브에 있음을 알린다.

ALU

열심히 한 끝에 결국 2장의 마지막 끝판왕 ALU까지 오게 되었다.

아 이 ALU를 하려고 1장부터 몇번을 다시 봤는지 모르겠고,

기초를 차곡차곡 쌓는게 중요하다고 다시한번 느낀 그런 날이다.

이전 내용은 2장 불연산 앞부분 회고를 참고하면 되겠다.

일단 ALU는 산술논리연산장치 로서

덧셈, 뺄셈 같은 두 숫자의 산술연산과 배타적 논리합, 논리곱,

논리합 같은 논리연산을 계산하는 디지털 회로이다.

ALU는 CPU의 내부 구성 요소중 하나이며, 실제적인 연산을 담당하는 하드웨어 모듈이다.

컴퓨터시스템의 다른 부품들은 사실 ALU가 연산하기 위해 처리될

데이터를 가져오거나,

그 결과를 저장하기 위한 용도라고 해도 틀린 말이 아니다.

ALU는 구성이 산술, 그리고 논리로 되어있다.

산술

ALU는 덧셈만 있지 나눗셈, 곱셉은 존재하지 않는다.

곱셈은 그냥 * 5 해줄것을 5번 더해주는것으로 생각하고

나눗셈은 / 5 해주는 것을 5번 빼주는식으로 해주면 된다.

그래서 ALU는 덧셈으로 산술연산을 모두 처리할 수 있다.

논리

이 논리 연산으로 같은 값인지? 다른값인지 누가더 큰지 이런것을 비교하여 값을 받을 수가 있다.

회로도

조건식은

  • zx
    • if zx then x = 0
  • nx
    • if nx then x = !x
  • zy
    • if zy then y = 0
  • ny
    • if ny then y = !y
  • f
    • if f then out = x + y else out = x & y
  • no
    • if no then out = !out

이 셀렉터들의 조건이 있다.

그리고 아래는 회로도를 직접 그려본 것이다.

IMG_2742

처음에 위의 저 조건들은 당연히 이해를 했다.

그래서 아 이것도 쉽구나 하고 쭉 구현해보려고 했는데 문제가 있는 부분은

현재 그림으로 no 셀렉터 값을 가진 16비트 멀티플렉서 를 지나는 지점부터 이해가 잘 되지 않았다.

왜냐면 생각이 안들기 때문이었다. ㅋㅋㅋㅋㅋㅋ

그러니까 기초가 부족했던 탓이라고 생각했다. 😅

이제 출력값이 세개였는데

Output : 16비트 출력
zr : out = 0 일때 True 출력
ng : out < 0 일 때만 출력

이 세개가 왜 나와야하는지 그리고 저게 뭘 의미하는지 사실 잘 몰랐었다.

근데 2장을 앞부분으로 되돌아가면

16비트는 정상 출력을해주고

out이 0보다 작다는 소리는 맨 앞부분이 부호비트 라는 것을 생각해보면

아! 맨 앞 비트를 뽑아주면 되는구나!

까지 생각했다.

그래서 저 두개 부분은 금방 해결을 했다.

마지막 해결 부분

마지막 out = 0 일때 True 출력

이 부분은 사실 8개씩 나누어 두개의 Or8Way 를 사용하였다.

비트를 압축해야 하는데 자른 8개의 비트중에 하나라도 1이라면 그냥 1이 되게 되어있다.

그래서 no 라는 셀렉터가 값을 반전시키기 때문에 전부 0이 나올때만 1로 출력되게 해주어야 했다.

그래서 Or8Way로 비트를 2개로 압축한다. 이 결과를 다시 Or조건으로 넣으면

16비트 전부 0이 나오면 이제 그 0을 반대값으로 환산해서 모두가 0일때

참으로 나와주게끔 구현을 한 것이다.

아래는 nand2tetris의 코드이다.

CHIP ALU {
    IN  
        x[16], y[16],  // 16-bit 입력        
        zx,
        nx,
        zy,
        ny,
        f,
        no;

    OUT 
        out[16],
        zr,
        ng;

    PARTS:

    // zx, zy
    Mux16(a=x, b=false, sel=zx, out=o1);
    Mux16(a=y, b=false, sel=zy, out=o2);

    //nx
    Not16(in=o1, out=noto1);
    Mux16(a=o1, b=noto1, sel=nx, out=o3);

    //ny
    Not16(in=o2, out=noto2);
    Mux16(a=o2, b=noto2, sel=ny, out=o4);

    // f연산
    And16(a=o3, b=o4, out=andxy);
    Add16(a=o3, b=o4, out=addxy);
    //add or and 정의해놓고 멀티플렉서로 선택함
    Mux16(a=andxy, b=addxy, sel=f, out=result);

    //no연산
    Not16(in=result, out=notresult);

    //위와 같이 not 구현해놓고 not연산에 해당하는 값을 멀티플렉서로 선택함
    Mux16(a=result, b=notresult, sel=no, out=out, out[0..7]=front, out[8..15]=back, out[15]=ng);

    Or8Way(in=front, out=frontout);
    Or8Way(in=back, out=backout);
    Or(a=frontout, b=backout, out=orout);
    Not(in=orout, out=zr);

}

결론

드디어 끝을 보니까 너무 후련했다.

위의 코드를 HardwareSimulator를 실행해서 테스트 케이스를 통과할 때의 그 기분은 느낀 사람만 알 것이다.

확실히 손으로 그려보고 딱딱 만들어 가는 과정이 나는 더 잘 맞는것 같다.

근데 이 고통을 겪어서 그런가 감히 다음장에 나오는 플립플롭, 레지스터에 도전하고 싶다.

728x90

'CS' 카테고리의 다른 글

Cache  (0) 2022.08.09
불 연산 회고  (0) 2022.08.09
불 논리 정리 2  (0) 2022.08.09
불 논리 회고  (0) 2022.08.07
728x90

불 연산 회고

이 2장 정리를 진작에 했어야 했는데 시간이 나지를 않아서 지금 포스팅하게 되었다.

모든 코드는 깃허브에 있다.

일단 저번 회고 때 마지막으로 정리했던것은 카르노맵 이었다.

카르노맵으로 뭔가의 공식들을 도출할 수 있으니까 논리식을 간소화한 후에 회로도를 셋팅하는 정도는 이제 무리가 없었다.

이게 정말 다행이었고 여기서부터는 이제 입력 값이 3개이기 때문에 3변수 카르노맵을 통해

풀게 되었다.

일단 2진수의 덧셈을 생각해보면 2진수는 0과 1로 이루어져있다.

일반적인 10진수에서 덧셈을 할때를 보면 10일때는 올림처리를 한다.

2진수도 마찬가지다 도합 2가 되면 올림처리를 해주는 것이다.

그것을 Carry라고 부른다.

KakaoTalk_20211023_155551656

자 반가산기 이다. 반가산기는 덧셈을 수행하는 가산기이며 입력값이 두가지 이다.

그러니까 처음 계산할 때 대부분 반가산기를 사용할 것이다.

2진수로 1 + 1 = 0 이 성립한다.

이때 X = 1, Y = 1인것이고 Carry(올림)이 1이다 왜냐면 저 0은 더해서 2가 되어서

올림 처리를 해주고 남은 0이라서 합계가 0이 되는 것이다.

그래서 진리표대로 잘 나오는 것을 확인하였고 이것을 토대로 카르노맵을

Carry의 입장, Sum의 입장에서 하나씩 구현을 했다.

이 반가산기는 언제쓰나 했더니 자세히 보니까 한자리 덧셈만 할 때,

이 때만 수행해준다.

전가산기

KakaoTalk_20211023_155551179

이 전가산기가 나온이유가 바로 위에서 반가산기 덧셈 후에

그러니까 맨 처음 덧셈을 수행할 때만 이렇게 해주고

나머지는 올림처리한 입력값 까지 같이 계산을 해야되니까

이 전가산기를 통해 나머지 연산을 해주어야 한다.

16비트 가산기

그래서 이 16비트 가산기가 설명을 하자면

예를들어 1111 + 1010 이 두개를 더한다고 해보자

1 1 1 1

1 0 1 0

맨 오른쪽인

1과 0을 보면 이제 이 부분이 반가산기로 연산을 수행하는 것이다.

1 + 0 은 0이고 Carry 또한 발생하지 않았기 때문에 그대로 진행하고 그 다음부터는 올린 수를 고려해야 하기 때문에

3개 입력 -> 이전 Carry 0 + 1 + 1이 되는 것이다.

HalfAdder

FullAdder

...

를 수행하게 된다.

이렇게 반가산기, 전가산기 두개로 가산기에 대해서 알아보았다.

ALU라는 2장의 끝판왕이 존재하는데 저번 회고에서

유튜브 페이지에 들어가서 Computer Science 탭에 ALU설명을 듣고나니까 구현할 수 있을것만 같다.

토요일에 구현해보고 다시 회고를 작성해보겠다.

728x90

'CS' 카테고리의 다른 글

Cache  (0) 2022.08.09
ALU 구현  (0) 2022.08.09
불 논리 정리 2  (0) 2022.08.09
불 논리 회고  (0) 2022.08.07
728x90

2진수

2진수는 0과 1로만 이루어져있는 수이다.

1bit의 단위를 가진다.

bit란?

2진수의 기본 단위이다.

패리티 비트

패리티 비트는 전달하는 데이터의 오류를 검증해주는 비트이다.

문자열 내 1비트의 모든 숫자가 짝수 또는 홀수인지를 보증하기 위해 전송하고자 하는 데이터의 각 문자에 1 비트를 더하여 전송하는 방법으로

2가지 종류의 패리티 비트(홀수, 짝수)가 있다. 패리티 비트는 오류 검출 부호 에서 가장 간단한 형태로 쓰인다.

실질적으로 오류를 확인할 수 있는 비트이지, 그 오류에 대한 해결을 할 수는 없다.

보수

보수는 사전적 의미로 보충을 해주는 수 라고 정의되어 있다.

숫자 2의 10의 보수는 8이 되는 것이다.

논리 회로에서의 보수는 뺄셈 연산을 할 때, 좀 더 효과적으로 수행하며

논리적 조작의 용이성을 위해 사용한다.

  • 1의 보수
    • 2^n -1 자리수 최대값에서 해당하는 이진수 값을 빼주면 그것이 바로 1의 보수
  • 2의 보수
    • 2^n - 1자리수 최대값에서 해당하는 이진수 값을 빼준 후에 1을 더해주는 보수

KakaoTalk_Photo_2021-10-30-15-58-08

1의 보수 감산

  1. 1의 보수 감산은 주어진 수들을 일단 2진수로 변환한다.
  2. 음수로 되어있는 수는 1의 보수를 취해준다.
  3. 서로의 자릿수가 다르다면 자리수를 맞춰준다.
  4. 앞에 양수면 0, 음수면 1의 부호비트를 붙여준다.
  5. 덧셈을 진행한다.
  6. 진행한 후에 자리올림(Carry)가 발생하면 최하위 비트에 1을 더해준다.

그 후 맨앞의 부호비트는 떼어놓고 나머지 수들의 자릿수만큼의 경우의 수(절대값)은

2^n-1에서 떼어놓은 나머지 수를 빼주고 10진수로 변환한다.

그리고 나서 부호비트를 표현해주면 그것이 보수감산하여 나온 값이 된다.

2의 보수 감산

2의보수는 1의보수 + 1 이기 때문에 1의보수 방법에서

보수로 변환을 해준 뒤에 + 1을 시켜준다.

마찬가지로 부호비트를 추가해주고 덧셈을 수행하면 된다.

그리고 나서 보수감산이 진행되고 나서 다시 +1을 해준다.

불대수 법칙

이전 포스팅에서도 정리를 했었는데, 다 알고있던건 수기로 작성했고

드모르간 정리는 저번 포스팅 때에 존재하였기 때문에,

제2 분배법칙을 따로 증명해보았다.

그게 바로 X + YZ = (X+Y)(Y+Z) 이런 법칙이다.

(X+Y)(X+Z)를 분배해서 일일이 나열하게 되면

XX + XY + XZ + YZ 가 되는데,

여기서 XX + XY를 X에 대해 묶어주면

X(1 + Y)가 된다.

기본법칙에서 1 + 변수는 1이되므로 생략이 된다.

그래서 저 식을 풀면 그냥 X가 된다.

정리를해보면 X + XZ + YZ 가 남게 되는데 여기서 또,

X에대해서 X + XZ를 묶어주게 되면

X(1 + Z)가 되어서 또다시 X가 된다.

그래서 최종적으로 보게 되면 X + YZ 가되어 두식이 같다는걸 증명했다.

이제 좀 탄력을 받아서 ALU 구현하는데에 도전해볼 것 같다는 확신이 든다.

유튜브 페이지에 들어가서 Computer Science

아주 좋은 강의가 순서대로 있었다.

논리게이트부터 CPU까지만 지금 들었지만 굉장히 이해하기 쉽고 재밌게 설명해서

보면서 감탄만 한것 같다.

728x90

'CS' 카테고리의 다른 글

ALU 구현  (0) 2022.08.09
불 연산 회고  (0) 2022.08.09
불 논리 회고  (0) 2022.08.07
HTTP  (0) 2022.08.07

+ Recent posts