다른 언어들과 마찬가지로 Javascript 역시 Scope 를 갖고 있으며, 이를 기본적으로 이해하는 것이 아주 중요하다.

 

Javascript 의 Scope 에는 다음 2가지가 있다.

 

(1) Local scope

지역 스코프를 말하며, Braclet({}) 안에서 정의되는 항목으로 정의된 해당 범위 내에서만 변수의 사용을 허용한다.

다른 범위에서 참조가 불가능하다.

 

(2) Global scope

전역 스코프를 말하며, 바닐라 자바스크립트에서 일반적으로 Braclet({}) 에 포함되지 않도록 정의된다.

전역으로 선언된 변수는 Window 객체로 포함되어 웹페이지 내에서 어디서든 참조가 가능하다.

 

그리고 응용된 Scope 로 Javascript 에서는 Nested Scope 라는 개념을 가질 수 있다.

 

(3) Nested Scope

Scope 내에서 별도의 Scope 를 정의한 경우 바깥쪽 Scope 에서 안쪽 Scope 에 접근이 불가능하지만 안쪽 Scope 에서는 바깥쪽 Scope 의 변수에 접근이 가능하다. 가령 다음과 같은 경우를 보자.

 

outerFoo() 함수의 출력 결과는 함수 내에서 innerFoo를 호출하기 때문에 70이 된다. innerFoo 함수에서 outerFoo 의 변수에 접근해서 곱하고 있는 모습이다. Nested Scope 에 의해 그 역은 성립되지 않는다.

이를 Lexical Scope 라고 하는데, 이는 변수나 함수가 문맥적으로 정의된 곳(Callee) 의 Context 를 참조한다는 일종의 원칙이다.

(이에 대한 대칭점으로 오래된 언어들에는 Dynamic Scope 라는 개념이 있고, 이는 변수나 함수가 불려진 곳(Caller) 의 Context 를 참조하는 개념이다.)

 

 

Javascript 의 Scope 는 기본적인 개념이라 익숙하면서도 자바스크립트의 특징적인 개념 중 하나이며, 중요한 개념인 Closure 를 이해하는 데 필수적이므로 잘 익혀두도록 하자.

 

 

구글 Protobuf 는 강력한 데이터구조이며 환경간 이식성이 뛰어나고 패킷으로 사용할 때, 자체의 성능(통신 속도 및 작은 패킷 크기)이 뛰어나지만, 정해진 형식이 있다보니 사용에 있어서 알아두어야할 점들이 몇가지 있다.

 

실무에서 사용하면서 알아두어야 했던 점들 및 특징적인 점들 몇가지를 정리해보았다.


1. protobuf 는 패킷간 상속과 추상화를 지원하지않는다.

프로토버프를 도입하기 가장 꺼려지는 이유인데, protobuf 의 Data Structure는 상속과 추상화와 같은 생산성을 위한 개발자들의 구조를 부정한다.

Protobuf 는 애초에 범용(?) 목적으로 설계되었다기 보다는 확실한 단일 목적에 대해 Compact 하게 설계된 Data Structure 이다.

즉, 확실히 의미를 갖는 필요한 데이터만 저장할 수 있게 되어있으며, 그렇기 때문에 Stream 으로 사용할 시 필요한 정보만 주고받는 효율성을 이점으로 취할 수 있는 반면, 데이터를 담는 데 있어서 유연하지 못하다.

 

그렇기 때문에 프로토콜 버퍼를 패킷으로 통신을 해야 한다면 모든 패킷에 필요한 정보들을 분류해서 정의해주는 것이 필요하다.

 


2. protobuf 의 패킷 넘버링은 생각보다 엄격하지않다. 하지만 패킷의 순서는 매우 중요하다.

다음과 같은 에러 처리를 위한 프로토콜 버퍼 Data Structure가 있다고 가정해보자.

위의 데이터 구조에서 time_stamp 패킷의 넘버링을 4로 바꿔도 무방하다. 프로토콜 버퍼에서 구조 내에서 중요한 것은 패킷들의 순서를 지키는 일이다.

다만, Protocol Buffer 를 enum 형태로 정의해서 쓰는 경우라면 조금 얘기가 다른데,

위와 같은 Enum 패킷에서는 numbering 이 의미를 갖는다. enum 의 경우 넘버링이 Enum 클래스의 ordinal 과 동치되기 때문에, Compile 해서 사용할 경우 패킷이 다르다면 예기치 못한 에러가 발생할 수 있다.

 

 

3. Protobuf 구조가 프로토콜로 정해졌다면 데이터 구조는 변경하지 않는 것이 좋다.

통신에 있어서 Protobuf 를 사용하는데, 기존 데이터 구조가 삭제 또는 변경된다면 그 구조를 삭제 & 변경하는 것이 아니라 새로 패킷을 추가하는 편이 좋다.

그 이유는 하위호환성 때문으로, 통신하는 양쪽의 Protobuf 빌드가 항상 동기화가 완벽하다면 문제가 없지만, 개발을 하다보면 버전이 달라질 수밖에 없다.

이 때 문제를 방지하기 위해 데이터 구조의 크기를 늘리는 방법을 선택해야 한다.

하지만 패킷의 크기가 커질지는 염려하지 않아도 된다. Protobuf 의 특징 상 입력되지않는 패킷은 보내지않도록 퍼포먼스 측면에서 최적화가 지원된다.

 


4. 구글이 지원해주는 protobuf 라이브러리가 있으며, 이를 사용하면 프로토버프의 사용이 매우 편리해진다. 

다음 링크를 참조하자.

https://github.com/protocolbuffers/protobuf

 

protocolbuffers/protobuf

Protocol Buffers - Google's data interchange format - protocolbuffers/protobuf

github.com

protobuf 라이브러리는 언어별로 필요한 기능들을 유틸리티 형태로 지원한다.

아주 방대하니 전체를 쓸 생각보다는 환경에 맞게 필요한 만큼만 모듈을 가져다 쓰는 것이 좋다.

또한 protobuf 라이브러리에는 proto 파일 내에서 사용할 수 있는 공통 protobuf 형식들도 정의되어 있으므로 참고하는 것이 좋다. 
가령 proto 파일 내에서 Collection 이나 timestamp 등의 기능을 사용할 수 있도록 정리되어있다. 

 

 

5. 당연한 얘기지만 proto 파일의 주석조차 compile 결과로 빌드된 소스에 포함 된다. 

만약 소스 자체로 어떤 스크립트가 실행되어야 한다면 환경에 주의하자.

주석에 한글 특수문자가 포함된 proto 파일을 자동화 스크립트로 돌리다가 문제가 생기는 일이 더러 있었다.

 

 

프로토콜 버퍼는 구글이 지원하고 있는 직렬화방식이고 강력하며 쓰임새도 다양하다.

하지만 현재로써는 아는만큼 사용할 수 있는 도구임이 분명하다. 사용에 있어 유의하고 항상 공식 지원을 참고하는 것이 좋겠다.

 

면접에서 단골처럼 등장하는 질문이자, 컴퓨터 공학과 시험에서 한번쯤은 보았을 법한 CS 기본 지식을 정리하고자 한다.

 

컴퓨터는 데이터를 저장할 수 있는 몇가지 종류의 공간들을 갖고 있고, 해당 공간들은 쓰임새가 다르고 만들어진 이유가 다르기 때문에 각각 I/O 작업에 있어서 다른 퍼포먼스를 낸다.

 

그 중에서도 Access 에 대한 다음 Computing Operation 의 속도 비교는 알아두어야 한다.

 

 - CPU Register

 - Context Switch

 - Memory Access (RAM)

 - Disk Seek (HDD)

 

위의 Operation 들에 대한 속도 비교 결과는 빠른 순서대로 다음과 같다.

 

1. CPU Register Access

2. Memory Access

3. Context Switching

4. Disk Seek

 

(1) CPU 레지스터에 대한 접근은 단 한번의 CPU 사이클만으로 이루어지기 때문에 즉각적으로 이루어진다.

한 사이클이라는 것은 말그대로 번개와 같은 속도로 이루어진다는 뜻이다.

 

(2) Memory Access 는 일반적으로 RAM 에서 데이터를 읽어내는 것을 말하며, 당연히 RAM 의 목적에 맞게 HDD 로부터 읽어오는 것보다 빠르다.

일반적인 상태에서의 작업은 레지스트리에 접근하는 것에 비견될만큼 빠를 수 있지만 논리 구조 위에서 동작하기 때문에 Virtual Memory Swapping 과 같은 작업에서 자유로울 수 없으며 이런 경우에는 Disk Access 만큼 느려질 수도 있다. 

 

(3) Context Switching 는 대체적으로 빠른 접근이 보장이 된다. 하지만 여러개의 프로세스가 동시에 실행되며 스위칭이 빈번하게 이뤄질 경우 굉장히 느려질 수도 있다.

 

(4) Disk Seek. HDD 에 대한 Disk Seek 은 위에 언급한 Operation 들에 비해 빠를 수가 없는 작업이지만 캐싱을 통해 비약적인 성능 향상이 가능하다.

BUS 에서의 병목을 피할 수 있으며 캐싱을 통해 Main Memory 에 Access 하는 것 만큼의 퍼포먼스를 기대할 수도 있다.

 

 

면접에서 갑작스레 질문받은 내용이라 당황했던 적이 있었다.

알고있었던 내용이라 답변은 잘 했으나... 끝나고나서 다시 점검해볼만큼 기본기가 아직 충분치 못한 것 같아 정리해둔다.

 

 

 

Frontend 에 익숙하지않은 많은 개발자들이 과제 이상의 어느정도 규모있는 Frontend 를 작업하게 되면 HTML 과 CSS 의 구조에 대해 놀라는 경향이 있다.

 

HTML 과 CSS 는 생각보다 정교한 구조를 이루고 있고, 초보 수준 이상의 Frontend 실력을 갖기 위해서는 필수적으로 구조에 대해 이해하고 어떻게 브라우저가 웹(Web)을 스타일링하는 지 알아야 한다.

 

이해해야할 잘 갖춰진 Frontend 의 특징 중 하나로, "HTML 문서의 모든 프로퍼티(properties) 들은 상속가능하다" 는 점이 있다.

 

계층적으로 구조화된 DOM 모델에서 한 Element 는 Parent Element 의 속성을 지니며, 모든 element 들의 root parent 는 <html> 이 된다.

HTML 태그의 DOM 모델들 간 상속은 다른 포스팅에서 다루도록 하고, 본 포스팅에서는 CSS 의 상속에 대해 다루기로 한다.

 

복잡한 종류의 UI 를 구조할 때, 비슷한 CSS 스타일링을 중복해서 사용하게 되는 경우가 많이 있다.

가령, 웹페이지 전체의 UI 는 어느정도 일관성이 있어야 하기 때문에, 검색을 위한 검색바(Bar)와 검색 추천을 위한 표시바(Bar) 가 있는데 이 둘의 스타일을 비슷하게 가져가되 하이라이트 컬러 정도만 바꾸고자 한다고 해보자.

 

이 때, 세세하게 작업된 모든 디자인 스타일을 복사해서 사용하는 것은 비효율적인 일이다.

 

이럴 때에는 다음과 같이 CSS 상속을 이용해 쉽게 해결할 수 있다.

 

위의 코드에서 bar-focus 라는 CSS 스타일은 bar 스타일을 차용하되 background-color 에 특징을 입힌다.

위의 마크업은 다음과 같은 페이지를 나타내게 된다.

 

색배합이 끔찍하지만, 스타일을 재활용할 수 있다는 점정도는 쉽게 알 수 있다.

 

쉽지않은가? 이런식으로 스타일의 재활용을 통해 노가다로 여겨질 수 있는 Frontend 의 디자인 코드를 확연하게 정리할 수 있다.

 

하지만 CSS 의 상속과 관련되어 알아야할 성질들은 조금 더 있다.

 

먼저 브라우저가 상속을 처리하는 순서이다.

브라우저는 CSS 상속을 Inline > Internal > External 순서로 처리한다.

 

여기서 Inline, Internal, External 은 CSS를 정의하는 방법에 대한 분류를 말하며, 다음과 같다.

 - Inline : 해당 태그 안에 style=”” 속성을 통해 정의

 - Internal : 해당 파일 안에 <css> </css> 태그를 작성하고 해당 Scope 내에 정의

 - External : 별도의 파일로 CSS 를 분리하고 해당 파일을 HTML 에서 import

 

즉, CSS를 적용할 때에 위의 순서를 유의해야 해당 스타일이 반영될 수 있다.

 

또 당신이 프론트엔드 개발자라면 주의해야할 사항으로 태그의 속성 관리 부분이 있다.

 

만약, CSS 가 적용된 태그의 속성이 "float" 을 가지고 있는 경우 해당 DOM 은 상속관계 및 레이아웃에 영향을 받지않는 붕-뜬 상태가 된다. 

해당 요소에 상속성을 적용시키기 위해서는 overflow 속성의 적용이 필요하다.

 

 

유클리드 알고리즘은 어렸을적부터 접하는 익숙한 알고리즘이지만, 수학적인 부분이 포함되어 있어 막상 구현하고자 한다면 기억해내기가 쉽지 않다.

 

그 중에서도 알고리즘 문제 해결 시 심심치않게 등장하면서도 중요한 최대공약수(GCD) 와 최소공배수(LCM) 를 구하기 위한 유클리드 알고리즘을 정리해보았다.

 

가장 원시적인(Naive) 방법으로 GCD와 LCM을 구하는 방법으로는, 브루트포스(Bruteforce) 알고리즘이 있다.

 

이는 숫자 목록을 전부 순회하면서 해당 숫자가 주어진 숫자 세트의 최대공약수 / 최소공배수 인지를 판별하는 알고리즘이다.

 

이 방법 자체가 복잡하다거나 한 것은 아니지만, 불필요한 값들의 조회가 많이 일어나는 비효율적인 알고리즘이다.

 

유클리드 알고리즘은 "유클리드 호제법" 이라는 방식을 통해 최대공약수를 구해내고, 이를 바탕으로 최소공배수를 구해낸다.

 

이 원론은 숫자 A와 B에 대하여 A를 B로 나눈 나머지 R에 대해 A와 B의 최대공약수가 B와 R의 최대공약수와 같음을 이용하며, 이 나머지 연산을 나머지가 0이 될때까지 반복함으로써 최대공약수를 구해낸다.

 

최소공배수를 구하는 데 있어서는 다음 성질을 이용한다.

 

A와 B의 최대공약수 G에 대해서 최소공배수 L = A * B / G 를 만족한다.

 

이를 통해 알고리즘적으로 구현하면 다음과 같이 구해낼 수 있다.

 

 

좀 더 자세한 수학적 원리는 다음을 참고하자.

(http://staff.www.ltu.se/~larserik/applmath/chap10en/part3.html)

 

Lazy Evaluation 이란 프로그래밍이 동작하는 데 사용되는 계산 전략의 한 종류를 말한다.

 

일반적인 프로그래밍에서 계산식이 즉발적으로 수행되는데 비해 Lazy Evaluation 은 해당 계산식의 실제 수행을 필요할 때까지 늦춘다.

이렇게 되면 장점으로는 필요하지 않은 시점에서 해당 식의 "불필요한 수행" 을 막을 수 있으며 어떤 경우에는 실제 수행되는 시점에조차 수행 과정에서의 불필요한 동작들을 최적화시킬 수 있다.

다음의 예시를 확인해보자.

 

위의 코드에서 일반적인 수행이라면 checked 의 값은 무조건 먼저 계산이 된다.

foo 함수가 얼마만큼의 비용을 수반하더라도, foo(val) 의 Return 값이 checked 가 가져야할 값이므로 일반적인 수행에서 checked 값은 foo 함수의 결과를 저장하며 그에 따라 아래의 조건식을 수행하게 된다.

 

만약 위의 코드가 Lazy 하게 동작한다면 흐름은 조금 다르다.

먼저 변수 checked 는 foo 메서드가 val 을 argument 로 전달받아서 수행하게된 "어떤 값" 이라고 간주되고, 이를 이후 필요한 시점, 즉 조건문을 체크하는 아래 로직에서 실제로 사용하게 된다.

 

첫번째 조건문인 if(true && checked) 조건문에서 foo 함수는 호출되게 된다. checked 가 true 여야지만 해당 조건이 참이되기 때문이다.

하지만 두번째 조건문에서는 foo 함수는 호출되지 않는다. 알다시피 true 가 이미 or 의 조건문에 포함되어 있기 때문에 compiler 는 최적화를 통해 이를 수행하지 않는다.

 

<참고로 위의 예제코드는 이해를 돕기 위해 작성한 것일 뿐 실제로 Lazy 하게 동작하지 않음을 주의한다.>

 

이러한 Lazy Evaluation 은 프로그래밍에서 종종 사용되는 개념이며 실제로 많은 경우에 퍼포먼스의 향상을 가져올 수 있다.

 

Java에서도 Lazy Evaluation 을 사용할 수 있으며 보통 Java8 의 Lambda 를 이용해 이를 지원한다.

 

이제 위의 메서드를 Lambda 식을 이용해서 Lazy Evaluation 형태로 바꾸어 보고 비교해보자.

 

 

위의 예제를 변형한 코드이다.

코드에서는 Lazy Evaluation 을 함수형 인터페이스의 Supplier 메서드를 이용해 구현한 모습으로,

위의 main 동작에서 가장 마지막 조건문의 수행 시, foo 함수는 호출되지 않는다.

 

이와 같은 Lazy Evaluation 을 이용하면 사소한 부분이지만 만약 foo 함수의 비용이 막대한 경우에 큰 성능 상 이점을 가져올 수 있다.

 

특히 Lambda 가 대중화되고 Lazy Evaluation 과 같은 처리가 익숙해진 최근의 추세에서 확실히 개념을 이해하고 사용하는 것이 중요하겠다.

 




Spring 은 기본적으로 Framework 의 사용자, 즉 개발자가 비즈니스 로직의 구현에만 집중할 수 있게 서블릿 처리와 같은 기타 작업을 대신해주는 잘 구성된 프레임워크이다.


개발을 하다보면 비즈니스 로직 이외에도 Request 와 Response 에 대해 직접 처리하거나 비즈니스 로직을 처리하기 이전, 혹은 이후에 작업을 처리해야할 때가 있다.


예를 들어 Request 와 Response 에 대한 로깅이나 API 전반에 걸친 인증 등 Framework Layer 에서 처리할 수 있는 작업들이 있으며, 이 때 Filter 와 Interceptor 로 작업을 처리한다.





<출처 : https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/>



1. 필터 (Filter)



public interface Filter {

public void init(FilterConfig filterConfig) throws ServletException;

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException;

public void destroy();
}




Filter 는 Servlet Container 에 의해 동작이 제어되는 Java Class 로 HTTP Request 가 Service 에 도착하기 전에, HTTP Response 가 Client 에 도착하기 전에 제어할 수 있다.


Filter 는 Request 를 처리할 때 Dispatcher Servlet 이 작업을 처리하기 전에 동작하고, Response 를 처리할 때에는 Dispatcher Servlet 에 의해 작업이 끝난 이후에 동작한다.


정확히 분류하자면 Filter 는 J2EE 의 표준이며 Servlet 2.3 부터 지원되는 기능으로 Spring Framework 에서 지원을 하고는 있지만 Spring 프레임워크만의 기능은 아님을 알아두자.


Filter 는 Filter Chain 을 갖고 있으며 Application Context 에 등록된 필터들이 WAS 구동 시에 Context Layer 에 설정된 순서대로 필터 체인을 구성한다.


구성된 체인은 맨 처음 인스턴스 초기화(init())를 거친 후 각 필터에서 doFilter() 를 통해 필터링 작업을 처리하고 Destroy 된다.


이 때의 환경 설정은 주로 톰캣을 사용할 경우 web.xml 또는 Java Configuration 을 이용해서 구현하게 된다.




2. 인터셉터 (Interceptor)



public interface HandlerInterceptor {

    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception;

    void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception;

    void afterCompletion(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception;

}


Interceptor 는 필터와는 다르게 Spring 레벨에서 지원하는 Servlet Filter 이다. Spring Context 내에서 HTTPRequest 와 HTTPResponse 처리에 대해 강력한 기능을 제공한다.


Java Servlet 레벨에서 동작하는 Filter 와 다르게 Spring Context 레벨에서 동작하므로 Dispatcher Servlet 이 Request 및 Response 를 처리하는 시점에 Interceptor Handler 가 동작한다.


Dispatcher Servlet 에서 요청이 처리되고 나면 요청받은 URL 에 대해 등록된 Interceptor 가 호출되며 Prehandle - Controller 실행 - PostHandle - AfterCompletion 의 순서로 인터셉터가 작업을 처리한다.


이 때의 환경 설정은 주로 servletContext.xml 또는 Java Configuration 을 이용해서 구현하게 된다.



필터와 인터셉터는 현업에서도 자주 사용되는 유용한 도구이니 확실하게 알아두고 꼭 필요할 때 응용해서 사용할 필요가 있다.



참조 : 

http://www.mkjava.com/tutorial/filter-vs-interceptor/

https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle

https://supawer0728.github.io/2018/04/04/spring-filter-interceptor/




메시지 지향 미들웨어(Message Oriented Middleware)란 독립될 수 있는 서비스간에 데이터를 주고받을 수 있는 형태의 미들웨어를 말한다.


구성요소간 통신을 하는 방법에 있어서 네트워크(Network) 를 이용하거나 Process 간 통신 등의 중계를 해주는 미들웨어의 경우에도 Message Oriented Middleware 라고 부를 수 있지만, 일반적으로 Server to Server 로 전달되는 Message 서비스를 말한다.


메시지 지향 미들웨어의 사용은 통신을 통해 Service 들 간의 분리와 독립, 그럼에도 불구하고 안정적인 기능을 제공해줌으로써 Software Architecture 를 설계하는 데 큰 유연성을 제공해준다.


메시지 지향 미들웨어는 대부분 메시지큐(Message Queue) 의 형태로 구현되어 있으며 줄여서 MQ라고 부른다.


MQ의 종류에는 흔히 알려진 Rabbit MQ 가 있으며 이런 종류의 메시지큐들은 AMQP(Advanced Message Queueing Protocol) 를 이용하여 구현되어 있다.


MQ 시스템을 인프라에 구축했을 때에 아키텍처상으로 이를 브로커(Broker)라고 부르기도 한다.


Message Queue 를 이용할 때 얻을 수 있는 장점으로는 다음과 같은 것들이 있다.


(1) Redundancy : 메시지의 실패는 보관 및 관리가 되며 메시지가 실패한다고 하더라도 시스템에 영향을 주지 않는다.


(2) Traffic : 메시지 큐에 메시지를 쌓아두면 무분별하게 증가할 수 있는 트래픽에 대해 느리더라도 안정된 처리가 가능하다.


(3) Batch : 메시지 큐는 메시지에 대한 일괄처리를 수행하며 특히 부하가 심한 Database 에 대한 처리에 대한 효율성을 기대할 수 있다..


(4) Decouple : 소프트웨어와 MQ는 분리되며, 이는 소프트웨어 설계에 큰 이점을 준다.


(5) Asynchronous : 큐에 넣기 때문에 영향을 받지않고 비즈니스 로직을 처리할 수 있다.


(6) Ordering : 트랜잭션의 순서가 보장되며 동시성 문제에서도 안전하다.


(7) Scalable : 다수의 서비스들이 접근하여 원하는 서비스를 이용하는데 용이하다. 이는 다수의 서비스를 분리하는 데에도 큰 도움을 준다.


(8) Resiliency : 연결된 서비스에 이상이 생기더라도 다른 서비스에 영향을 미치지 않는다.


(9) Guarantee : MQ 는 트랜잭션방식으로 설계되어 있으며 메시지 처리의 트랜잭션을 보장한다.


이러한 장점들을 갖고 있기 때문에 주로 API 서버를 구축할 때 중요하거나 그 자체로 무거울 수 있는 기능들을 별도의 서버로 분리하고, MQ 를 통해 통신하는 방식이 선호된다.


이는 프로젝트의 규모가 커짐에도 유연성을 확보할 수 있는 좋은 수단이며 서비스 자체에서 처리하기 부담스러운 요구사항들을 만족시킬 수 있는 옵션을 제공한다.



다음 링크들을 참조했습니다.


https://docs.oracle.com/cd/E19435-01/819-0069/intro.html

https://stackify.com/message-queues-12-reasons/

https://heowc.tistory.com/35





Cache 가 저장된 데이터를 Flush 시키는 전략은 캐시 서버의 유지에 있어서 중요한 부분이다.


가령 캐시에 저장된 데이터가 실제 데이터와 동기화가 안되어 잘못된 값이 참조된다거나, 이전 데이터가 만료되지 않는 경우,

혹은 저장된 정보인데 Key 값이 Mismatch 나거나 불필요하게 Flush 되어 Cache hit-ratio 가 낮아진다면 이는 전체 서버 구조에 영향을 미치게 된다.


다음은 Cache 서버를 사용할 때 유의해야할 몇가지 Cache 의 Flushing 전략 들을 정리해보았다.


주로 사용되는 캐시인 Redis 위주로 정리가 되어있으며, Cache 자체에 대한 범용적인 정리이지만 예시는 Redis 위주이며 캐시의 종류에 따라 차이가 있을 수 있다.



1. Cache 의 Flushing 은 일반적으로 Active 한 방법과 Passive 한 방법이 있음을 이해하자.


 : Active 한 방법은 별도의 요청에 따라 Cache 를 Flush 시키는 방법이고, Passive 한 방법은 자체에 Expire Time 을 이용해서 외부에서 별도 요청없이 캐시를 비우게끔 만드는 전략이다.

 

 만약 Active Flushing 을 하지 않는다면, 해당 캐시는 수동적으로만 갱신되므로 실시간적인 데이터 동기화 및 캐싱이 불가능하다.

 Passive Flushing 을 하지 않는다면, Active 한 방식으로 Flushing 되지않는 데이터는 영구히 캐시 서버에 남아서 공간을 차지하게 되며 이는 큰 비용이 될 수 있다.


 반대로 지나치게 Active 한 Flushing 이 많아진다면, Cache 는 Read 요청 대비 Write 요청의 비율이 높아져 쓰는 효율이 없어지게 되고, Passive Flushing 도 지나치게 짧은 단위로 Flushing 이 일어난다면 Cache hit ratio 가 줄어들 것이므로 설계에 유의가 필요하다.

 

 일반적으로 API 의 설계에 맞춰 Active Flushing 타이밍을 정확히 맞추고, Passive Flushing 을 위한 Expire Time 의 설정은 선험적 지식 또는 벤치마킹 등에 의해 측정되어 설정되게 된다.

 


2. Cache Refresh / Expire reset 기준을 확인하는 것이 중요하다.


 : 사용하는 캐시 서버 및 설정에 따라 다를 수 있지만, Cache Refresh 의 기준을 확인하는 것은 중요하다. 

 가령 Redis 의 경우 Cache hit Read 기준으로 Expire reset 이 발생하기 때문에, 계속적으로 Cache hit 이 발생하면 자동으로 Expire 는 되지않는다.

 단순 기능이라면 큰 문제가 안되겠지만 캐시를 바라보는 경우가 많을 경우 문제가 생기기 쉬운 상황이므로 잘 체크해주어야 한다.


 즉, 지속적으로 Cache-hit 이 발생하는 한, Active 한 방식의 Flushing 이 아닌 Passive Flushing 을 기대하는 건 잘못된 방식이다.

 개인적으로 QA 프로세스에서 Caching 문제가 발생하고 있었는데, 이 부분을 간과해서 문제가 해결되지 않은 버전이 Live 에 포함될 뻔 한 일이 있었다.



3. 사용하는 캐시 서버의 Expire 로직을 이해하자.


 : Cache 를 사용할 때 캐시 서버의 로직을 파악하고 있는 것이 중요하다. 중요한 것은 사용하는 Cache 서버가 어떻게 내부적으로 동작하냐는 것이다.

 가령 Redis 의 경우 일정한 주기를 갖고 랜덤하게 key 들을 뽑아서 expire 을 검사하고 expiration rate 가 높을 경우 다시 일정 주기로 검사하는 루틴을 지닌다.

 이러한 구조를 이해하는 것은 서버 설정하는 데 있어서 기본이 되며 보다 나은 환경을 구축하는 데 도움을 준다.




캐시에 대한 현업에서 이슈들로 겪으면서 가장 중요했던 기본 내용들만 정리해보았다.


실제 이슈와 그에 대한 트러블 슈팅은 따로 포스팅에 정리하도록 하겠다.





얼마전 Spring 의 Circular Dependency 이슈가 있어서 좀 더 자세히 알아보다가 궁금해져서 정리한 내용이다.


본 포스팅은 이슈를 정리한 내용이므로 다음 내용을 선행적으로 참조해볼 필요가 있다.

(https://jins-dev.tistory.com/entry/Spring-DIDependency-Injection-%EC%9D%98-%EC%A0%95%EC%9D%98%EC%99%80-%EC%82%AC%EC%9A%A9?category=760012)


먼저 이슈가 된 내용은 서버가 올라갈 때 Circular Dependency 관계에 있는 Bean 들 간의 설정에 있어서 @Autowired 어노테이션을 실수로 빼먹은 부분이었는데, 그로인해 참조된 Bean 이 null 상태로 초기화되는 문제 때문이었다.



Spring Reference manual 에 따르면 스프링은 먼저 각 Bean 들을 초기화하고 다른 Bean 들에 Inject 하는 방식으로 기본적으로 Circular Dependency 문제를 해결한다.


즉, 껍데기만 만들어놓고 Bean 을 먼저 Injection 한다는 것이 옳다.


여기서 주의할 부분은 상호간에 Bean 이 Inject 될 시에 Inject 되는 Bean 은 완전히 Initialized 된 상태가 아니라는 것이다.


하지만 이런 Spring Framework 레벨에서의 처리가 있음에도 Spring team 은 결국 Circular Dependency 문제는 발생하지 않는 경우가 최선이며, 그런 안전한 구조를 설계하기 위한 Constructor Injection 을 권장하고 있다.


Spring framework 를 이용한 생산성과 편의성을 보다 추구하는 측에서도 Setter Injection 의 사용이나 @Lazy 어노테이션을 이용한 Lazy Init 을 권장하고 있다.




참조


https://stackoverflow.com/questions/3485347/circular-dependency-in-spring


https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/beans.html#d0e2299


https://www.logicbig.com/tutorials/spring-framework/spring-core/circular-dependencies.html





+ Recent posts