백엔드를 개발하다보면, Client 의 요청을 자체 처리하는 경우가 아닌 다른 url 핸들러에게 위임하는 경우가 종종 있다.


이렇게 클라이언트 요청에 대해서 다른 리소스로 연결을 할 때, redirect 와 forward 라는 2가지 테크닉이 있다.


Redirect 와 Forward 모두 클라이언트 요청에 대한 처리를 다른 url 로 보내서 처리를 위임하는 개념이지만, 


두 개념에는 차이가 있다. 각각의 개념을 통해 차이점을 정리해보자.



redirect - 클라이언트의 요청에 대해 서버가 다른 url 로 요청을 하게끔 만듬.




이 때, 원래 클라이언트가 요청한 url 의 핸들링 시, 서버는 다른 url 로 클라이언트가 "요청" 을 던지게끔 한다.


이 후 클라이언트는 서버로부터 전달받은 다른 url, 즉 위의 예제에서 /home 으로 다시 요청을 하게 하고, 해당 url 매퍼에서 요청에 대한 응답이 처리되게 된다.


즉, 연결이 끊기고 재연결이 들어가는 등 요청이 여러번 왕복하기 때문에 Rquest - Response 쌍이 하나 이상 생기게 된다.




forward - 서버가 클라이언트의 요청을 다른 서버로 넘김(forwarding)




서버는 클라이언트로부터 요청을 전달받았을 때, 이 요청을 서버 내부에서 다른 url 핸들러로 요청을 "전달" 한다.


즉, 클라이언트가 다시 서버에 대한 요청을 할 필요 없이 서버가 다른 url 매퍼에서 처리된 응답을 받기만 하면 되는 구조가 된다.


실제 처리는 "다른 url" 에서 처리되었지만 응답은 초기 url 핸들러로부터 내려받으며, 서버와 클라이언트 간 Request - Response 쌍은 하나만 존재하며 연결이 끊기지 않음. (연결이 하나로 유지됨)




실무에서도 많이 쓰이는 개념이고 익숙해지면 차이를 헷갈릴 수 있으니 틈틈히 정리하고 알아두는게 필요하다.



'Server > Basic' 카테고리의 다른 글

Message Oriented Middleware 및 Message Queue 에 대한 설명  (0) 2019.03.13
COMET 이란?  (0) 2019.01.23
Java Servlet 에 대하여  (0) 2018.12.23
HTTP/2 특징들에 대한 정리  (0) 2018.12.17
무중단 배포의 원리와 솔루션 종류  (0) 2018.12.09

 

Servlet 은 WAS에서 동작하는 Java 의 클래스를 말하며, 단순히 HTTP Request 에 대해 HTTP Response 를 응답하는 고차원 추상화를 제공하는 클래스를 말한다.

Java로 웹 어플리케이션 제작 시 동적인 처리를 담당한다.

 

Web Server 의 성능 향상을 위해 사용되는데, 외부 요청에 대해 Thread 로 할당하여 응답하므로 아주 가벼운 서버로 구현되고 Java 의 특성 상 다양한 플랫폼에서도 동작이 가능하다.

 

Servlet 은 일반적으로 HttpServlet  클래스를 상속받으며 웹페이지 개발시 JSP 와 Servlet 을 함께 이용하는 것이 도움이 된다.

(JSP 는 HTML 문서 안에서 Java 코드를 포함하는 반면, Servlet 은 Java 코드 안에서 HTML 코드를 사용하곤 한다.)

 

Servlet 3.0 미만의 버전에서는 web.xml 파일에 Servlet 을 등록하고 사용하도록 되어있지만, 

Servlet 3.0 이상에서는 web.xml 파일을 사용하지 않으며 대신 Annotation 을 이용해 정의한다.

 

3.0 이상에서 어노테이션을 이용해 서블릿을 작성할 때에는

 

@WebServlet("/test")
public class TestServlet extends HttpServlet {

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 response.setContentType("text/html;charset=utf-8");
 PrintWriter out = response.getWriter();
 out.print("Hello world");
 out.close();
 }
}

위와 같이 작성하게 된다.

 

Java Servlet 은 Servlet Container 위에서 동작가능한데, Tomcat 과 같은 서블릿 컨테이너 들은 다음과 같은 기능들을 지원함으로써 서블릿의 동작을 돕는다.

 

 - Network Services : Remote System 및 네트워크 서비스를 접근하고 이용할 수 있도록 제공한다.

 - Decode and Encode MIME based Message : MIME 타입의 메세지에 대한 인코딩 / 디코딩을 제공한다.

 - Manage Servlet container : 서블릿 들의 라이프사이클을 관리해준다.

 - Resource Management : HTML, JSP 등과 같은 static 및 dynamic 리소스 들을 관리해준다.

 - Security Service : 리소스 접근에 대한 인증과 권한을 담당한다.

 - Session Management : 세션을 관리한다.

 

Tomcat 과 같은 Servlet Container 들은 서블릿들을 구동시켜 WAS 로써 동작할 수 있도록 해준다.

 


HTTP/1.1 은 변해가는 웹서비스 환경에서 발생하는 상당한 규모의 문제들을 처리하는데 훌륭한 방법을 제시해주었고, 


그 결과 성능적으로나 서비스적으로나 변해가는 웹생태계에 걸맞는 진화를 보여주었다.


하지만 그럼에도 불구하고 웹의 사용자는 계속해서 웹서비스에게 많은 리소스를 요구하고, 많은 리소스로 클라이언트를 충족해줘야만 살아남을 수 있게 바뀌어 가고 있다.


실제로 웹 초창기에 비해 평균 다운로드하는 리소스의 용량은 60배 이상 늘었으며, 이 추세는 계속될 것으로 보인다.


이에 HTTP/2 는 HTTP/1.1 에서 다소 불안정하던 부분을 해소하고 웹어플리케이션을 더 빠르고 효율적으로 만들어주는데 초점을 두었다.


HTTP/2 의 근본을 이해하기 위해서는 Google 이 2000년대에 진행했던 프로젝트인 SPDY 를 이해해야 한다.


SPDY(스피디) 란 Google이 개발한 비표준 네트워크 프로토콜로 패킷 압축, Multiplexing 을 기반으로 인터넷에서의 Latency 를 줄이기 위해 고안된 프로토콜로, 초창기 크롬 브라우저에 탑재되어 높은 로딩 속도를 자랑하게 했던 구글의 자체 프로토콜이다.


HTTP/2 는 바로 이 SPDY 에 기반을 둔 HTTP 프로토콜 Layer 하위의 TCP 통신 레이어에 새로운 Binary 계층을 도입하여 HTTP 의 기반이 되는 TCP 연결의 호율성을 추구하였다.


HTTP/2 의 특징들은 다음과 같다.


(1) Binary Framework


 HTTP/2 는 TCP 계층과의 사이에 새로운 Binary Framework 를 통해 네트워크 스택을 구성한다.

기존에 텍스트 기반으로 Header 와 Data 가 연결되고 있던 1.1 이하 버전과 다르게 HTTP/2 는 전송에 필요한 메시지들을 Binary 단위로 구성하며 필요 정보를 더 작은 프레임으로 쪼개서 관리한다. 


여기서 데이터를 Binary Encoding 방식으로 관리하는데, 인코딩된 데이터를 다루기 위해서는 반드시 Decoding 과정이 필요하게 된다.


즉, 이말은 HTTP/1.1 버전의 클라이언트는 HTTP/2 버전의 서버와 통신이 불가능하다는 뜻이다.

그래도 걱정할 필요는 없다. 해당 메커니즘은 이미 도입이 되었고 충분히 지원하는 상황이기 때문에 이슈가 생기지 않는다면 추가로 신경쓸 필요는 없다.



(2) Packet Capsulation


 HTTP/2 의 패킷들은 더 작은 단위로 Capsulation 된다. 여기서 Frame 과 Message, Stream 이라는 개념이 도입된다.


 - Frame : HTTP/2 의 통신 최소단위로 모든 패킷에는 하나의 Frame Header 가 포함된다.

 - Message : Frame 의 시퀀스 데이터를 말한다.

 - Stream : 연결의 흐름을 의미한다. 


HTTP/2 의 모든 연결은 TCP 기반의 Stream 이며 양방향으로 Frame Header 를 지닌 Message 들을 통신한다.

데이터는 위에서 언급한 바 대로 Binary 인코딩된 데이터들이며 Multiplexing 과 성능 최적화 알고리즘들이 적용된다.



(3) Multiplexing 개선


<출처 : https://medium.com/@factoryhr/http-2-the-difference-between-http-1-1-benefits-and-how-to-use-it-38094fa0e95b>


 

 HTTP/1.1 에도 멀티플렉싱을 지원하기 위한 노력은 있었지만, HTTP/1.1 은 이 부분에서 다소 한계점을 갖고 있었다. 

(참고 : http://jins-dev.tistory.com/entry/HTTP11-%EC%9D%98-HTTP-Pipelining-%EA%B3%BC-Persistent-Connection-%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC)


요청과 응답의 동시처리는 이루어지나 결국 응답처리를 지연시키는 블로킹 방식이었기 때문에, 한 개의 Connection 이 하나의 Request / Response 를 처리하는 한계를 극복하기는 어려웠으며 그로 인해 결국 HOL(Head Of Line) 문제 발생과 연결에 있어서 비효율성을 갖는다는 아쉬운 부분이 있었다.


HTTP/2 는 이부분에서 개선된 멀티플렉싱을 지원하며, Connection 하나에서 다수의 입출력이 가능하게끔 지원한다.


이것이 가능한 이유는 HTTP/2는 패킷을 Frame 단위로 세분화하여 순서에 상관없이 받는쪽에서 조립하도록 설계하였기 때문이다. 


그렇기 때문에 HTTP/2 에서는 각 요청과 응답을 병렬로 전달할 수 있으며 하나의 Connection 에서도 여러 응답 / 요청을 처리할 수 있게 되었고 HTTP/1.1 에서 사용하던 임시방편을 사용할 필요가 없어졌다.



(4) Header Compression


 HTTP/2 의 Header 필드는 Huffman code 로 인코딩되며 이에 따라 텍스트적인 압축이 수반된다. 여기서 패킷을 더 최적화해 만들어진 HPACK 알고리즘은 Header 의 크기를 80% 이상 압축해서 전송한다고 알려져있다.



(5) Server Push


 HTTP/2 에서 서버는 단일 클라이언트의 요청에 대해 추가적인 응답을 내려줄 수 있다.

HTTP에서 Push 문제는 쉽지않은 이슈이지만, HTTP/2 에서는 이를 PUSH_PROMISE 라는 Frame 을 이용해 제공한다.

이렇게 제공되는 Push 리소스는 캐싱되거나 재사용 또한 가능해서 유용하게 사용이 가능할 것으로 보인다.



그 외에도 보안적인 측면이나 세세한 부분에서 HTTP/2 는 개선을 위한 노력들이 많이 보이며 확실히 효율적인 프로토콜로 자리매김한 것으로 보인다.


일각에서는 여전히 쿠키 보안에 취약하며 성능에 있어서의 향상 수준이 호환성을 저해할 만큼 대단한 것인가에 대한 의문도 있는 모양이지만, 개인적으로는 상당히 훌륭하고 배울 부분이 많은 프로토콜로 생각된다.





Hashing 이란 임의의 데이터를 고정된 길이의 구분되는 데이터로 매핑하는 과정을 말한다.


해시 알고리즘은 Computer Science 전반에 걸쳐 사용되는데 암호학적 Hash 또는 자료구조로써 해시테이블에서 구별된 인덱스를 나타내기 위한 도구로도 사용된다.


해시 알고리즘의 특징은 다음과 같다.


(1) 단방향(One-way) : 해시 알고리즘은 데이터를 단방향으로 변경하며, 그 역연산은 계산이 사실상 불가능하다. 

이는 수학적으로 "One-way Function" 을 사용하기 때문이며, 이 함수는 충분히 큰 가능한 경우의 수에 대해 다항 시간 안에 입력을 구하기 어렵게 한다.


* 물론 현재 수학적으로 정의된 One-way Function 이 실제 일방향인지는 증명되지 않았다. 그렇기 때문에 해시 알고리즘은 뚫린 알고리즘이 있을 수 있다.

만약 수학적으로 One-way Function 이 실제로 역방향 연산이 불가능한지가 증명된다면 가장 큰 수학적 난제가 풀리는 상황이며, 이는 P != NP 문제를 증명하는 셈이 된다.



(2) 임의성(Randomness) : 해시 알고리즘은 일정한 포맷을 기준으로 데이터의 변경에 따라 유추할 수 없게 임의로 인코딩된다. 

값 하나만 바뀌어도 전체 해시 값은 이전과 아예 다른 방향으로 패턴이 만들어진다.



(3) 공개된 함수(Hash function) : 해시 알고리즘은 아무나 알수없는 키값을 가지는 것이 아닌 모두에게 공유된 해시 함수(Hash function)을 사용한다. 이 말은 공격하는 측도 함수를 알고 있다는 뜻이다. 

대신 암호화와 다른 부분이므로 매우 빠른 속도로 함수가 수행된다.



Hash Algorithm 은 단방향인만큼 해커가 공격 시에 역추적을 시도하는 일은 거의 없다. 


대신 주로 공격할 때에는 입력값을 다변화해서 Brute-Force 를 적용하거나 특정 키를 바탕으로 범위를 축소하는 방향으로 공격이 이루어진다.


이런 점을 보완하기 위해 대부분의 Hash Algorithm 은 알고리즘을 여러번 수행하거나 해시와 암호화 알고리즘을 같이 병행해서 수행하거나 임의의 키값(Salt) 를 추가하기도 한다.



반면, 암호화 알고리즘은 양방향성을 지닌다.


암호화 알고리즘을 위해선 암호화(Encrypt)에 쓰이는 Key 가 필요하며, Key 와 사용자 데이터를 알고리즘을 통해 묶어서 암호문을 생성해낸다.

역으로 암호문은 Key 를 통해 복호화(Decrypt)되며 사용자 데이터를 꺼낼 수 있게 주어진다.


암호화는 해시 알고리즘과 달리 데이터의 인덱싱보다는 전달에 의미를 두기 때문에 좀 더 복잡하며 보안을 위한 다양한 방법이 수반된다.


실제 보안에서는 암호학적 해시 알고리즘은 암호화에 주로 같이 사용된다.


유명한 암호화 해시 알고리즘으로는 SHA-1, SHA-256, SHA-512, MD5 등이 있으며, 해시 값을 만들어내는 Hash function 자체로는 Murmur hash 가 있다. 



 무중단 배포란 알고있는대로, 서버를 실제로 서비스할 때 서비스적 장애와 배포에 있어서 부담감을 최소화할 수 있게끔 서비스가 중단되지 않고도 코드를 Deploy할 수 있는 기술이다


예전에는 배포 자체가 하나의 거대한 일이었고, 이를 위한 팀과 개발팀이 날을 잡고 새벽에 배포하는 일이 잦았지만, 최근에는 무중단 배포 기능을 탑재한 Deploy 자동화 툴을 이용해서 개발자들이 스스로 배포까지 담당하는 DevOps 의 역할을 하게되면서, DevOps 의 필수 기술 중 하나가 되었다.


 무중단 배포 방식에는 주로 사용되는 것들에 AWS에서 Blue-Green 무중단 배포(Blue는 기존버전, Green은 새로운버전. Router를 통해 Blue로 이동하는 트래픽을 Green으로 변경시켜준다. 원리는 동일하다.), Docker를 이용한 웹서비스 무중단 배포가 있다


IDC에서 직접 L4 스위치를 이용해서 하는 방안도 간단하지만, 이는 비용적으로 효율적이지 않아 많이 없어지는 추세이다


또한 NginX 등을 이용해서 저렴하게 무중단 배포를 하는 방식도 있다


이 방법을 사용하면 클라우드 인프라가 갖춰져있지 않아도 되고 별도의 인스턴스를 갖고 있지 않아도 가능하다

(Spring jar 2개를 여러 포트에 나눠서 배포하고 그 앞에 NginX 로 밸런싱해주면 된다.)


 무중단 배포의 원리




간단한 원리는 위와 같다. 핵심은 Reverse Proxy 가 서로 다른 인스턴스의 각기 다른 포트와 서브 도메인으로 연결하고, 지속적으로 Health Check 하면서 배포시 서브도메인을 메인 도메인으로 Switching 해주고, 배포가 끝나면 다시 메인도메인으로 Reload 해주는 것이다


이 구조에서 주의해야할 점은, 배포가 서비스에 영향을 주지 않도록 해야한다는 것이다. 예를들어 DB의 구조를 바꾸는 JPA와 같은 기술들이 사용되어 있을 경우 검토가 필요하다.


 도커 컨테이너를 이용하면 이는 매우 간단해진다

하나의 이미지에서 여러 컨테이너를 생성해서 호스트의 docker 명령어를 이용해서 손쉽게 서버 이중화 및 Switch, Reload 가 이루어진다

빌드 서버에서 이미지를 만들고 해당 이미지를 distribution 을 통해 다른 서버에서 이를 가져오는 식으로 구성된다.


 도커의 Service Discovery 라는 개념을 이용하면 nGinX 를 통해 배포할 때의 단점인 설정 파일의 수정과 재시작이 수반되어야 한다는점과 Proxy 대상 IP Port 가 고정이어야 한다는 점, Health Check 오버헤드를 피할 수 있다


Service Discovery는 서버들의 정보를 포함한 정보들을 저장해서 가져오고, 값의 변화가 일어날 때 이벤트 형식으로 설정을 수정하고 재시작하는 개념이다.




위의 구조에서는 Key/value 스토어를 이용해서 서버 정보를 저장하였으며 Configuration Manager 가 이를 watch하면서 이벤트 방식으로 설정 파일을 만들고 기존 파일에 덮어 쓰는 작업을 하고 있다


docker에서 대표적인 Service discovery tool docker-gen 이 있다


자세한 내용 참조

(https://subicura.com/2016/06/07/zero-downtime-docker-deployment.html)


실습가능한 참조 링크

http://jojoldu.tistory.com/267




nginx 의 설정은 nginx 를 구성하는 핵심으로 nginx 는 설정파일만 갖고 동작을 결정한다고 해도 과언이 아니다.


보통 nginx 의 환경설정을 구성하는 파일은 /usr/local/nginx/conf 폴더 하위에 위치하며 nginx.conf 파일을 수정함으로써 환경설정을 정의할 수 있다.


환경설정은 document 를 보는 것보다 예제로 파악하는게 좋아보이므로 예제를 중심으로 기술한다.



http {
### Load balancing ###
upstream {
### Target Proxy Host ###
server server01;
server server02;
}
### Main server configuration ###
server {
listen 80;
server_name nginx.sample.com;
client_header_timeout 10;
client_body_timeout 10;
charset UTF-8;
root /home/my-doc;
######### log #######
access_log logs/access.log main;
error_log logs/error.log;
#####################
############## internal location ############################
location /monitor {
allow 127.0.0.1;
deny all;

proxy_pass http://monitor.proxy:8080;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
############## main location ############################
location / {
proxy_pass http://localhost:8080;
}
}
}


nginx 설정은 계층적 구조를 이루며 하위 블록에서의 선언은 상위 블록에서의 선언을 덮어쓴다.


http 블록은 nginx 설정의 루트 블록이라고 할 수 있다. 기본적으로 여러개 정의할 수는 있으나 관리상의 이유로 보통 하나의 블록 안에서 해결한다.


server 블록은 Host 의 개념으로 웹서버의 메인 설정이라 할 수 있다.


upstream 블록은 로드밸런싱을 하는데 사용되며 nginx 를 로드밸런서로 사용할 경우 기술된 서버로 해당 요청을 중계해준다.


옵션값으로 default(선언하지 않을 경우 라운드로빈), least_conn (연결이 가장 적은 서버로 중계), ip_hash(ip값을 이용한 해시주소로 요청 분배) 등 분산 알고리즘의 설정이 가능하다.


log 항목은 nginx 의 중요 기능 중 하나로 nginx 를 거치는 모든 로그 및 에러 로그를 기록하는 데 사용된다.


location 블록은 서버 내에서 요청을 다르게 라우팅하고 싶을 경우 사용한다. nginx 로 프록시 서버를 구성할 때 해당 경로로 proxy pass 를 지정함으로써 라우팅을 처리할 수 있다.


nginx 의 설정 반영은 서버를 재시작해야 적용되므로 설정 후에는 OS 에 맞게 서비스를 재시작 해주도록 하자.





Cache 는 서버의 동작을 이해하는 데 있어서 빼놓을 수 없는 부분이며, 서버의 부하를 줄여주고 서버가 가진 능력을 최대한으로 활용할 수 있게 해줄 뿐만 아니라, 클라이언트에도 중요한 역할을 한다.


흔히 말하는 Cache 의 종류로는 Redis 나 Memcached 를 사용하며 대부분 인메모리 형태로 서버의 값을 저장하고, 필요할 때에 해당 값을 반환함으로써 서버의 작업 공수를 줄여준다.


Spring Framework 는 프레임워크 레벨에서 캐시의 추상화를 지원해준다.


Cache의 추상화란, 흔히 캐시를 사용할 때 작업이 필요한 부분에 대한 인터페이스를 제공해준다는 뜻이다.


가령, 웹서버에 캐싱 기능을 적용하기 위해서는 다음과 같은 캐싱의 기본 로직이 탑제되어야 한다.


(1) Memory 혹은 원격 캐시에 연결된 객체를 생성한다. (이를 Cache Manager라 한다.)


(2) 캐시의 값을 불러온다.


(3) 캐시에 값이 존재하지 않는다면 캐싱할 값을 일정한 기준을 갖고 등록한다.


(4) 등록된 캐시값에 대해 조회가 가능하다.


(5) 필요할 때에 캐시의 값을 불러오고 적당한 때에 캐시를 업데이트 한다.


위의 단계들은 기본적으로 캐시가 가져야할 역할이며, 위의 역할 정도는 수행할 수 있어야 서버측에서 "캐시" 로써 동작한다고 할 수 있다.


Spring 에서 위와 같은 단계는 다음 Annotation 들로 대체될 수 있다.


Cache 의 생성 : Spring 의 Cache Configuration 참조


Cache Key Value 의 등록 : @Cacheable



@Cacheable(value="user")
public List<User> getUserListFromDB() {
return selectUserListFromDB();
}

@Cacheable(value="user", key="#uid", condition="#result!=null")
public User getUserFromDB(String uid) {
return selectUserFromDB(uid);
}


@Cacheable 어노테이션과 함께 저장할 캐시의 이름을 value에 명시하고, key 값을 지정하면 해당 결과값을 설정된 Cache에 캐싱할 수 있다.


값은 캐싱될 뿐 아니라, 다시 해당 함수로 접근 시 캐싱된 값이 있다면 내부 함수를 수행하지 않는다.


condition 은 해당 캐시에 적용 시 어떤 항목들에 대해 캐싱하거나 캐싱하지 않을 지를 결정할 수 있다.



Cache Key Value 의 삭제 : @CacheEvict



@CacheEvict(value="user", key="#user.uid", beforeInvocation=false)
public void putUserToDB(User user) {
insertUserToDB(user);
}


@CacheEvict 어노테이션을 이용하면 해당 캐시 이름과 Key 에 저장되어 있는 Cache Value 를 제거할 수 있다. 

이 때 종종 사용되는 옵션으로 beforeInvocation 옵션이 있는데, 이 옵션을 true 로 지정하면 함수가 시작되기 전에 캐시를 비우는 작업을 수행한다.


Cache 의 갱신 : @CachePut


@CachePut 어노테이션은 값이 변경되었을 경우에만 해당 캐시를 비운다.


여러 개의 Caching 동작에 대해 : @Caching



@Caching(evict = {
@CacheEvict(value="user", key="#user.uid")),
@CacheEvict(value="userGroup", key="#user.groupNo")
})
public void addNewUserToDB(User user) {
insertUserToDB(user);
insertUserGroupToDB(user.getUserGroup());
}


Cache 에 대한 여러 동작을 수행하고자 할 때에는 @Caching 어노테이션을 사용한다.



Spring 의 Cache 어노테이션은 내부적으로 SPEL(Spring Expression Language) 라는 문법을 사용한다.

위의 간단한 예시만으로도 사용하는 데 큰 지장은 없을 것이다.




Spring 의 Bean 이 정의될 경우 정의하는 Bean 의 Scope 에 대해 옵션을 부여할 수 있다.


예를 들어 Prototype Scope 는 해당 Bean 을 사용할 때마다 재정의하게끔 성질을 부여한다.


반면 Bean 을 Spring project 전역에서 단 하나의 객체로 취급하고 접근하고자 할 경우 해당 Bean 의 Scope 는 Singleton 으로 정의되는 것이 옳다.


Spring 프레임워크는 현재 5개의 Scope 를 제공하고 있다.


(1) Singleton : Spring 컨테이너 하나당 한개의 Bean 만이 생성되고 해당 Bean 이 요청될 때마다 모든 객체는 한 객체를 가리킨다.


(2) Prototype : 하나의 Bean 은 사용할 때마다 새로운 객체를 할당하여 사용하게 된다.


(3) Request : HTTP request 가 상주하는 Spring Application Context 내에서만 유효한 객체를 생성하고 재사용한다. (web-aware)


(4) Session : Bean 을 해당 세션이 가진 Application Context에 바인딩한다. (web-aware)


(5) Global session : HTTP 세션 전역에서 같은 Bean 을 사용가능하다. (web-aware)


Spring Bean 들의 Default scope 는 Singleton 이기 때문에 대부분 Bean 을 사용할 경우 @Autowired 를 통해 간편하게 사용이 가능하다.


만약 어플리케이션에 따라 다른 종류의 Scope 가 필요하다면 다음 예시와 같이 설정에서 변경이 가능하다.



<bean id = "..." class = "..." scope = "prototype">
//...
</bean>



보통의 어플리케이션에서는 Singleton 으로 설계하고 사용할 수 있으나 간혹 prototype 등으로 특정할 수 있으니 상황에 따라 유용하게 사용할 수도 있다.


<보다 나은 예시 참조 : https://www.tutorialspoint.com/spring/spring_bean_scopes.htm>



Spring 의 Bean 들은 대게 Singleton 패턴으로 정의되며 Application Context 에서 관리되는 형태로 개발자는 사용하게 된다.


그렇게 때문에 상당수의 개발자들이 Spring 의 각 Bean 들은 Thread Safety 가 보장된 안전한 Bean Object 이며 Spring 은 해당 동시성 문제에서 자유롭다고 여기는 경향이 있다.


하지만 정확히 Spring Bean 들은 Thread 의 Safety 와 무관하다. 


Spring Container 는 Object 들 각각에 대한 Life cycle 을 갖고 있으며 Container 내에서 하나만 존재하도록 보장하지만 그것이 Thread-Safe 를 말하는 것은 결코 아니며, Spring framework 는 오히려 이 책임을 개발자에게 맡긴다.


만약 Non-Thread-Safe Object 가 Injection 된다면 해당 객체는 쓰레드에 안전하지 않으며 개발자가 직접 핸들링해주어야 한다.


그렇다면 Bean 에 대해 어떤 방식으로 Thread Safety 를 부여할 수 있을까?


(1) Builder Pattern 의 사용

  간단하면서도 Tricky 한 방식으로 Builder Pattern 을 사용하면 좋다.

 Builder 패턴을 이용하면 객체의 Setter 를 정의하지 않은 상태에서 생성자만으로 객체의 Mutation 을 관리할 수 있으며, Spring 의 Bean 들은 Container 에 의해 life cycle 관리가 위임된다.

 따라서 Builder Pattern 을 통해 Set 을 관리하면 간단하면서도 명확히 Thread Safe 를 구현할 수 있다.


(2) Stateless Bean

  Bean 을 상태값과 무관하게 동작할 수 있는 Bean 으로 설계하는 것이다. 

  Bean 이 특정 상태를 나타내는 변수를 계속 메모리에 들고 상주하는 형태가 아닌, 가장 단순한 형태의 도메인 모델로 사용이 추천된다.


(3) Lock the beans

  가장 최후의 수단으로 여겨야 하는 방법으로 Bean 에 대해 Thread Safe 하게 설계를 하는 것이다. Spring 은 Lower level Library 들을 통해 Bean 단위의 Lock 을 지원하고 있으며 이를 통해 병렬처리에 있어서 동시성 문제의 해결이 필요하다.


물론 실제로 Safe 하지 않은 상황을 고려해야할 경우는 많지 않지만, 각기 다른 Request 를 공통으로 분류해야한다거나, 내부에서 Internal Thread 를 구동하는 경우에는 반드시 신경써보자.





Spring 의 Application Context 는 Spring 에서 설정 정보의 제공을 위한 핵심 인터페이스이다.


런타임 이전에 로드가 되며, 런타임에서 일반적으로 Read-Only 로 구성되지만, 사용에 따라 런타임에 Re-load 하는 경우가 종종 있다.


프로젝트의 설정 작업을 하는 클래스 들의 경우 ApplicationContextAware 등의 인터페이스를 상속해서 ApplicationContext 에 접근하곤 한다.


ApplicationContext 를 이용하면 다음과 같은 작업이 가능하다.


 - Application 에서 Component 로 갖고 있는 Bean 에 대해 접근이 가능하다. (즉, Bean factory 를 제공한다)


 - Resource 를 직접 가져올 수 있다. 리플렉션과 유사하게 명시된 이름을 주로 사용한다.


 - 이벤트를 직접 발행할 수 있으며 이벤트는 context 에 등록된 Listener 에 작용한다.


 - 상속된 Context 컴포넌트들에 접근이 가능하다.


ApplicationContext 에 접근하는 클래스는 다음과 같이 만들 수 있다.




@Component
public class ContextAccessor implements ApplicationContextAware {
private ApplicationContext applicationContext;

    /*
     *
     * @see org.springframework.context.ApplicationContextAware#setApplicationContext
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.applicationContext = applicationContext;
    }
}



위와 같이 ApplicationContextAware 를 구현하면, ApplicationContext 에 접근이 가능하며, 클래스를 컴포넌트화 시켜서 프로젝트에 광범위하게 사용하곤 한다.



+ Recent posts