요즘 프로젝트들은 대부분 프레임워크 기반으로 개발을 진행한다. 프레임워크 사용법만 익히면 웬만한 기능들을 큰 오류없이, 성능의 큰 손실 없이, 빠르게 개발할 수 있기 때문이다.
그런데, 프레임워크란 무엇인가?
라이브러리가 개발에 필요한 도구들을 단순히 나열해 놓은 것이라면, 프레임워크는 동작에 필요한 구조를 어느정도 완성해 놓은 반제품 형태의 도구라고 할 수 있다.
(스프링 프레임워크는 단순히 빈 관리를 위한 IoC 컨테이너를 넘어서 웹 MVC 아키텍처와 보안, 모바일 등 다양한 분야의 기반 기술을 제공한다고 하는데, 지금은 일단 넘어가자)
따라서 프레임워크를 제대로 사용하기 위해서(그리고 프레임워크로 해결할 수 없는 요구사항들을 해결하기 위해서는) ‘동작’에 대해 알아야 하고, 이를 구현하는 ‘구조’ 들이 갖는 장/단점 들을 알아야 한다. ‘열혈강의 웹 개발 워크북’은 그런 취지에서 만들어진 듯 싶다.
우선, 프로그램을 만든다고 할 때, 데스크톱 애플리케이션 형태를 생각해볼 수 있다. 이 경우에 예상되는 문제점은 무엇인가?
제품 출시 후에 기능을 추가하거나 변경할때마다 다시 배포해야 한다. 기업간의 경쟁이 심화되어 제품의 생산주기가 짧아져 자주 애플리케이션을 배포해야할 경우 비효율적인 구조이다. (물론 이는 자동 갱신 등을 통해 해결할 수 있다.) 또한 DB에 접속해야 하는 경우 애플리케이션 소스코드 안에 접속정보가 들어가게 되므로 보안상 취약하다.
이러한 문제들은, 비즈니스/데이터 로직은 서버에, 프레젠테이션 로직은 클라이언트에 두어 해결할 수 있다. 이 구조에서는 서버에서 계산을 수행하고 그 결과를 클라이언트에 넘겨주므로 신규 기능이 추가되더라도 서버 쪽만 변경하면 된다. 그리고 클라이언트가 DB에 직접 접속하지 않기 때문에 DB 접속정보가 노출되는 사고를 막을 수 있다.
그러나 이 구조는 프로그래밍하기가 복잡하다. 데이터 통신을 위한 네트워크 프로그래밍도 해야하고 다중 클라이언트의 요청을 처리하기 위한 스레드 프로그래밍도 해야한다. 또한, DB 연결을 관리하거나 트랜잭션, 보안, 자바 빈 등 다양한 애플리케이션 자원을 관리하기 위한 프로그래밍도 필요하다.
웹 애플리케이션 구조에서는 클라이언트와의 통신을 웹 서버가 전담함으로써 네트워크 및 멀티 스레드 프로그래밍으로부터 탈출할 수 있다. 그리고 표준 웹 프로토콜인 HTTP를 이용하여 데이터를 송수신함으로써 이기종 플랫폼간에 매끈한 연결을 지원하여 스마트 폰, 스마트 패드, 스마트 TV 등 다양한 멀티 스크린 환경에 대해 일관되고 유연하게 대응할 수 있다. 즉, 개발자는 어떤 업무를 처리하고 무엇을 출력할 것인가에 대해서만 고민하면 되며, 작성한 웹 애플리케이션을 WAS에 배치하면 된다.
이 구조에서 예상되는 문제점은, 매번 출력화면을 서버에서 만들어 클라이언트(웹 브라우저)에서 이 화면을 내려받게 되어 오버헤드가 발생할 수 있는 부분인데, 같은 화면에서 데이터만 바뀔 때는 Ajax를 사용하여 데이터만 받아오도록 하여 해결할 수 있다.
이 구조는 결국 사용자가 웹 서버를 통해 간접적으로 웹 애플리케이션을 실행시키는 구조라고 볼 수 있다. 웹 서버는 클라이언트가 요청한 프로그램을 찾아서 실행하고, 해당 프로그램은 작업을 수행한 후 그 결과를 웹 서버에 돌려준다. 그러면 웹 서버는 그 결과를 HTTP 형식에 맞추어 웹 브라우저에게 보낸다.
이제 웹 애플리케이션 아키텍처를 좀 더 자세히 살펴보자.
처음 웹 서버를 구현할 때는 InputStream을 받아 필요한 작업을 처리하고 HTTP Header와 Body를 직접 작성하여 OutputStream을 작성하여 응답하는 형태로 직접 코딩했을 것이다. HTTP 웹 서버 구현하기
하지만 이는 몇가지 문제점이 있다.
1. 동시 접속자가 많을 경우 대응하는 Thread의 수도 비례해서 증가함으로써 메모리가 부족한 상황이 발생할 수 있다.
2. http 요청과 응답에 대해 http header와 body를 조작하는 것은 많은 작업량을 필요로 한다.
3. Controller가 추가될 때마다 RequestMapping에 url과 Controller 인스턴스를 추가해야 한다.
4. 정적인 html만 지원이 가능하다.
5. 웹 서버에 데이터를 저장할 경우 서버를 재시작하면 데이터가 초기화된다.
따라서 웹 애플리케이션을 개발할 떄마다 HTTP 웹 서버를 직접 구현하지 않고, 웹 애플리케이션 개발에 집중할 수 있도록 지원하는 것이 ServletContainer와 Servlet이다.
웹 서버와 웹 애플리케이션 사이에는 데이터를 주고받기 위한 규칙이 있는데 이것을 ‘CGI(Common Gateway Interface)’라고 한다. 그래서 보통 웹 애플리케이션을 ‘CGI 프로그램’이라고 한다. 특히 자바로 만든 웹 애플리케이션을 Servlet이라고 부르는다. 서블릿 명칭은 Server와 Applet의 합성어이다. 즉 ‘클라이언트에게 서비스를 제공하는 작은 단위의 서버 프로그램’이라는 뜻으로, 사용자의 요청에 대한 처리와 처리 결과에 따른 응답을 담당한다. 즉, 웹 개발자가 실질적인 구현을 담당할 부분이다. 자바 서블릿이 다른 CGI 프로그램과 다른 점은, 웹 서버와 직접 데이터를 주고받지 않으며, 전문 프로그램에 의해 관리된다는 점이다.
서블릿 컨테이너는 서블릿의 생성에서 실행, 소멸까지 서블릿의 생명주기를 관리하는 프로그램이다. (스프링의 빈 컨테이너는 빈의 라이프사이클을 관리한다.) 인스턴스를 생성하고 관리하는 것은 개발자가 하는 것이 아니라 컨테이너가 한다. (IoC. 제어의 역전)
그리고 서블릿 개발자는 더 이상 CGI 규칙에 대해 알 필요 없이 Servlet 규칙을 알아야 한다. 클라이언트로부터 요청이 들어오면, 서블릿 컨테이너는 호출 규칙에 따라 서블릿의 메서드를 호출한다. 서블릿 호출 규칙은 javax.servlet.Servlet 인터페이스에 정의되어 있다. 따라서 서블릿을 만들 때는 반드시 Servlet 인터페이스를 구현해야만 한다.
javax.servlet.Servlet 인터페이스
- 서블릿의 생명주기와 관련된 메서드 : init(), service(), destory()
-init(): 서블릿 컨테이너가 서블릿을 생성한 후 초기화 작업을 수행하기 위해 호출하는 메서드
(DB 연결, 외부 스토리지 서버와 연결, 프로퍼티 로딩 등 클라이언트 요청을 처리하는데 필요한 자원 준비)
-service(): 클라이언트가 요청할 때 마다 호출되는 메서드로, 실질적인 서비스 작업을 수행하는 메서드
-destory() : 서블릿 컨테이너가 종료되거나 웹 애플리케이션이 멈출 때, 또는 해당 서블릿을 비활성화 시킬 때 호출된다.
서비스 수행을 위해 확보했던 자원을 해제하거나 데이터를 저장하는 등의 마무리 작업을 수행한다.
- 그 외 : getServletConfig(), getServletInfo()
- getServletConfig() : 서블릿 설정 정보(서블릿 이름, 초기 매개변수 값, 환경정보 등)을 다루는 ServletConfig 객체를 반환한다.
(init 호출 시 매개변수로 받은 객체를 인스턴스 변수에 저장해두었다가 호출함)
- getServletInfo() : 서블릿을 작성한 사람에 대한 정보, 서블릿 버전, 권리 등을 담은 문자열을 반환한다.
서블릿 라이브러리는 서블릿을 좀 더 편리하게 개발할 수 있도록 javax.servlet.GenericServlet이라는 추상 클래스를 제공한다. GenericServlet 추상 클래스는 Servlet 인터페이스에 선언된 메서드 중에서 service()를 제외한 나머지 메서드를 모두 구현하였다. 따라서 서블릿을 만들 때 Servlet 인터페이스를 직접 구현하는 것보다 GenericServlet 클래스를 상속받으면 service() 구현에만 집중하면 된다.
service()의 매개변수 ServletRequest, ServletResponse
1. ServletRequest
- 클라이언트의 요청정보를 다룰 때 사용 (GET이나 POST 요청으로 들어온 매개변수 값을 꺼낼 때 사용)
- getRemoteAddr(), getScheme, getProtocol, getParameterNames(), getParameterValues(), getParameterMap(), setCharacterEncoding() 등
2. ServletResponse
- 클라이언트에게 출력하는 데이터의 인코딩 타입을 설정하고, 문자 집합을 지정하며, 출력 데이터를 임시 보관하는 버퍼의 크기를 조정하거나,
데이터를 출력하기 위해 출력 스트림을 준비할 때 이 객체를 사용함
- setContentType(), setCharacterEncoding(),getWriter() 등
서블릿 컨테이너는 서버가 생성할 수 있는 Thread 수를 제한한다. Thread Pool을 사용해 제한된 Thread를 재사용하는 방식으로 동작한다.
Tomcat에서 Thread 수 설정이 가능하다. (TOMCAT_HOME/conf/server.xml)
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="10000"
redirectPort="8443"
maxThreads="400" // max-Threads : Connector에 의해 처리될 수 있는 최대 요청 수. 기본 값은 200
acceptCount="150" /> // acceptCount : Maximum queue length. 기본값은 100
따라서 메소드 내의 지역 변수로만 사용해야 한다. 스프링 빈도 싱글 인스턴스이므로 주의해서 사용해야 한다.
(Heap 메모리에는 여러 쓰레드가 공유하는 인스턴스 변수 등이, Stack 메모리는 쓰레드별로 따로 사용되는 지역변수 등이 사용된다.)
Servlet이라는 규칙 외에 JSP를 만들고 실행하는 규칙, EJB(Enterprise JavaBeans)라는 분산 컴포넌트에 관한 규칙, 웹 서비스에 관한 규칙 등 기업용 애플리케이션 제작에 필요한 기술들의 사양을 정의한 것을 Java EE라고 한다. Java EE는 기능 확장이 쉽고, 이기종 간의 이식이 쉬우며, 신뢰성과 보안성이 높고, 트랜잭션 관리와 분산 기능을 쉽게 구현할 수 있는 기술을 제공한다.
Java EE 사양 중에서 Servlet과 JSP 기술만 구현한 서버를 서블릿 컨테이너라 하며, 자바로 웹 애플리케이션을 개발한다는 것은 이 두 기술을 이용하여 애플리케이션을 개발한다는 것을 의미하기도 한다. 그리고 서블릿과 서블릿 컨테이너와 같이 웹 기술을 기반으로 동작하는 애플리케이션 서버를 WAS(Web Application Server)라고 부른다. Tomcat, Jetty 등은 서블릿 컨테이너 기능을 가지고 있어 WAS라고 한다.
결국 서블릿이 하는 일이란, 클라이언트가 요청한 데이터를 다루는 일이다.
Servlet이 데이터를 가져오거나 입력, 변경, 삭제 등을 처리하기 위해서는 데이터베이스의 도움을 받아야 한다. 데이터베이스를 사용하려면, 데이터베이스에 요청을 전달하고 결과를 받을 때 사용할 도구(JDBC)와 데이터베이스에 명령을 내릴 때 사용할 언어(SQL)가 필요하다.
여기서 잠깐, 서블릿이 데이터베이스에 요청을 전달하여 결과를 받는 과정을 살펴보자.
1. DriverManager가 사용할 JDBC 드라이버 등록
-DriverManager.registerDriver(new com.mysql.jdbc.Driver());
2. 데이터베이스에 연결
-DriverManager.getConnection(JDBCURL, DBMS 사용자 아이디, DBMS 사용자 암호)
3. SQL 실행 객체 준비
-Connection 구현체를 이용하여 SQL문을 실행할 객체를 준비(createStatement(), prepareStatement(), prepareCall(), commit(), rollback())
- createStatement가 반환하는 것은 java.sql.Statement 인터페이스의 구현체(executeQuery(), executeUpdate(), excute(), executeBatch())이다.
4. 데이터베이스에 SQL문 보내기
- executeQuery()가 반환하는 객체는 java.sql.ResultSet 인터페이스의 구현체이다.
5. 모든 데이터를 처리한 이후에는 finally 구문을 사용하여 ResultSet, SQL 실행 객체, Connection 객체들을 닫는다.
데이터를 등록, 조회, 변경, 삭제하는 것을 보통 CRUD라고 한다.
데이터 조회나 삭제 요청처럼 간단한 데이터를 보내는 경우에는 GET 방식이 적합하고, 데이터 등록이나 변경, 로그인과 같은 대량의 데이터를 보내거나 브라우저의 주소창에 노출되지 말아야 할 데이터를 보내는 경우에는 POST 방식이 적합하다.
HttpServlet을 상속받아서 서블릿을 만들면 GET과 POST 요청을 쉽게 구분하여 처리할 수 있다. (HttpServlet은 GenericServlet클래스의 하위 클래스이다.) 이 클래스를 상속받아 서블릿을 만들 때는 service()를 오버라이딩 하는 대신에 doGet()과 doPost()를 재정의해야 한다. 그 이유는 HttpServlet의 service() 메서드는 클라이언트로부터 들어온 요청에 따라 GET이면 doGet()을 호출하고 POST면 doPost()를 호출하도록 프로그램되어 있기 때문이다.
그렇다면 데이터를 받아 처리한 후엔 어떻게 하는가?
데이터를 다룬 후 다른 페이지로 이동하는 방법으로, 리프래시하는 방법과 리다이렉트 하는 방법이 있다. 리프래시는 응답 헤더에 Refresh를 설정하거나, HTML의 meta 태그로 설정할 수 있다. 리다이렉트는 HttpServletResponse의 sendRedirect()를 호출하면 된다. 리프래시와 리다이렉트의 차이점은 리프래시는 클라이언트에 본문을 보낸다는 것이고, 리다이렉트는 본문을 보내지 않는다는 것이다. 사용자에게 요청 결과를 출력하고 다른 페이지로 이동한다면 리프래시 방법을 사용하면 된다.
1. 리프래시
- 응답 헤더를 이용한 리프레시 : reponse.addHeader("Refresh","1;url=list");
-HTML의 meta 태그를 이용한 리프래시 : <meta http-equiv='Refresh' content='1; url=list'>
2. 리다이렉트
- response.sendRedirect("list");
추가적으로 DB 연결과 관련해 알아두어야 할 것은,
DB 연결 정보와 같은 몇몇 설정 정보는 서버에 웹 애플리케이션을 배치한 후에도 언제든지 변경할 수 있어야 한다는 것이다. 그런데 이런 정보를 소스 코드에 넣는다면 설정 정보가 바뀔때마다 매번 소스코드를 변경해야 하므로 유지보수가 어려워진다. 이런 점을 해결하기 위해서는 서블릿 초기화 매개변수와 컨텍스트 초기화 매개변수를 사용하면 되며, 특히 여러 서블릿에서 공통으로 사용할 정보라면 컨텍스트초기화 매개변수를 사용하면 된다.
1. 서블릿 초기화 매개변수
서블릿을 생성하고 초기화할 때, 즉 init()를 호출할 때 서블릿 컨테이너가 전달하는 데이터이다.
서블릿 초기화 매개변수는 DD파일(web.xml)의 서블릿 배치 정보에 설정할 수 있고, 애노테이션을 사용하여 서블릿 소스코드에 설정할 수 있다.
(가능한 소스코드에서 분리하여 외부 파일에 두는 것을 추천된다.)
- DD 파일에 서블릿 초기화 매개변수 설정 : <init-param>2. 컨텍스트 초기화 매개변수
여러 서블릿이 사용하는 JDBC 드라이버와 DB 연결 정보가 같다면, 각각의 서블릿마다 초기화 매개변수를 선언하는 것은 비효율적이다.
컨텍스트 초기화 매개변수는 같은 웹 애플리케이션에 소속된 서블릿들이 공유하는 매개변수이다.
- DD 파일에 컨텍스트 초기화 매개변수 설정 : <context-param>
만약, 서블릿을 실행하기 전이나 후에 특별한 작업을 수행하고자 한다면 필터를 사용하면 된다. 주로 Servlet에서 발생하는 중복들을 제거하는 용도로 사용되며, 이는 transaction 처리나 캐싱 등에서도 사용된다.
일반적으로 doFilter() 사후 작업들(암호화, 압축 등)은 apache, nginx 등에서 작업한다.