집에 스프링으로 서버를 만들어 놓고 안드로이드로 접속하는 형태로 쓰고 있었는데 간만에 서버를 켜서 접속하니 타임 아웃을 발생시키며 접속이 되질 않았다. 서버를 킨 컴퓨터에서 크롬으로 접속하니 문제가 없는 걸로 보아 서버에 문제는 없어 보이고 외부에서 접속이 안되는 것 같았다. 혹시나 하여 방화벽을 일시적으로 해제하고 안드로이드에서 접속을 시도했더니 접속되었다. 하지만 개인 컴퓨터에 편의상 서버를 열어놓고 혼자 쓰는 터라 방화벽을 마냥 열어 놓을 수는 없었다.
이 문제를 해결하기 위해서 시도한 방법은 아래 두 가지다.
인바운드 규칙에서 서버로 사용 중인 포트 번호 허용시켜주기
Java(TM) Platform SE binary 연결 허용하기
포트 번호 허용하기
[제어판 → Window Defender→ 고급 설정] 으로 들어간다. 제어판은 시작을 누른 뒤 바로 "control"을 입력하거나 실행에서 "control"을 입력하면 들어갈 수 있다.
인바운드 규칙에서 우클릭하여 [새 규칙] 선택.
규칙 종류에서 포트 선택.
서버에 사용 중인(혹은 사용 예정인) 포트 번호 입력.
연결 허용(기본값)을 선택 후 다음.
네트워크 환경에 맞게 선택한다.
규칙을 식별하기 위해서 인바운드 규칙의 이름을 지정한다. 설명은 굳이 적지 않아도 되지만 코드의 주석처럼 나중에 이 규칙을 왜 생성했는지 혹은 규칙에 대한 정보를 기록하는 용도로 사용할 수 있다.
이름 좌측에 초록색 체크 아이콘이 있다면 규칙이 적용된 것이고 만약 체크 아이콘이 없다면 우클릭하여 규칙 사용을 눌러주면 된다.
결과는 실패다. 몇 년 전에 서버를 연습하면서 지금과 같은 문제를 해결한 적이 있어서 시도해보았는데 이번엔 이게 문제가 아니었나보다.
Java(TM) Platform SE binary 연결 허용하기
다시 인바운드 규칙으로 돌아온다. 이름 순으로 정렬하여 J로 시작하는 규칙 중에서 "Java(TM) Platform SE binary"를 찾는다. 설정을 따로 건드린 적이 없기 때문에 아마도 기본값이라고 생각되는데 TCP 프로토콜에 해당하는 두 규칙에 차단 아이콘이 표시되어 있는 것을 볼 수 있다. 두 규칙에서 차이는 적용할 프로필 정도로 보이는데 공용과 개인으로 나누어져 있다. 네트워크 환경에 따라 하나 혹은 두 규칙 모두 허용해 줄 것이다. 필자의 경우 소규모 개인 서버이기 때문에 4번 째 개인에 해당하는 규칙을 허용해 줄 것이다.
사진 상에 4번째에 해당하는 프로필이 개인, 프로토콜이 TCP인 값을 우클릭하여 속성에 들어간다.
아래 작업 란을 보면 연결 차단으로 지정되어 있는 것을 볼 수 있다. 연결 허용으로 바꾸어 준 뒤 확인을 누른다.
초록색 체크 아이콘이 즉, 규칙이 활성화된 것을 확인했다면 외부에서 접속을 시도해보자. 필자는 이걸로 해결되었다.
갑자기 외부 접속이 막힌 이유를 추측하건데 며칠 전에 게임 때문에 설치한 유료 VPN이 무슨 설정을 건드린게 아닌가 싶다. 서버를 공부하고 사용한 햇수만 5년 정도가 되었는데 포트 규칙 추가 말고는 단 한 번도 방화벽을 건드린 적이 없었기 때문에 이번 문제를 해결하는데 시간이 좀 걸린 것 같다.
컴포넌트 스캔은 특정 패키지를 기준으로 현재 패키지와 하위 모든 패키지를 탐색하여 특정 조건이 갖추어진 클래스들을 찾아 스프링 컨테이너에 bean으로 생성하는 것이다.
컴포넌트 스캔은 스프링의 설정 파일을 xml 방식으로 구성하느냐 java 방식으로 구성하느냐에 따라 선언 위치가 달라진다. xml으로 구성할 경우 servlet-context.xml과 root-context.xml에서, java로 구성할 경우 @Configuration이 적용된 클래스에서 선언한다.
Component Scan 등록 조건
컴포넌트 스캔 시 bean 등록 대상이 되려면 클래스 레벨에 @Component 어노테이션이 선언되어 있거나 @Component가 적용된 어노테이션이어야 한다.
MVC 구조로 스프링 프로젝트를 만들다 보면 Controller, Service, Repository 등의 어노테이션을 자주 보게 되는데 이 어노테이션들을 보면 @Component가 적용되어 있기 때문에 컴포넌트 스캔 시 bean 등록 대상이 된다.
또한 @Component가 적용된 @Controller 어노테이션을 적용한 경우에도 컴포넌트 스캔 대상이 된다. 결국 상위 어느 어노테이션이든 @Component가 적용되어 있기만 하면 된다는 것이다.
Component Scan - xml 방식
스프링 프로젝트를 생성하면 기본적으로 xml 방식으로 설정 파일이 구성된다. 컴포넌트 스캔이 수행되는 시점은 서버 구동 시 배포 설명자(web.xml)가 로드될 때다. 배포 설명자가 실행되면 파일에 정의된ContextLoaderListener, DispatcherServlet이 생성되는데 각각 root-context.xml, servlet-context.xml을 기반으로 실행되기 때문에 각 파일에 적절하게 컴포넌트 스캔을 명시해놓으면 된다.
일반적으로 view에 가까운 bean(controller 등)은 servlet-context.xml에, 비지니스 로직 처리에 관련된 bean(service, dao, repository)은 root-context.xml에서 수행한다.
servlet-context.xml 의 일부 코드
<context:component-scan> 태그를 사용하여 컴포넌트 스캔을 수행한다. base-package 값으로 패키지를 설정하면 해당 패키지를 포함하여 하위 모든 패키지에 있는 클래스를 탐색, @Component가 적용된 클래스를 bean 객체로 등록하게 된다. servlet-context.xml에는 초기에 HomeController를 bean으로 등록하기 위해서 기본적으로이 태그가 적용되어 있지만 root-context.xml에는 직접 추가해야 한다.
Component Scan - java 방식
xml에서 태그를 사용했다면 자바에서는 어노테이션을 사용한다. @Configuration을 선언한 클래스, 즉 스프링 설정관련 클래스에 @ComponentScan 어노테이션을 선언하고 basePackages 속성에 스캔할 패키지를 정의하면 된다.
만약 패키지 스캔이 아닌 클래스를 특정하여 지정하고 싶은 경우 basePackageClasses 속성을 사용하여 클래스를 직접 지정할 수도 있다.
스프링 프로젝트를 생성하면 기본적으로 생성되는 web.xml에 대해서 알아보겠습니다. 이 포스팅은 Spring Starter Project가 아닌 Spring Legacy Project 기준으로 작성되었습니다.
들어가기 전에
포스팅에서 web.xml은 "Servers/server_name-config/web.xml"이 아닌 "src/main/webapp/WEB-INF/web.xml"이다.
WAS를 Servlet Container로 부르기도 하지만 여기서는 서블릿을 관리하는 서버 사이드의 웹 어플리케이션으로 구분짓는다. 즉, WAS(혹은 톰캣)와 서블릿 컨테이너는 구분된다.
Servlet Container = (Web Container)
Spring Container = (Application Context, Application Container, IoC Container, Singleton Container)
web.xml
web.xml 파일은 배포 설명자(DD; Deployment Descriptor)라고 불린다. "src/main/webapp/WEB-INF/web.xml"에 위치한 이 파일은 웹 어플리케이션 실행 시 메모리에 로딩되는데 서버를 구동될 때 메모리에 로딩되어야 할 설정(객체) 등이 정의되어 있다. 스프링 프로젝트의 web.xml은 기본적으로 Context Loader Listener와 Dispatcher Servlet에 대한 설정이 정의되어 있다. 기본 설정 외에도 필요에 따라 서블릿, 필터를 정의할 수도 있다.
웹 어플리케이션은 반드시 단 하나의 web.xml을 가져야 하기 때문에 아파치 톰캣 + 스프링 프레임워크로 구성된 서버에서는 톰캣의 web.xml과 스프링 프레임워크의 web.xml 총 2개가 생성된다.
web.xml에 정의 해놓은 설정들은 서버가 구동될 때 메모리에 올라가는 것도 있고 사용자의 요청에 따라 호출되었을 때 비로서 메모리에 올라가는 것도 있다. 이 부분에 대해서는 아래에서 흐름에 맞게 설명할 것이다.
web.xml
[6~9] root-context.xml의 경로를 파라미터로 등록한다. 여기서 정의된 파라미터는 Servlet Context 객체에서 읽어올 수 있다.
<context-param> 태그를 사용하면 스프링 컨테이너에서 사용 할 수 있는 파라미터를 선언할 수 있다.
[15~23] <servlet> 태그 내에 생성된 "contextConfigLocation" 파라미터를 참조하여 Dispatcher Servlet에 관련된 컨텍스트를 생성한다.
<servlet> 태그는 Servlet을 상속받아 작성된 클래스를 서블릿으로 등록한다.
<servlet-name> 태그는 서블릿의 식별자를 지정한다.
<servlet-class> 태그는 서블릿으로 등록할 클래스를 지정한다. 반드시 패키지 이름까지 풀네임으로 적어줘야 한다.
<init-param> 태그는 서블릿 초기화에 사용될 파라미터를 설정한다. 이 경우 디스패처 서블릿이 객체화되고 초기화를 위해 init() 메서드가 호출될 때 참조될 설정 파일의 경로를 파라미터 값으로 설정하며 1개 이상의 파라미터를 설정할 수 있다. <context-param>과 다른 점은 <servlet>내에 선언된 만큼 참조 범위가 해당 서블릿 내로 제한된다.
<load-on-startup>태그는 주어진 값이 1 이상일 경우 서버 구동 시 서블릿이 바로 메모리에 올라갈 수 있도록 한다. 여기에서 값은 서블릿의 로드 순서(혹은 우선순위)를 나타내는데 숫자가 높을수록 우선순위가 낮다.
[25~28] : 앞서 [15~23]에서 등록한 Dispatcher Servlet에 대한 URL을 매핑한다. <servlet-mapping>은 web.xml에 등록한 서블릿 이름과 클라이언트로부터 받는 url을 매핑할 수 있다.
<servlet-mapping> 태그는 등록한 서블릿에 대한 URL을 매핑하기 위해 사용한다.
<servlet-name> 태그는 매핑할 서블릿 이름(위에서 설정한 식별자)을 지정한다.
<url-pattern> 태그는 지정한 서블릿이 어떤 URL에 매핑될 것인지를 지정한다. 기본적으로 루트("/")에 매핑되어 있기 때문에 모든 URL이 매핑 대상이 된다. 평소 와일드 카드 문자(*)에 익숙한 사람이라면 '/'가 아니라 '/*' 아닌가라는 생각을 할 수 있지만 '/'를 사용하는 것이 맞다.
Context Loader Listener
web.xml의 일부
<context-param>은 서블릿 컨텍스트에 파라미터를 등록할 수 있으며 필요에 따라 파라미터를 추가할 수 있다. 자바에서 변수를 선언하듯 param-name으로 이름을 부여하고 값으로 설정 파일(root-context.xml)의 경로를 부여하여 파라미터 이름을 통해 설정 파일에 접근할 수 있게 한다.
Context Loader Listener는 스프링 컨테이너를 생성하고 컨테이너에 루트 컨텍스트를 생성한다. <listener-class>값을 보면 컨텍스트 로더 리스너가 추상적인 개념이 아닌 실제 클래스임을 알 수 있고 패키지 명을 보면 스프링 프레임워크에서 제공하는 클래스임을 알 수 있다. 컨텍스트 로더 리스너는 org.springframework.context.ApplicationContext 인터페이스를 구현하는 것으로 스프링 컨테이너를 생성한다. 또한 이 때 위에서 생성한 파라미터 contextConfigLocation을 읽어서 설정 파일(root-context.xml)에 정의된 bean을 컨테이너에 생성하는 것이다.
web.xml에서 둘은 연관성을 찾아볼 수 없지만 ContextLoaderListener 클래스가 상속받고 있는 ContextLoader 클래스를 들여다보면 일부 메서드가 ServletContext를 통해 contextConfigLocation 파라미터에 접근하는 것을 볼 수 있다.
Dispatcher Servlet
web.xml에서 초기화, 실행되도록 정의되어 있다.
디스패처 서블릿 역시 Servlet이기 때문에 서블릿 컨테이너에 생성된다.
모든 요청은 컨트롤러로 가기 전에 디스패처 서블릿을 거쳐야 한다.
컨트롤러들이 공통적으로 수행해야 하는 작업을 처리한 뒤 적절한 컨트롤러로 요청을 넘긴다.
디스패처 서블릿이 공통된 작업을 처리해주기 때문에 각 컨트롤러는 비지니스 로직 처리에만 집중할 수 있다. 이렇게 하나의 컨트롤러를 놓고 모든 요청을 한 곳으로 집중, 일괄 처리하는 디자인 패턴을 Front Controller 패턴이라 한다.
요청에 해당하는 컨트롤러를 찾는 작업은 HandlerMapping에게 위임하고 결과를 반환받는다. 다시 HandlerMapping에게 받은 데이터를 HandlerAdapter에게 전달하여 컨트롤러 실행을 위임한다.
컨트롤러는 실행 결과로 JSON, XML, View 등을 반환하는데 View가 반환될 경우 사용자에게 표시될 View 파일(*.html, etc)의 이름을 전달받는다. 이 데이터를 ViewResolver 객체에게 전달하여 실제 View를 찾는다.
모든 서블릿을 파싱하여 Map 형태(Key-Value)로 관리한다.
1개 이상의 디스패처 서블릿이 존재할 수 있으며 디스패처 서블릿은 각자의 서블릿 컨텍스트를 갖는다.
web.xml의 일부
디스패처 서블릿은 "src/main/webapp/WEB-INF/web.xml"에 정의되어 있다. <servlet-class>의 값을 보면 디스패처 서블릿은 추상적인 개념이 아니라 클래스로 존재한다는 것을 알 수 있고 패키지 명을 보면 자바가 아닌 스프링 프레임워크에서 제공하는 클래스면서 디스패처 서블릿 역시 서블릿의 한 종류라는 것을 알 수 있다.
디스패처 서블릿도 컨텍스트 로더 리스너와 마찬가지로 설정 파일의 경로를 파라미터로 생성, 참조하여 스프링 컨테이너에 컨텍스트를 생성하게 된다. 스프링 컨테이너에 컨텍스트를 생성하는 것은 다름이 없지만 생성 과정에서 다소 차이가 있는데 디스패처 서블릿은 org.springframework.web.context.WebApplicationContext 인터페이스로 컨텍스트를 구현한다. 또한 컨텍스트 로더 리스너와 달리 파라미터가 서블릿 태그 내부에 선언되었기 때문에 디스패처 서블릿이 생성하는 컨텍스트의 bean들은 외부에서 참조가 불가능하다는 차이가 있다.
설정 파일을 파라미터로 선언함에 있어 차이를 보이는 것은 그냥 어쩌다보니 그렇게 된 것이 아니라 의도적으로 설계된 것이다. 계층 아키텍쳐에서 비지니스 로직을 처리하는 객체들이 View에 해당하는 객체들을 참조할 수 없도록하고 반대는 가능하도록 참조 관계를 나눈 것이다. 이는 부모 관계로도 표현할 수 있는데 root-context.xml에 정의된 bean들이 부모, servlet-context.xml에 정의된 bean들이 자식에 해당한다. 부모 클래스는 자식 클래스에 있는 변수, 메서드의 존재를 알 수 없기 때문에 접근이 불가능한 반면 자식 클래스는 부모 클래스를 물려받았기 때문에 부모 클래스에 있는 변수, 메서드에 접근할 수 있다. 왜 이렇게 나누어졌느냐고 묻는다면 이유는 간단하다. 컨트롤러는 비지니스 로직을 처리하기 위해서 서비스를 호출해야만 하는 반면 서비스나 DAO는 뷰를 직접 호출할 일이 없기 때문이다.
Dispatcher Servlet이 메모리에 올라가는 시점
일반적인 서블릿이 메모리에 올라가는 시점, 즉 객체화되는 시점은 클라이언트로부터 요청이 발생했을 때다. 어노테이션 혹은 web.xml에 등록되어 있다가 해당 서블릿에 대한 요청이 발생하면 객체화되고 int()을 호출하여 초기화된다. 디스패처 서블릿도 서블릿이지만 일반적인 서블릿과는 달리 웹 어플리케이션 구동 시 객체화되는데 일반적인 서블릿을 등록하는 방식과 다르지 않음에도 불구하고 서버 구동 시 바로 객체화되는 이유는 <load-on-startup> 태그 때문이다. 이 태그의 값이 0보다 클 경우 서블릿 컨테이너가 생성 및 초기화될 때 해당 서블릿이 같이 초기화된다.
디스패처 서블릿이 서블릿과 달리 서버 구동 시에 객체화되어야 하는 이유는 무엇일까? 서블릿은기본적으로해당 서블릿에 대한 요청이 발생해야만 객체화되는데, 이 말은 어떤 서블릿은 서버가 구동되고 나서 한 번도 호출되지 않을 수 있다고도 해석될 수 있다. 반면 디스패처 서블릿은 프론트 컨트롤러이기 때문에 어떠한 요청이라도 반드시 호출된다. 서블릿은 최초 요청이 발생했을 때 메모리에 올리는 시간이 소모되기 때문에 요청이 있는 한 반드시 호출되는 디스패처 서블릿을 사전에 등록하여 요청이 발생했을 때 메모리에 올리는 시간을 없애주고 즉시 사용을 보장해주는 것이다.
Dispatcher Servlet 동작 과정
디스패처 서블릿의 요청 처리과정을 이미지로 나타내었다. 기본적으로 존재하는 컴포넌트와 필수 요소만으로 최소 구성하였기 때문에 Filter, Interceptor 등은 없다.
클라이언트로부터 요청 발생, 웹 서버를 거쳐 서블릿 컨테이너로 요청 전달
HandlerMapping 객체에게 요청 URL에 대한 컨트롤러의 존재 여부 확인, 존재한다면 컨트롤러의 이름을 요청.
HandlerAdapter를 호출하여 컨트롤러 실행을 위임
HandlerAdapter는 요청받은 URL과 매핑되는 Controller 호출.
Controller는 비지니스 로직을 처리하기 위해 적절한 Service 호출.
Service는 비지니스 로직 처리에 필요한 데이터를 요청하기 위해 DAO/Repository 호출.
데이터베이스에 데이터 조회 및 수정.
비지니스 로직을 처리한 결과가 View(의 이름)일 경우 ViewResolver를 호출하여 View를 반환.
WAS(Web Application Server)는 한마디로 정의되는 용어지만 한마디로 이해할 수 있을 만큼 쉬운 개념은 아니다. 이 포스팅에서는 WAS가 무엇인지, 왜 필요한지, 내부적으로 어떻게 요청을 처리하는지 알아볼 것이다. WAS를 이야기 하려면 Web Server를 빼놓을 수 없기에 Web Server와 함께 WAS를 풀어가도록 하겠다.
Web Application : Web Application이란 말 그대로 웹 상에서 실행되는 어플리케이션이다. 어플리케이션이 실행되는 위치에 따라 Client-side, Server-side로 나뉜다.
Web Server: Web Server는 클라이언트로부터 HTTP 요청을 받았을 때 정적인 컨텐츠를 제공하는 서버로 C언어로 작성되어 있다. 여기서 정적인 컨텐츠란 html, css, js, jpg, gif 등과 같이 항상 일정한 UI를 제공하는 파일을 말한다.
WAS : WAS는 클라이언트로부터 HTTP 요청을 받았을 때 동적인 컨텐츠를 처리, 제공하기 위한 미들웨어로 Java로 작성되어 있다. 여기서 동적인 컨텐츠란 사용자의 입력에 따라 결과가 달라지는 컨텐츠를 말한다. WAS는 Web Server의 기능도 겸하고 있으며 내부적으로 Servlet Container를 가지고 있는 것이 가장 큰 특징이다.
Middleware : 미들웨어란 2개의 포인트 사이에서 데이터를 주고 받을 수 있도록 매개체 역할을 하는 소프트웨어다.
3-tier Architecture: 3계층 구조란 특정 플랫폼을 3분할하여 물리적으로 구현한 것을 말한다. 여기서 특정 플랫폼이란 일반적으로 서버를 말한다. (layer = 논리적인 계층 구조, tier = 물리적 구조)
3-tier Architecture
Web Server - WAS - DB Server
[Web Server ↔ WAS ↔ DB Server]로 나누어 3계층으로 보는 구조다. 여기서 클라이언트는 계층에 포함시키지 않고 3 계층으로 보는데 클라이언트를 포함시켜 4-tier라고 부르는 경우도 있다. 일반적으로 웹 페이지가 위와 같이 구성된다.
Client - WAS - DB Server
[Client ↔ WAS↔ DB Server]로 나누어 3계층으로 보는 구조로, Web Server가 별도로 존재하지 않고 WAS 가 Web Server의 역할을 같이 한다는 것이 특징이다. 웹 사이트에 접속할 때에는 URL을 통해 해당 사이트에 대한 html을 요청하고 다운받은 html을 브라우저에 표시하는 것인데 일반적인 모바일 어플리케이션의 경우 GUI가 이미 구성되어 있고 UI에 표시할 데이터만 요청한다. 이러한 경우 Web Server가 생략될 수 있고 3-tier에 클라이언트를 포함시킨 위와 같은 구성이 쓰인다고 볼 수 있다.
3-tier Architecture라고 해서 반드시 위와 같이 구성되는 것은 아니며 클라이언트 아래에 적어 놓은 웹이나 앱도 하나의 예시일 뿐이다. 제시한 구조 외에도 WAS가 Web Server, DB Server가 하는 일을 모두 할 수 있기 때문에 WAS만 사용하는 1-tier로 구성되기도 한다. 위에 나온 방식들은 서버를 구성하는 하나의 구조일 뿐이며 서버나 프로젝트 규모, 환경에 따라 얼마든지 구조가 달라질 수 있음을 인지하도록 하자.
위에서 알아본 내용을 정리하면 Web Server는 정적인 컨텐츠를 처리하는 서버, WAS는 동적인 컨텐츠를 처리하기 위한 미들웨어다. 또한 서버를 환경에 따라 분할하는 것을 n-tier Architecture 라고 하며 분할의 기준은 프로젝트 규모, 환경, 제공하는 서비스 등에 따라 얼마든지 달라질 수 있다.
서버를 분할하는 이유
앞서 언급했듯 WAS는 크게 Web Server, Container 기능을 수행한다. 이 외에도 트랜잭션, 보안, 트래픽 관리, DBCP, 사용자 관리 등의 기능을 제공한다. 나열한 기능들을 살펴 보면 WAS가 Web Server, DB Server의 기능을 모두 가지고 있는 것을 알 수 있는데 WAS만으로 서버를 구성할 수 있음에도 굳이 서버를 나누는 이유가 무엇일까?
서버를 분할했을 때 성능 측면에서 보면 처리해야 할 작업을 적절하게 나누어 처리량을 분산시키는 효과가 있다. 사용자에게 보여 줄 인터페이스에 대한 요청은 Web Server가, 비지니스 로직을 처리해야 하는 작업은 WAS가, 비지니스 로직 처리 중 DB 조작이 필요하면 DB Server에서 데이터를 읽고 쓰는 것으로 역할을 분산시킨 것이다. 이렇게 Web Server의 작업을 세분화하면 하나의 물리적인 서버에 트래픽이 집중되어 과부화가 걸리는 것을 방지할 수 있다. 그 외에도 속도, 보안, 성능 개선, 서버 장애 대처 등의 이유가 있다.
Container
WAS에 대해 검색하면 "WAS는 Web Container 혹은 Servlet Container 라고도 부른다." 라는 문장을 단골 멘트처럼 볼 수 있다. 두 개의 이명에 공통적으로 들어가 있는 Container는 위에서 WAS의 기능을 소개할 때 Web Server와 함께 등장했었다. Servlet Engines라고도 불리는 Servlet Container에 대해 알아보자.
Container : 컨테이너란 JSP, Servlet과 같은 동적인 컨텐츠를 생성하고 관리하는 서버 사이드의 소프트웨어다. 클라이언트의 요청을 받으면 Thread Pool에서 wait 상태인 스레드를 깨우고 요청에 맞는Servlet을 찾는다. 스레드가 서블릿을 통해 요청을 처리한 결과를 컨테이너에게 반환하면 그 결과를 클라이언트에게 반환한다. 이 과정에서 Servlet의 생성, 동작, 소멸 등 Servlet에 대한 전반적인 생명 주기를 관리한다.
Servlet : WAS에서 동적인 컨텐츠를 처리하는 소프트웨어로 Java 언어로 작성된다. 서버가 구동된 뒤 특정 서블릿에 대한 요청이 발생할 때 비로서 메모리에 올라가며 스레드에 의해 동작한다.
Servlet Life cycle : 서블릿은 [int → service → destroy] 의 생명주기를 갖는다. 서블릿의 생명주기는 생성부터 소멸까지 모두 컨테이너에 의해 관리된다.
JSP(Java Server Page) : Servlet과 유사한 기술인 JSP는 사용자에게 보여질 View(페이지)를 작성할 때 사용되며 html 기반에서 스크립트 형태로 작성된다.
JSP Container : JSP를 서블릿으로 변경할 때 사용되는 소프트웨어로, 서블릿으로 구현되어 있다.
Thread Pool : 작업을 처리하기 위해서 특정 개수의 스레드를 생성해놓고 작업 큐에 들어오는 작업을 하나씩 할당받아 처리하는 것을 말한다. 스레드는 요청이 발생했을 때 컨테이너에 의해 실행되고 서블릿을 실행한다. Tomcat의 옛 버전(3.2 이전)에서는 작업이 발생할 때마다 스레드를 생성하고 서블릿 실행을 맡긴 뒤 작업이 끝나면 스레드를 소멸시켜주는 등 Thread에 대한 전반적인 관리가 필요했고 이는 많은 문제를 야기하였다. Thread Pool의 등장으로 스레드 관리가 훨씬 간단해졌다.
Filter : 서블릿 컨테이너의 서블릿은 서버의 최종 자원이라고 할 수 있다. 여러 과정을 거쳐 마지막에 요청을 처리한 뒤 클라이언트에 결과를 반환하는 요청 처리 과정의 종점인 것이다. 필터는 순서상 서블릿 앞에 존재하며 데이터를 처리하기 전, 데이터를 처리한 후에 반드시 거치게 영역이다. 서블릿을 처리하기 전에 반드시 수행해야 하는 작업(사용자 인증, 인코딩 등)을 수행하게 된다.
알아본 내용을 간단하게 정리해보자. Container란 JSP, Servlet과 같이 동적인 컨텐츠를 생성 및 관리한다. 클라이언트로부터 요청이 발생하면 Thread Pool에서 대기 중인 스레드를 할당받는다. 요청을 처리하기 앞서 필터링을 수행하고 관련된 서블릿을 찾아 비지니스 로직을 처리, 다시 필터링을 거친 뒤 결과를 반환한다.
서블릿은 최종적으로 javax.servlet.Servlet를 구현한 HttpServlet을 상속한 클래스로 구현된다. (HttpServlet 클래스는 Servlet 인터페이스를 상속받고 있다.)
HttpServlet를 상속받은 클래스는 일반적으로 doGet, doPost 라는 메서드를 오버라이드하게 되는데 HTTP 프로토콜의 메서드 타입에 따라 달리 호출된다.
doGet, doPost 메서드는 파라미터로 HttpServletRequest, HttpServletResponse 객체를 가지는데 각각 요청 정보, 응답 정보를 가지고 있다.
WebSocket 어노테이션, 설정 파일(web.xml)에서 url을 매핑해줄 수 있다. 이 값은 서블릿을 식별하는 식별자가 될 것이다.
WAS 동작 흐름
WAS에서 동작을 그림으로 표현하였다. Web Server가 따로 존재하지 않고 WAS에서 Web Server 역할을 같이 한다고 가정하자. 클라이언트로부터 "http://localhost:8080/app/search?keyword=aws" 라는 요청을 받았고 "/search"는 keyword 값인 "aws" 해당하는 검색 결과를 반환해주는 서블릿이라고 가정한다.
먼저 요청받은 URL을 파싱해보자.
localhost(IP or Domain) : URL에서 ip 혹은 도메인에 해당하는 영역이다. 클라이언트와 서버가 같은 네트워크 상에 있다면 localhost를 사용한다.
8080 : 서버가 사용하는 포트에 해당하는 영역이다. ip에 해당하는 컴퓨터를 찾아 간 후 8080 포트를 사용 중인 서버를 찾아간다.
app : 웹 어플리케이션 이름에 해당하는 영역이다. app이라는 이름을 사용 중인 웹 어플리케이션을 찾아간다.
/search : "/search"에 해당하는 서블릿을 찾아간다.
동작 과정
(빨간 화살표가 내부 흐름이며 그림의 번호는 화살표의 순서를 의미할 뿐 설명 순서와 상관없음.)
사용자가 웹 주소창에 "http://localhost:8080/app/search?keyword=aws" 요청.
클라이언트로부터 Request 발생.
Web Server에 요청이 전달되었지만 동적 콘텐츠 요청이므로 요청을 패스.
작업 큐에 작업 적재.
스레드 풀에 대기 중인 스레드가 작업을 할당.
요청하는 서블릿 컨테이너에 접근, 필터링을 수행.
필터링 수행 후 "/search"와 매핑된 서블릿이 메모리에 있는지 확인.
서블릿이 메모리에 없기 때문에 Class Loader가 서블릿 클래스를 로딩.
서블릿 객체 인스턴스화. (서블릿 생명주기 - init)
비지니스 로직 처리 (서블릿 생명주기 - service)
비지니스 로직 처리 후 결과값을 반환하기 위해 필터링 수행 후 결괏값 반환.
작업이 끝난 스레드는 자원 반납.
포스팅에 관련된 질문 혹은 잘못된 내용, 오타 등이 있다면 댓글로 말씀해주시면 감사하겠습니다.
포스팅 1, 2에서 스프링 기초 환경과 mybatis 설정을 마쳤다. 테스트를 수행하기에 앞서 서버 생성과 관련하여 주의해야 할 부분을 몇 개 짚어보고 가자.
서버 추가하기
이클립스에 서버를 추가해보자. 콘솔이 있는 하단부에서 Servers 탭을 누르고 빈 공간에 우클릭, [New → Server]를 클릭한다.
도입부에 명시해놓았듯 Apache Tomcat 9.0을 사용하기 때문에 목록에서 톰캣 9.0을 선택했다. 그리고 서버 목록에서 식별자로 쓰일 Server name만 지정해주고 다음으로 넘어간다.
서버에서 운용할 프로젝트를 선택하고 Add를 클릭한다. 프로젝트가 오른쪽 목록으로 넘어간 것이 확인되었으면 Finsh로 창을 종료한다.
정상적으로 서버가 생성되었다면 프로젝트 목록에 Servers 폴더가 생성된다. 그리고 위와 같이 방금 생성한 서버에 대한 설정 파일이 담긴 폴더가 생성되어 있는 것을 볼 수 있다. 생성한 Server name에 맞는 폴더를 열어 server.xml에 들어간다.
server.xml 수정 전server.xml 수정 후
하단 끝으로 내리면 위와 같은 설정을 볼 수 있는데 여기서는 Context Path를 수정할 수 있다. 컨텍스트 패스란
웹 어플리케이션 이름으로서 클라이언트가 서버에 접속할 때 IP, Port 다음으로 식별 대상이 되는 웹 어플리케이션의 이름을 말한다. 서버에 URL을 요청했을 때 네트워크에서ip로 물리적인 컴퓨터를 찾고 컴퓨터에서 Port 번호로 서버를 식별, 컨텍스트 패스로 웹 어플리케이션을 식별하여 최종적으로 도달하게 되는 것이다.
context path의 기본 값은 프로젝트를 생성할 때 입력했던 패키지 명 끝 파트로 결정된다. 이 값을 그대로 두면 접속할 때마다 포트번호 뒤에 "/ku_develog"(본 프로젝트 패키지명 기준)를 붙여줘야 하는데 테스트에 걸리적 거리기 때문에 루트 패스("/")만 남기고 지워주겠다. 사용하고 싶다면 그대로 둬도 되고 "path" 값을 수정하여 사용해도 된다.
이것으로 데이터를 요청할 때 "http://localhost:8080/ku_develog/getAll"에서 "http://localhost:8080/getAll"로 간결하게 호출할 수 있게 되었다.
서버를 생성했다면 포트를 변경해 줄 필요가 있다. 오라클과 톰캣은 기본적으로 같은 8080 포트를 쓰고 있기 때문에 초기 포트가 8080이라면 충돌을 일으키게 된다. Servers 탭에서 방금 생성한 서버를 더블 클릭하고 창 하단에서 Modules를 클릭하면 위와 같은 화면을 볼 수 있다. 여기서 우측의 HTTP/1.1의 포트 번호를 현재 컴퓨터 내에서 사용하지 않는 포트 번호로 변경한 후 저장한다.
앞서 스프링과 오라클이 연동된 것을 확인했다. 이 상태에서 바로 JDBC를 사용할 수도 있지만 JDBC를 보다 편하게 사용할 수 있도록 MyBatis를 연동해보자. 해야 할 작업을 요약하면 다음과 같다.
DTO, DAO, Service, ServiceImpl 패키지 생성
MemberDTO.java 작성
MemberDAO.java 작성
MemberService.java 작성
MemberServiceImpl.java 작성
mybatis-config.xml 작성
memberMapper.xml 작성
MemberController.java 작성
root-context.xml에서 mybatis 설정 파일 참조, mapper, DAO Scan 등 지정
패키지 구성
연동에 필요한 클래스들을 작성하기에 앞서 패키지를 위와 같이 미리 구성해놓도록 하자.
DTO class
package com.tistory.ku_develog.dto;
public class MemberDTO {
private String id, name;
private int age;
public String getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return new StringBuilder().append("{id=").append(id).append(", name=")
.append(name).append(", age=").append(age).append("}").toString();
}
}
Member 테이블과 대응될 데이터 클래스를 작성한다. 테이블과 대응되지만 동시에 클라이언트 간의 데이터 전송에도 사용되기 때문에 둘을 따로 구분하지 않고 DTO 이름으로 생성했다. 이 데이터 클래스는 테이블과 매칭될 것이기 때문에 테이블이 가진 모든 속성을 멤버 변수로 가져야 한다.
DAO Interface
package com.tistory.ku_develog.dao;
import java.util.List;
import com.tistory.ku_develog.dto.MemberDTO;
public interface MemberDAO {
public List<MemberDTO> getAll();
public MemberDTO getMember(String id);
public boolean insertMember(MemberDTO memberDto);
public boolean deleteMember(String id);
public MemberDTO updateMember(MemberDTO memeberDto);
}
DAO 인터페이스에는 DB에 접근할 때 사용할 메서드를 작성한다. 인터페이스지만 이 인터페이스는 구현(implements)되지 않고 뒤에서 생성하는 mapper.xml에서 작성하는 쿼리와 1:1로 매핑된다. 인터페이스의 구성을 보면 테이블의 모든 데이터를 읽어오는 getAll, 특정 id와 일치하는 데이터를 읽어오는 getMember, 그리고 데이터 삽입, 삭제, 수정 등을 수행하는 메서드가 존재한다.
DAO를 작성할 때 주의해야 할 것이 있는데 하나의 DAO Interface는 반드시 하나의 테이블에만 매핑되어야 한다는 것이다. 프로젝트에 다른 테이블을 추가로 사용해야 한다면 그 테이블에 대응하는 DAO를 추가로 생성해야 한다. 데이터베이스에 접근하는 객체라는 의미의 DAO(Data Access Object)가 접미사로 붙지만 마이바티스가 적용된 프로젝트에는 Mapper를 붙이기도 한다.
Service interface
package com.tistory.ku_develog.service;
import java.util.List;
import com.tistory.ku_develog.dto.MemberDTO;
public interface MemberService {
public List<MemberDTO> getAll();
public MemberDTO getMember(String id);
public boolean insertMember(MemberDTO memberDto);
public boolean deleteMember(String id);
public MemberDTO updateMember(MemberDTO memeberDto);
}
서비스는 비지니스 로직을 처리하기 위한 클래스로서 이 인터페이스는 서비스 클래스가 구현해야 할 메서드를 정의한다.
혹시 서비스 인터페이스와 DAO 인터페이스가 똑같음을 눈치챘는가? 여담으로 나는 처음 마이바티스가 적용된 예제를 따라해봤을 때 서비스 인터페이스와 DAO 인터페이스의 구조, 선언된 메서드가 완전히 같은 것을 보고 큰 혼란에 빠졌었다. 아예 같은 구조에 이름만 다른데 굳이 두 개의 인터페이스가 존재할 필요가 있는 건가 하는 생각이 머리를 지배했다. 한 두 명이 아닌 많은 블로그에서 비슷한 구조를 띄고 있었기 때문에 분명 의미가 있을거라 생각하고 스프링의 구조, MyBatis의 구조, MVC로 나누어진 구조를 제대로 공부하고 나서 보니 그제서야 왜 두 인터페이스가 같은지 깨달았다.
이유는 프로젝트의 규모가 매우 작기 때문이었다. 현재 이 프로젝트는 예제이기 때문에 Member에 대한 DAO, 서비스, 컨트롤러만 작성되어 있다. DAO는 테이블 하나에서 데이터를 읽어 오는 메서드만을 정의하기 때문에 한정적이지만 서비스는 테이블 하나에 얽매이지 않고 비지니스 로직을 처리하기 위해 지금 예제보다 더 많은 메서드를 정의할 수 있다. 하지만 이 프로젝트는 마이바티스를 어떻게 사용하는지 보여주기 위한 소규모 예제이기 때문에 별도의 메서드를 가지지 않고 둘 다 같은 구조를 띄게 된 것이다.
앞서 생성한 Service 인터페이스를 구현하는 ServiceImpl 클래스다. 이 클래스에서는 비지니스 로직을 처리하기 위한 코드를 작성하게 되며 비지니스 로직을 처리하기 위해서 DAO를 사용하여 데이터베이스에 접근, 데이터를 읽어올 수 있다. 서비스에 작성한 메서드들은 컨트롤러에서 호출된다.
사용된 어노테이션
Service : 클래스 레벨에서 이 어노테이션을 선언하면 스프링 컨테이너에 bean으로 등록된다. 파라미터로 bean 이름을 별도로 지정할 수 있는데 지정하지 않을 경우 클래스 이름을 카멜 표기법으로 변환하여 사용한다.
Autowired : 스프링에서 지원하는 DI 어노테이션으로, 멤버 변수 레벨에서 사용할 수 있다. 선언한 멤버 변수의 클래스 타입과 일치하는 bean이 스프링 컨테이너에 있다면 멤버 변수에 의존성을 주입해주는 역할을 한다. 코드를 보면 MemberDAO 객체에 대해 초기화 작업을 수행하지 않은 채로 객체를 사용하고 있는데 일반적인 자바 코드라면 Null Point Exception을 일으켜야 정상이지만 Autowired를 통해 정상적으로 의존성이 주입되었다면 에러를 일으키지 않는다.
Transactional : 이름에서 유추할 수 있듯이 트랜잭션에 관한 어노테이션으로 이 어노테이션이 적용된 메서드는 트랜잭션 처리된다. 앞서 pom.xml에서 트랜잭션에 관한 의존성을 주입(spring-tx)하였고 어노테이션으로 사용하기 위해서 root-context.xml에서 tx:annotation-driven을 사용해 선언적 트랜잭션 제어를 활성화하였다.(어노테이션 기반으로 트랜잭션 설정하는 것을 선언적 트랜잭션 이라고 봐도 된다.)
mybatis-config.xml 생성하기
mybatis에 대한 설정 파일을 작성하기 위해 "src/main/resources" 폴더에 우클릭 후 위와 같이 "mybatis-config.xml" 파일을 추가한다. 이름을 바꿔도 되지만 이름을 바꿀 경우 root-context.xml에서 이 파일을 환경설정 파일로 지정할 때 이름도 맞춰서 적어줘야 한다.
파일을 생성하면 XML 선언부만 존재하는데 바로 아래 줄에 위와 같이 DTD 설정을 추가한다.
루트 앨리먼트인 configuration 태그에서는 mybatis에 관한 설정을 추가할 수 있다. 예제에 사용된 typeAliase는 클래스의 별명을 지정해줄 수 있는데 이 별명은 바로 다음 파트에서 작성할 "memberMapper.xml"에서 유용하게 사용될 것이다. xml은 java와 다르게 다른 클래스를 import 할 수 없기 때문에 앞서 만든 MemberDTO를 xml에서 사용하려면 "com.tistory.ku_develog.dto.MemberDTO"와 같이 풀 패키지명을 적어주어야 한다. 하지만 빈번하게 사용하는 클래스의 경우 코드가 길어짐은 물론 오타가 발생할 가능성도 있다. 하지만 위와 같이 별명을 부여하면 "memberDto"로 간편하게 사용할 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tistory.ku_develog.dao.MemberDAO">
<select id="getAll" resultType="memberDto">
SELECT *
FROM member_t
ORDER BY id
</select>
<select id="getMember" parameterType="String" resultType="memberDto">
SELECT * FROM member_t
WHERE id = #{id}
ORDER BY id
</select>
<insert id="insertMember" parameterType="memberDto">
INSERT INTO member_t
VALUES(#{id}, #{name}, #{age})
</insert>
<delete id="deleteMember" parameterType="String">
DELETE FROM member_t
WHERE id = #{id}
</delete>
<update id="updateMember" parameterType="memberDto">
UPDATE member_t SET
name = #{name}, age = #{age}
WHERE id = #{id}
</update>
</mapper>
Mapper.xml은 테이블에 사용할 쿼리를 작성하는 곳이다. 루트 앨리먼트인 <mapper>를 보면 namespace 속성에 앞서 작성한 DAO 인터페이스가 지정되어 있는 것을 볼 수 있는데 이는 mapper.xml 파일 하나와 DAO 인터페이스 하나가 1:1로 매핑되어 사용된다는 것을 의미한다.
쿼리를 작성할 때 예제처럼 <select>, <insert>, <update>, <delete> 등을 사용한다. 사용된 태그들의 속성을 자세히 살펴보면 DAO 인터페이스에서 작성했던 메서드와 비슷한 꼴임을 알 수 있는데 id는 메서드의 이름, parameterType은 메서드가 받을 파라미터 타입, resultType은 메서드의 반환형이다. SQL의 결과에 따라 대충 끼워 맞춘 것이 아닌 DAO에 작성했던 메서드 하나하나의 구조와 정확히 매칭되고 있음을 알 수 있다.
id가 DAO에 작성한 메서드의 이름과 1:1로 매칭된다고 했으니 중복이 있어서는 안되며 DAO에는 없는 메서드 이름을 id로 사용하면 안된다. 네임 스페이스에 사용한 패키지 경로와 메서드 이름을 결합하면 "com.tistory.ku_develog.dao.MemberDAO.getMember()"와 같이 되기 때문이다.
메서드의 매핑에 관해 설명할 때 잠깐 나왔지만 resultType 속성은 쿼리 결과를 어떤 타입으로 매핑할 것인가를 결정하고 parameterType은 쿼리에 사용할 파라미터 타입을 결정한다. 여기서 마이바티스의 장점을 볼 수 있는데 기존 JDBC로 데이터를 조회하면 ResultSet 형태로 조회 결과를 받고 레코드를 하나씩 꺼내 결과를 파싱하여 데이터 객체에 세팅하는 과정을 수행해야 한다. 하지만 마이바티스는 모든 과정을 생략하고 파라미터 타입, 리턴 타입만을 지정하면 자동으로 결과를 가공해준다.
resultType의 데이터 타입은 기본 자료형, 커맨드 객체, 컬렉션 등을 지정할 수 있다. 위 예제에서 resultType에 대해 다소 특이한 부분이 있는데 쿼리 결과로 리스트를 반환하는 getAll과 객체 하나를 반환하는 getMember의 반환 타입이 같다는 것이다. getAll은 결과가 없거나 1개 이상의 결과를, getMember는 결과가 없거나 1개의 결과를 반환함에도 불구하고 왜 getAll에는 리스트를 사용하지 않았을까? resultType에 지정하는 타입은 레코드 하나를 저장할 타입을 지정하는 것이다. 마이바티스는 결과가 2개 이상일 경우 자동으로 결과를 리스트화해서 반환해주기 때문에 리스트의 타입만 제대로 설정해주면 된다.
속성 "parameterType"은 쿼리에 필요한 데이터 타입 혹은 객체를 지정한다. 메서드에서 작업을 처리하기 위해서 파라미터를 받는 것과 같다고 보면 되는데 실제로 MemberDAO에서 작성한 메서드의 파라미터 타입과 매핑되고 있음을 볼 수 있다. 파라미터를 SQL 문에서 사용하려면 #{parameter}와 같이 사용할 수 있으며 DAO에서 작성한 파라미의 이름을 사용할 수 있다. 또한 타입이 데이터 클래스인 경우 멤버 변수 이름을 쿼리에 그대로 사용하는 것도 가능하다.
컨트롤러는 클라이언트의 요청을 받는 역할을 한다. 클라이언트로부터 요청이 오면 디스패처 서블릿에 의해서 URL에 맞는 메서드가 호출될 것이다. 여기서는 오직 요청의 분리, 적절한 서비스 호출만을 생각하면 되기 때문에 앞서 작성한 서비스를 호출하여 비지니스 로직을 처리하고 결과를 클라이언트로 넘긴다.
사용된 어노테이션
RestController : 클래스 레벨에서 사용하는 어노테이션으로 @Controller와 @ResponseBody 어노테이션을 같이 사용한 것과 같다. 이 어노테이션이 사용되면 클래스 내 모든 메서드에 @ResponseBody 어노테이션이 적용된다.
ResponseBody : 여기서 사용되지는 않았지만 RestController로 인해 메서드 레벨에 선언되어 있는 것이나 마찬가지이기 때문에 짚고 넘어가자. 이 어노테이션이 적용되면 클라이언트와의 통신에서 XML, JSON 형태로 데이터를 주고 받는 것이 가능해진다.
Resource : 자바에서 지원하는 DI 어노테이션으로 bean의 이름을 특정해서 의존성을 주입받을 수 있다. 파라미터로 전달한 "memberService"는 앞서 MemberServiceImpl 클래스에 선언한 @Service 어노테이션으로 지정한 값으로, 컨테이너에서 "memberService"라는 key 값을 가진 bean의 의존성을 주입받겠다는 의미를 가진다. 이는 스프링 컨테이너가 key-value 형태로 bean을 관리하기 때문에 가능한 것이다.
RequestMapping : API를 호출할 때 사용되는 URL을 지정하는 역할을 하며 value는 호출할 url, method는 HTTP 통신에서 사용할 메서드 타입을 지정한다.
"src/main/resources"에 생성한 마이바티스 설정 파일, 매퍼 파일들을 sqlSessionFactory에 세팅한다. root-context.xml로 돌아와서 sqlSessionFactory에 "configLocation", "mapperLocations" 프로퍼티를 추가하고 파일의 경로를 지정한다.
configLocation은 mybatis의 설정 파일을 지정해줄 수 있지만 필수는 아니다. 지정해주지 않을 경우 디폴트 설정을 가진 마이바티스가 빌드된다. mapperLocations는 xml 파일 기반의 매퍼를 탐색한다. 프로퍼티의 값인 "/mapper/*.xml"은 아까 "src/main/resources"에 생성한 mapper 폴더에서 xml 확장자를 가진 모든 파일(* = 와일드 카드)을 매퍼 파일로 지정한다는 의미를 갖는다.
스프링 컨테이너에 bean 등록하기
마지막으로 앞서 생성한 controller, service, dao 클래스들을 스프링 컨테이너에 bean으로 등록해야 한다. 클래스 하나하나 bean으로 등록하는 방법도 있지만 Component Scan을 사용하는 것이 일반적이다. 컴포넌트 스캔은 특정 패키지 경로를 지정하여 지정된 패키지를 포함 하위 모든 패키지를 탐색하여 @Component가 적용된 클래스를 bean으로 등록시킨다. 앞서 사용했던 @Controller, @Service도 내부를 들여다 보면 @Component가 선언되어 있기 때문에 bean 등록 대상이 된다.
servlet-context.xmlroot-context.xml
servlet-context.xml에는 컨트롤러와 관련된 패키지를, root-context.xml에는 비지니스 로직과 관련된 패키지를 스캔한다.
servlet-context.xml에는 기본적으로 컴포넌트 스캔 태그가 존재하는데 스캔 경로(base-package)의 기본값으로 프로젝트를 생성할 때 입력한 패키지가 지정된다. 그리고 프로젝트를 생성할 때 입력한 패키지에는 HomeController가 자동 생성되기 때문에 처음 아무것도 설정하지 않아도 프로젝트를 실행하면 HomeController가 bean으로 등록되어 서버를 실행했을 때 "/" 경로로 동작 여부를 테스트할 수 있게 된다.
root-context.xml에서 사용한 태그를 자세히 보면 서비스는 component-scan이지만 dao는 "mybatis-spring:scan"인 것을 볼 수 있다. 이 스캔 방식은 지정한 패키지를 포함한 하위 모든 패키지를 탐색하여 발견되는 모든 인터페이스를 bean으로 등록해버리기 때문에 사용에 주의가 필요하다. 패키지 구분을 하지 않고 클래스를 한꺼번에 모아놨다면 bean으로 생성할 생각이 없었던 인터페이스들도 모두 bean으로 등록된다.
이로서 모든 연동 작업이 끝났다. 다음 포스팅에서는 프로젝트가 제대로 구성되었는지 테스트해 볼 것이다.
spring-context의 버전과 spring-test의 버전이 맞물리지 않을 경우 에러가 발생하는데 굳이 spring-context의 버전을 찾을 필요 없이 위와 같이 스프링 context 와 같은 변수를 사용한다.
#3 Project update
언제부턴가 수동으로 업데이트해주지 않고 ctrl + s 만 해주어도 업데이트가 되었다. 하지만 실수로 spring-test의 버전을 context와 다르게 주었을 때 ctrl+s 는 먹히지 않았다. 정확히는 하단의 상태 표시줄 우측 끝에 작업이 진행되는 듯한 프로그래스 바는 바쁘게 움직였지만 에러는 사라지지 않았다.
수동으로 [프로젝트 우클릭 → Maven → Update Project] 를 통해서 프로젝트를 업데이트 해준다.
이 포스팅은 Spring과 mybatis, Oracle을 연동하는 과정에 대해 다룹니다. 프로젝트 생성부터 mybatis에 관한 초기 설정 및 연동, 테스트로 이어집니다. 테스트는 유저 정보를 가지는 테이블을 생성하고 더미 데이터를 넣어 데이터를 읽고 데이터를 추가하는 것으로 확인합니다.
과정도 과정이지만 단계가 넘어가면서 현재 단계가 이전 단계와 어떤 관계가 있는지, 생성하는 파일과 코드의 의미는 무엇인지 등에 대해서 자세히 서술해 보았습니다.
의존성 주입에 있어서는 생성자 주입이 아닌 어노테이션을 통한 자동 주입으로 구성하였습니다. 이 포스팅을 보는 사람이라면 스프링에 익숙치 않은 사람일 것이라 생각해 가장 간단한 자동 주입을 선택했습니다.
프로젝트 명 입력 후 템플릿에서 Spring MVC Project 선택 후 Next 선택.
"com.example.app_name"과 같이 패키지 명 입력 후 finish 선택. - package 이름 마지막은 클라이언트가 서버로 접속할 때 ip 주소, 포트 다음으로 구분되는 웹 어플리케이션 이름의 기본값이 된다. - 이 때 입력하는 패키지 명은 servlet-context.xml에 기본적으로 설정되는 component-scan 태그의 기본 값이 된다.
스프링 프로젝트가 정상적으로 생성되었다면 프로젝트를 진행하기 위해 필요한 의존성을 주입해야 한다. pom.xml 파일을 열고 아래에 있는 코드를 추가하거나 MavenRepository에 접속하여 원하는 버전을 찾도록 한다. 각 dependency 태그에 달린 주석에 있는 url로 접속해도 된다.
mybatis: 아파치에서 제공하는 자바 퍼시스턴스 프레임워크로 기존 JDBC를 사용할 때 꼭 처리해줘야 했던 작업(close, PreparedStatement, ResultSet 등)을 자동으로 해주고 처리 과정을 간소화 해준다. 또한 SQL과 Java 코드를 분리해서 관리할 수 있고 쿼리도 기존보다 쉽게 사용할 수 있다.
mybatis-spring: 스프링에서 mybatis와의 연동을 돕는 프레임워크.
spring-jdbc: 스프링에서 JDBC 처리를 도와주는 라이브러리.
spring-tx: 트랜잭션 처리를 위한 라이브러리.
commons-dbcp: Database Connection Pool을 사용하기 위한 라이브러리.
ojdbc6 : 오라클에서 제공하는 JDBC. 원래는 Maven에서 ojdbc를 지원하지 않아 수동으로 추가해줘야 했으나 20년 12월부터 지원하기 시작한 것으로 보인다.
spring-test: 스프링에서 JUnit과 같은 단위 테스트를 위한 라이브러리. (JUnit은 기본적으로 추가되어 있음)
annotation-api : Java 버전에 따라 더 이상 지원하지 않게 된 어노테이션(Resource 등)을 사용할 수 있게 해주는 API로 프로젝트에 설정한 자바 버전에 따라 필요하지 않을 수도 있으나 자바 버전이 높을 수록 추가해야 할 확률이 높다. 잘 모르겠다면 추가하자.
jackson-core, jackson-databind : 본 예제에서는 클라이언트와 상호작용 할 수 있는 화면, 즉 view를 만들어주지 않는다. URL로 데이터를 요청하고 쿼리 수행 결과만 JSON 형태로 받는 이른바 API 서버 역할을 하게 될 것이다. Jackson 라이브러리는 클라이언트에 대한 응답으로 JSON 또는 XML로 응답할 수 있도록 해준다.
추가적으로 jUnit의 버전을 수정한다. 스프링 프로젝트를 생성하면 jUnit에 대한 의존성이 기본적으로 주입되어 있는데 여기서 버전만 4.7 → 4.12 로 수정해준다.(JUnit의 버전은 낮을수록 최신이다.) 의존성 주입, 버전 수정이 모두 끝났다면 프로젝트를 우클릭하여 [Maven → Update Project]로 프로젝트를 업데이트해준다.
앞서 언급했듯 Maven은 원래 ojdbc를 지원하지 않았기 ojdbc.jar 을 다운받고 프로젝트에 수동으로 추가해주는 작업이 필요했었다. 혹시 모르니 수동으로 추가하는 방법도 아래 접어 놓겠다.
오라클 공식 홈페이지 Oracle JDBC Download에 접속해서 자신이 사용중인 오라클 버전에 맞는 ojdbc를 다운받는다. 필자는 가장 범용성이 좋은 ojdbc8.jar을 다운받았다.
버전에 따라 리스트 UI가 상이할 수 있음.
ojdbc를 다운받았다면 이클립스로 돌아와 프로젝트를 우클릭 하여 [Build path → Configure Build Path...]로 들어간다. 위와 같은 화면이 나오면 상단 탭이 "Libraries"인 것을 확인하고 Modulepath를 클릭, 우측의 "Add External JARs..."를 선택해 방금 다운로드한 ojdbc8.jar을 추가한다. 라이브러리 목록에 ojdbc가 추가되었다면 하단의 Apply를 눌러 저장해준다.
클릭하면 크게 보입니다.
좌측 상단의 검색창에 assembly를 검색하면 나오는 "Deployment Assembly"를 클릭하면 위와 같은 화면으로 진입한다. 우측의 "Add.."를 클릭하고 나오는 창에서 "Archives from File System"을 선택하고 다음을 눌러준다.
다음 화면에서 우측의 "Add.."를 선택해 ojdbc를 추가해준다.
프로젝트로 돌아와보면 Referenced Libraries가 추가된 것을 볼 수 있고 펼쳐보면 안에 "ojdbc8.jar"이 있는 것을 확인할 수 있다.
bean 등록하기
스프링에서는 싱글톤 특성을 지니는 객체를 컨테이너에 등록하여 사용하며 컨테이너에 등록된 객체를 bean 객체라고 한다. 데이터베이스에 접근하는 객체는 특정한 서비스에서만 사용되는 것이 아니기 때문에 여러 서비스에서 호출하는 것이 가능해야 한다. 그렇기 데이터베이스에 접근할 객체를 bean으로 등록해 줄 것이다.
데이터베이스 연결에 관련된 객체들을 등록하기 위해서 root-context.xml에 들어간다.
처음 root-context.xml을 열었다면 위와 같이 기본적인 코드만 존재할 것이다. 여기서 하단 탭의 namespace를 누른다.
1. Marketplace로 들어가 Spring을 다운로드할 때 "Spring Tools Add-on..."을 다운로드 하였는지, 다운로드 했었다면 업데이트 항목이 있는지 확인한다. 설치하지 않았다면 설치해주고 Update 표시가 있다면 Update를 진행한다.
2. 프로젝트의 root-context.xml 파일에 우클릭하여 [Open-with → Other..]를 선택한다. 에디터를 선택하는 창이 나오는데 상단 체크박스가 "Internal editors"인 것을 확인한 후 spring을 검색하여 나오는 목록에서 "Spring Config Editor"를 선택하고 종료한다.
Namespaces를 누르면 위와 같은 화면을 볼 수 있는데 Namespaces의 목록은 pom.xml에서 주입한 의존성에 따라 구성된다. root-context에 추가하고자 하는 namespace를 선택하면 실제 코드 <beans>태그에 추가된다. 위 처럼 체크한 뒤 다시 하단 탭의 Source를 눌러 xml 코드로 이동한다.
정상적으로 수행됬다면 위와 같이 <beans> 태그가 수정된 것을 확인할 수 있다. 이제부터 bean 객체를 추가한다.
root-context.xml 에 위와 같은 코드를 추가한다. <bean> 태그는 기본적으로 id와 class를 가지는데 id는 식별자, class는 객체화하는 클래스를 나타낸다. xml로 생성한다 한들 결국 객체로 생성하는 것이기 때문에 객체화 할 클래스를 지정해주는 것이라고 보면 된다.
bean 객체 생성에 사용된 클래스를 살펴보자.
BasicDataSource: DBMS에 접속하기 위한 정보와 DBCP를 사용하기 위한 기본 설정을 초기화한다. JDBC를 사용할 때처럼 url, driver, user id, user password 등을 입력한다. 이는 SqlSessionFactory를 빌드할 때 참조될 것이다.(민감한 정보는 따로 빼내서 관리하는 것이 일반적이지만 예제이기에 하드 코딩으로 되어 있음을 주의.)
SqlSessionFactoryBean: 스프링에서 SqlSessionFactory를 빌드하는 컴포넌트로, SqlSessionFactory는 SqlSession을 생성 및 관리하는 역할을 한다. 위에서 생성한 dataSource를 참조하여 SqlSessionFactory를 빌드한다.
SqlSessionTemplate: MyBatis에서의 SqlSession을 대체한다. MyBatis의 SqlSession은 SQL구문을 실행하고 트랜잭션을 제어하는 역할을 하는데 Thread-safe 하지 않다는 문제점을 가지고 있었고 이 때문에 필요에 따라 Thread를 생성하고 새로운 인스턴스를 할당해야 했다. 하지만 mybatis-spring 프레임워크에서 제공하는 SqlSessionTemplate은 SqlSession을 Thread-safe하게 사용할 수 있어 따로 관리해 줄 필요가 없다. 추가적으로 constructor-arg 속성을 사용하였는데 이름에서 유추할 수 있듯이 생성자를 지정할 수 있다. 앞서 bean 태그는 스프링 컨테이너에 bean 객체를 생성한다고 했는데 SqlSessionTemplate 클래스는 인자가 없는 생성자가 없기 때문에 이 옵션을 필수로 작성해야 한다.
DataSourceTransactionManager: JDBC 기반의 트랜잭션 관리자다. 트랜잭션 관리자를 사용하려면 DataSource에 대한 의존 설정이 필요하기 때문에 앞서 생성한 dataSource 객체를 참조할 수 있도록 한다.
<tx:annotation-driven transaction-manager="id"> : id에 TransactionManager bean 객체의 id를 지정하면 어노테이션 기반의 트랜잭션을 활성화할 수 있다. 이 옵션을 활성화하면 프로젝트가 구동될 때 @Transactional이 적용된 메서드를 탐색하게 된다.
DataSource의 경우 DBMS에 연결하기 위한 설정과 DBCP(Database Connection Pool)를 사용하기 위한 초기 설정을 갖는데 필자는 dbms에 연결하기 위한 최소한의 정보인 driverClassName, url, username, password만을 입력했다. 오라클 DBMS를 설치하면서 별 다른 설정을 건드리지 않았으면 driver와 url은 그대로 사용하면 되고 username과 password는 본인이 사용 중인 db에 접속할 계정 정보를 입력해주면 된다. 필자는 예제로 가장 많이 사용되는 계정인 scott / tiger를 생성해 사용하였다. 이 외에 필요에 따라 initialSize, maxActive, maxIdle 등을 사용하여 커넥션 풀에 대한 프로퍼티를 추가할 수 있다.
DataSource, SqlSession Test
연결이 정상적으로 되었는지 확인하기 위해서 테스트를 수행한다. 스프링에서는 동적 웹 프로젝트에서 했던 것처럼 특정 파일만을 선택해서 서버에 올리는 것이 불가능하다. 프로젝트를 통째로 서버에 올려서 테스트해 볼 수도 있겠지만 기본적으로 의존성 주입이 되어 있는 jUnit을 사용하여 단위 테스트를 진행하도록 하자.
스프링 프로젝트에서는 테스트를 위한 디렉터리를 따로 제공하고 있다. "src/test/java"를 열고 테스트를 위한 클래스(JDBCTests.java)를 생성하고 다음 코드를 입력한다.
사용된어노테이션과 클래스는 아래 접은 글에 표시해두었으니 궁금하다면 펼쳐 보기 바란다. 테스트는 "DataSource 객체에서 커넥션을 얻어올 수 있는가"와 "SqlSessionFatctory에서 SqlSession 객체를 얻어올 수 있는가"를 확인한다. root-context.xml를 작성할 때 코드를 주의 깊게 봤다면 사용하려는 멤버 변수들이 앞서 등록한 bean 객체와 연관이 있음을 알 수 있다.
일반적인 try-catch-finally 구문이 아닌 try with resources구문을 사용하여 테스트한다. try( .. ) 안에는 자원을 사용한 후 반납할 필요가 있는 객체를 선언할 수 있는데 여기에 선언된 객체들은 작업( {..} )이 끝나면 자동으로 자원을 반납한다. 단, 자바 7 이후부터 지원하는 기능이기 때문에 버전이 낮다면 일반적인 try-catch-finally를 구성하도록 한다.
테스트를 수행하기 전에 파일을 저장하겠냐는 메시지 혹은 자동 저장이 되지 않기 때문에 파일을 수동으로 저장해야 한다. 테스트 할 메소드인 ConnectionTest에 커서를 위치한 후 우클릭을 하여 [Run as... → JUnit Test]를 눌러 테스트를 진행한다.
- JUnit의 테스트 범위를 확장할 때 사용하는 어노테이션이다. JUnit은 Java에서 단위 테스트를 지원하기 위한 프레임워크로, Spring에서 제공하는 프레임워크가 아니기 때문에 스프링에서 사용할 수 있도록 확장해주는 작업이다. - SpringJUnit4ClassRunner 를 지정해주면 테스트를 수행할 때 Application Context, 즉 스프링 컨테이너를 만들고 관리하는 작업을 해준다.
ContextConfiguration
반드시 RunWith와 같이 사용해야 하는 어노테이션이다. 그 이유는 이 어노테이션이 RunWith로 인해 생성될 Application Context 설정 파일 위치를 지정하는 역할을 하기 때문이다. 설정 파일은 기본적으로 servlet-context.xml, root-context.xml 등이 있는데 테스트하려는 bean에 따라서 하나만 넣거나 중괄호를 사용해 둘 다 넣어줄 수 있다.
Spring-test Annotation
Test
테스트를 수행 할 메서드를 지정한다.
Before
테스트를 수행하기 전에 수행될 메서드를 지정한다.
After
테스트를 수행한 후 수행될 메서드를 지정한다.
멤버 변수
DataSource : CP(Connection Pool)에 있는 Connection을 관리하기 위한 클래스. getConnection() 메서드를 호출하면 CP에서 Free 상태인 Connection 객체를 얻을 수 있다.
SqlSessionFactory : MyBatis에서 제공하는 SqlSession 객체를 가지고 있는 클래스.
Logger : Log4j에서 제공하는 로그를 출력하기 위한 클래스.
콘솔 탭과 JUnit 탭에 위와 같이 출력되었다면 테스트가 성공한 것이다. 커넥션 풀에서 얻어온 커넥션 객체의 정보와 SqlSessionFactory에서 얻은 SqlSession 정보가 출력된 것을 볼 수 있다.
Spring과 오라클 간의 연동이 성공적으로 수행된 것을 확인하였다. 이제 테스트에 사용할 테이블을 생성하자.
테스트를 위한 회원 정보 table 생성하기
CREATE TABLE member_t (
id varchar(20) not null primary key,
name varchar2(30) not null,
age number not null
);
INSERT INTO member_t VALUES('user1', '홍길동', 36);
INSERT INTO member_t VALUES('user2', 'Robert', 22);
INSERT INTO member_t VALUES('user3', '이영희', 21);
테이블 생성은 Sql developer로 수행하였다. 다른 툴을 사용해도 되고 콘솔로 하는 것이 편하다면 그렇게 해도 된다. 테이블은 테스트 용으로 만들어진 임시 테이블인 만큼 간단하다. 아이디, 사용자 이름, 나이를 가지며 아이디가 주키가 된다. 빠른 테스트를 위해 미리 데이터를 삽입해놓도록 하자.