Anyframe

Version 4.0.0

본 문서의 저작권은 삼성SDS에 있으며 Anyframe 오픈소스 커뮤니티 활동의 목적하에서 자유로운 이용이 가능합니다. 본 문서를 복제, 배포할 경우에는 저작권자를 명시하여 주시기 바라며 본 문서를 변경하실 경우에는 원문과 변경된 내용을 표시하여 주시기 바랍니다. 원문과 변경된 문서에 대한 상업적 용도의 활용은 허용되지 않습니다. 본 문서에 오류가 있다고 판단될 경우 이슈로 등록해 주시면 적절한 조치를 취하도록 하겠습니다.


I. Overview
1. 특징
2. 주요 기능
2.1. Lightweight 컨테이너
2.1.1. POJO 기반 개발 지원
2.1.2. Dependency Resolution 지원
2.1.3. Aspect Oriented Programming 지원
2.1.4. Life-cycle 관리
2.1.5. 신규 기능 추가 용이
2.2. 기술 공통 서비스
2.2.1. Generic Service
2.2.2. DynamicModule Service
2.2.3. DataSource 서비스
2.2.4. Query 서비스
2.2.5. ID Generation 서비스
2.2.6. Properties 서비스
2.2.7. Transaction 서비스
2.2.8. Hibernate 서비스
2.2.9. Logging 서비스
2.2.10. Remoting 서비스
2.2.11. Web Services
2.3. 웹 화면 개발 시 필요한 공통 기능
2.3.1. 화면 흐름 제어(Screen Workflow Control)
2.3.2. 공통 클래스 제공
2.3.3. 국제화(i18N), 지역화(L10n) 지원
2.3.4. 사용자 입력값 유효성 검증(Validation)
2.3.5. Tag Library를 통한 View 개선
2.3.6. 에러 처리(Exception Handling)
2.3.7. 요청(Request) 권한 처리
2.3.8. Double Submit 방지
2.3.9. 다양한 웹 클라이언트(UI) 연계 지원(X-internet Integration)
II. Installation
3. Installation
3.1. [선택] Maven 설치 및 환경 설정
3.2. [필수] Foundation Plugin 설치
3.3. [선택] Other Plugins 설치
3.4. [선택] Plugin 설치 확인
3.5. [선택] 샘플 DB 변경
3.6. [선택] Eclipse WTP만 이용하여 Tomcat 실행
4. Anyframe Plugins
4.1. Anyframe Plugins
4.2. Foundation Plugin 구조
4.3. Other Plugins 구조
4.4. Custom Plugin 추가 정의
5. Anyframe Maven Commands
5.1. anyframe-maven-plugin 정의 확인
5.2. Anyframe Maven Commands
III. Spring
6. IoC(Inversion of Control)
6.1. Basic
6.1.1. Container와 Bean
6.1.2. Container
6.1.2.1. BeanFactory
6.1.2.2. ApplicationContext
6.1.2.3. 설정 메타데이터
6.1.2.4. Spring IoC Container 인스턴스화 시키는 예제
6.1.2.5. XML 기반 설정 메타데이터 조합
6.1.3. Beans
6.1.3.1. Bean
6.1.3.2. Bean 명명하기
6.1.3.3. Bean 인스턴스화
6.1.4. How to refer to Beans
6.1.4.1. 비즈니스 레이어
6.1.4.2. 프리젠테이션 레이어
6.2. Dependencies
6.2.1. Dependency Injection(DI)
6.2.1.1. Setter Injection
6.2.1.2. Constructor Injection
6.2.1.3. Setter Injection vs. Constructor Injection
6.2.1.4. 생성자 인자 분석
6.2.2. Bean Property와 생성자 인자
6.2.2.1. XML 기반의 설정 메타데이터 간략화
6.2.2.2. 혼합된 Property 명(Compound Property) - shortcut 기능 제공
6.2.3. depends-on 속성 사용
6.2.4. Lazy Instantiation
6.2.5. Autowiring
6.2.5.1. 장점
6.2.5.2. 단점
6.2.6. Dependency Check
6.3. Method Injection
6.3.1. Lookup Method Injection
6.3.2. Method Replacement
6.4. Bean과 Container의 확장
6.4.1. Bean Scope
6.4.1.1. Singleton
6.4.1.2. Prototype
6.4.1.3. Other Scopes
6.4.1.4. Custom
6.4.2. Bean Life Cycle
6.4.2.1. Initialization
6.4.2.2. Destruction
6.4.3. Bean 상속
6.4.4. Container 확장
6.4.4.1. Bean 후처리
6.4.4.2. BeanFactory 후처리
6.4.5. ApplicationContext 활용
6.4.5.1. MessageSource를 활용한 국제화(I18N) 지원
6.4.5.2. Event
6.4.5.3. BeanFactory와 ApplicationContext 특징 비교
6.5. XML 스키마 기반 설정
6.6. Resources
7. Aspect Oriented Programming
7.1. AOP 구성 요소
7.1.1. JointPoint
7.1.2. Pointcut
7.1.2.1. Pattern Matching Examples
7.1.2.2. Pointcut Designators
7.1.3. Advice
7.1.4. Weaving 또는 CrossCutting
7.1.5. Aspect
7.2. Annotation based AOP
7.2.1. Configuration
7.2.2. @Aspect 정의
7.2.3. @Pointcut 정의
7.2.4. @Advice 정의
7.2.4.1. Before Advice
7.2.4.2. AfterReturning Advice
7.2.4.3. AfterThrowing Advice
7.2.4.4. After(finally) Advice
7.2.4.5. Around Advice
7.2.5. Aspect 실행
7.3. XML based AOP
7.3.1. Aspect 정의
7.3.2. Pointcut 정의
7.3.3. Advice 정의 및 구현
7.3.3.1. Before Advice
7.3.3.2. AfterReturning Advice
7.3.3.3. AfterThrowing Advice
7.3.3.4. After(finally) Advice
7.3.3.5. Around Advice
7.3.4. Aspect 실행
7.4. AspectJ based AOP
7.4.1. 시작하기 전에
7.4.2. Aspect 정의
7.4.3. Pointcut 정의
7.4.4. Advice 정의
7.4.4.1. Before Advice
7.4.4.2. AfterReturning Advice
7.4.4.3. AfterThrowing Advice
7.4.4.4. After(finally) Advice
7.4.4.5. Around Advice
7.5. AOP Examples
7.5.1. AOP Example - Logging
7.5.1.1. Configuration
7.5.1.2. Aspect 정의
7.5.1.3. Aspect 실행
7.5.2. AOP Example - Exception Transfer
7.5.2.1. Aspect 정의
7.5.2.2. Advice 구현
7.5.3. AOP Example - Profiler
7.5.3.1. Configuration
7.5.3.2. Aspect 정의
7.5.3.3. Aspect 실행
7.5.4. AOP Example - Design Level Assertions
7.5.4.1. Interaction Rule 정의 예제
7.5.4.2. Naming Rule 정의 예제
7.5.4.3. Refactoring
7.6. Resources
8. Spring Remoting
8.1. RMI(Remote Method Invocation)
8.1.1. Server Configuration
8.1.1.1. Samples
8.1.2. Client Configuration
8.1.2.1. Samples
8.2. Hessian
8.2.1. Server Configuration
8.2.1.1. Samples
8.2.2. Client Configuration
8.2.2.1. Samples
8.2.3. Hessian과 Burlap의 차이점
8.3. Burlap
8.3.1. Server Configuration
8.3.1.1. Samples
8.3.2. Client Configuration
8.3.2.1. Samples
8.3.3. Hessian과 Burlap의 차이점
8.4. HTTP Invoker
8.4.1. Server Configuration
8.4.1.1. Samples
8.4.2. Client Configuration
8.4.2.1. Samples
8.5. Resources
9. Annotation
9.1. Bean Management
9.1.1. Auto Detecting
9.1.2. Using Filters to customize scanning
9.1.3. Scope Definition
9.2. Dependency Injection
9.2.1. @Resource
9.2.2. @Autowired
9.2.3. @Qualifier
9.2.4. @Resource vs. @Autowired
9.3. LifeCycle Annotation
9.3.1. @PostConstruct
9.3.2. @PreDestroy
9.3.3. Combining lifecycle mechanisms
9.4. Resources
IV. Spring MVC
10. Architecture
11. Configuration
11.1. web.xml 작성
11.1.1. DispatcherServlet 등록
11.1.2. Spring 설정 파일 위치 등록
11.2. action-servlet.xml 작성
11.2.1. action-servlet.xml 설정
11.2.1.1. Handler Mapping
11.2.1.2. View Resolver
12. Controller
12.1. AbstractController
12.2. MultiActionController
12.3. AbstractCommandController
12.4. SimpleFormController
12.5. UrlFilenameViewController
12.6. ParameterizableViewController
13. View
13.1. Tag library
13.1.1. configuration
13.1.2. form
13.1.3. input
13.1.4. checkbox
13.1.5. checkboxes
13.1.6. radiobutton
13.1.7. radiobuttons
13.1.8. password
13.1.9. select
13.1.10. option
13.1.11. options
13.1.12. textarea
13.1.13. hidden
13.1.14. errors
13.1.15. sample
13.1.15.1. 입력 화면
13.1.15.2. Controller 클래스
13.1.15.3. 출력 화면
13.2. Tiles
13.2.1. Tiles view class 정의
13.2.2. TilesConfigurer 정의
13.2.3. Tiles definition 파일 작성
14. File Upload
15. Internationalization
15.1. 다국어 지원 기능
15.1.1. Locale Resolver를 이용한 Locale 변경
15.1.2. LocaleChangeInterceptor를 이용한 Locale 변경
15.2. Locale Resolver
15.2.1. AcceptHeaderLocaleResolver
15.2.2. CookieLocaleResolver
15.2.3. SessionLocaleResolver
15.2.4. FixedLocaleResolver
16. Validator
16.1. Validator 생성
16.2. Validator 등록
16.3. form:errors 태그 사용
17. Exception Handling
17.1. 특정 error 페이지로 이동하여 에러 메시지 출력
17.2. 에러 페이지에 에러 메시지 출력
17.3. Presentation Layer에서 message key를 이용한 locale 변경
17.3.1. Business Layer의 BaseException 발생
17.3.2. Presentation Layer에서 꺼낸 message key 값에 새로운 Locale로 셋팅
18. Spring Integration
18.1. Listener 등록과 Spring 설정 파일 목록 위치 정의
18.2. Dependency Injection을 통한 Business Service 호출
18.3. Resources
19. Annotation based Spring MVC
19.1. Configuration
19.1.1. Handler 설정
19.1.2. Component Scan 설정
19.1.2.1. Using Filters to customize scanning
19.2. Controller
19.2.1. @Controller
19.2.2. @RequestMapping
19.2.2.1. Form Controller 구현
19.2.2.2. Multi-action Controller 구현
19.2.2.3. Supported argument types
19.2.2.4. Supported return types
19.2.3. @RequestParam
19.2.4. @ModelAttribute
19.2.5. @SessionAttributes
19.3. Dependency Injection
19.4. Double Submit Prevention
19.4.1. Annotation을 이용한 Double Submit 방지
19.5. Resources
V. Spring MVC Extensions
20. Controller
20.1. ForwardController
20.2. AnyframeFormController
20.3. AnyframeMiPController
21. View
21.1. Tag library
21.1.1. Page Navigator Tag
21.1.2. Message Tag
22. Double Submit Prevention
22.1. property 정의하기
22.2. AnyframeFormController를 상속받아 컨트롤러 클래스 구현하기
22.2.1. 페이지 출력 요청 (Ex. /userForm.do)
22.2.2. 폼 submit 요청(Ex. /getUser.do)
22.3. messageSource 추가하기
22.4. Resources
23. JasperReports Integration
23.1. Installation
23.1.1. 다운로드
23.1.2. 설치 환경
23.1.3. Report Designer 설치
23.1.3.1. Configuration
23.2. Report Designer
23.2.1. 목표 결과물
23.2.2. 디자인 파일(JRXML) 작성
23.2.2.1. Step 1 : Open JasperAssistant Perspective
23.2.2.2. Step 2 : Create a new Report
23.2.2.3. Step 3 : Design a report using Palette
23.2.2.4. Step 5 : Preview Report
23.3. Configuration
23.3.1. web.xml 작성하기
23.3.2. jasper-servlet.xml 작성하기
23.4. Controller
23.4.1. HTML Reporting
23.5. Resources
VI. Spring Web Flow
24. configuration
24.1. 기본 설정
24.1.1. FlowRegistry 정의
24.1.1.1. Flow Registry 정의 방법
24.1.1.2. Flow ID 생성
24.1.2. FlowExecutor 정의
24.2. Spring MVC와 연계하기 위한 설정
24.2.1. FlowHandlerAdaptor 정의
24.2.2. FlowHandlerMapping 정의
24.2.3. Spring MVC의 ViewResolver 지정
25. 플로우 정의
25.1. 필수 요소
25.1.1. view-state
25.1.2. transition
25.1.3. end-state
25.2. 메소드 호출
25.2.1. evaluate
25.3. Transition Decision
25.3.1. action-state
25.3.2. decision-state
25.4. Expression Language
25.4.1. Special EL variables
26. View
26.1. model 바인딩
26.2. view backtracking
26.2.1. discard
26.2.2. invalidate
27. Subflow
27.1. subflow-state
27.2. input
27.3. output
28. 플로우 상속
28.1. flow 레벨 상속
28.2. state 레벨 상속
29. Validator
29.1. model 객체 내에 validate 메소드 구현
29.2. validator 클래스 및 메소드 구현
29.3. Resources
VII. Struts
30. Architecture
30.1. Controller Structure
30.2. Request의 흐름
31. Configuration
31.1. web.xml
31.1.1. servlet, servlet-mapping 설정
31.1.1.1. <servlet>설정
31.1.1.2. <servlet-mapping>설정
31.1.1.3. Samples
31.1.2. taglib 설정
31.1.2.1. JSP에서의 설정
31.1.2.2. Samples
31.2. struts-config.xml
31.2.1. controller
31.2.1.1. <controller>설정
31.2.1.2. Samples
31.2.2. message-resources
31.2.2.1. <message-resources>설정
31.2.2.2. Samples
31.2.3. plug-in
31.2.3.1. <plug-in>설정
31.2.3.2. Samples
31.2.4. form-beans
31.2.4.1. <form-beans>설정
31.2.4.2. Samples
31.2.4.3. DynaActionForm
31.2.5. action-mappings
31.2.5.1. <action-mappings>설정
31.2.5.2. <action>의 주요 attribute
31.2.5.3. Samples
31.2.6. global-forwards
31.2.6.1. <global-forwards> 설정
31.2.6.2. Samples
32. Controller
32.1. ActionServlet
32.1.1. ActionServlet의 역할
32.1.2. 초기화 프로세스
32.1.3. 실행 시(ActionServlet 인스턴스가 HTTP Request를 받을 때)
32.1.4. ShutDown 프로세스
32.2. RequestProcessor
32.2.1. RequestProcessor의 역할
32.2.2. process() 메소드의 Request 처리 절차
32.2.3. Sample
32.3. Action
32.3.1. Action의 역할
32.3.2. Action의 구현
32.3.3. Sample
32.4. ActionForward
32.4.1. ActionForward의 역할
32.5. Actions Package
32.5.1. org.apache.struts.actions 패키지에 미리 정의되어 있는 Action
32.5.2. org.apache.struts.actions.ForwardAction
32.5.3. org.apache.struts.actions.IncludeAction
32.5.4. org.apache.struts.actions.DispatchAction
32.5.5. org.apache.struts.actions.LookupDispatchAction
32.5.6. org.apache.struts.actions.SwitchAction
33. View
33.1. Taglib
33.1.1.
33.1.1.1. Taglib의 특징
33.1.1.2. Struts Taglib
33.1.1.3. JSP Standard Tag Library
33.1.1.4. 기타 Taglib
33.2. Tiles
33.2.1. Page Layout 구성 방법
33.2.1.1. 구성 방법
33.2.2. Tiles 설치
33.2.3. Tiles 사용
33.2.3.1. Tiles 적용 시 고려점
33.2.3.2. Tiles Tag Library의 속성
33.2.4. Tiles Layout 정의
33.2.4.1. JSP 로 레이아웃을 정의한 예
33.2.4.2. XML 로 레이아웃을 정의한 예
34. Internationalization
34.1. Internationalization의 특징
34.1.1. Internationalization의 필요성
34.1.2. 지역 (Locale)
34.2. Internationalization Sample
34.2.1. Sample
35. Validator
35.1. Plug-in 등록
35.1.1. struts-config.xml에 plug-in 등록
35.1.2. Samples
35.2. Validator Rules
35.2.1. Struts Validator Rules 기본 기능
35.3. ActionForm
35.3.1. ValidatorForm의 상속
35.3.2. Samples
35.4. formset 설정
35.4.1. formset 설정 방법
35.4.2. Sample
35.5. Action 매핑 설정
35.5.1. struts-config.xml의 Action 매핑 설정
35.5.2. Sample
36. Exception Handling
36.1. Global Level Exception Handling
36.1.1. Global Level Exception Handling의 특징
36.1.2. Samples
36.2. Action Level Exception Handling
36.2.1. Action Level Exception Handling의 특징
36.2.2. Samples
36.3. Resources
VIII. Struts Extensions
37. Controller
37.1. DefaultActionServlet
37.2. DefaultRequestProcessor
37.2.1. DefaultRequestProcessor 기능
37.3. AbstractActionSupport
37.3.1. Action Sample
37.4. DefaultDispathActionSupport
37.4.1. Action Sample
37.5. DefaultForwardAction
37.6. AnyframeMiPAction
37.6.1. Sample Action
38. View
38.1. Tag Library
38.1.1. Page Navigator Tag
38.1.2. Messages Tag
38.1.2.1. Error Page 구성
39. Double Submit Prevention
39.1. Double Submit의 개념
39.2. 일반적인 Token 처리
39.3. 선언적인 Token 처리
39.3.1. Samples
39.3.2. 참고 사항
40. Exception Handling
40.1. 선언적인 Exception Handling
40.1.1. Samples
40.2. DefaultBaseExceptionHandler 확장
41. Authentication and Authorization
41.1. Authentication
41.1.1. Samples
41.2. Authorization
41.2.1. 접근 권한 제어 프로세스
41.2.2. Samples
42. Spring Integration
42.1. Configuration
42.1.1. ContextLoaderListener, ContextConfigLocation 정의
42.2. Action
42.3. Resources
IX. Tech.Service
43. Common Configuration
43.1. Anyframe MessageSource
43.1.1. Samples
43.2. Configuration Tag 확장
43.2.1. Samples
44. Generic Service
44.1. Domain Model 클래스 생성
44.1.1. BaseObject
44.1.2. Samples - Query Service 사용 시
44.1.3. Samples - Hibernate/JPA 사용 시
44.2. Service 클래스 생성
44.2.1. GenericService
44.2.2. GenericServiceImpl
44.2.3. Samples
44.3. DAO 클래스 생성
44.3.1. GenericDao
44.3.2. GenericDaoQuery
44.3.3. GenericDaoHibernate
44.3.4. Samples
44.4. Test Code 생성
44.4.1. Unit Test Case
44.4.2. Integration Test Case
44.4.3. Samples
44.5. Resources
45. DynamicModule Service
45.1. Project Structure
45.2. Configuration
45.2.1. module.properties
45.2.1.1. 공통(common) 프로젝트
45.2.1.2. 서비스(service) 프로젝트
45.2.1.3. 웹(web) 프로젝트
45.2.2. web.xml
45.2.3. moduledefinitions.xml
45.2.4. anyframe.properties
45.2.5. impala configuration xml files
45.2.6. context-dynamic.xml
45.3. Build
45.3.1. Eclipse 내에서 웹 어플리케이션 구동
45.3.1.1. anyframe.properties
45.3.1.2. 공통(common) 타입 프로젝트 빌드
45.3.2. WAS에 실제 배포하여 웹 어플리케이션 구동
45.3.2.1. anyframe.properties
45.3.2.2. jar 파일 배포
45.3.2.3. class 파일 배포
45.3.2.4. Default Value Setting (Workspace Root, Version)
45.4. Resources
46. MiPlatform Service
46.1. Controller
46.1.1. MiPController
46.2. Service
46.2.1. MiPService
46.2.2. MiPServiceImpl
46.3. Dao
46.3.1. MiPDao
46.3.2. MiPDaoQuery
46.4. Extension of MiPServiceImpl
46.4.1. [참고] IMiPActionCommand
46.5. Testcase
46.6. Resources
47. Cache Service
47.1. DefaultCacheService
47.1.1. Samples
47.2. Resources
48. DataSource Service
48.1. JDBCDataSource Configuration
48.1.1. Samples
48.2. DBCPDataSource Configuration
48.2.1. Samples
48.3. C3P0DataSource Configuration
48.3.1. Samples
48.4. JNDIDataSource Configuration
48.4.1. Samples
48.4.2. jee schema 를 통한 JNDIDataSource 사용
48.5. Test Case
48.6. Resources
49. Hibernate Service
49.1. Resources
49.2. Mapping File
49.2.1. Mapping File의 작성
49.2.1.1. Mapping File 구성
49.2.2. Data Type의 매핑
49.2.2.1. Data Type의 매핑
49.2.3. Hibernate Generator
49.2.3.1. Hibernate 기본 Id Generator
49.2.3.2. 직접생성
49.3. Persistence Mapping
49.3.1. Persistence Mapping - Association
49.3.1.1. One to One Mapping
49.3.1.2. One to Many Mapping
49.3.1.3. Many to Many Mapping
49.3.2. Persistence Mapping - Inheritance
49.3.2.1. Table per Class Hierarchy
49.3.2.2. Table per Subclass
49.3.2.3. Table per Concrete Class
49.4. Basic CRUD
49.4.1. 단건 조회
49.4.2. 단건 저장
49.4.2.1. Tip. A:B=1:m인 경우 A에 대한 save()
49.4.3. 단건 수정
49.4.4. 단건 저장 또는 수정
49.4.5. 단건 삭제
49.4.6. 복수건 저장
49.5. HQL(Hibernate Query Language)
49.5.1. 구성 요소
49.5.1.1. [선택] SELECT 절
49.5.1.2. [필수] FROM 절
49.5.1.3. [선택] WHERE 절
49.5.1.4. [선택] ORDER BY 절
49.5.1.5. [선택] GROUP BY 절
49.5.2. 기본적인 사용 방법
49.5.2.1. Case 1. Basic
49.5.2.2. Case 2. Join
49.5.3. 원하는 객체 형태로 전달
49.5.3.1. Case 1. 특정 객체 형태로 전달
49.5.3.2. Case 2. Map 형태로 전달
49.5.3.3. Case 3. List 형태로 전달
49.5.4. XML에 HQL 정의하여 사용
49.5.5. Pagination
49.5.6. HQL을 이용한 CUD
49.5.6.1. 등록 (Insert)
49.5.6.2. 수정 (Update)
49.5.6.3. 삭제 (Delete)
49.6. Criteria Queries
49.6.1. 기본적인 사용 방법
49.6.1.1. Case 1. Basic
49.6.1.2. Case 2. Join
49.6.2. 원하는 객체 형태로 전달
49.6.2.1. Case 1. 특정 객체 형태로 전달
49.6.2.2. Case 2. Map 형태로 전달
49.6.3. Pagination
49.7. Native SQL
49.7.1. 기본적인 사용 방법
49.7.1.1. Case 1. Basic
49.7.1.2. Case 2. Join
49.7.1.3. Case 3. 검색 조건 명시
49.7.2. XML에 Native SQL 정의하여 사용
49.7.3. Pagination
49.7.4. Callable Statement
49.7.4.1. Case 1. XML에 정의한 Procedure 호출
49.7.4.2. Case 2. Function을 이용한 HQL 실행
49.8. Performance Strategy
49.8.1. Cache
49.8.1.1. 1LC (1 Level Cache)
49.8.1.2. 2LC (2 Level Cache)
49.8.1.3. 분산 Cache
49.8.2. Fetch Strategy
49.8.2.1. Batch를 이용하여 데이터 조회
49.8.2.2. Sub-Query를 이용하여 데이터 조회
49.8.2.3. join fetch를 이용하여 데이터 한꺼번에 조회
49.9. Concurrency
49.9.1. Optimistic Locking
49.9.2. Pessimistic Locking
49.9.3. Offline Locking
49.10. Transaction Management
49.10.1. JDBC - HibernateTransactionManager
49.10.2. JTA - JTATransactionManager
49.11. Spring Integration
49.11.1. Hibernate 속성 정의 파일 작성
49.11.1.1. Session Factory 속성 정의
49.11.1.2. Dynamic HQL, Dynamic Native SQL 실행을 위한 DynamicHibernateService 속성 정의
49.11.2. Mapping XML 파일 작성
49.11.3. DAO 클래스 생성
49.11.3.1. DAO 속성 정의 파일 작성
49.11.3.2. DAO 클래스 개발
49.11.4. Test Code 작성
49.11.5. 선언적인 트랜잭션 관리
49.12. DynamicHibernateService 활용
49.12.1. DynamicHibernateService
49.12.1.1. DynamicHibernate Configuration
49.12.1.2. Dynamic HQL 정의 파일
49.12.1.3. DynamicHibernateService 활용 예제
49.13. Hibernate Configuration
49.13.1. DataSource 속성 정의
49.13.2. Generated SQL 속성 정의
49.13.3. Cache 속성 정의
49.13.4. Logging 속성 정의
49.13.5. 기타 속성 정의
49.13.6. 매핑 파일 정의
50. Id Generation Service
50.1. UUIdGenerationService
50.1.1. Samples
50.2. SequenceIdGenerationService
50.2.1. Samples
50.3. TableIdGenerationService
50.3.1. Samples
50.4. How to use a Generation Strategy
50.4.1. MixPrefix property 정의 방법
50.4.2. Id Generation Strategy를 implements하는 방법
50.5. Resources
51. Logging Service
51.1. Logging Service Configuration 정의하기
51.1.1. appender
51.1.2. logger
51.1.3. root
51.2. Logging Service 사용하기
51.2.1. 기본적인 사용 방법
51.2.2. ResourceBundle을 이용하는 방법
51.3. Tip. SQL문을 로그로 남기기
51.3.1. Step 1. Log4jdbc 라이브러리 다운로드
51.3.2. Step 2. Simple Logging Facade for Java 라이브러리 다운로드
51.3.3. Step 3. DataSource 속성 정의
51.3.3.1. JDBCDataSource를 사용할 경우
51.3.3.2. JNDIDataSource를 사용할 경우
51.3.4. Step 4. Query 서비스 속성 정의
51.3.5. Step 5. Logger 정의
51.4. Resources
52. Properties Service
52.1. PropertiesServiceImpl
52.1.1. Samples
52.2. Sample Property File
52.3. Resources
53. Query Service
53.1. Configuration
53.1.1. jdbcTemplate
53.1.2. sqlRepository
53.1.3. pagingSQLGenerator
53.1.4. lobHandler
53.1.5. Samples
53.1.6. TestCase
53.1.6.1. INSERT
53.1.6.2. SELECT
53.1.6.3. UPDATE
53.1.6.4. DELETE
53.2. Mapping XML Files
53.2.1. table-mapping 정의 방법
53.2.2. queries 정의 방법
53.3. Usecases
53.3.1. Result Mapping
53.3.1.1. 조회 결과 매핑이 별도로 정의되어 있지 않은 경우
53.3.1.2. <result-mapping> 없이 <table-mapping>을 이용할 경우
53.3.1.3. <table-mapping>,<result-mapping>없이 <result>만을 이용할 경우
53.3.1.4. <result-mapping>을 이용할 경우
53.3.1.5. 테스트 코드 Sample
53.3.2. Embedded SQL
53.3.2.1. 속성 정의 파일 Sample
53.3.2.2. 테스트 코드 Sample
53.3.3. OR Mapping
53.3.3.1. 속성 정의 파일 Sample
53.3.3.2. 매핑 XML 파일 Sample
53.3.3.3. OR Mapping시 사용할 매핑 클래스 Sample
53.3.3.4. 테스트 코드 Sample
53.3.4. Dynamic Query
53.3.4.1. 속성 정의 파일 Sample
53.3.4.2. 매핑 XML 파일 Sample
53.3.4.3. 테스트 코드 Sample
53.3.5. Pagination
53.3.5.1. 속성 정의 파일 Sample
53.3.5.2. 매핑 XML 파일 Sample
53.3.5.3. 테스트 코드 Sample
53.3.6. Batch Update
53.3.6.1. 속성 정의 파일 Sample
53.3.6.2. 매핑 XML 파일 Sample
53.3.6.3. 테스트 코드 Sample
53.3.7. Callable Statement
53.3.7.1. 속성 정의 파일 Sample
53.3.7.2. 매핑 XML 파일 Sample
53.3.7.3. 테스트 코드 Sample
53.3.8. CLOB, BLOB
53.3.8.1. Oracle 9i 이상일 경우
53.3.8.2. Oracle 8i일 경우
53.3.9. Named Parameter 'vo' 활용
53.3.9.1. 속성 정의 파일 Sample
53.3.9.2. 매핑 XML 파일 Sample
53.3.9.3. 테스트 코드 Sample
53.3.10. extends AbstractDAO
53.3.10.1. 매핑 XML 파일 Sample
53.3.10.2. DAO 클래스 코드 Sample
53.3.10.3. DAO 클래스 속성 정의 파일 Sample
53.3.10.4. DAO 클래스 테스트 코드 Sample
53.3.11. implements IResultSetMapper
53.3.11.1. 속성 정의 파일 Sample
53.3.11.2. 매핑 XML 파일 Sample
53.3.11.3. ResultSetMapper 코드 Sample
53.3.11.4. 테스트 코드 Sample
53.4. Resources
53.5. Extensions
53.5.1. MiPQueryService 활용
53.5.1.1. MiPQueryService 속성 정의 파일 Sample
53.5.1.2. 매핑 XML 파일 샘플
53.5.1.3. 테스트 코드 Sample
53.5.2. RiaQueryService
53.5.2.1. 속성 정의 파일 Sample
53.6. Resources
54. Scheduling Service
54.1. Quartz Scheduler
54.1.1. Advanced Quartz
54.1.2. Samples
54.2. Resources
55. Service Locator Service
55.1. ServiceLocator
55.1.1. Samples
55.2. Resources
56. Transaction Service
56.1. Declarative Transaction Management
56.1.1. Annotation을 이용한 Transaction 관리
56.1.1.1. Configuration
56.1.1.2. Transaction 관리 대상 정의
56.1.1.3. 테스트 클래스 실행
56.1.2. XML 정의를 이용한 Transaction 관리
56.1.2.1. Configuration
56.1.2.2. Transaction 관리 대상 정의
56.1.2.3. 테스트 클래스 실행
56.1.3. [참고] Propagation Behavior, Isolation Level
56.1.3.1. Propagation Behavior
56.1.3.2. Isolation Level
56.1.4. 테스트 케이스 상세
56.2. Programmatic Transaction Management
56.2.1. TransactionTemplate을 이용한 Transaction 관리
56.2.1.1. Configuration
56.2.1.2. Transaction 관리
56.2.1.3. 테스트 클래스 실행
56.2.2. TransactionManager를 직접 이용한 Transaction 관리
56.2.2.1. Configuration
56.2.2.2. Transaction 관리
56.2.2.3. 테스트 클래스 실행
56.3. Resources
57. Exception Handling
57.1. 서비스 구현 부분에서의 Exception 처리
57.2. Data Access 부분에서의 Exception 처리
57.3. Exception 처리 비용
X. Web Services
58. Web Services
58.1. Web Services 개념
58.1.1. Architecture
58.1.2. SOAP(Simple Object Access Protocol)
58.1.3. WSDL(Web Services Description Language)
58.1.4. 기술 표준
58.2. 구현 기술
58.2.1. JAX-RPC vs. JAX-WS
58.2.2. XML Schema
58.2.3. 기타 구현 기술
58.3. Web Services Framework
58.3.1. Web Services Framework 종류
58.3.2. Apache CXF 특징
58.4. Tools
59. JAX-WS Frontend
59.1. Web Service 작성
59.1.1. Samples
59.2. Spring Configuration XML - jaxws:endpoint tag 사용
59.2.1. Samples
59.3. Spring Configuration XML - jaxws:server tag 사용
59.3.1. Samples
59.4. Server: JAX-WS Frontend API 사용
59.4.1. Samples
59.5. Spring Configuration XML - jaxws:client tag 사용
59.5.1. Samples
59.6. Client: JAX-WS Frontend API 사용
59.6.1. Samples
59.7. Annotation 작성
59.7.1. @WebService (javax.jws.WebService)
59.7.2. @WebParam (javax.jws.WebParam)
59.7.3. @WebMethod (javax.jws.WebMethod)
59.7.4. @OneWay (javax.jws.OneWay)
59.7.5. @WebResult (javax.jws.WebResult)
59.7.6. Samples
59.8. [참고] Spring Configuration XML Schema
59.9. Resources
60. Simple Frontend
60.1. Web Services 작성
60.1.1. Samples
60.2. Server: Simple Frontend API 코드 사용
60.2.1. Samples
60.3. Spring Configuration XML - simple:server tag 사용
60.3.1. Samples
60.4. Client: Simple Frontend API 코드 사용
60.4.1. Samples
60.5. Spring Configuration XML - simple:client tag 사용
60.5.1. Samples
60.6. [참고] Spring Configuration XML Schema
60.7. Resources
61. Spring Support
61.1. Web Services 작성
61.1.1. Samples
61.2. Spring Configuration XML - simple:server tag 사용
61.2.1. Samples
61.3. Spring Configuration XML - simple:client tag 사용
61.3.1. Samples
61.4. Spring Configuration XML - ClientProxyFactoryBean 사용
61.4.1. Samples
62. Databinding
62.1. JAXB Databinding
62.1.1. Server Configuration
62.1.1.1. Samples
62.1.2. 유의 사항
62.1.2.1. SEI 클래스에서 정의되지 않은 Java Type 클래스가 Runtime시 Databinding되어야 하는 경우
62.2. Aegis Databinding
62.2.1. Server Configuration
62.2.1.1. Samples
62.2.2. Client Configuration
62.2.2.1. Samples
62.3. MTOM Databinding
62.3.1. Server Configuration
62.3.1.1. Samples
62.3.2. Client Configuration
62.3.2.1. Samples
62.3.3. 참고 - MTOM에 관련된 내용
62.4. Resources
63. Asynchronous Invocation
63.1. Server Configuration
63.1.1. Samples
63.2. Client Configuration
63.2.1. Samples
63.3. Resources
64. RESTful Services
64.1. JAX-RS 활용한 RESTful 서비스 구현
64.1.1. Server Configuration
64.1.1.1. Samples
64.1.2. Client Configuration
64.1.2.1. Samples
64.2. HTTP Binding(JRA) 활용한 RESTful 서비스 구현
64.2.1. Server Configuration
64.2.1.1. Samples
64.2.2. Client Configuration
64.2.2.1. Samples
64.2.3. 유의 사항
64.2.3.1. Samples
64.3. HTTP Binding(Naming Convention) 활용한 RESTful 서비스 구현
64.3.1. Server Configuration
64.3.1.1. Samples
64.3.2. Client Configuration
64.3.2.1. Samples
64.4. JAX-WS Provider/Dispatch API 활용한 RESTful 서비스 구현
64.4.1. Server Configuration
64.4.1.1. Samples
64.4.2. Client Configuration
64.4.2.1. Samples
64.5. [참고] Spring Configuration XML Schema
64.6. Resources
65. WAS(Web Application Server) Configuration
65.1. Tomcat
65.1.1. Tomcat 5.5.23
65.2. JEUS
65.2.1. JEUS 5
65.2.2. JEUS 6
65.3. WebLogic
65.3.1. WebLogic 9.2, 10.1

I.Overview

Anyframe은 Spring 기반에서 다양한 best-of-breed 오픈 소스를 통합 및 확장하여 구성한 어플리케이션 프레임워크와 MVC 아키텍처를 준수하여 웹 어플리케이션의 프리젠테이션 레이어를 구조적으로 개발할 수 있도록 지원하는 웹 프레임워크를 제공한다. 또한 프레임워크를 기반으로 업무용 프로그램 개발을 효과적으로 진행할 수 있도록 기술 공통 서비스, 템플릿 기반의 프로젝트 구조 및 샘플 코드, 매뉴얼 등을 제공함으로써 설계 및 개발 기간을 단축하고 유지보수를 용이하게 진행할 수 있도록 지원한다.

1.특징

Anyframe은 다음과 같은 특징을 제공한다.

  • 오픈 소스 통합 및 Best Practice 제공을 위한 플러그인 환경 제공 : 사용자가 원하는 Plugin 들을 적절히 선택하고 설치함으로써 해당 프로젝트에 최적화된 샘플 프로젝트를 손쉽게 구성할 수 있도록 지원한다. 자세한 내용은 Anyframe Plugins를 참고하도록 한다.

  • 순수 객체 중심의(POJO) 어플리케이션 개발 지원 : 프레임워크로 인해서 기본 설계와 상세 설계가 이중으로 진행되거나, 개발 시 설계 모델이 구현체와 불일치 되는 것을 줄이기 위해 순수 객체 중심의(POJO) 어플리케이션 개발을 지원한다.

  • Dependency Injection을 통한 의존 관계 처리 : 인터페이스 중심의 개발을 가이드하고 객체나 컴포넌트간의 참조 관계는 Dependency Injection을 통해 처리함으로써 구현체의 변경에 따른 영향력을 최소화한다.

  • 개발자는 비즈니스 로직에만 집중하여 구현 : 로깅, 트랜잭션, 예외처리 등과 같은 비기능 영역에 대한 코드가 업무 기능 개발 영역에서 분리될 수 있도록 함으로써, 개발자는 비즈니스 로직에만 집중하여 구현하도록 한다.

  • 재사용 가능한 기술 공통 서비스 제공 : DB 접근 및 SQL 처리, 캐쉬, WAS와 연동 등과 같은 중요 기능에 대해 재사용 가능한 기술 공통 서비스를 제공함으로써 보다 빠르고 안정적인 개발을 지원한다.

  • 선언적으로 트랜잭션 통제 : Java EE 환경과 독립적으로 JTA이나 JDBC 데이터 소스에 대해 별도의 트랜잭션 처리를 위한 코딩없이 간단한 설정만으로 선언적으로 트랜잭션을 통제할 수 있는 기능을 지원한다.

  • Singleton, Factory 패턴 등 유용한 패턴 실행 지원 : 직접적인 패턴 구현 없이도 Singleton, Factory 패턴 등의 실행을 지원함으로써, 어플리케이션 개발시 인스턴스의 생성 관리, 데이터 무결성 확보 등을 위해 유용한 패턴 등을 직접 구현하는 어려움을 해결해준다.

  • MVC Model2 아키텍처 제공 : Layered Architecture에 기반한 Java EE 웹 어플리케이션을 작성할 때 프리젠테이션 로직과 비지니스 로직을 완전히 분리하여 프리젠테이션 레이어를 구조적으로 개발할 수 있다.

  • 웹 화면 개발 시 필요한 공통 기능 제공 : 어플리케이션 개발에 공통적으로 필요한 화면흐름 제어, 에러처리, 일원화된 권한처리 등 다양한 부분을 프레임워크화하여 Model2 방식의 일관되고 쉬운 개발을 지원한다.

  • Struts/Spring MVC의 활용 최적화 : 프로젝트에서 많이 사용되고 있는 Struts와 Spring MVC를 기반으로 구성된 웹 프레임워크로 둘 중 어느 것을 선택하더라도 Struts와 Spring MVC의 기능들이 최적화된 형태로 활용될 수 있도록 가이드되고 있다.

  • 다양한 웹 클라이언트 기술과 용이한 연계 가능 : 최근 관심이 높아지고 있는 Ajax, 상용 X-internet 툴 등 다양한 웹 클라이언트 기술과 쉽게 연동되는 구조를 제공한다.

2.주요 기능

Anyframe을 통해 활용할 수 있는 주요 기능은 다음과 같다.

2.1.Lightweight 컨테이너

Anyframe의 Lightweight 컨테이너는 순수 POJO(Plain Old Java Objects) 기반 개발을 지원하며, 순수 POJO 기반으로 설계/개발된 모듈들을 엮어서 해당 어플리케이션이 제대로 된 기능을 제공할 수 있도록 지원한다. 반면에 EJB와 같은 컨테이너 기반에서 어플리케이션을 개발하기 위해서는 개발자가 해당 컨테이너에 종속된 인터페이스를 구현해야 하거나 정의된 컴포넌트 모델을 그대로 준수해야 한다. 즉, 전형적인 컨테이너는 정의된 개발 모델을 강제하기 때문에 어플리케이션 코드 내에 컨테이너 의존적인 코드가 추가될 수 밖에 없게 된다.

Anyframe의 Lightweight 컨테이너는 다음과 같은 특징을 가지고 있다.

2.1.1.POJO 기반 개발 지원

설계 결과물에 컨테이너 의존적인 코드를 추가하지 않아도 순수 POJO 기반으로 어플리케이션 개발이 가능하도록 지원한다. 즉, Lightweight 컨테이너 기반 개발시 프레임워크로 인한 기본 설계와 상세 설계가 이중으로 진행되거나, 개발시 설계 모델과 구현체가 불일치되는 것을 방지할 수 있다.

2.1.2.Dependency Resolution 지원

어플리케이션 구성 모듈간 의존 관계를 처리하기 위한 방법을 제공한다. 특정 모듈의 코드 내에서 참조할 모듈을 직접적으로 생성하여 참조함으로써 참조 모듈간에 tightly-coupled 되지 않도록 하기 위해, 대부분의 Lightweight 컨테이너들과 마찬가지로 DI(Dependency Injection)와 DL(Dependency Lookup)을 지원하다.

  • DI란 각 클래스 사이의 의존 관계를 설정 정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말한다. 컨테이너가 참조 관계를 자동적으로 연결시켜주기 때문에 개발자들이 컨테이너 API를 이용하여 의존 관계에 관여할 필요가 없게 되므로 특정 컨테이너의 API에 종속되는 것을 줄일 수 있고 개발자들은 단지 설정 파일에 참조 관계가 필요하다는 정보를 추가적으로 정의해 주기만 하면 된다.

  • DL은 의존 관계에 놓인 특정 모듈을 사용하기 위해 개발자가 해당 모듈의 소스 코드 내에서 리소스들을 관리하는 컨테이너를 통해 직접적으로 찾는 것을 말한다.

    • [1] Dependency Injection : 각 서비스 사이의 의존 관계를 속성 파일을 기반으로 컨테이너가 자동 처리

    • [2] Service Registration : 속성 파일을 기반으로 서비스 컨테이너의 서비스 목록에 해당 서비스 등록

    • [3] Service Lookup : 컨테이너에서 제공하는 API를 이용하여 사용하고자 하는 서비스 Lookup

    • [4] Retrieve Service Reference : 컨테이너는 해당 서비스의 인스턴스를 찾아 전달

    • [5] Invoke Methods : 클라이언트에서는 전달받은 인스턴스에 대해 특정 메소드 호출을 통해 원하는 기능 수행

2.1.3.Aspect Oriented Programming 지원

  • AOP는 어플리케이션 전체에 걸쳐 사용되나 쉽게 분리된 모듈로 작성하기 힘든 로깅, 인증, 권한체크, DB 연동, 트랜잭션, 락킹, 에러처리 등과 같은 공통 기능을 재사용 가능하도록 컴포넌트화 할 수 있는 기법이다. AOP에서는 이러한 공통 기능을 Crosscutting Concerns, 해당 어플리케이션이 제공하는 비즈니스 기능을 Core Concerns라고 지칭한다.

  • 즉, Core Concerns 모듈 내에 필요한 Crosscutting Concerns를 직접 추가하는 대신에 AOP에서는 Weaving이라는 작업을 통해 Core Concerns 모듈의 코드를 직접 건드리지 않고도 Core Concerns 모듈의 사이 사이에 필요한 Crosscutting Concerns 코드가 엮어져 동작되도록 한다. 이를 통해 AOP는 기존의 작성된 코드들을 수정하지 않고도 필요한 Crosscutting Concerns 기능을 효과적으로 적용해 낼 수도 있게 되는 것이다.

AOP에는 이외에도 새로운 용어가 많이 등장한다. AOP를 이용하여 개발을 수행하기 위해서는 본 매뉴얼의 Aspect Oriented Programming 부분을 참조하도록 한다.

2.1.4.Life-cycle 관리

Lightweight 컨테이너는 정의된 모듈의 Life-cycle 관리 즉, 해당 모듈들을 초기화시키고 종료시키는 역할을 수행함으로써 개발자가 비즈니스 로직에 집중하여 개발할 수 있게 된다.

2.1.5.신규 기능 추가 용이

XML 기반의 설정을 통해서 간단하게 컨테이너 기반 위에 신규 기능을 추가할 수 있도록 지원한다.

2.2.기술 공통 서비스

Anyframe은 자체 개발 또는 오픈 소스 활용 및 확장을 통해 어플리케이션 개발시 용이하게 재사용할 수 있는 Cache, DB 연결, 쿼리문 처리, 트랜잭션 관리, 로깅 등과 같은 다양한 기술 공통 서비스들을 제공한다. 이러한 기술 공통 서비스들은 앞서 언급한 Lightweight 컨테이너에서 동작 가능하도록 설계/개발되었으며, 인터페이스와 구현 클래스로 분리되어 구현되어 있으므로, 인터페이스 규약에 맞게 구현 클래스를 추가하거나 제공된 구현 클래스를 확장함으로써 언제든지 해당 어플리케이션의 용도에 맞게 변경이 용이하다. Anyframe에서 제공하는 주요 기술 공통 서비스는 다음과 같으며 이에 대한 보다 자세한 사항은 매뉴얼을 참조하도록 한다.

2.2.1.Generic Service

Java 5부터 지원하는 Generics 개념을 기반으로 개발되었으며, 도메인 클래스 기반의 Service 인터페이스/구현 클래스, DAO 인터페이스/구현 클래스(Hibernate/JPA, Query Service 지원) 등 기본 CRUD 메소드 기능이 모두 구현된 클래스를 직접 이용하거나 상속받아서 사용할 수 있는 기능을 제공하고 있다.

2.2.2.DynamicModule Service

공통/서비스/웹 등과 같이 타입별로 프로젝트를 구성하는 경우, 몇가지 설정을 추가함으로써 Dynamic Reloading 기능을 제공하는 서비스이다. 리로딩이 되는 단위는 각각의 프로젝트 단위이며 런타임 환경이 아닌 개발 환경에서 사용하도록 한다.

2.2.3.DataSource 서비스

주어진 데이터베이스에 연결하여 생성된 Connection 객체를 전달해주는 서비스이다.

2.2.4.Query 서비스

쿼리문이나 객체의 입력만으로 데이터베이스 내에 저장된 데이터 조작을 가능하게 하는 서비스이다. Query 서비스는 JDBC(Java Database Connectivity)를 이용한 데이터 액세스 수행 부분을 추상화함으로써 간편한 데이터 액세스 방법을 제공하고, JDBC 사용시 발생할 수 있는 공통 에러를 줄여준다.

2.2.5.ID Generation 서비스

시스템 개발 시 공통적으로 많이 쓰이는 기능 중의 하나로, 해당 항목에 대해 유일한 ID를 생성하기 위해 사용할 수 있는 서비스이다.

2.2.6.Properties 서비스

외부 파일이나 환경 정보에 구성되어 있는 key, value의 쌍을 내부적으로 가지고 있으며, 어플리케이션이 특정 key에 대한 value에 접근할 수 있도록 해주는 서비스이다.

2.2.7.Transaction 서비스

Transaction 관리에 대하여 일관성 있는 추상화된 방법을 제공하는 서비스로, Spring에서 제공하는 TransactionManager를 그대로 사용한다. Anyframe 매뉴얼에서는 Spring의 TransactionManager 활용 방법에 대해 가이드하고 있다.

2.2.8.Hibernate 서비스

객체 모델링(Object Oriented Modeling)과 관계형 데이터 모델링(Relational Data Modeling) 사이의 불일치를 해결해 주는 ORM 도구인 Hibernate를 효율적으로 사용할 수 있도록 세부 항목별로 활용 방법을 가이드하고 있다. 또한 Hibernate 기반에서도 입력 조건에 따라 HQL(Hibernate Query Language), Native SQL을 Dynamic하게 변경할 수 있도록 Velocity와 연계한 DynamicHibernateService를 추가로 제공한다.

2.2.9.Logging 서비스

Logging 서비스는 Log4j를 그대로 이용하며 이를 통해 테스팅 코드와 운영 코드를 동일하게 가져가면서, 로깅을 선언적으로 관리할 수 있고, 운영시 성능 오버헤드를 최소화할 수 있게 한다. Anyframe 매뉴얼에서는 Log4j 활용 방법에 대해 가이드하고 있다. 또한 Log4jdbc3를 이용하여 실행된 SQL을 Logging할 수 있는 방법도 제시한다.

2.2.10.Remoting 서비스

클라이언트 어플리케이션과 원격 어플리케이션에서 제공하는 서비스 간의 의사소통 기능을 제공하는 서비스이다. Anyframe 매뉴얼에서는 Spring Remoting 기능을 그대로 활용한 원격 기술 유형(RMI, Hessian/Burlap, HTTP Invoker)별 활용 방법에 대해 가이드하고 있다.

2.2.11.Web Services

Web Services는 인터넷 네트워크를 통하여 다수의 기존 어플리케이션 시스템을 표준화된 기술로서 상호 작용시키고, 이러한 표준 기술을 이용하여 모든 비즈니스를 가능하게 한다. Web Services는 언제, 어디에서건 원하는 정보나 서비스를 제공해 주는 역할을 수행하며 기존의 다른 소프트웨어처럼 완벽한 정의를 지정하여 구성하는 것이 아니라, 서로 주고받는 데이터 표준에 대한 정의를 규정함으로써 매우 유연하며 서로의 이질적인 운영시스템, 이질적인 프로그램 언어 간의 커뮤니케이션 차이를 극복해 주는 연결고리 역할을 해 준다. Anyframe 매뉴얼에서는 이러한 Web Services 기능에 대한 활용 방법에 대해 가이드한다.

2.3.웹 화면 개발 시 필요한 공통 기능

Struts와 Spring MVC를 확장하여 웹 화면 개발시 공통적으로 요구되는 다양한 추가 기능을 제공한다. 자세한 사항은 Anyframe 매뉴얼을 참고하도록 한다.

2.3.1.화면 흐름 제어(Screen Workflow Control)

화면 네비게이션 흐름을 JSP나 클래스 내에서 처리하지 않고 설정 파일(XML)을 통해서 제어할 수 있게 한다. 공통 에러 페이지 등 프로젝트에서 필요한 공통 페이지 설정도 설정 파일을 통해 가능하다.

2.3.2.공통 클래스 제공

프리젠테이션 레이어를 개발하는 개발자들은 Struts 사용 시 Action 클래스를, Spring MVC 사용 시 Controller 클래스를 개발해야 한다. 이를 위해 공통 Action 클래스와 Controller 클래스를 제공함으로써 이를 상속받은 모든 하위 클래스에서는 로깅, Exception 처리, Double Submit 방지 등의 기능을 사용할 수 있으며 특정 액션 수행 이전과 이후에 공통적으로 수행해야 할 로직을 구현하기 쉬워진다. 아래 그림은 Struts 사용 시 활용할 수 있는 공통 클래스의 모습이다.

2.3.3.국제화(i18N), 지역화(L10n) 지원

국제화를 지원한다는 것은 언어나 지역에 영향을 받는 부분과 영향을 받지 않는 코드를 분리하여 쉽게 지역화될 수 있게 만들었다는 것을 의미하는 것으로 소스 코드의 수정없이 다양한 언어를 지원할 수 있도록 한다.

2.3.4.사용자 입력값 유효성 검증(Validation)

Jakarta Commons 프로젝트의 하나로 Validation 모듈이 제공되는데 이를 Struts/Spring MVC와 연계하여 제공하고 있다. 유효성 검증 로직을 소스 코드 내에 작성하거나 소스 외부에서 설정 파일(XML)을 통해 관리할 수 있다. 이 외에 일반적으로 많이 사용되는 자바 스크립트를 통해 입력값 검증을 수행할 수도 있다.

2.3.5.Tag Library를 통한 View 개선

JSP 개발 시 Struts, Spring MVC에서 제공하는 기본 태그 라이브러리와 JSTL의 표준 태그 라이브러리 및 Anyframe에서 확장한 태그 라이브러리를 사용하여 JSP 내에 자바 코드를 추가하지 않고도 개발할 수 있도록 지원함으로써 HTML 디자인 개발과 프리젠테이션 레이어 개발을 분리하여 진행할 수 있게 한다. 목록 조회 페이지에서 페이지 네비게이션 정보를 표현하는 부분에 사용되는 태그 라이브러리가 그 대표적인 예이다.

2.3.6.에러 처리(Exception Handling)

특정 메소드 내에서 try~catch 구문을 이용하여 Exception을 처리하지 않고 설정 파일(XML)을 이용하여 선언적으로 처리할 수 있다. 에러 처리 페이지에서는 Exception 발생 시 설정된 메시지 키에 의한 메시지 내용을 화면에 디스플레이되도록 한다.

2.3.7.요청(Request) 권한 처리

Struts를 사용하는 경우, RequestProcessor 클래스의 processRoles() 메소드를 확장하여 구현함으로써 사용자가 해당 요청(Request)에 대한 권한을 가지고 있는지 판별할 수 있도록 한다. Spring MVC를 사용하는 경우, Spring Security 등 다른 오픈 소스와 연계하여 인증 및 권한 부분을 처리할 수 있다.

2.3.8.Double Submit 방지

설정 파일(XML)에 선언적인 방식으로 Double Submit 방지 기능을 정의하여 사용할 수 있다. 이를 통해 Form submit 중복(브라우저 Refresh, Submit 2회 이상 수행)으로 인한 오동작을 방지할 수 있는 기능을 제공한다.

2.3.9.다양한 웹 클라이언트(UI) 연계 지원(X-internet Integration)

X-internet과 Anyframe을 연계할 수 있도록 확장 모듈을 제공하고 있다.

II.Installation

Anyframe 4.0이후부터는 Anyframe Maven Command와 Anyframe Plugin을 이용하여 로컬에 별도 라이브러리나 샘플 프로젝트를 설치하지 않고도 Maven을 이용하여 프로젝트에 적합한 Anyframe 기반의 샘플 프로젝트를 구성할 수 있도록 지원한다. Plugin 기반의 프로젝트 기반 구성 방법 및 Anyframe Maven Command 방법, Plugin 정의 방법에 대해 자세히 살펴보도록 하자.

3.Installation

Anyframe 4.0.0 이후부터 오픈 소스 기반으로 어플리케이션을 개발할 때 요구되는 다양한 오픈 소스들이 통합된 템플릿 기반의 프로젝트 구조 및 샘플 코드를 Maven을 이용하여 자동으로 구성할 수 있도록 지원한다. 따라서, 어플리케이션 개발 초기에 프로젝트 특성에 맞는 개발 환경을 구성하는데 소요되는 시간을 대폭 줄이고, 프로젝트에 필요한 최적의 샘플을 제공받을 수 있게 될 것이다. 다음에서는 Anyfrae 4.0.0 이후부터 새롭게 변경된 Anyframe 설치 방법에 대해 알아보도록 하자.

3.1.[선택] Maven 설치 및 환경 설정

Maven(http://maven.apache.org/)은 POM(Project Object Model) 정보를 기반으로 대상 프로젝에 대한 빌드, 리포팅, 문서화 등을 지원하는 오픈소스 툴이다. Maven을 설치한 후, Anyframe 설치를 위한 환경 설정 방법에 대해 알아보도록 하자.

  1. Anyframe 설치 대상 PC에 Maven이 설치되어 있지 않은 경우, Maven을 설치한다. (본 문서에서는 Maven Ver.2.2.1을 기반으로 설치 작업을 진행할 것이다.) Maven 사이트로부터 Maven(apache-maven-2.2.1-bin.xxx)을 다운로드받은 후, 원하는 위치에 압축을 해제한다.

  2. 설치된 Maven을 기반으로 Anyframe 설치 작업을 진행할 때, Anyframe Repository(http://dev.anyframejava.org/artifactory/anyframe-repository)로부터 참조 라이브러리를 다운로드할 수 있도록 하기 위해 [MAVEN 설치 폴더]\conf\settings.xml 파일을 열고 다음과 같이 속성 정의를 추가한다.

    <profiles>
        <profile>
            <id>myprofile</id>
            <repositories> 
                <repository>
                    <id>anyframe</id>
                    <name>repository for Anyframe</name>                 
                    <url>http://dev.anyframejava.org/artifactory/anyframe-repository</url>
                    <snapshots>
                        <enabled>true</enabled>
                    </snapshots>
                </repository>	
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>anyframe-plugin</id> 
                    <name>repository for Anyframe</name>       
                    <url>http://dev.anyframejava.org/artifactory/anyframe-repository</url>           
                </pluginRepository>
                <pluginRepository>
                    <id>central</id> 
                    <name>Internal Mirror of Central Plugins Repository</name>       
                    <url>http://www.ibiblio.org/maven2/plugins</url>           
                </pluginRepository>
                <pluginRepository>
                    <id>remote</id> 
                    <name>Internal Mirror of Central Plugins Repository</name>       
                    <url>http://repo1.maven.org/maven2</url>           
                </pluginRepository>    
            </pluginRepositories>  
        </profile>
    </profiles>
    중략...
    <activeProfiles>
        <activeProfile>myprofile</activeProfile>
    </activeProfiles>
  3. 작업 대상 PC에서 MAVEN을 인식할 수 있도록 하기 위해서 시스템 변수로 MAVEN_HOME을 추가하고, 압축을 해제한 폴더 위치를 값으로 지정해준다.

    또한 다음과 같이 시스템 변수 PATH에 'MAVEN_HOME\bin'을 추가한다.

  4. 설치 및 환경 설정 작업이 완료되었다면 Maven이 성공적으로 설치되었는지 확인해 보도록 하자. Command 창을 띄우고 mvn -version과 같이 명령어를 입력하여 다음과 같은 정보가 에러없이 표시되는지 확인한다.

3.2.[필수] Foundation Plugin 설치

Anyframe 4.x에서는 다양한 오픈 소스들이 통합된 템플릿 기반의 프로젝트 구조와 샘플 코드, 참조 라이브러리 집합을 Plugin이라 칭하며, 다양한 유형의 Plugin을 제공한다. (Plugin에 대한 자세한 내용은 Anyframe Plugins를 참조하도록 한다.) 먼저 다른 Plugin 설치를 위한 기반을 제공하는 기본 Plugin인 Foundation Plugin을 설치해 보도록 하자.

  1. Command 창을 띄우고 다음과 같이 명령어를 입력하여 Foundation Plugin 설치를 시작한다.

    mvn archetype:generate 
        -DarchetypeCatalog="http://dev.anyframejava.org/maven/repo/archetype-catalog.xml"

    위와 같이 명령어를 입력하면 Command 창에 archetypeCatalog 속성값으로 정의된 http://dev.anyframejava.org/maven/repo/archetype-catalog.xml 파일 내의 Maven Archetype 목록이 제시될 것이다.

    위 그림에서와 같이 archetype-catalog.xml 파일 내에 정의된 anyframe.plugin.foundation 이라는 이름의 archetype이 Command 창에 제시됨을 알 수 있다.

  2. 제시된 Maven Archetype 목록 중 anyframe.plugin.foundation에 해당하는 번호('1')를 선택하고 요구하는 입력값을 추가 정의해준다.

    위 그림에서와 같이 설치 대상 PC에 anyframe.plugin.foundation-x.x.x.jar 라이브러리를 다운로드받지 않은 경우 Anyframe Repository로부터 이를 다운로드하고, 다음과 같은 인자에 대한 값을 입력하도록 요구할 것이다.

    표 3.1. Input Parameters for Installing Maven Archetype

    ParameterDescriptionDefault Value
    groupId설치 대상 프로젝트의 groupId이다.N/A
    artifactId설치 대상 프로젝트의 artifactId로써 해당 프로젝트를 설치할 대상 폴더명과 프로젝트명이 된다.myproject
    version설치 대상 프로젝트의 version이다.1.0.0
    package설치 대상 프로젝트 내 소스 코드에 대한 대표 패키지명이 된다.groupId로 정의한 값

  3. 다음은 Foundation Plugin 설치를 통해 구성된 샘플 프로젝트의 모습이다. 설치된 샘플 프로젝트명은 myproject이며, 하위에 다양한 용도의 폴더를 포함하고 있다.

    Plugin 설치 완료 후에는 [선택] Plugin 설치 확인 방법을 참조하여 정상 동작 여부를 확인하도록 한다.

기존 Maven 사용자 유의 사항

Anyframe에서는 Maven 기반에서 Anyframe 관련 라이브러리 다운로드시에 참조 관계에 놓인 모든 3rd-party 라이브러리들이 한꺼번에 다운로드되는 현상을 막기 위해 3rd-party 라이브러리들 간의 참조 관계를 끊은 상태로 Anyframe Repository에 배포하고 있다. 때문에 기존 Maven 사용자들은 Local Repository에 이미 존재하는 3rd-party 라이브러리가 가진 참조 관계에 문제가 생겨 Anyframe 설치시에 오류가 발생할 수 있다. 따라서 설치에 문제가 있는 경우에는 Local Repository를 삭제한 후 재설치해 볼 것을 권장한다.

3.3.[선택] Other Plugins 설치

Foundation Plugin 설치 완료 이후에는 Anyframe에서 제공하는 anyframe-maven-plugin을 이용하여 Anyframe에서 제공하는 다양한 Plugin들을 추가로 설치할 수 있게 된다. 이번에는 Foundation Plugin 이외 Plugin들을 설치하는 방법에 대해서 살펴보기로 하자. (본 절에서 언급되는 anyframe-maven-plugin에서 지원하는 모든 Maven 명령어 종류는 Anyframe Maven Commands를 참조하도록 한다.)

  1. Command 창을 띄운 후, Foundation Plugin 설치로 생성된 샘플 프로젝트가 있는 위치로 이동하여 다음과 같이 Maven 명령어를 입력함으로써 현재 설치 가능한 Plugin 목록 및 설치된 Plugin 목록을 확인할 수 있다.

    mvn anyframe:list

    다음은 Foundation Plugin 설치 후, 위와 같이 명령어를 실행하였을 때 Command 창에 나타난 결과를 보여주는 그림으로 전체 15개의 Plugin에 대해 설치 가능하며 Foundation Plugin만 설치된 상태임을 알 수 있다.

  2. Command 창의 샘플 프로젝트가 있는 위치에서 다음과 같이 Maven 명령어를 입력함으로써 원하는 Plugin을 선택하여 설치할 수 있다. pluginName이라는 속성에 대해서 설치 대상 Plugin 이름을 정의해주면 된다.

    mvn anyframe:install -DpluginName=hibernate
  3. Plugin 설치 완료 후에는 [선택] Plugin 설치 확인 방법을 참조하여 정상 동작 여부를 확인하도록 한다.

3.4.[선택] Plugin 설치 확인

특정 Plugin 설치 결과 구성된 샘플 어플리케이션이 정상 동작하는지 확인하기 위해서는 Command 창에 명령어를 직접 입력하거나 Eclipse를 이용할 수 있다.

  1. 샘플 어플리케이션에 대한 정상 동작 여부를 체크하기 위해서는 먼저 사용할 DB가 시작되어 있어야 한다. Anyframe Plugin 설치로 구성되는 샘플 프로젝트는 기본적으로 HSQL DB를 사용하도록 구성되어 있다. 따라서, [샘플 프로젝트 설치 폴더]/db/hsqldb/start.cmd (or start.sh) 파일을 더블 클릭하여 DB를 시작시키도록 한다. (HSQL DB가 아닌 다른 DB를 사용하고자 하면 [선택] 샘플 DB 변경 방법을 참조하도록 한다.)

    [샘플 프로젝트 설치 폴더]/db/hsqldb 폴더 내에 제공되는 sqltool.cmd (or sqltool.sh) 파일은 HSQL DB용 SQL Editor를 시작시키는 용도로 제공된다. DB 작업 수행 후 결과를 확인하고자 할 때 유용하게 활용할 수 있을 것이다.

  2. 다음에서는 Maven Command를 직접 입력하거나 Eclipse를 이용하여 샘플 어플리케이션을 시작시키는 방법에 대해서 살펴보도록 한다. (본 문서에서는 Jetty, Tomcat을 기준으로 설명이 진행된다.)

    • Maven Command를 직접 이용하여 Jetty 실행

      Command 창을 띄운 후, 샘플 프로젝트 설치 폴더 위치로 이동하여 다음과 같이 Maven 명령어를 입력하면 Jetty 기반에서 샘플 어플리케이션을 시작시킬 수 있다.

      mvn jetty:run

      Jetty가 정상적으로 실행되면 Started Jetty Server라는 INFO 레벨의 로그가 콘솔창에 보일 것이다.

    • Eclipse WTP, m2eclipse를 이용하여 Tomcat 실행

      Eclipse를 이용하여 샘플 어플리케이션을 실행시키려는 경우 Eclipse에서 Maven 관련 작업을 수행할 수 있도록 지원하는 m2eclipse plugin을 설치할 것을 권장한다. (m2eclipse plugin이 미설치된 경우, Update Site(http://m2eclipse.sonatype.org/update/)를 통해 설치할 수 있다.) m2eclipse plugin을 설치하지 않고 Eclipse WTP만을 이용하여 샘플 어플리케이션을 실행시키고자 하는 경우에는 본 문서의 [선택] Eclipse WTP만을 이용하여 Tomcat 실행 방법을 참조하도록 한다.

      설치된 샘플 프로젝트를 Eclipse 내로 import한 이후 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하여 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택함으로써 샘플 프로젝트 관련 Problems를 해결하도록 한다.

      이 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하여 컨텍스트 메뉴에서 Run As > Run on Server를 선택함으로써 Tomcat Server를 시작시키도록 한다.

  3. WAS가 정상적으로 시작되었으면 브라우저를 열고, 주소창에 http://localhost:8080/myproject (http://localhost:8080/${샘플 프로젝트 이름})와 같이 입력하여 샘플 어플리케이션이 정상적으로 실행되는지 확인해본다. 다음은 Foundation Plugin만 설치된 경우 샘플 어플리케이션의 초기 화면과 Foundation Sample 링크를 클릭하였을 때 이동한 화면에 대한 모습이다.

    Plugin이 추가 설치될 때마다 첫번째 화면에 추가된 Plugin에 대한 샘플을 확인하기 위한 링크가 추가될 것이다.

Anyframe에서 제공하는 Plugin을 설치함으로써 생성된 샘플 프로젝트는 기본적으로 Maven을 기반으로 하고 있으므로 Maven에서 지원하는 기능들을 동일하게 실행할 수 있다. 예를 들어 'mvn test'와 같은 명령어를 실행함으로써 샘플 프로젝트 내의 테스트 코드를 실행해 볼 수 있으며 'mvn package'와 같은 명령어를 실행함으로써 샘플 프로젝트를 war 형태로 패키징 가능하다.

3.5.[선택] 샘플 DB 변경

앞서 언급한 바와 같이 Plugin 설치를 통해 생성된 샘플 프로젝트는 기본적으로 HSQL DB를 기반으로 동작하도록 구성되어 있다. 그런데 만약 샘플 프로젝트의 실행 DB를 변경하고자 원한다면 다음과 같은 작업을 수행해야 한다.

  1. 설치된 샘플 프로젝트 하위의 pom.xml 파일을 열고 <properties/> 내에 정의된 DB 정보를 적절하게 수정한다.

    <properties>
        <db.name>hsqldb</db.name>
        <db.driver>org.hsqldb.jdbcDriver</db.driver>
        <db.url>jdbc:hsqldb:hsql://localhost/sampledb</db.url>
        <db.userId>sa</db.userId>
        <db.password></db.password>
        <db.lib>db/hsqldb/hsqldb-1.8.0.10.jar</db.lib>		
    </properties>

    위에 제시된 각 속성은 다음과 같은 의미를 지닌다.

    표 3.2. DB Propperties

    PropertyDescription
    db.name 해당 DB에 대한 대표명을 정의한다. 특정 Plugin이 실행해야 할 DB 스크립트를 포함하고 있는 경우 db.name 값을 포함하고 있는 스크립트(${plugin name}-insert-data-${db.name}.sql, ${plugin name}-delete-data-${db.name}.sql) 파일이 실행되도록 하는데 사용된다. (현재 제공되는 Plugin 중 실행 대상 DB Script를 포함하고 있는 Plugin은 mipsample과 security임. 예를 들어, db.name이 oracle일 때 security plugin을 설치하면 security-insert-data-oracle.sql 파일이 실행됨.)
    db.driver해당 DB에 대한 Driver Class명을 정의한다.
    db.url해당 DB에 대한 URL을 정의한다.
    db.userId해당 DB에 접근하기 위한 User Id를 정의한다.
    db.password해당 DB에 접근하기 위한 Password를 정의한다.
    db.lib 해당 DB에 접근하여 Connection을 얻어오기 위해 참조해야 하는 DB Library 위치를 정의한다. (샘플 프로젝트 위치 기준) 샘플 어플리케이션 실행시 DB Library를 인식할 수 있도록 하기 위해서 [샘플 프로젝트 설치 폴더]/src/main/webapp/WEB-INF/lib 폴더 내에 저장할 것을 권장한다. 만약 Maven 기반에서 실행하고자 한다면 로컬 Maven Repository 내에 DB Library가 존재해야 하며 샘플 프로젝트의 pom.xml 내에 이를 dependency 정보로써 추가해야 한다.

  2. [샘플 프로젝트 설치 폴더]/src/main/resources/spring 폴더 내에 위치한 spring 속성 정의 파일(context-datasource.xml, context-query.xml, context-hibernate.xml)의 DB 관련 속성 정의 부분을 수정한다.

    다음은 context-datasource.xml 파일의 일부이다. drvierClassName, url, username, password를 해당 DB에 맞게 수정하도록 한다.

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName" value="net.sf.log4jdbc.DriverSpy"/>
        <property name="url" value="jdbc:log4jdbc:hsqldb:hsql://localhost/sampledb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>	

    다음은 context-query.xml 파일의 일부이다. PagingSQLGenerator를 해당 DB에 맞게 수정하도록 한다. QueryService를 통해 제공되는 PagingSQLGenerator는 QueryService 설정 정보 중 [pagingSQLGenerator] 부분을 참고하도록 한다.

    <bean name="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
        <property name="pagingSQLGenerator" ref="pagingSQLGenerator"/>
        <property name="sqlRepository" ref="sqlLoader"/>				
    </bean>
    	
    중략...
    
    <bean id="pagingSQLGenerator" 
        class="anyframe.core.query.impl.jdbc.generator.HSQLPagingSQLGenerator"/>

    다음은 context-hibernate.xml 파일의 일부이다. Hibernate Plugin을 설치한 경우에 한해 Dialect 클래스를 해당 DB에 맞게 수정하도록 한다.

    <bean id="sessionFactory" 
        class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"
        lazy-init="true">
        <property name="dataSource" ref="dataSource" />
        중략...
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
                중략...
            </props>
        </property>
    </bean>
  3. [샘플 프로젝트 설치 폴더]/db/scripts 폴더 내에 있는 DB 스크립트를 이용하여 해당 DB에 테이블을 생성하고, 초기 데이터를 입력하도록 한다. 현재 Anyframe에서는 HSQL DB, Oracle DB 스크립트만을 제공하고 있다.

3.6.[선택] Eclipse WTP만 이용하여 Tomcat 실행

Eclipse WTP만을 이용하여 Maven Project가 아닌 일반 Dynamic Web Project 형태로 샘플 어플리케이션을 실행시키기 위해서는 해당 어플리케이션에서 참조 라이브러리를 인식할 수 있도록 필요한 라이브러리를 [샘플 프로젝트 설치 폴더]/src/main/webapp/WEB-INF/lib 폴더에 추가해주는 작업이 필요하다.

  1. Command 창을 띄운 후, 샘플 프로젝트가 있는 위치로 이동하여 다음과 같이 Maven 명령어를 입력함으로써 해당 어플리케이션이 참조하는 라이브러리들을 [샘플 프로젝트 설치 폴더]//src/main/webapp/WEB-INF/lib 폴더에 복사한다.

    mvn anyframe:inplace
  2. 설치된 샘플 프로젝트를 Eclipse 내로 import한 이후 해당 프로젝트에 대해 컴파일 에러가 존재하지 않는지 체크한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하여 컨텍스트 메뉴에서 Run As > Run on Server를 선택함으로써 Tomcat Server를 시작시키도록 한다.

대상이 되는 샘플 어플리케이션에 대해 특정 Plugin이 설치/삭제될 때마다 참조 라이브러리에 대해 변경 사항이 발생하게 된다. 따라서 Eclipse WTP만 이용하여 Tomcat을 실행하는 경우 변경된 사항을 해당 어플리케이션에 반영하기 위해서는 매번 Maven 명령어(mvn anyframe:inplace)를 실행해주어야 한다는 점에 유의하도록 한다.

4.Anyframe Plugins

Anyframe 3.x.x에서는 참조용 샘플 프로젝트와 라이브러리를 중심으로 개발자가 직접 프로젝트에 적합한 샘플 프로젝트를 구성해야 했다.

Anyframe 4.x.x부터는 Spring, Hibernate, CXF, Struts 등과 같은 다양한 오픈 소스들을 중심으로 참조 라이브러리와 샘플 코드를 엮어서 구성된 다양한 Plugin들을 제공함으로써 사용자가 원하는 Plugin들을 적절히 선택하고 설치함으로써 해당 프로젝트에 필요한 기능들을 갖춘 샘플 프로젝트를 손쉽게 구성할 수 있도록 지원한다. 이 샘플 프로젝트는 비즈니스 어플리케이션의 기반으로 활용할 수 있게 된다. 따라서 제공된 Plugin들을 재사용함으로써 단시간에 해당 프로젝트에 적합한 개발 기반을 구성할 수 있게 되는 것이다.

다음에서는 Anyframe에서 제공하는 Plugin 및 Plugin 구조에 대해서 살펴보고 Custom Plugin을 정의하는 방법에 대해 알아보도록 하자.

4.1.Anyframe Plugins

다음은 Anyframe에서 제공하는 Plugin 목록이다.

표 4.1. The List of Plugin

NoPlugin NameDescription
1foundation 모든 Plugin 설치를 위한 기반을 제공하는 Plugin이다. 샘플 프로젝트의 구조를 정의하고 있으며, SpringMVC + Spring + Query Service를 이용한 상품 관리 기능을 제공한다.
2cacheOSCache와 Anyframe에서 구현한 Cache Service를 이용한 상품 관리 기능을 제공한다.
3cxfApache CXF를 이용한 상품 관리 기능을 포함하고 있으며 생성된 WSDL을 확인할 수 있도록 한다.
4dynamicmodule특정 어플리케이션에 대한 Dynamic Reloading을 위해 필요한 참조 라이브러리만을 제공한다.
5hibernateHibernate와 Anyframe에서 구현한 Dynamic Hibernate Service를 이용한 상품 관리 기능을 제공한다.
6jasperJasperReports와 Spring을 연계하여 상품 현황을 HTML, PDF 형태의 Report로 보여준다.
7miplatformTOBE 소프트에서 제공하는 X-Internet 솔루션인 MiPlatform을 Anyframe과 연계하여 상품 관리 기능을 제공한다.
8mipsample TOBE 소프트에서 제공하는 X-Internet 솔루션인 MiPlatform을 Anyframe과 연계하여 활용할 수 있는 다양한 UI Sample을 제공한다. mipsample plugin 기능 확인을 위해 DB에 추가되어야 할 샘플 데이터가 함께 제공된다. (mipsample plugin이 정상 설치된 경우 별도로 DB 데이터를 추가할 필요는 없으며 DB 연계에 문제가 있는 경우에 한해 제공되는 DB 스크립트를 이용하여 샘플 데이터를 추가하도록 한다.)
9remotingSpring Remoting 기법 중 HttpInvoker를 활용하여 상품 관리 기능을 제공한다.
10schedulingQuartz를 이용하여 주기적으로 월별 상품 등록 현황을 생성하는 기능을 제공한다.
11security Spring Security를 이용하여 사용자별 Authentication, 역할별 Authorization 기능을 제공한다. security plugin 기능 확인을 위해 DB에 추가되어야 할 샘플 데이터가 함께 제공된다. (security Plugin이 정상 설치된 경우 별도로 DB 데이터를 추가할 필요는 없으며 DB 연계에 문제가 있는 경우에 한해 제공되는 DB 스크립트를 이용하여 샘플 데이터를 추가하도록 한다.)
12spring-optionalPlugin을 통해 설치되지 않는 Spring Optional 라이브러리만을 제공한다.
13strutsStruts를 이용하여 상품 관리 기능을 제공한다.
14test테스트 코드 실행에 필요한 참조 라이브러리만을 제공한다.
15webflowSpring Webflow를 이용하여 상품 관리 기능을 제공한다.

4.2.Foundation Plugin 구조

모든 Plugin 설치를 위한 기반을 제공하는 Plugin으로 Maven Archetype 형태로 구성되어 있어 명령어 mvn archetype:generate를 이용하면, Foundation Plugin 내에 정의된 프로젝트 템플릿을 정해진 위치에 설치할 수 있도록 하고 있다.

다음은 Foundation Plugin 내 src/main/resources 하위에 위치한 주요 구성 요소를 표현한 그림이다.

archetype-resources는 리소스 템플릿을 관리하기 위한 용도의 폴더로써 pom.xml 파일과 다음과 같은 하위 폴더를 가진다. pom.xml 파일 내에는 Foundation Plugin 설치 결과 생성될 샘플 어플리케이션 실행에 필요한 참조 라이브러리와 Maven 기반 샘플 어플리케이션 실행에 필요한 Maven Plugin 등이 정의되어 있다.

표 4.2. Structure of Foundation Plugin - archetype-resources

FolderDescription
.metadataPlugin 설치 정보를 관리하는 anyframe-plugins-metadata.mf 파일을 가진다.
.settingsEclipse 프로젝트 정보를 관리한다.
db/hsqldb샘플 어플리케이션 실행에 필요한 샘플 DB이다.
db/scripts DB 샘플 데이터 생성을 위해 사용된 DB 스크립트 파일을 제공한다. 현재 HSQL DB, Oracle DB 스크립트를 제공하고 있다.
src/main/java 소스 코드를 관리한다. 소스 코드의 패키지는 Plugin 이름인 foundation으로 시작한다. 단, Domain 클래스 및 Exception, Aspect 클래스는 각각 domain, common 패키지로 구분지어져 있다.
src/main/resources Spring, SpringMVC 기반의 어플리케이션 실행을 위한 속성 정의 파일과 메시지 파일, 쿼리문을 정의하고 있는 매핑 XML 파일들을 관리한다.
src/main/webapp웹 어플리케이션을 위한 웹 리소스(*.jsp, *.css, *.js ...)들을 관리한다.
src/test/java테스트 코드를 관리한다. 테스트 코드의 패키지는 Plugin 이름인 foundation으로 시작한다.
src/test/resources테스트 코드 실행에 필요한 리소스들을 관리한다.

META-INF는 리소스 템플릿에 대한 Meta 정보를 관리하기 위한 용도의 폴더로써 다음과 같은 하위 폴더를 가진다.

표 4.3. Structure of Foundation Plugin - META-INF

FolderDescription
maven 리소스 템플릿을 이용하여 샘플 프로젝트를 생성하기 위해 필요한 Meta 정보를 관리하는 archetype-metadata.xml 파일을 가진다.

4.3.Other Plugins 구조

Foundation Plugin 설치 후 anyframe-maven-plugin을 이용하여 Hibernate, Japser, Miplatform 등과 같은 Plugin들을 추가 설치할 수 있도록 구성하기 위하여 Foundation Plugin 이외의 Plugin들은 Maven Archetype과 흡사한 형태로 구성되어 있다.

다음은 Security Plugin 내 src/main/resources 하위에 위치한 주요 구성 요소를 표현한 그림이다.

plugin-resources는 리소스 템플릿을 관리하기 위한 용도의 폴더로써 pom.xml, plugin-descriptor.xml 파일과 여러 하위 폴더를 가진다.

pom.xml 파일 내에는 해당 Plugin 설치로 인해 추가될 기능 실행에 필요한 참조 라이브러리가 정의되어 있다.

또한 plugin-descriptor.xml 파일은 리소스 템플릿을 이용하여 샘플 프로젝트를 생성하기 위해 필요한 Meta 정보를 관리한다. plugin-resources 하위 폴더 내에 정의된 리소스를 샘플 프로젝트에 추가할 때 대표 패키지명을 추가할 것인지, Velocity를 이용하여 리소스 템플릿과 사용자의 입력값을 Merge한 리소스를 추가할 것인지 여부를 정의하는데 사용된다. 다음은 plugin-descriptor.xml 파일에 대한 예이다.

<?xml version="1.0" encoding="UTF-8"?>
<archetype-descriptor name="hibernate">
    <fileSets>
        <fileSet filtered="true" packaged="true">
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.java</include>
            </includes>
            <excludes>
                <exclude>**/*.properties</exclude>
            </excludes>
        </fileSet>
        중략...
   </fileSets>
</archetype-descriptor>

위 파일을 살펴보면 <fileSet/> 내에서는 특정 디렉토리에 속한 파일 그룹에 대해 filetered, packaged 속성값을 정의하고 있음을 알 수 있다. 만일 리소스 파일 중의 하나인 CataegoryService.java가 다음과 같이 구성되어 있다라고 가정해 보자.

#if($package && !$package.equals(""))
package ${package}.hibernate.sales.service;
#else
package hibernate.sales.service;	
#end

import java.util.List;

import anyframe.core.generic.service.GenericService;

#if($foundationpackage && !$foundationpackage.equals(""))
import ${foundationpackage}.domain.Category;
#else
import domain.Category;
#end

public interface CategoryService extends GenericService<Category, String> {

	List getDropDownCategoryList() throws Exception;
}

filtered 속성값이 true인 경우, #if ~ #end 부분이 Velocity Engine에 의해 해석되어 입력된 패키지명이 'anyframe' 이라면 'package anyframe.hibernate.sales.service;'로 입력된 패키지명이 없으면 'package hibernate.sales.service;' 구문으로 재조합되어 샘플 프로젝트 내에 CategoryService.java 파일이 추가될 것이다. filtered 속성값이 false인 경우, 정의된 모든 부분에 대한 변경없이 샘플 프로젝트 내에 CategoryService.java 파일이 추가될 것이다.

다음으로 packaged 속성에 대해 알아보도록 하자. plugin-resources/src/main/java 하위에 hibernate/sales/service/CategoryService.java라는 리소스가 정의되어 있다라고 가정해 보자. packaged 속성값이 true인 경우 입력된 패키지명이 'anyframe'이라면 샘플 프로젝트 내 src/main/java/anyframe/hibernate/sales/service 하위에 해당 리소스가 추가될 것이다.

다음은 plugin-resources 하위 폴더에 대한 설명이다.

표 4.4. Structure of Other Plugins - plugin-resources #1

FolderDescription
db/scripts 해당 Plugin 설치로 인해 추가될 기능이 기본 DB 데이터 이외의 데이터를 필요로 하는 경우를 위해 실행될 DB 스크립트를 포함한다. 현재 HSQL DB, Oracle DB 스크립트를 제공하고 있다.
src/main/java 소스 코드를 관리한다. 단, 모든 소스 코드의 패키지는 Plugin 이름으로 시작하도록 정의해야 한다.
src/main/resources Spring, SpringMVC 기반의 어플리케이션 실행을 위한 속성 정의 파일과 메시지 파일, 쿼리문을 정의하고 있는 매핑 XML 파일들을 관리한다. 단, Spring 속성 정의 파일명은 context-${plugin name}-xxx.xml, SpringMVC 속성 정의 파일명은 ${plugin name}-servlet.xml으로 정의해야 한다. 이 외, 리소스 파일이 필요한 경우에는 Plugin명과 동일한 폴더를 생성하여 관리하도록 한다.
src/main/webapp 웹 어플리케이션을 위한 웹 리소스(*.jsp, *.css, *.js ...)들을 관리한다. 단, JSP 파일은 WEB-INF/jsp 하위에 Plugin명과 같은 폴더를 생성하여 관리하도록 한다.

표 4.5. Structure of Other Plugins - plugin-resources #2

FolderDescription
src/test/java테스트 코드를 관리한다. 단, 테스트 코드의 패키지는 Plugin 이름으로 시작하도록 정의해야 한다.
src/test/resources테스트 코드 실행에 필요한 리소스들을 관리한다. src/main/resources와 동일한 규칙을 따라 리소스를 정의한다.

4.4.Custom Plugin 추가 정의

앞서 언급했던 Anyframe Plugin 외에 사용자가 Plugin을 자체 정의하여 프로젝트 내부적으로 재사용할 수도 있다. 여기에서는 Custom Plugin 정의 방법에 대해 살펴보도록 하자.

  1. Custom Plugin을 정의한다.

    1. Custom Plugin 정의를 위한 프로젝트를 생성하고 해당 프로젝트에 대한 pom.xml 파일을 다음과 같이 정의한다. groupId와 artifactId,version은 원하는 대로 적절하게 정의하면 된다.

      <?xml version="1.0" encoding="UTF-8"?>
      <project>
          <modelVersion>4.0.0</modelVersion>
          <groupId>anyframe</groupId>
          <artifactId>anyframe.plugin.custom</artifactId>
          <packaging>jar</packaging>
          <version>4.0.0</version>
      
          <build>
              <resources>
                  <resource>
                      <filtering>false</filtering>
                      <directory>src/main/resources</directory>
                  </resource>
              </resources>
          </build>
      </project>
    2. 해당 프로젝트 내에 src/main/resources 폴더를 생성하고 하위에 plugin-resources라는 폴더를 생성한다. plugin-resources 폴더 하위에는 Custom Plugin 설치 결과 샘플 프로젝트에 추가되어야 할 리소스(Java, XML, ... 등)들에 대한 템플릿과 plugin-descriptor.xml, pom.xml 파일을 정의해 주어야 한다.

    3. 샘플 프로젝트에 추가되어야 할 리소스(Java, XML, ... 등)는 Anyframe Plugin의 폴더 및 파일 명명 규칙에 맞추어 정의하도록 한다. (Anyframe Others Plugins에 대한 구조 참고)

    4. plugin-descriptor.xml 파일 내에는 앞서 정의한 리소스에 대한 Meta 정보를 정의한다. (Anyframe Others Plugins에 대한 구조 참고)

    5. 해당 Plugin 설치/삭제시 anyframe-maven-plugin을 통해 수행되는 기본 작업 외에 별도로 처리해야 하는 작업이 필요한 경우에는 Interceptor 클래스 구현을 통해 처리할 수 있다. Interceptor 클래스는 Custom Plugin 프로젝트 하위의 src/main/java 하위에 위치시키고 다음과 같이 구현할 수 있다.

      public class CustomPluginInterceptor {
      
          // 설치 후 별도 작업이 필요한 경우
          public void postInstall(String baseDir, File pluginJarFile)
              throws Exception {
              System.out.println("#### call postInstall ####");
          }
      
          // 삭제 후 별도 작업이 필요한 경우
          public void postUninstall(String baseDir, File pluginJarFile)
              throws Exception {
              System.out.println("#### call postUninstall ####"); 
          }
      
          // 설치 전 별도 작업이 필요한 경우
          public void preInstall(String baseDir, File pluginJarFile) throws Exception {
              System.out.println("#### call preInstall ####");
          }
      
          // 삭제 후 별도 작업이 필요한 경우
          public void preUninstall(String baseDir, File pluginJarFile)
              throws Exception {
              System.out.println("#### call preUninstall ####");
          }
      }
    6. 해당 Plugin 실행을 위해 별도로 DB 데이터가 추가되어야 하는 경우에는 plugin-resourcs/db/scripts 하위에 ${plugin name}-insert-data-${db.name}.sql, ${plugin name}-delete-data-${db.name}.sql 파일을 정의하고 필요한 DDL, DML을 정의해 두면 해당 Plugin 설치/삭제시에 anyframe-maven-plugin에 의해 실행될 것이다.

    7. Command 창에서 'mvn install' 명령어를 실행시킴으로써 로컬 Maven Repository에 Custom Plugin을 배포한다.

  2. Custom Plugin 목록을 정의한다.

    1. Custom Plugin 목록 정의를 위한 프로젝트를 생성하고 해당 프로젝트에 대한 pom.xml 파일을 다음과 같이 정의한다. 다음에 제시된 바와 같이 해당 프로젝트의 groupId는 anyframe, artifactId는 anyframe.plugin.custom-list로 정의해 주도록 한다.

      <?xml version="1.0" encoding="UTF-8"?>
      <project>
          <modelVersion>4.0.0</modelVersion>
          <groupId>anyframe</groupId>
          <artifactId>anyframe.plugin.custom-list</artifactId>
          <packaging>jar</packaging>
          <version>4.0.0</version>
          <name>Define Custom Plugin List</name>
      	
          <build>
              <resources>
                  <resource>
                      <filtering>false</filtering>
                      <directory>src/main/resources</directory>
                  </resource>
              </resources>
          </build>	
      </project>
    2. 해당 프로젝트 내에 src/main/resources 라는 폴더를 생성하고 plugin-list.xml 파일을 다음과 같이 정의한다.

      <list>
          <plugin>
              <pluginName>custom</pluginName>
              <groupId>anyframe</groupId>
              <artifactId>anyframe.plugin.custom</artifactId>
              <version>4.0.0</version>
              <interceptor>custom.interceptor.CustomPluginInterceptor</interceptor>
          </plugin>					
      </list>

      <list/> 내에는 Custom Plugin 목록을 정의하며, <plugin/> 내에는 각 Custom Plugin에 대한 상세 정보를 정의한다. anyframe-maven-plugin은 Plugin 목록 조회시 이 정보를 읽게 될 것이다. 또한 해당 Plugin 설치/삭제시 anyframe-maven-plugin을 통해 수행되는 기본 작업 외에 별도로 처리해야 하는 작업이 있어 Interceptor를 구현한 경우에는 <interceptor/> 내에 Interceptor 클래스명을 기술해 주도록 한다.

    3. Command 창에서 'mvn install' 명령어를 실행시킴으로써 로컬 Maven Repository에 Custom Plugin 목록을 배포한다.

Command 창에서 'mvn anyframe:list' 명령어를 실행시켜 Custom Plugin이 설치 가능한 Plugin 목록에 추가되었는지 확인해 보도록 하자. 정상적으로 추가된 경우에는 Anyframe에서 제공하는 다른 Plugin과 동일하게 설치/삭제가 가능해진다.

5.Anyframe Maven Commands

Anyframe에서는 자체 구현한 anyframe-maven-plugin이라는 Maven Plugin을 제공함으로써 Anyframe Repository로부터 다양한 Anyframe Plugin들을 다운로드하여 설치/삭제할 수 있도록 지원한다.

5.1.anyframe-maven-plugin 정의 확인

Maven 기반의 프로젝트를 대상으로 anyframe-maven-plugin을 사용하기 위해서는 해당 프로젝트의 pom.xml 파일 내에 다음과 같은 정의가 추가되어 있는지 확인해 보도록 한다.

<project>
    중략...
    <build>
        <plugins>
            중략...						
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>anyframe-maven-plugin</artifactId>
                <version>4.0.0</version>
            </plugin>
        </plugins>
    </build>
</project>

5.2.Anyframe Maven Commands

anyframe-maven-plugin은 Anyframe만의 고유 기능을 수행하기 위해 다양한 Maven Mojo(Maven-old-java-object) 클래스들로 구성되어 있으며 각 Mojo는 한 개의 Goal과 매핑된다. 여기에서 Goal이란 Maven에서 사용되는 용어로써 한번의 실행으로 이루어지는 특정 기능 단위라고 볼 수 있다. 다음은 anyframe-maven-plugin을 구성하는 Goal의 목록으로써 사용자는 Command 창에서 mvn anyframe:${goal}과 같이 명령어를 입력함으로써 원하는 기능을 실행할 수 있게 된다.

표 5.1. The List of Goal

GoalDescription
help사용 가능한 Command 목록을 보여준다.
install Foundation Plugin 설치후, 생성된 샘플 프로젝트에 특정 Plugin을 설치한다. 다음과 같은 Option을 가진다.

pluginName : 설치 대상 Plugin 명을 정의한다.

package : 설치 대상 Plugin으로 인해 추가될 샘플 코드에 대표 Package를 부여할 수 있다.

uninstall 이미 설치된 Plugin을 삭제한다. 다음과 같은 Option을 가진다.

pluginName : 삭제 대상 Plugin 명을 정의한다.

list설치 가능한 Plugin 목록과 Plugin 설치 현황을 보여준다.
install-listPlugin 설치 현황을 보여준다.
inplace pom.xml 파일을 기반으로 하여 Plugin 설치로 생성된 샘플 프로젝트 하위의 src/main/webapp/WEB-INF/lib 폴더 내로 샘플 어플리케이션 실행에 필요한 모든 참조 라이브러리를 다운로드한다.

III.Spring

Spring은 객체의 라이프 사이클을 관리하고 객체들간의 의존 관계를 최소화할 수 있는 Lightweight 컨테이너를 제공한다. 다음은 Spring Lightweight 컨테이너의 주요 특징이다.

  • POJO 기반 개발 지원

    설계 결과물에 컨테이너 의존적인 코드를 추가하지 않아도 순수 POJO 기반으로 어플리케이션 개발이 가능하도록 지원하다. 즉, Lightweight 컨테이너 기반 개발시 프레임워크로 인한 기본 설계와 상세 설계가 이중으로 진행되거나, 개발시 설계 모델과 구현체가 불일치되는 것을 방지할 수 있다.

  • Dependency Resolution 지원

    어플리케이션 구성 모듈간 의존 관계를 처리하기 위한 방법을 제공한다. 특정 모듈의 코드 내에서 참조할 모듈을 직접적으로 생성하여 참조함으로써 참조 모듈간에 tightly-coupled 되지 않도록 하기 위해, 대부분의 Lightweight 컨테이너들과 마찬가지로 DI(Dependency Injection)을 지원하며, 이외에 DL(Dependency Lookup)도 가능하다.

  • Aspect Oriented Programming 지원

    AOP는 어플리케이션 전체에 걸쳐 사용되나 쉽게 분리된 모듈로 작성하기 힘든 로깅, 인증, 권한체크, DB 연동, 트랜잭션, 락킹, 에러처리 등과 같은 공통 기능을 재사용 가능하도록 컴포넌트화 할 수 있는 기법이다. AOP에서는 이러한 공통 기능을 Crosscutting Concerns, 해당 어플리케이션이 제공하는 비즈니스 기능을 Core Concerns라고 지칭한다. 즉, Core Concerns 모듈 내에 필요한 Crosscutting Concerns를 직접 추가하는 대신에 AOP에서는 Weaving이라는 작업을 통해 Core Concerns 모듈의 코드를 직접 건드리지 않고도 Core Concerns 모듈의 사이 사이에 필요한 Crosscutting Concerns 코드가 엮어져 동작되도록 한다. 이를 통해 AOP는 기존의 작성된 코드들을 수정하지 않고도 필요한 Crosscutting Concerns 기능을 효과적으로 적용해 낼 수도 있게 되는 것이다

  • Life-cycle 관리

    Lightweight 컨테이너는 정의된 모듈의 Life-cycle을 관리하여 해당 모듈들을 초기화시키고 종료시키는 역할을 수행함으로써 개발자가 비즈니스 로직에 집중하여 개발할 수 있게 된다.

  • 신규 기능 추가 용이

    XML 또는 Annotation 기반의 설정을 통해서 간단하게 컨테이너 기반 위에 신규 기능을 추가할 수 있도록 지원한다.

여기에서는 Spring Lightweight 컨테이너를 통해 지원되는 주요 기능들에 대해 살펴볼 것이다. 이와 함께 클라이언트 어플리케이션과 원격 어플리케이션에서 제공하는 서비스 간의 의사 소통을 위한 Spring Remoting 기법에 대해서도 알아보자.

6.IoC(Inversion of Control)

Anyframe은 Spring 기반에서 다양한 best-of-breed 오픈 소스를 통합 및 확장하여 구성한 어플리케이션 프레임워크를 포함하고 있다. Anyframe 4.0은 Spring Framework 2.5.6을 기반으로 하고 있다.

Spring Framework가 가지는 가장 핵심적인 기능이 IoC이다. IoC 개념은 과거에도 많은 곳에서 사용된 개념이지만 최근 Spring Framework과 같은 Lightweight Container 개념이 등장하면서 많은 개발자들에게 관심의 대상이 되고 있다. IoC 개념은 Spring Framework 뿐만 아니라 컨테이너 기능을 가지는 모든 영역에서 사용되고 있는 개념이므로 반드시 이해할 필요가 있다.

  • IoC(Inversion of Control)개념

    IoC는 Inversion of Control의 약자이다. 우리나라 말로 직역해 보면 "역제어"라고 할 수 있다. 제어의 역전 현상이 무엇인지 살펴본다. 기존에 자바 기반으로 어플리케이션을 개발할 때 자바 객체를 생성하고 서로간의 의존 관계를 연결시키는 작업에 대한 제어권은 보통 개발되는 어플리케이션에 있었다. 그러나 Servlet, EJB 등을 사용하는 경우 Servlet Container, EJB Container에게 제어권이 넘어가서 객체의 생명주기(Life Cycle)를 Container들이 전담하게 된다. 이처럼 IoC에서 이야기하는 제어권의 역전이란 객체의 생성에서부터 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다. Spring Framework도 객체에 대한 생성 및 생명주기를 관리할 수 있는 기능을 제공하고 있다. 즉, IoC Container 기능을 제공하고 있다.

    Inversion of Control(이하 IoC)이란?

    • Component dependency resolution, configuration 및 lifecycle을 해결하기 위한 Design Pattern

    • DIP(Dependency Inversion Principle) 또는 Hollywood Principle (Don't call us we will call you)라는 용어로도 사용

    • 특정 작업을 수행하기 위해 필요한 다른 컴포넌트들을 직접 생성하거나 획득하기 보다는 이러한 의존성들을 외부에 정의하고 컨테이너에 의해 공급받는 방법으로 동작

    이러한 IoC는 다음과 같은 장점을 가지고 있다.

    • 클래스 / 컴포넌트의 재사용성 증가

    • 단위 테스트 용이

    • Assemble과 configure를 통한 시스템 구축 용이

  • IoC와 Dependency Injection간의 관계

    Spring Framework의 가장 큰 장점으로 IoC Container 기능이 부각되어 있으나, IoC 기능은 Spring Framework이 탄생하기 훨씬 이전부터 사용되던 개념이었다. 그러므로 "IoC 기능을 Spring Framework의 장점이라고 이야기하는 것은 적합하지 않다."고 반론을 제기하면서 "새로운 개념을 사용하는 것이 적합하다."고 주장한 사람이 Martin Flowler이다. Lightweight 컨테이너들이 이야기하는 IoC를 Dependency Injection이라는 용어로 사용하는 것이 더 적합하다고 이야기하고 있다. Martin Flowler의 이 같은 구분 이후 IoC 개념을 개발자들마다 다양한 방식으로 분류하고 있으나 다음 그림과 같이 IoC와 Dependency Injection 간의 관계를 분류하는 것이 일반적이다.

    • Dependency Lookup

      저장소에 저장되어 있는 Bean에 접근하기 위하여 Container에서 제공하는 API를 이용하여 사용하고자 하는 Bean을 Lookup 하는 것을 말한다. 따라서, Bean을 개발자가 직접 Lookup하여 사용함으로써 Container에서 제공하는 API와 의존관계 발생하게 된다.

      • 객체 관리 저장소(Repository)

        모든 IoC Container는 각 Container에서 관리해야 하는 객체들을 관리하기 위한 별도의 저장소(Repository)를 가진다. Servlet Container는 web.xml에서 Servlet을 관리하고 있으며, EJB Container는 ejb-jar.xml에 설정되어 있는 정보들이 JNDI 저장소에 저장되어 관리되고 있다. 이처럼 Spring Framework도 POJO들을 관리하기 위하여 별도의 저장소로 XML 파일을 가지게 된다.

      • Dependency Lookup 예시

        구현 클래스는 다음과 같이 작성한다.

        public class IoCServiceImpl1 implements IoCService1, ApplicationContextAware {
            public void setApplicationContext (ApplicationContext context) {
                IoCService2 iocService2 = (IoCService2)context.getBean("IoCService2");
            }
        }

        속성 정의 파일은 다음과 같이 작성한다.

        <bean id="IoCService1" class="….IoCServiceImpl1">
            중략...
        </bean>
        <bean id="IoCService2" class="….IoCServiceImpl2">
            중략...
        </bean>
    • Dependency Injection (DI)

      각 클래스 사이의 의존관계를 빈 설정(Bean Definition)정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말한다. 컨테이너가 의존관계를 자동적으로 연결시켜주기 때문에 개발자들이 컨테이너 API를 이용하여 의존관계에 관여할 필요가 없게 되므로 컨테이너 API에 종속되는 것을 줄일 수 있다. 개발자들은 단지 빈 설정파일(저장소 관리 파일)에서 의존관계가 필요하다는 정보를 추가하기만 하면 된다. 또한 Dependency Injection은 Setter Injection과 Constructor Injection 형태로 구분한다.

      • Dependency Injection 예시

        구현 클래스는 다음과 같이 작성한다.

        public class IoCServiceImpl implements IoCService {	
            public void setDependencyBean(DepBean dependencyBean) {
                this.dependencyBean = dependencyBean;
            }
            중략... 
        }

        속성 정의 파일은 다음과 같이 작성한다.

        <bean id="IoCService" class="….IoCServiceImpl">
            <property name="dependencyBean" ref="depBean"/>
        </bean>
    • Dependency Lookup과 Dependency Injection의 차이점

      Bean을 개발자가 직접 Lookup하여 사용하는 것을 Dependency Lookup이라고 하고, Dependency Injection은 이와 달리 각 계층 사이, 각 클래스 사이에 필요로 하는 의존관계가 있다면 이 같은 의존관계를 Container가 자동적으로 연결시켜주는 것을 말한다. Dependency Lookup을 사용할 경우 Bean을 Lookup하기 위하여 Container에서 제공하는 API와 의존관계가 발생한다. 이처럼 Container API와 많은 의존관계를 가지면 가질수록 어플리케이션이 Container에 대하여 가지는 종속성은 증가할 수 밖에 없다. 따라서 가능한 Dependency Lookup을 사용하지 않는 것이 Container와의 종속성을 줄일 수 있게 된다. Container와의 종속성을 줄이기 위한 방법으로는 이후에 다루게 될 Dependency Injection을 통하여 가능하게 된다.

6.1.Basic

Spring Framework는 기본적으로 어플리케이션의 비즈니스 서비스를 구동시키고 관리하는 Spring Container와 이러한 Container에 의해 관리되는 Bean으로 구성된다. Bean은 Container를 통해서 인스턴스화되는 객체이며 Container에 의해 다른 Bean들과 Wiring(엮기)되고 관리된다.

6.1.1.Container와 Bean

Bean은 Spring Framework에서 어플리케이션의 중요 부분을 형성하고 Spring IoC Container에 의해 관리된다.

  • Bean 설정, 생성, Life Cycle 관리

  • Bean Wiring(엮기) - Bean들과 각각에 대한 Dependency 관계는 Spring IoC Container에 의해 사용되는 설정 메타데이터로 반영

6.1.2.Container

Spring IoC Container는 다음 두 가지 유형의 Container를 제공한다.

  • BeanFactory

    설 명
    설 명 Bean의 생성과 소멸 담당
    Bean 생성 시 필요한 속성 설정
    Bean의 Life Cycle에 관련된 메소드 호출
    다수의 BeanFactory 인터페이스 구현 클래스를 제공하며 이중 가장 유용한 것은 XmlBeanFactory임
  • ApplicationContext

    설 명
    BeanFactory의 모든 기능 제공
    ResourceBundle 파일을 이용한 국제화(I18N) 지원
    다양한 Resource 로딩 방법 제공
    이벤트 핸들링
    다양한 Resource 로딩 방법 제공
    이벤트 핸들링
    Context 시작 시 모든 Singleton Bean을 미리 로딩(preloading) 시킴-> 초기에 설정 및 환경에 대한 에러 발견 가능함
    다수의 ApplicationContext 구현 클래스 제공 (XmlWebApplicationContext, FileSystemXmlApplicationContext, ClassPathXmlApplicationContext)

    org.springframework.beans 와 org.springframework.context 패키지가 Spring Framework의 IoC Container를 위한 기본을 제공한다. BeanFactory는 객체를 관리하는 고급 설정 기법을 제공하고 ApplicationContext는 Spring의 AOP기능, 메시지 자원 핸들링, 이벤트 위임, 웹 어플리케이션에서 사용하기 위한 WebApplicationContext와 같은 특정 ApplicationContext 통합과 같은 기능을 추가 제공한다. 즉, BeanFactory가 설정 프레임워크와 기본 기능을 제공하는 반면 ApplicationContext는 BeanFactory의 모든 기능 뿐 아니라 전사적 중심의 기능이 추가되어 있다. ApplicationContext가 제공하는 부가 기능과는 별개로, ApplicationContext와 BeanFactory의 또 다른 차이점은 Singleton Bean을 로딩하는 방법에 있다. BeanFactory는 getBean() 메소드가 호출될 때까지 Bean의 생성을 미룬다. 즉 BeanFactory는 모든 Bean을 늦게 로딩(Lazy loading)한다. ApplicationContext는 Context를 시작시킬 때 모든 Singleton Bean을 미리 로딩함으로써, 그 Bean이 필요할 때 즉시 사용될 수 있도록 보장해준다. 즉, 어플리케이션 동작 시 Bean이 생성되기를 기다릴 필요가 없게 된다.

6.1.2.1.BeanFactory

Bean을 포함하고 관리하는 책임을 지는 Spring IoC Container의 실제 표현이다.가장 공통적으로 사용되는 BeanFactory의 구현체인 XmlBeanFactory 클래스는 XML 형태로 어플리케이션과 객체간의 참조 관계를 조합하는 객체를 정의함으로써 XML 설정 메타데이터를 기반으로 완전히 설정된 시스템이나 어플리케이션을 생성한다. 또한 아래의 예와 같이 XmlBeanFactory는 XML 파일에 기술되어 있는 정의를 바탕으로 Bean을 Loading해준다. (생성자에 org.springframework.core.io.Resource타입의 객체 넘겨줌)

BeanFactory factory = new XmlBeanFactory(new FileInputStream("beans.xml"));

org.springframework.beans.factory.BeanFactory인터페이스에 관한 API는 여기를 참고한다.

Resource ImplementationPurpose
org.springframework.core.io.ByteArrayResourceDefines a resource whose content is given by an array of bytes
org.springframework.core.io.ClassPathResourceDefines a resource that is to be retrieved from the classpath
org.springframework.core.io.DescriptiveResourceDefines a resource that holds a resource description but no actual readable resource
org.springframework.core.io.FileSystemResourceDefines a resource that is to be retrieved from the file system
org.springframework.core.io.InputStreamResourceDefines a resource that is to be retrieved from an input stream
org.springframework.web.portlet.context. PortletContextResourceDefines a resource that is available in a portlet context
org.springframework.web.context.support. ServletContextResourceDefines a resource that is available in a servlet context
org.springframework.core.io.UrlResourceDefines a resource that is to be retrieved from a given URL

6.1.2.2.ApplicationContext

다음은 org.springframework.context.ApplicationContext 인터페이스의 대략적인 구조이다.

자주 사용되는 ApplicationContext의 구현 클래스는 아래와 같다.

  • XmlWebApplicationContext - 웹 기반의 Spring 어플리케이션을 작성할 때 내부적으로 사용

  • FileSystemXmlApplicationContext - 파일 시스템에 위치한 XML 설정 파일을 읽어들이는 ApplicationContext

  • ClassPathXmlApplicationContext - 클래스 패스에 위치한 XML 설정 파일을 읽어들이는 ApplicationContext

ApplicationContext 구현 클래스를 아래와 같이 사용할 수 있다.

ApplicationContext context =  new FileSystemXmlApplicationContext("c:/beans.xml”);
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml”);

6.1.2.3.설정 메타데이터

Container에 의해 "인스턴스화, 설정, 그리고 조합[어플리케이션내 객체를]"하기 위한 설정 방법에 대해 알아 보기로 하자. 대부분은 간단하고 직관적인 XML 형태로 제공되며 XML 기반의 설정 메타데이터를 사용하여 Bean을 정의하도록 한다. 다음은 XML 기반의 설정 메타데이터의 기본 구조 예제이다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
    <bean id="…" class="…">
        <!-- collaborators and configuration for this bean go here -->
    </bean>
    <!-- more bean definitions go here -->
</beans>

XML 기반의 메타데이터 정의는 설정 메타데이터의 가장 많이 사용되는 형태이다. XML 외에 Java Properties 파일을 이용하거나 프로그램으로 처리(Spring의 Public API를 사용하여)함으로써, 설정 메타데이터를 제공할 수 있다. Spring IoC Container 자체는 설정 메타데이터의 형태로부터 분리될 수 있기 때문이다.

6.1.2.4.Spring IoC Container 인스턴스화 시키는 예제

  1. BeanFactory 사용한 예제

    Resource resource = new FileSystemResource("beans.xml");
    BeanFactory factory = new XmlBeanFactory(resource); 
    ClassPathResource resource = new ClassPathResource("beans.xml");
    BeanFactory factory = new XmlBeanFactory(resource);
  2. ApplicationContext 사용한 예제

    ApplicationContext context = new ClassPathXmlApplicationContext(new String ("beans.xml"));
    // of course, an ApplicationContext is just a BeanFactory
    BeanFactory factory = (BeanFactory) context;

6.1.2.5.XML 기반 설정 메타데이터 조합

XML 기반 설정 메타데이터는 다중 XML파일로 분리하여 정의할 수 있다. 여기서 주의할 점은 <import>를 <bean> 이전에 두어야만 하는 것이다.

<beans>
	<import resource="services.xml"/>
	<import resource="resources/messageSource.xml"/>
	<import resource="/resources/themeSource.xml"/>

	<bean id="bean1" class="…"/>
	<bean id="bean2" class="…"/>
</beans>

위의 예제에서 외부 Bean정의는 3개의 파일(services.xml, messageSource.xml, 과 themeSource.xml)로부터 로드된다. 모든 위치 경로는 import를 수행하는 XML 파일에 상대적이다. 그래서 이 경우에 messageSource.xml 과 themeSource.xml이 import 대상 XML 파일의 위치 아래의 resources 에 두어야 하는 반면에 services.xml은 import를 수행하는 파일과 같은 디렉토리나 클래스패스 경로 내에 두어야만 한다. 이 예제처럼 /는 실제로 무시된다. import된 파일의 내용은 <beans>를 가장 상위 레벨에 포함하는 스키마나 DTD에 따라 완전히 유효한 XML Bean 정의 파일이어야만 한다.

6.1.3.Beans

6.1.3.1.Bean

Spring IoC Container에 의해 관리되는 객체로 Container에 제공된 설정 메타데이터 내 정의(대개 XML <bean> 형태로)에 의해 생성되며 실제로 아래 표로 나타낸 주요 메타데이터 정보를 포함하는 BeanDefinition 객체로 표현한다.

주요 메타데이터 속성설명
idBean의 구분을 위한 정보로 해당 bean에 접근하기 위한 Key임
class정의된 Bean의 실제 구현클래스로 항상 full name으로 작성
scope정의된 Bean의 인스턴스 생성 유형 정의. singleton, prototype, request, session, globalSession 중 선택. Default는 singleton이며, 보다 자세한 Bean Scope에 대해서는 본 매뉴얼의 Extensions Bean Scope 을 참고하도록 한다.
init-method해당 bean이 초기화된 후 context에 저장되기 전 호출되는 초기화 메소드 정의
desrtoy-method해당 bean 제거 시 호출되는 메소드 정의
factory-method해당 bean 생성 시 생성자를 사용하지 않고 특정 factory method를 호출하여 생성 시 정의
lazy-inittrue/false 값을 가지며 해당 bean이 호출되기 전에 초기화 시킬지 여부를 결정함. Default는 false이며 true인 경우, 해당 bean이 호출되는 시점에 초기화됨

6.1.3.2.Bean 명명하기

Bean 정의 시 Bean들을 구분하기 위해 'id' 혹은 'name' 속성을 사용하는데 'id'를 사용하는 경우, 하나의 Bean은 Container내에서 Unique한 id를 가지도록 한다. 일반적으로 Bean을 명명할때 인스턴스 필드명에 대한 표준 Java 규칙을 사용한다. Bean 이름은 소문자로 시작하고 camel-cased(첫 번째 단어는 소문자로 시작하고 두 번째 단어는 대문자로 시작)된다. 이러한 이름의 예제는 ‘categoryService', 'productDao', 'loginController' 등이다. Bean을 명명하는 일관적인 방법을 적용하는 것은 설정을 좀 더 읽기 쉽고 이해하기 쉽도록 만들어준다. 이러한 명명표준을 적용하는 것은 어려운 일이 아니다. Spring AOP를 사용한다면 특정 Bean 이름과 관련된 Bean의 세트에 advice를 적용할 때 용이해질 수 있다.

6.1.3.3.Bean 인스턴스화

  • 생성자를 이용한 인스턴스화

    특정 인터페이스를 구현하거나 특정 형태로 코딩 할 필요가 없다.

    <bean id="exampleBean" class="examples.ExampleBean"/>
    <bean name="anotherExample" class="examples.ExampleBeanTwo"/>

  • static factory 메소드를 사용한 인스턴스화

    Bean 객체가 factory 메소드를 호출하여 생성되는 것으로, 반환 객체의 타입을 명시하지 않고 factory 메소드를 포함하는 클래스를 정의하고 있음에 주의한다. 아래 예제에서 createInstance() 메소드는 static 메소드이어야 한다.

    <bean id="exampleBean" class="examples.ExampleBean2" factory-method="createInstance"/>

  • 인스턴스 factory 메소드를 사용한 인스턴스화

    'class' 속성을 정의하지 않고 'factory-bean' 속성에 factory메소드를 포함하는 Bean을 정의한다.

    <!-- the factory bean, which contains a method called createInstance() -->
    <bean id="myFactoryBean" class="…"/>
    
    <!-- the bean to be created via the factory bean -->
    <bean id="exampleBean"
      factory-bean="myFactoryBean"
      factory-method="createInstance"/>
    중략...

6.1.4.How to refer to Beans

비즈니스 레이어와 프리젠테이션 레이어에서 Spring Bean에 접근하는 방법에는 여러 가지가 있다.

6.1.4.1.비즈니스 레이어

비즈니스 레이어에서 사용하고자 하는 Spring Bean에 접근하는 방법은 크게 2가지 형태로 구분할 수 있다. Dependency Lookup과 Dependency Injection 방식이 그것이다.

  • Dependency Lookup

    저장소에 저장되어 있는 Bean에 접근하기 위하여 사용하고자 하는 Bean을 Lookup 한다. 이때 Bean을 개발자가 직접 Lookup하여 사용함으로써 Container에서 제공하는 API와 의존관계가 발생한다. Spring IoC 컨테이너 Dependency Lookup에 대한 자세한 사항은 본 매뉴얼의 IoC 를 참고한다.

    구현 클래스는 다음과 같이 작성한다.

    public class IoCServiceImpl1 implements IoCService1, ApplicationContextAware {
        public void setApplicationContext (ApplicationContext context){
            IoCService2 iocService2 = (IoCService2)context.getBean("IoCService2");
        }
    }

    속성 정의 파일은 다음과 같이 작성한다.

    <bean id="IoCService1" class="….IoCServiceImpl1">
        중략...
    </bean>
    <bean id="IoCService2" class="….IoCServiceImpl2">
        중략...
    </bean>

  • Dependency Injection

    각 클래스 사이에 필요로 하는 의존 관계가 있는 경우, 의존관계를 Container가 자동적으로 연결시켜 줌으로써 Container에서 제공하는 API와 의존관계가 없다. Spring IoC 컨테이너 Dependency Injection에 대한 자세한 사항은 본 매뉴얼의 Dependencies 를 참고한다.

    구현 클래스는 다음과 같이 작성한다. 이 예제에서는 Setter Injection 방식을 보여주고 있다.

    public class IoCServiceImpl implements IoCService {
        public void setDependencyBean(DepBean dependencyBean) {
            this.dependencyBean = dependencyBean;
        }
        중략... 
    }

    속성 정의 파일은 다음과 같이 작성한다.

    <bean id="IoCService" class="….IoCServiceImpl">
        <property name="dependencyBean" ref="depBean"/>
    </bean>

6.1.4.2.프리젠테이션 레이어

프리젠테이션 레이어에서 Spring Bean에 접근하는 방법은 비즈니스 레이어와 마찬가지로 Dependency Lookup과 Dependency Injection 방식 2가지 중 선택할 수 있는데 이때 사용하는 Web Framework이 무엇인지에 따라 사용 가능한 방식이 제한될 수 있으므로 주의하도록 한다. Web Framework 사용과 관련된 설정 방법은 Spring MVC를 참조하도록 한다.

  • Dependency Lookup (Struts)

    Web Framework으로 Struts를 사용하는 경우, Struts Action 내에서 Spring의 Web ApplicationContext를 얻어내어 Spring Bean을 Lookup하도록 한다. Spring에서 제공해주는 ActionSupport 클래스의 getWebApplicationContext() 메소드를 이용하여 ApplicationContext를 얻는다.

    Action 클래스는 다음과 같이 작성한다. 이 예제는 Anyframe 을 이용하여 작성된 코드로 Anyframe 의 DefaultActionSupport을 상속한 UpdateProductAction 클래스의 일부이다. productService Bean을 사용하고 있으며 이때 Bean의 id 값이 Action 클래스에 명시되어야 함에 유의하도록 한다.

    public class UpdateProductAction extends DefaultActionSupport {
        public ActionForward process(ActionMapping mapping, ActionForm form,
                HttpServletRequest req, HttpServletResponse res) throws Exception {
                
            ApplicationContext ctx = getWebApplicationContext();
            ProductService productService = (ProductService) ctx.getBean("productService");
            중략...
        }
    }

    속성 정의 파일은 다음과 같이 작성한다.

    <bean id="productService"
    	class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
        중략...
    </bean>

  • Dependency Injection (Spring MVC)

    Web Framework으로 Spring MVC를 사용하는 경우, Controller 클래스 내에서 Dependency Injection 방식을 이용하여 Spring Bean을 참조할 수 있다.

    Controller 클래스는 다음과 같이 작성한다. 이 예제는 Anyframe 을 이용하여 작성된 코드로 Anyframe 의 AnyframeFormController을 상속한 ProductController 클래스의 일부이다. productService Bean을 사용하고 있으며 이때 Bean의 id 값이 Spring MVC 속성 정의 파일에서 정의되고 있다.

    public class ProductController extends AnyframeFormController {
        private ProductService productService;;
    
        public void setProductService(ProductService productService) {
            this.productService = productService;
        }
        중략...
        
        public ModelAndView list(HttpServletRequest request,
              HttpServletResponse response) throws Exception {
              
            ProductSearchVO searchVO = new ProductSearchVO();
            bind(request, searchVO);
            Page resultPage = productService.getPagingList(searchVO);
            중략...
        }
    }

    속성 정의 파일은 다음과 같이 작성한다.

    <bean name="/foundationProduct.do"
              class="anyframe.example.foundation.sales.web.ProductController">
        <property name="productService" ref="foundationProductService"/>
        <property name="categoryService" ref="foundationCategoryService"/>
        <property name="idGenenrationService" ref="idGenerationService"/>
        <property name="methodNameResolver" ref="paramResolver" />
        <property name="success_addView" 
            value="/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp"/>
        <property name="success_add" 
            value="/foundationProduct.do?method=list"/>
        <property name="success_get" 
            value="/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp" />
        <property name="success_update" 
            value="/foundationProduct.do?method=list" />
        <property name="success_list" 
            value="/WEB-INF/jsp/foundation/sales/product/listProduct.jsp" />
        <property name="success_delete" value="/foundationProduct.do?method=list" />		
    </bean>

  • Dependency Lookup (Spring MVC)

    Web Framework으로 Spring MVC를 사용하는 경우, Controller 클래스가 아닌 일반 클래스에서 Dependency Lookup 방식으로 Spring Bean을 참조할 수 있다. 웹에서 Spring 설정 파일을 읽어들인 후, WebApplicationContext를 생성하고 이것을 해당 웹 어플리케이션의 ServletContext에 저장하므로 ServletContext에 접근 가능하다면 일반 클래스에서도 WebApplicationContext를 얻어낼 수 있게 된다.

    일반 클래스에서 다음과 같이 작성한다.

    WebApplicationContext ctx = 
        WebApplicationContextUtils.getWebApplicationContext(servletContext);
    ProductService productService = (ProductService) ctx.getBean("productService");
    중략...

    속성 정의 파일은 다음과 같이 작성한다.

    <bean id="productService"
        class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
        중략...
    </bean>

6.2.Dependencies

전형적인 기업용 어플리케이션은 한 개의 객체(또는 Spring내 Bean)로 만들어지지는 않는다. 가장 간단한 어플리케이션조차도 함께 작동하는 소량의 객체를 가진다는 것을 의심할 필요가 없을 것이다. 이 장에서는 독립적인 많은 수의 Bean들이 객체가 몇 가지 목표(대개 최종사용자가 원하는 것을 수행하는 어플리케이션)를 달성하기 위해 함께 작동하는 방법에 대해 알아보기로 한다.

6.2.1.Dependency Injection(DI)

각 클래스 사이의 의존관계를 빈 설정(Bean Definition)정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말한다. 컨테이너가 의존관계를 자동적으로 연결시켜주기 때문에 개발자들이 컨테이너 API를 이용하여 의존관계에 관여할 필요가 없게 되어 컨테이너 API에 종속되는 것을 줄일 수 있고 개발자들은 단지 Bean 설정파일(저장소 관리 파일)에서 의존 관계가 필요하다는 정보를 추가하기만 하면 된다. 이는 Setter Injection과 Constructor Injection 형태로 구분한다.

6.2.1.1.Setter Injection

setter 메소드 구현을 통해 초기화 시 Container로부터 의존 관계에 놓인 특정 리소스를 할당받는 방법으로 인자가 없는 생성자나 인자가 없는 static factory 메소드가 Bean을 인스턴스화하기 위해 호출된 후 Bean의 setter 메소드를 호출하여 실제화된다. 다음은 구현 클래스인 ProductServiceImpl.java 의 Setter Injection 부분이다.

public class ProductServiceImpl extends GenericServiceImpl<Product, String>
            implements ProductService {
    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }
    중략... 
}

다음은 Setter Injcetion 속성 정의 파일인 context-foundation-services.xml 의 일부이다.

<bean id="foundationProductService"
        class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
    <property name="productDao" ref="foundationProductDao" />
</bean>
	
<bean id="foundationProductDao" class="anyframe.example.foundation.sales.dao.impl.ProductDaoImpl"/>

6.2.1.2.Constructor Injection

Constructor 구현을 통해 초기화 시 Container로부터 의존 관계에 놓인 특정 리소스를 할당받는 방법으로 각각의 협력자를 표시하는 다수의 인자를 가진 생성자를 호출하여 실제화된다. 추가적으로, Bean을 생성하기 위한 특정 인자를 가진 static factory 메소드를 호출하는 것은 대부분 동등하게 간주될 수 있다. 다음은 구현 클래스인 ProductServiceImpl.java 의 Constructor Injection 부분이다.

public class ProductServiceImpl extends GenericServiceImpl<Product, String>
        implements ProductService {
    ProductDao productDao;
    
    public ProductServiceImpl(ProductDao productDao) {
        super(productDao);
        this.productDao = productDao;
    }
    중략...
}

다음은 Constructor Injcetion 속성정의 파일인 context-foundation-services.xml 의 일부이다.

<bean id="foundationProductService" 
        class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
    <constructor-arg ref="foundationProductDao"/>
</bean>
        
<bean id="foundationProductDao" class="anyframe.example.foundation.sales.dao.impl.ProductDaoImpl">
        중략...
</bean>

type 속성 정의를 이용하면, Constructor의 argument에 대한 클래스 타입을 명시적으로 정의할 수도 있다.

<bean id="foundationProductService" 
  class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
  <constructor-arg type="anyframe.example.foundation.sales.service.BeanA" ref="beanA"/>
  <constructor-arg type="anyframe.example.foundation.sales.service.BeanB" ref="beanB"/>
</bean>

Constructor의 argument 개수가 2개 이상이고, 동일한 클래스 타입의 argument가 존재할 경우 모호함을 없애기 위해, index 속성 정의를 통해 argument의 순서대로 할당할 값을 정의할 수 있다.

<bean id="foundationProductService"
		class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
    <constructor-arg index="0" ref="beanA" /> 
    <constructor-arg index="1" ref="beanB" /> 
</bean>

6.2.1.3.Setter Injection vs. Constructor Injection

Setter Injection 장점Constructor Injection 장점
- 생성자 Parameter 목록이 길어 지는 것 방지- 강한 의존성 계약 강제
- 생성자의 수가 많아 지는 것 방지- Setter 메소드 과다 사용 억제
- Circular dependencies 방지- 불필요한 Setter 메소드를 제거함으로써 실수로 속성 값을 변경하는 일을 사전에 방지
  • Circular dependencies

    Constructor Injection 사용 시 주의해야 한다. 다음과 같이 두 개의 서로 다른 Bean이 생성자 Argument로 서로의 Bean을 참조하는 경우가 그 예이다.

    <bean id="beanFirst" class="test.BeanFirst">
        <constructor-arg ref="beanSecond" />
    </bean>
    
    <bean id="beanSecond" class="test.BeanSecond">
        <constructor-arg ref="beanFirst" />
    </bean>

6.2.1.4.생성자 인자 분석

생성자 인자 분석 시 사용되는 방법에는 타입 대응과 인덱스가 있다.

  • 생성자의 인자 타입 대응(match)

    'type' 속성을 사용하여 생성자의 인자 타입을 명확하게 명시함으로써 간단한 타입으로의 타입 매치를 사용할 수 있다.

    <bean id="exampleBean" class="examples.ExampleBean">
        <constructor-arg type="int"><value>7500000</value></constructor-arg>
        <constructor-arg type="java.lang.String"><value>42</value></constructor-arg>
    </bean>

  • 생성자의 인자 인덱스

    생성자의 인자는 index 속성을 사용하여 명확하게 명시된 인덱스를 가질 수 있다. 또한 인덱스를 명시하는 것은 생성자의 인자들이 같은 타입을 가질 경우 발생하는 모호함의 문제도 해결한다. (인덱스는 0 부터 시작된다는 것에 주의하여야 한다.)

    <bean id="exampleBean" class="examples.ExampleBean">
        <constructor-arg index="0" value="7500000"/>
        <constructor-arg index="1" value="42"/>
    </bean>

6.2.2.Bean Property와 생성자 인자

Bean Property와 생성자의 인자는 다른 관리 Bean(협력자), 또는 인라인으로 정의된 값을 참조할 수 있다. Spring의 XML-기반의 설정 메타데이터는 이러한 목적을 위한 <property> 와 <constructor-arg> 내에서 많은 수의 하위 태그를 지원한다.

  • Primitive Type - 순수값 지원

    <value>는 사람이 읽을 수 있는 문자열 표현처럼 Property나 생성자의 인자를 명시한다.

    <bean id="myDataSource" destroy-method="close">
      <property name="driverClassName">
        <value>com.mysql.jdbc.Driver</value>
      </property>
    </bean>

  • <ref> 요소

    다른 bean에 대한 참조인 <ref>는 <constructor-arg> 또는 <property> 내부에 허용되는 마지막 요소이다. 이것은 Container에 의해 관리되는 다른 Bean을 참조하기 위해 Property의 값을 셋팅하는데 사용된다. 모든 참조는 궁극적으로 다른 객체에 대한 참조이지만 다른 객체의 id/name을 명시하는 방법은 3가지가 있다. <ref>의 bean 속성을 사용하여 대상 bean을 명시하는 것이 가장 일반적인 형태이고 같은 Container(같은 XML파일이든 아니든)나 부모 Container 내에서 어떠한 Bean에 대한 참조를 생성하는 것을 허용할 것이다. 'bean' 속성의 값은 대상 bean의 'id' 속성이나 'name' 속성의 값 중 하나가 될 것이다.

    • 타 Bean 참조

      <!-- ‘bean’ 속성 값은 타 Bean의 ‘id’ 속성 혹은 ‘name’ 속성이다. -->
      <ref bean="someBean"/>

      <!-- ‘local’ 속성 값은 동일 XML 파일 내 타 Bean의 ‘id’ 속성이다. -->
      <ref local="someBean"/>

    • parent context에 존재하는 타 Bean 참조(parent 속성 사용)

      <!-- in the parent context -->
      <bean id="accountService" class="com.foo.SimpleAccountService">
          <!-- insert dependencies as required as here -->	
      </bean>

      <!-- in the child (descendant) context -->
      <bean id="productService" class="com.foo.SimpleProductService">
          <ref parent="accountService"/>
      </bean>

  • inner Bean

    <property> 나 <constructor-arg> 내부의 <bean> 은 inner bean이라 불리는 것을 정의하기 위해 사용된다. inner bean 정의시 언급된 id나 name, scope값은 Container에 의해 무시되기 때문에 id나 name값을 명시하지 않는 것이 가장 좋다. inner bean은 언제나 익명이고 prototype 형태로 동작한다.

    <bean id="outer" class="…">
      <!-- instead of using a reference to a target bean, simply define the target inline -->
      <property name="target">
        <bean class="com.mycompany.Person"> 
        <!-- this is the inner bean -->
          <property name="name" value="Fiona Apple"/>
          <property name="age" value="25"/>
        </bean>
      </property>
    </bean>

  • Collection

    <list> , <set> , <map>과 <props>은 Java Collection의 List, Set, Map and Properties의 타입으로 매핑된다. 또한 객체 Array 타입의 경우에도 콤마(,)를 이용하여 값을 설정할 수 있다(ex. String]).

    <bean id="moreComplexObject" class="example.ComplexObject">
      <!-- results in a setAdminEmails(java.util.Properties) call -->
      <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@somecompany.org</prop>
        </props>
      </property>
    
      <!-- results in a setSomeList(java.util.List) call -->
      <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
      </property>
    
      <!-- results in a setSomeMap(java.util.Map) call -->
      <property name="someMap">
        <map>
            <entry>
                <key>
                    <value>entry key</value>
                </key>
                <value>entry value</value>
            </entry>
        </map>
      </property>
    
      <!-- results in a setSomeSet(java.util.Set) call -->
      <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
      </property>
    
      <!-- results in a setSomeArray(String[]) call -->
      <property name="someArray" value="str1,str2,str3,str4"/>  
      
    </bean>

  • Collection 병합

    부모 역할을 하는 <list> , <map > , <set> 또는 <props>를 정의하고 이를 상속받는 <list> , <map> , <set> 또는 <props>를 정의하는 것이 가능하다. 예를 들면, 자식 collection의 값은 부모 collection내 명시된 값과 자식 collection내 명시된 값을 병합하여 얻어진다.

    예제 설명) child Bean의 adminEmails Property의 <props>에서 merge=true속성을 사용하면, child Bean이 Container에 의해 실질적으로 분석되고 인스턴스화 될때, 부모의 adminEmails collection과 자식의 adminEmails collection이 병합된 형태의 adminEmails collection을 가지게 된다. 이 병합 행위는 <list> , <map>, 그리고 <set> collection 타입에 유사하게 적용된다. 단, <list>의 경우, 이 의미는 List collection 타입과 관련된다. 이를테면, value의 ordered collection의 개념은 유지관리된다. 부모값은 모든 자식 목록의 값에 선행한다. Map, Set, Properties collection 타입의 경우, Container에 의해 내부적으로 사용되는 Map, Set 그리고 Properties 객체 타입에 관련된 collection 타입의 영향을 받는다.

    <beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
         <property name="adminEmails">
           <props>
              <prop key="administrator">administrator@somecompany.com</prop>
              <prop key="support">support@somecompany.com</prop>
           </props>
          </property>
    </bean>
    <bean id="child" parent="parent">
         <property name="adminEmails">
           <!-- the merge is specified on the *child* collection definition -->
           <props merge="true">
             <prop key="sales">sales@somecompany.com</prop>
             <prop key="support">support@somecompany.co.uk</prop>
           </props>
          </property>
    </bean>
    <beans>

    위 설정 결과 adminEmails Collection은 다음과 같이 구성된다.

    administrator=administrator@somecompany.com sales=sales@somecompany.com support=support@somecompany.co.uk

  • <null> 요소

    <null>은 null 값을 다루기 위해 사용된다.

    <bean class="ExampleBean">
      <property name="email"><null/></property>
    </bean>

    위코드는 Java Code의 exampleBean.setEmail(null)과 동일하다. 다음과 같이 정의한 경우에는 Java Code의 exampleBean.setEmail("")과 동일하다.

    <bean class="ExampleBean">
      <property name="email"><value></value></property>
    </bean>

6.2.2.1.XML 기반의 설정 메타데이터 간략화

value나 Bean 참조를 위해 필요한 공통사항이다. 완전한 형태의 <value> 와 <ref>를 사용하는 것보다 간략화한 몇 가지 형태를 사용할 수 있다. <property>, <constructor-arg>, 그리고 <entry> 모두 완전한 형태의 <value> 요소 대신에 'value' 속성을 지원한다. 예를 들어 코드1이 코드2의 형태로 간략화 될 수 있다.

<!-- 코드 1 -->
<property name="myProperty"><value>hello</value></property>

<!-- 코드 2 -->
<property name="myProperty" value="hello"/> 

6.2.2.2.혼합된 Property 명(Compound Property) - shortcut 기능 제공

복합적인 형태의 Property 정의가 가능하다. 마지막 Property명을 제외한 나머지 Property는 null이 아니어야 함에 유의하도록 한다.

<bean id="foo" class="foo.Bar">
  <property name="fred.bob.sammy" value="123" />
</bean>
위 예제에서 foo bean은 bob Property를 가지는 fred Property를 가진다. 그리고 bob Property는 sammy Property를 가지고 마지막 sammy Property는 123값으로 셋팅된다. 이렇게 되도록 하기 위해서는 foo의 fred Property, 그리고 fred의 bob Property는 bean이 생성된 후에 null이 아니어야만 한다. 그렇지 않으면 NullPointerException이 던져질 것이다.

6.2.3.depends-on 속성 사용

'depends-on' 속성은 Bean 이전에 초기화되어야 하는 하나 이상의 Bean을 명시적으로 강제하기 위해 사용된다. 다음은 depends-on 속성이 설정되어 있는 context-foundation-services.xml 파일의 일부이다.

<bean id="foundationProductService"
		class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" 
		autowire="byType" depends-on="foundationProductDao">
</bean>

다중 bean에 의존성을 표시할 필요가 있다면 아래의 예와 같이 콤마, 공백 그리고 세미콜론과 같은 모든 유효한 구분자를 사용하여 'depends-on'속성의 값으로 bean 이름 목록을 정의할 수 있다. 그러나 이 ‘depends-on’ 속성을 사용하게 될 상황은 매우 드물다.

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
  <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

위의 예제는 beanOne Bean이 생성되기 이전에 manager Bean이 생성되어 특정 서버를 구동시켜놓거나 특정 리소스에 대한 작업을 수행해놓고 있어야 beanOne Bean이 정상적으로 동작하므로 강제적으로 manager Bean을 초기화시킨다.

6.2.4.Lazy Instantiation

기본적으로 Spring IoC Container가 Start될 때 singleton Bean에 대해서는 모두 인스턴스화한다.

- 특정 singleton Bean을 Container가 Start될 때 인스턴스화 시키지 않고 처음 Bean 요청이 들어왔을 때 인스턴스화 시키고자 하면 ‘lazy-init’ 속성을 설정한다. 다음은 Lazy Instantiation 속성이 설정되어 있는 파일인 context-foundation-services.xml 파일의 일부이다.

<bean id="foundationProductDao" 
    class="anyframe.example.foundation.sales.dao.impl.ProductDaoImpl" lazy-init="true">
<bean id="foundationProductService" 
    class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl"/>

- 모든 Bean들에 대해서 기본적으로 Lazy 인스턴스화 시키고자 하면 ‘default-lazy-init’ 속성을 설정하면 된다.

<beans default-lazy-init="true">
   <!-- no beans will be eagerly pre-instantiated -->
</beans>

6.2.5.Autowiring

Spring IoC Container는 Bean들 사이의 관계를 autowire 할 수 있다. 이것은 BeanFactory의 내용을 조사함으로써 Spring이 자동적으로 협력자(다른 bean)를 분석하는 것이 가능하다는 것을 의미한다. autowiring을 사용하면 명백하게 많은 양의 타이핑을 줄이고 Property나 생성자의 인자를 명시할 필요를 줄이거나 제거하는 것이 가능해진다. XML-기반의 설정 메타데이터를 사용할 때, Bean정의를 위한 autowire 모드는 <bean>의 autowire 속성을 사용하면 된다. <bean>의 autowire 속성에 정의할 수 있는 값은 다음과 같다.

속성설명
no[기본 설정] Autowiring 기능 사용 안 함
byNameProperty 명과 동일한 id나 name을 가진 Bean을 찾아 Autowiring 기능 적용
byType해당 Property 타입의 Bean이 하나 존재한다면 Autowiring되나 하나 이상 존재 시 UnsatisfiedDependencyException 발생됨. 만약 대응되는 Bean이 없다면 Property 셋팅 안됨
constructor이것은 byType과 유사하지만 생성자의 인자에 적용됨. BeanFactory내 생성자의 인자 타입과 맞는 Bean이 정확하게 하나가 아닐 경우 UnsatisfiedDependencyException 발생됨
autodetectconstructor 모드 수행 후 byType 모드가 수행됨
default <beans>의 default-autowire 속성에 설정한 autowire 모드가 해당 Bean에 적용됨

다음은 Autowiring 속성이 설정되어 있는 context-foundation-services.xml 파일의 일부이다.

<bean id="foundationProductService"
	class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" 
	autowire="byType" depends-on="foundationProductDao">
</bean>

6.2.5.1.장점

  • Property나 생성자의 인자를 XML에 설정할 필요 없음

  • XML 파일 크기 줄어듬

  • 참조 관계에 있는 타 Bean들의 변경 및 추가 시 XML 파일의 변경이 최소화됨

  • 동일한 이름의 Bean을 XML에 중복 정의하여 사용하는 혼동을 없애 줌

6.2.5.2.단점

  • Bean들의 관계가 명시적으로 문서화되지 않음으로써 기대되지 않는 결과를 가지지 않게 주의해야 함

  • 타입에 의한 Autowiring은 잠재적인 모호함을 가져올 수 있음

* Autowiring 대상에서 특정 Bean을 제외하려면 autowire-candidate 속성을 false로 설정해주어야 한다.

<bean id="bean" class="example.TestBean” autowire-candidate="false" />

6.2.6.Dependency Check

해당 Bean에 설정된 모든 Property들(Primitive Type/Collection 및 Bean 참조)이 제대로 설정되었는지 확인한다.

  • <bean>의 dependency-check 속성 설정

    모드설명
    none [기본 설정] 의존성 확인 안 함. 참조관계의 Bean이 존재하지 않는 경우 Property 설정 안 함
    simple Primitive Type과 collection을 위해 의존성 확인 수행
    object 참조관계의 Bean을 위해 의존성 확인 수행
    all simple과 object 모드를 모두 수행

    다음은 Dependency Check의 속성 정의 예시이다.

    <bean id="foundationProductService" 
        class="anyframe.example.….ProductServiceImpl" dependency-check="object">
        <property name="foundationProductDao" ref="foundationProductDao" />
    </bean>

    또한 다음과 같은 방법으로 모든 Bean들에 대해서 동일하게 Dependency Check 여부를 설정할 수 있다.

    <beans default-dependency-check="none" >
        <!-- no beans will be eagerly pre-instantiated -->
    </beans>

6.3.Method Injection

Dependency Injection의 방법인 setter injection과 constructor injection을 사용할 경우, Singleton Bean은 참조하는 Bean들을 Singleton 형태로 유지하게 된다. 그런데 특별한 경우에는 Singleton Bean이 Non Singleton Bean(즉, Prototype Bean)과 Dependency 관계를 가질 수 있다. 이 같은 상황이 발생할 때 Lookup Method Injection을 사용하여 해결하는 것이 가능하다. 동일한 상황에서 BeanFactoryAware를 구현하여 해결하는 방법도 존재하나 Spring Container API에 종속적으로 Bean 코드가 변경되므로 바람직한 해결 방법이 아니다.

  • Lookup Method Injection

  • Method Replacement

6.3.1.Lookup Method Injection

Singleton Bean이 Prototype Bean을 참조해야 할 경우 <lookup-method>를 설정한다. 다음은 Lookup Method Injection을 이용하여 참조 관계를 정의한 context=foundation-services.xml 의 일부이다.

<bean id="foundationProductService"
      class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
    <!-- method injection -->
    <lookup-method name="getProductDao" bean="foundationProductDao"/> 		
</bean>

<!-- change scope from singleton to prototype (non singleton) -->
<bean id="foundationProductDao" 
    class="anyframe.example.foundation.sales.dao.impl.ProductDaoImpl" scope="prototype"/>

해당 lookup 메소드는 다음과 같이 ProductDao를 리턴하는 형태로 메소드를 구현하도록 한다.

public class ProductServiceImpl … {
    public ProductDao getProductDao(){
    // do nothing - this method will be overrided by Spring Container
    return null;
    }
    중략...
}  

6.3.2.Method Replacement

이미 존재하는 기존의 메소드를 수정하지 않은 상태에서 메소드의 기능을 변경하고자 할 때 <replaced-method>를 이용한다. 사용 예제는 다음과 같다.

  • 구현 클래스

    Spring Framework에서 제공하는 MethodReplacer 인터페이스를 구현한 클래스를 생성하고, reimplement 메소드 내에 로직을 구성한다.

    import org.springframework.beans.factory.support.MethodReplacer;
    public class SayHelloMethodReplacer implements MethodReplacer {
        public Object reimplement(Object target, Method method, Object[] args) 
                throws Throwable {
    중략...

  • 속성 정의 파일

    <bean id="beanFirst" class="test.BeanFirst"/>
              
    <bean id="beanSecond" class=" test.BeanSecond">
        <replaced-method name="sayHello" replacer="methodReplacer">
            <arg-type>String</arg-type>
        </replaced-method>
    </bean>
    
    <bean id="methodReplacer" class="test.SayHelloMethodReplacer"/>

    위 속성 정의 파일에서는 BeanSecond 클래스의 sayHello 메소드 실행 시점에, 앞서 정의한 MethodReplacer가 적용되도록 정의하고 있음을 알 수 있다.

6.4.Bean과 Container의 확장

Spring Framework의 Container는 기본적으로 확장이 되도록 설계되어 있다. 모든 어플리케이션 개발자들이 확장하여 사용할 필요는 없고 확장할 필요성이 있는 경우에 확장하여 사용하도록 한다. 다음 각각의 항목 별로 기본적으로 제공되는 내용과 확장하여 사용할 수 있는 내용을 설명한다.

  • Bean Scope

  • Bean Life Cycle

  • Bean 상속

  • Container 확장

  • ApplicationContext 활용

6.4.1.Bean Scope

Spring Framework에서 지원하는 5가지 Scope에 따라 Bean의 인스턴스 생성 메커니즘이 결정된다. 서비스 Scope은 설계, 개발 단계에서 결정하기 어려우므로, 기본적으로는 Default Scope인 Singleton으로 개발하고, 추후 해당 서비스의 성격에 따라 Scope을 정의하는 것이 좋다.

  • <bean>의 scope 속성값

    속성설명
    singleton [기본 설정] Spring IoC Container 내에서 Bean 정의 당 하나의 Bean 객체 생성
    prototype 매번 같은 Type의 새로운 Bean 객체 생성
    request WebApplicationContext 유형의 Container 사용 시, Http request 당 하나의 Bean 객체 생성
    session WebApplicationContext 유형의 Container 사용 시, Http session 당 하나의 Bean 객체 생성
    globalSession WebApplicationContext 유형의 Container 사용 시, portlet context 내에서만 유효하며 global Http session 당 하나의 Bean 객체 생성

    이 외에도, custom scope을 통해 신규 Scope에 대해 정의할 수 있다.

6.4.1.1.Singleton

Singleton Scope은 기본 Scope으로 여러 개의 요청에 대해 하나의 Bean 인스턴스를 생성하여 제공한다. 따라서 Client Request마다 유지해야 하는 Data가 있다면, Singleton Scope의 서비스는 적합하지 않다. 다음은 Singleton Scope의 속성 정의 예시이다.

<bean id="foundationProductService" 
        class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" scope="singleton”>
    <property name="foundationProductDao" ref="foundationProductDao" />
</bean>
<bean id="foundationProductDao" 
        class="anyframe.example.foundation.sales.dao.impl.ProductDaoImpl”>
    중략...
</bean>

위와 같이 singleton scope을 정의 할 수 있지만 scope의 기본 설정값이 singleton이므로 따로 정의해야 할 필요가 없다.

6.4.1.2.Prototype

Prototype Scope은 요청시마다 Bean 인스턴스를 생성하여 제공한다. 따라서 여러 Client가 동시에 한 Bean 인스턴스에 접근할 수 없다. 다음은 Prototype Scope의 속성 정의 예시이다.

<bean id="foundationProductService" 
        class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" scope="prototype”>
    <property name="foundationProductDao" ref="foundationProductDao" />
</bean>

※ 일반적으로 인스턴스의 Singleton 여부를 판단하기 위해서 전역변수의 존재 여부를 이용한다. 즉, 전역변수가 존재하지 않은 인스턴스의 경우에는 Singleton, 전역변수가 존재하는 경우에는 Prototype 으로 정의할 수 있다. 그러나 해당 전역변수가 read-only인지 writable 가능한지에 따라서 이 같은 구분은 변경될 수 있다. 따라서 인스턴스를 Singleton으로 생성할지 Prototype으로 생성할지에 대한 여부에 대해서는 개발자들이 해당 Scope의 인스턴스가 메모리에서 어떻게 사용되는지를 이해하는 것이 가장 좋다.

  • Singleton

    - Shared objects with no state

    - Shared object with read-only state

    - Shared object with shared state : 이 경우에는 Synchronization을 적절하게 사용하여 동시성을 제어하도록 해야 한다.

    - High throughput objects with writable state : 일반적으로 Object Pooling과 같은 기능을 사용하는 것을 예로 들 수 있다. 인스턴스를 생성하는데 많은 비용이 발생하거나 무수히 많은 인스턴스를 관리할 필요가 있는 경우에는 Object Pooling을 사용하고 Pooling 대상이 되는 인스턴스는 Singleton으로 사용할 수 있다. 이 경우에도 Writable State에 변경이 발생할 때 Synchronization을 적절하게 사용해야 한다.

  • Prototype

    - Objects with writable state

    - Objects with private state

6.4.1.3.Other Scopes

request, session, globalSession Scope 사용 시 주의 사항은 다음과 같다.

  • Web 기반의 ApplicationContext 사용시에만 이 Scope들을 사용할 수 있으며 그 외의 경우 사용하게 되면 IllegalStateException이 발생한다.

  • Scope이 다른 Bean에서 참조하는 경우 Bean 정의 시 <aop:scoped-proxy/>와 함께 작성해야 한다.(아래의 예시 참고)

    productPreferences Bean은 scope이 session이지만 foundationProductService Bean의 scope이 singleton(default가 singleton)이기 때문에 문제가 발생한다. 즉, 매 세션마다 ProductPreferences 객체를 만들어줘야 하지만 foundationProductService Bean에 의해 ProductPreferences 객체가 한 번만 생성되기 때문에 원하던 대로 동작하지 못하는 것이다. 따라서 매 세션 마다 새로운 객체를 만들어서 줄 Proxy를 만들기 위해서 <aop:scoped-proxy/>를 사용하도록 한다.

    <!-- a HTTP Session-scoped bean exposed as a proxy -->
    <bean id="productPreferences" 
          class="anyframe.example.foundation.sales.service.impl.ProductPreferences" scope="session">
        <!-- this next element effects the proxying of the surrounding bean -->
        <aop:scoped-proxy/>
    </bean>
    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="foundationProductService" 
          class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
        <!-- a reference to the proxied 'productPreferences' bean -->
        <property name="productPreferences" ref="productPreferences"/>
    </bean>

6.4.1.4.Custom

신규 Scope을 정의하기 위한 클래스를 생성하고, org.springframework.beans.factory.config.Scope 인터페이스를 implements한다. 또한 CustomScopeConfigurer를 이용하여 신규 정의한 Custom Scope을 등록하여 Custom Scope를 사용할 수 있도록 한다.

해당 프로젝트에 적합한 Scope을 아래의 예시와 같이 직접 정의할 수 있다.

<!-- 신규 Scope 정의를 위한 클래스를 정의하고, 
org.springframework.beans.factory.config.Scope 인터페이스를 implement한다.-->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <!-- CustomScopeConfigurer를 이용하여 Custom Scope 등록  -->
    <property name="scopes">
        <map>
            <entry key="thread">
                <bean class="com.foo.ThreadScope"/>
            </entry>
        </map>
    </property>
</bean>

<!-- Custom Scope 사용 -->
<bean id="bar" class="x.y.Bar" scope="thread">
    <property name="name" value="Rick"/>
    <aop:scoped-proxy/>
</bean>

6.4.2.Bean Life Cycle

Bean의 Life Cycle은 다음 그림에서와 같이 Initialization, Activation, Destruction으로 구성된다.

6.4.2.1.Initialization

Spring Container는 아래 그림에서 보여지는 여러 과정을 통해 구동된다. Spring Bean 클래스가 아래 그림에서 보여지는 각각의 인터페이스들을 구현하였을 때 각각의 메소드들이 호출된다.

Spring Framework에서 지원하는 Life Cycle 메소드를 그대로 사용할 경우 특정한 인터페이스를 구현해야 하므로, 해당 코드가 Spring Framework에 의존적일 수 있게 된다. 즉, 위 그림에서 제시하고 있는 Life Cycle 메소드를 사용하기 위해서는 Spring Bean 클래스에서 해당 Life Cycle 인터페이스 클래스를 구현해줘야 한다. 예를 들어, ApplicationContextAware 인터페이스 클래스를 구현한 Spring Bean에서는 setApplicationContext(ApplicationContext context) 메소드를 작성하고, Spring Bean 내부에서 ApplicationContext를 이용하여 ApplicationContext에서 제공하는 메소드를 호출할 수 있다.

public class IoCServiceImpl1 implements IoCService1, ApplicationContextAware {
    public void setApplicationContext (ApplicationContext context){
        IoCService2 iocService2 = (IoCService2)context.getBean("IoCService2");
    }
}

또다른 예로 MessageSourceAware 인터페이스 클래스의 경우, Spring Container에 정의된 MessageSource를 얻기 위해 사용될수 있다. MessageSourceAware 인터페이스 클래스를 구현한 Spring Bean에서 setMessages(MessageSource messages) 메소드를 작성하여 MessageSource에 접근할 수 있다.

public class IoCServiceImpl1 implements IoCService1, MessageSourceAware {
    private MessageSource messageSource;
    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
}

이와는 달리 Bean 속성(init-method, destroy-method) 정의를 통해 특정 인터페이스에 대한 구현없이 별도 Life Cycle 메소드를 정의할 수도 있다. 다음은 init-method 속성이 정의된 context-foundation-services.xml 의 일부이다.

<bean id="foundationProductService"
        class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" 
        init-method="productInitialize" destroy-method="productDestroy" parent="parent">
</bean>

모든 Bean에 대한 초기화 method 설정은 <beans>의 default-init-method 속성을 이용하도록 한다.

6.4.2.2.Destruction

Destruction 단계에서는 BeanFactory와 ApplicationContext가 동일하게 동작한다.

다음은 destroy-method 속성이 정의된 context-foundation-services.xml 의 일부이다.

<bean id="foundationProductService"
    class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" 
    init-method="productInitialize" destroy-method="productDestroy" parent="parent">
</bean>

모든 Bean의 소멸자 method 설정은 <beans>의 default-destroy-method 속성을 이용한다.

6.4.3.Bean 상속

Bean 정의는 여러 속성 정보들, 생성자 인자, Property 값을 포함하여 많은 양의 설정 정보를 포함한다. 자식 Bean은 부모 정의로부터 설정 정보를 상속하여 정의한다. 그러므로 값을 오버라이드하거나 다른 것을 추가할 수 있다. 상속 관계를 이용하여 Bean을 정의하는 것은 XML 파일의 양을 줄일 수 있으므로 템플릿 형태의 부모 Bean을 정의하는 것은 유용하다. XML 기반의 속성 정의시 자식 Bean은 부모 Bean을 명시하기 위해 'parent' 속성을 사용해야 한다.

  • 부모 Bean 정의

    특수 설정 없이 부모 Bean으로 사용이 가능하며 class 속성 값을 설정하지 않은 경우, 반드시 abstract 속성 값을 "true"로 설정한다. abstract 속성 값이 "true"인 경우 Bean의 인스턴스화가 불가능하다.

  • 자식 Bean 정의

    parent 속성 값에 부모 Bean의 id 혹은 name을 설정한다.

다음은 Bean 상속이 표현되어 있는 context-foundation-services.xml 의 일부이다.

<!-- register parent bean that has a dependency with foundationProductDao bean -->
<bean id="parent" abstract="true">
    <property name="foundationProductDao" ref="foundationProductDao" />	
</bean>

<bean id="foundationProductService"
    class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl" 
    init-method="productInitialize" destroy-method="productDestroy" parent="parent">
</bean>

6.4.4.Container 확장

6.4.4.1.Bean 후처리

Bean의 LifeCycle 중 Initialization 단계에서 Bean 초기화 시점 전후에 수행되는 것을 Bean 후처리라고 하며, BeanPostProcessor를 구현하면 기능을 확장할 수 있다. ApplicationContext 유형의 Container 사용 시에는 XML 파일에 BeanPostProcessor 인터페이스를 구현한 클래스를 등록만 시키면 Container가 해당 클래스를 BeanPostProcessor로 인식하여 각각의 Bean을 초기화하기 전과 후에 후처리 메소드를 호출해준다. 그러나 BeanFactory 유형의 Container를 사용하고 있다면 BeanFactory의 addBeanPostProcessor() 메소드를 이용하여 프로그램 상에서 등록해야 한다. 예시는 다음과 같다.

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
  // simply return the instantiated bean as-is
  public Object postProcessBeforeInitialization(Object bean, String beanName) 
                throws BeansException {
     return bean; // we could potentially return any object reference here
  }
  
  public Object postProcessAfterInitialization(Object bean, String beanName) 
                throws BeansException {
     System.out.println("Bean '" + beanName + "' created : " + bean.toString());
     return bean;
  }
}  

<bean class="scripting.InstantiationTracingBeanPostProcessor"/>

6.4.4.2.BeanFactory 후처리

BeanFactoryPostProcessor를 구현하여 BeanFactory 후처리 기능을 확장할 수 있다. 모든 Bean에 대한 정의가 로딩된 후, BeanPostProcessor Bean을 포함한 어떤 Bean이라도 인스턴스화되기 이전에 Spring Container에 의해 BeanFactoryPostProcessor의 postProcessBeanFactory() 메소드가 호출된다. 따라서, BeanFactoryPostProcessor 인터페이스를 구현한 클래스 내에서 postProcessBeanFactory 메소드를 작성하고, Bean으로 정의하면 된다. 예시는 다음과 같다.

public class BeanCounterBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) 
                    throws BeansException {
        중략...
} 
<bean class="test.BeanCounterBeanFactoryPostProcessor"/>
BeanFactoryPostProcessor는 BeanFactory 유형의 Container와 함께 사용될 수 없다. 유용한 BeanFactoryPostProcessor 구현 클래스는 PropertyPlaceholderConfigurer와 CustomEditorConfigurer이다.

다음은 PropertyPlaceholderConfigurer와 CustomEditorConfigurer에 대한 사용 예이다.

  • 설정 정보의 외부화

    PropertyPlaceholderConfigurer를 사용하여 하나 이상의 외부 Property 파일로부터 속성들을 로딩하고 그 속성들을 이용하여 Bean 정의 XML 파일에서의 위치소유자(placeholder) 변수들을 채운다.

    다음은 설정 정보 외부화를 위해 PropertyPlaceholderConfigurer 클래스를 Bean으로 등록하고 있는 context-foundation-services.xml 의 속성 정의 부분이다.

    <!-- set file locations --> 
    <bean id="configurer" 
          class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>productConfigurer.properties</value>
            </list>
        </property>
    </bean>
     
    <bean id="foundationProductService"
          class="anyframe.example.foundation.sales.service.impl.ProductServiceImpl">
        <property name="foundationProductDao" ref="foundationProductDao" />
        <!-- set productCompany value using key name in properties file -->
        <property name="productCompany" value="${product.company}"></property>
    </bean>

    위에서 외부 파일로 정의된 productConfigurer.properties 의 내용은 다음과 같다.

    product.company=SamsungSDS   

  • PropertyEditor 확장

    CustomEditorConfigurer를 사용하여 java.beans.PropertyEditor의 커스텀 구현 클래스를 등록하여 특성 값을 다른 특성 타입으로 번역할 수 있도록 한다. 확장한 PropertyEditor 클래스를 속성 정의 파일에 등록 후 PropertyEditor로 사용한다.

    <bean id="customEditorConfigurer"          
            class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="customEditors">
            <map>
                <entry key="com.springinaction.knight.PhoneNumber">
                    <bean id="phoneEditor"          
    	                 class="com.springinaction.springcleaning.PhoneNumberEditor" />
                </entry>
            </map>
        </property>
    </bean>

    <bean id="knight" class="com.springinaction.knight.KnightOnCall">
       <property name="url" value="http://www.knightoncall.com" />
       <property name="phoneNumber" value="940-555-1234" />
    </bean>

6.4.5.ApplicationContext 활용

6.4.5.1.MessageSource를 활용한 국제화(I18N) 지원

ApplicationContext 인터페이스는 MessageSource라고 불리는 인터페이스를 확장해서 메시징(국제화 지원)기능을 제공하며 HierarchicalMessageSource와 함께 구조적인 메시지를 분석하는 능력을 가진다. MessageSourceAware인터페이스를 구현하는 Bean은 ApplicationContext의 messageSource Bean을 사용할 수 있다.

다음은 context-common.xml 의 messageSource 속성 정의 부분이다.

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
        <list><value>message/message-productmgmt</value></list>
    </property>
</bean>

Resource Bundle 파일은 국제화 지원을 위해 Locale별 파일로 구성하며 위에서 참조하는 message-productmgmt.properties 파일은 다음과 같다.

errors.required={0} is a required field.  

또한 ProductServiceImpl.java 파일에 messageSource를 얻는 부분은 다음과 같이 구현되어 있다.

new String(messageSource.getMessage("errors.required", 
                new Object[] {"PROD_NO"}, Locale.KOREA).getBytes("8859_1"), "euc-kr")

messageSource 부분을 테스트 할수있는 ContainerTest.java 파일을 수행시키면 다음과 같은 message를 확인할 수 있다.

"PROD_NO" 필드는 반드시 필요하다.        

6.4.5.2.Event

ApplicationContext는 어플리케이션이 구동하는 동안 다수의 이벤트를 발생시킬 수 있으므로, Listener를 Bean으로 등록하게 되면, Container는 해당하는 Event가 발생하면 관련 Listener의 onApplicationEvent() 메소드를 호출한다.

  • Built-in Events

    이벤트설명
    ContextRefreshedEvent ApplicationContext가 초기화되거나 갱신(refresh)될 때 발생하는 이벤트 - 여기서 초기화는 모든 Bean이 로드되고 Singleton Bean들은 미리 인스턴스화되며 ApplicationContext는 사용할 준비가 된다는 것을 의미함
    ContextClosedEvent ApplicationContext의 close()메소드를 사용하여 ApplicationContext가 종료될 때 발생하는 이벤트 - 여기서 종료는 Singleton Bean들이 소멸(destroy)되는 것을 의미함
    RequestHandledEvent HTTP Request가 처리되었을 때 WebApplicationContext 내에서 발생하는 이벤트 - 이 이벤트는 Spring의 DispatcherServlet을 사용하는 웹 어플리케이션에서만 적용 가능함

    ApplicationListener를 구현한 Listener의 예시는 다음과 같다.

    public class RefreshListener implements ApplicationListener {
        public void onApplicationEvent(ApplicationEvent evt) {
            if (evt instanceof ContextRefreshedEvent) { 
                중략...
            }
        }
    }        

    앞서 구현한 RefreshListener 클래스에 대한 속성 정의 예시는 다음과 같다.

    <bean id="refreshListener" class="sample.RefreshListener"/>

  • Custom Event 발생

    사용자 정의 Event를 직접 발생시키고 해당 Event 발생 시 처리될 수 있도록 Listener를 등록하는 것도 가능하다. Event Listening을 하기 위해서는 Listener 등록이 필요하다. 다음은 Listener Bean을 등록하는 context-foundation-services.xml 파일의 일부이다.

    <bean id="productEventListener" class="anyframe.example.foundation.sales.service.impl.ProductEventListener"/>

    다음은 ProductEventListener.java 의 일부로, Custom Event인 ProductEvent를 처리하고 있음을 알 수 있다.

    public class productEventListener implements ApplicationListener {
      public void onApplicationEvent(ApplicationEvent evt) {
        if (evt instanceof ProductEvent) {
          ProductEvent event = (ProductEvent)evt;
        System.out.println("Received in ProductEventListener : " + event.getProductMessage());
        }
      }
    }

    다음은 ProductServiceImpl.java 파일로, Custom Event인 ProductEvent를 발생시키는 부분이다.

    this.ctx.publishEvent(new ProductEvent(this,"new product is added successfully."));

6.4.5.3.BeanFactory와 ApplicationContext 특징 비교

FeatureBeanFactoryApplicationContext
Bean instantiation/wiringYesYes
Automatic BeanPostProcessor registrationNoYes
Automatic BeanFactoryPostProcessor registrationNoYes
Convenient MessageSource access (for i18n)NoYes
ApplicationEvent publicationNoYes

대부분의 전형적인 어플리케이션 구축 시에는 ApplicationContext 사용을 권장한다.

6.5.XML 스키마 기반 설정

XML 스키마에 기초하여 새로운 XML 설정 문법이 나오고 있으며 점점 더 쉽게 XML을 설정할 수 있도록 Spring Framework은 진화하고 있다. 또한 XML 스키마를 확장하여 사용할 수도 있다.

  • 기본으로 제공되는 XML 스키마

    Spring Framework에서 기본으로 제공하는 XML 스키마의 종류는 다음과 같다.

    [util, jee, lang, jms, tx, aop, context, tool, beans] (각각의 사용법은 Spring 매뉴얼 사이트 를 참고하도록 한다.)

  • XML 스키마 확장 가능

    어플리케이션 개발 시 어플리케이션 도메인을 좀더 잘 표현할 자체적인 도메인 속성의 설정 태그를 정의할 수 있다.

    확장한 스키마를 실제 XML 파일에 적용하여 사용하는 방법은 Spring 매뉴얼 사이트 를 참고하도록 한다.

  • XML 스키마 참조 방법

    xmlns:~를 이용하여 사용하고자 하는 namespace를 정의하고, 해당 namespace의 XML 스키마를 정의한 XSD 파일의 location을 정의한다.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:lang="http://www.springframework.org/schema/lang"
       xmlns:jms="http://www.springframework.org/schema/jms"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
          http://www.springframework.org/schema/util 
          http://www.springframework.org/schema/util/spring-util-2.5.xsd
          http://www.springframework.org/schema/jee 
          http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
          http://www.springframework.org/schema/lang 
          http://www.springframework.org/schema/lang/spring-lang-2.5.xsd
          http://www.springframework.org/schema/jms 
          http://www.springframework.org/schema/jms/spring-jms-2.5.xsd
          http://www.springframework.org/schema/aop 
         http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
          http://www.springframework.org/schema/tx 
          http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
          http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-2.5.xsd">
          <!-- <bean/> definitions here -->
    </beans>

  • XML 설정에 대한 부담시 Annotation 활용 제안

    XML 기반에서 Bean을 정의하는 방식 외에 Annotation을 활용하면 XML 설정에 대한 부담을 덜 수 있다.

6.6.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.foundation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation을 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation을 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation을 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 6.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.foundation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

7.Aspect Oriented Programming

다음 내용은 ZDNet Korea의 제휴 매체인 마이크로소프트웨어에 게재된 내용 에서 발췌함. 관점지향 프로그래밍(Aspect Oriented Programming, 이하 AOP)은 지금까지의 프로그래밍 기술 변화의 흐름에 다른 차원의 관점을 제시함으로써 새로운 프로그래밍 패러다임을 이끌어내고 있다고 볼 수 있다. AOP의 필요성을 이해하는데 기초가 되는 개념은 Separation of Concerns로, 다음과 같이 거의 모든 프로그래밍 패러다임은 바로 이 Separation of Concerns 과정을 통해 문제 영역을 독립적인 모듈로 분해한다.

  • 절차적 프로그래밍 : 분리된 관심을 프로시저로 구성

  • 객체지향 프로그래밍(Object Oriented Programming, 이하 OOP) : 분리된 관심을 클래스로 작성

AOP 필요성

AOP는 OOP를 적용한다고 할지라도 결코 쉽게 분리된 모듈로 작성하기 힘든 요구사항이 실제 어플리케이션 설계와 개발에서 자주 발견된다는 문제 제기에서 출발한다. AOP에서는 이를 Crosscutting Concerns(횡단 관심)라고 한다. 또한 해당 시스템의 핵심 가치와 목적이 그대로 드러난 관심 영역을 Core Concerns(핵심 관심)라고 부른다. 이 Core Concerns는 기존의 객체지향 분석/설계(OOAD)를 통해 쉽게 모듈화와 추상화가 가능하지만 Crosscutting Concerns은 객체지향의 기본 원칙을 지키면서 이를 분리해서 모듈화하는 것이 매우 어렵다.

예를 들어, 은행 업무를 처리하는 시스템을 생각해보면 Core Concerns는 예금입출금, 계좌간이체, 이자계산, 대출처리 등으로 구분할 수 있다. 이는 전체 어플리케이션의 핵심 요구 사항과 기능들을 구분해서 모듈화할 수 있고 OOP에서라면 클래스와 컴포넌트 형태로 구성이 가능하다. 하지만 현실은 그렇지 못하다. 실제로 개발되어 돌아가는 각 모듈에는 해당 업무를 처리하기 위한 로직만 존재해서는 불완전할 수밖에 없다. 일단 각 업무를 처리하는 클래스와 구현된 메소드에는 향후 시스템을 분석하거나 추적을 위해 로그를 작성해야 하며, 인증받은 사용자가 접근하는지를 체크하고 권한 여부를 따지는 보안 기능이 필요하다. 또한 내부에서 사용하는 Persistence 처리를 위해 Transaction을 시작하고, 또 필요에 따라서 그것을 Commit 또는 Rollback하는 부분도 추가되어야 한다. 예외 상황이나 문제가 발생했을 때는 그것을 기록에 남기는 부분도 있어야 하고, 필요하면 관리자에게 이메일을 발송해야 한다.

이러한 부가적인 기능들은 독립적인 클래스로 구현될 수 있지만, 그렇게 구현된 기능들을 호출하고 사용하는 코드들이 핵심 모듈 안의 필요한 영역에 모두 포함될 수밖에 없다. 로깅, 인증, 권한체크, DB 연동, 트랜잭션, 락킹, 에러처리 등의 기능을 아무리 뛰어난 OOP 기술을 이용해 모듈로 구성하고 추상화를 통해 최대한 독립시킨다고 해도 핵심 모듈의 모든 클래스와 메소드 속에 이와 연동되는 부분이 매우 깊이 그리고 상당한 양을 갖으면서 자리 잡게 된다.

실제로 모듈화가 잘 된 어플리케이션 클래스를 보더라도 핵심 기능을 위한 코드보다 부가적인 기능과 처리를 위한 부분의 양이 더 많아지게 되는데 만약 다른 종류의 로깅 플랫폼을 사용해 로그 처리하는 클래스와 메소드가 달라지고 로그 메시지가 변경되어야 한다면 개발자들은 모든 클래스 안에 있는 로그 관련 코드를 일일이 다 수정해 주는 수밖에 없다. 그러다가 만약 중요한 클래스에서 한두 군데 로그 기록 코드가 빠졌고 이로 인해 결과를 확인하는데 문제가 생겼다면 이를 다시 확인하고 찾아내는 일만 해도 엄청난 작업이 아닐 수 없을 것이다. 이렇게 작성된 어플리케이션은 몇 가지 심각한 문제를 가지고 있다.

  • 중복되는 코드 : 복사-붙이기에 의해 만들어진 여러 모듈에서 중복되는 코드의 문제점은 이미 잘 알려져 있다. 하지만 AOP를 사용하지 않은 대부분의 어플리케이션에서는 어떠한 추상화와 리팩토링을 통해서도 반복되는 코드를 피하기가 어렵다.

  • 지저분한 코드 : Crosscutting Concerns과 관련된 코드들이 핵심 기능 코드 사이 사이에 끼어들어가 있기 때문에 코드가 지저분해지고 이에 따라 가독성이 떨어지며 개발자들의 실수나 버그를 유발하고 후에 코드를 유지보수하는데 큰 어려움을 준다.

  • 생산성의 저하 : 어플리케이션 개발자들이 자주 등장하는 Crosscutting Concerns을 구현한 코드를 함께 작성해야 하기 때문에 개발의 집중력을 떨어뜨리고 결과적으로 전체 생산성의 저하를 가져온다. 또 모듈별로 개발자들을 구분하고 분산시키는 것에 한계가 있다.

  • 재활용성의 저하 : OOP의 장점인 재활용성이 매우 떨어진다.

  • 변화의 어려움 : 새로운 요구사항으로 인해 전체적으로 많은 부분에 영향을 미치는 경우 쉽게 새로운 요구사항을 적용하기 힘들게 된다. 또 새로운 관심 영역의 등장이나 이의 적용을 매우 어렵게 한다.

대표적인 AOP 툴

AOP는 OOP의 확장에 가깝기 때문에 전용 언어나 독립된 개발 툴을 가지고 있지 않고 대신 기존의 OOP를 확장한 언어 확장(languageextension) 또는 툴이나 프레임워크 형태로 사용할 수 있게 되어 있다. 대표적으로 AOP 구현의 시초가 된 Eclipse 프로젝트의 AspectJ를 들 수 있다. AspectJ는 초기에 제록스 PARC 연구소에서 개발되었다가 2002년에 이클립스 프로젝트에 기증되었고, 현재 IBM의 전폭적인 지원을 받으면서 개발되어 사용되고 있다. 그리고 BEA가 중심이 되어 개발하고 있는 AspectWerkz가 있다. AspectWerkz는 AspectJ와 달리 자바 언어 자체를 확장하지 않고 기존의 자바 언어만으로 AOP의 사용이 가능하도록 되어 있다. 그리고 의존성 삽입(Dependency Injection, 이하 DI) 기반의 프레임워크로 유명한 SpringAOP가 있다. 가장 최근에 등장한 AOP로는 JBossAOP도 있다. SpringAOP와 함께 대표적인 인터셉터체인 방식의 AOP로 꼽힌다.

 AspectJAspectWerkzJBossAOPSpringAOP
출시2001200220042004
버전 1.2.12.01.3.01.2.5
Aspect 선언전용코드XML, AnnotationXML, AnnotationXML
Advice전용코드자바 메소드자바 메소드자바 메소드
JoinPoint메소드, 생성자, Advice, Field Access, 인스턴스메소드, 생성자, Advice, Field Access, 인스턴스메소드, 생성자, Advice, Field Access, 인스턴스메소드
Pointcut 매칭 Signature, WildCard, AnnotationSignature, WildCard, AnnotationSignature, WildCard, Annotation정규식
Weaving컴파일 및 로딩 타임, 바이트 코드 생성컴파일 및 로딩 타임, 바이트 코드 생성런타임 인터셉션 및 Proxy런타임 인터셉션 및 Proxy
IDE 지원Eclipse, JDeveloper, JBuilder, NetBeansEclipse, NetBeansEclipse 
  • AspectJ

    AspectJ의 가장 큰 특징은 다른 AOP 툴과는 달리 자바 언어를 확장해서 만들어진 구조라는 것이다. 마치 새로운 AOP 언어를 사용하듯이 aspect라는 키워드를 이용해 Aspect나 Pointcut, Advice를 만들 수 있다. 따라서 일반 자바 컴파일러로는 컴파일이 불가능하고 특별한 AOP 컴파일러를 사용해야 한다. 하지만 이렇게 만들어진 바이너리는 표준 JVM에서 동작 가능한 구조로 되어있기 때문에 특별한 클래스 로더의 지원 없이도 실행 가능하다. AspectJ는 가장 오래되고 가장 많이 사용되는 AOP 툴이다. 동시에 가장 풍부한 기능을 가지고 있고 확장성이 뛰어나기 때문에 가장 이상적인 AOP 툴로 꼽히고 있다. 하지만 자바 언어를 확장했기 때문에 새로운 문법과 언어를 이해할 필요가 있고 프로젝트 빌드시 특별한 컴파일러를 사용해야 하는 불편함이 있다. Weaving이 컴파일시에 일어나기 때문에 Pointcut에 의해 선택된 모든 클래스들은 Aspect가 바뀔 때마다 모두 다시 컴파일이 되어야 한다.

  • AspectWerkz

    AspectWerkz는 AspectJ와는 달리 자바 언어를 확장하지 않는다. 따라서 표준 자바 클래스를 이용해서 AOP를 구현해 낼 수 있다. 일반 클래스와 메소드를 이용해 쉽게 구현이 가능한 Advice와 달리 복잡한 문법이 필요한 Pointcut은 별도의 XML 파일을 이용해 설정할 수 있도록 되어 있다. 자바 클래스와 XML 설정 파일의 접근법에 익숙한 개발자들에게는 매우 편리한 접근 방식이라고 볼 수 있다. 최근에는 JDK5의 지원에 따라 Annotation을 이용할 수 있어 더욱 편리해졌다. Weaving은 특별한 클래스 로더를 이용한 로딩타임 바이트코드 생성을 이용한다. AspectJ 못지않은 다양한 JoinPoint와 AOP기능을 지원하고 있으며 편리한 개발을 위한 IDE 플러그인이 개발되어 있다.

  • JBossAOP

    JBossAOP는 기본적으로 컨테이너에서 동작하지만 컨테이너와 상관없는 독립된 자바 프로그램에서도 사용할 수 있다. 하지만 주 용도는 JBoss 서버와 앞으로 나올 EJB3 컨테이너 등에 AOP를 적용하는 데에 사용되어지는 것이다. AspectWerkz와 마찬가지로 Advice는 표준 자바 코드로 작성하고 Pointcut과 다른 설정은 XML 파일이나 JDK5의 Annotation으로 작성할 수 있다. 아직까지는 JBoss 사용자의 일부에서만 사용되고 있으나 향후 EJB3를 중심으로 한 POJO 기반의 엔터프라이즈 미들웨어 프레임워크가 개발되어짐에 따라 점차로 사용률이 올라갈 것으로 기대된다.

  • SpringAOP

    SpringAOP는 Spring Framework의 핵심기능 중의 한가지로 Spring의 Dependency Injection(이후 DI) 컨네이너에서 동작하는 엔터프라이즈 서비스에서 주로 사용된다. SpringAOP는 다른 AOP와 달리 기존 클래스의 바이트코드를 수정하지 않는다. 대신 JDK의 Dynamic Proxy를 사용해서 Proxy 방식으로 AOP의 기능을 수행한다. 이 때문에 다른 AOP의 기능과 비교해서 매우 제한적인 부분만을 지원한다. 하지만 SpringAOP의 구현 목적은 엔터프라이즈 어플리케이션에서 주로 사용되는 핵심적인 기능에 AOP의 장점을 살려 이를 Spring 내에서 사용하는 것이기 때문에 다른 AOP와 같은 AOP의 복잡한 전체 기능을 굳이 다 필요로 하지 않는다. 프록시 기반의 SpringAOP는 SpringIoC/DI와 매우 긴밀하게 연동이 된다. 따라서 SpringAOP를 사용하는 방법은 Spring 내에 ProxyBean을 설정해서 쉽게 사용할 수 있다. JDK의 표준 기능만을 사용하기 때문에 특별한 빌드 과정이 필요없고 클래스 로더를 변경한다거나 하는 번거로운 작업이 없다. 대신 JoinPoint의 종류가 메소드 기반으로 제한되나 대부분의 엔터프라이즈 어플리케이션에서 필요로 하는 주요 AOP 기능들은 메소드 호출을 기반으로 충분히 처리가 가능하기 때문에 SpringAOP는 그 제한된 AOP 기능에도 불구하고 현장에서 가장 빠른 속도로 적용되어 사용되는 AOP 솔루션 중의 하나이다. SpringAOP는 Advice와 Pointcut을 모두 표준 자바 클래스로 작성할 수 있다. 필요에 따라서 Pointcut은 설정 파일 내에서 Pointcut FactoryBean을 이용해서 정규식으로 표현이 가능하다. SpringAOP의 최대 단점은 복잡한 Proxy 설정 구조이다. Spring Bean을 정의한 파일에서 Proxy를 정의한 부분의 다른 XML기반의 AOP에 비해서도 복잡한 편인데 이 경우 SpringAOP가 지원하는 AutoProxyingCreatorBean 등을 이용하면 설정 코드를 매우 단순하게 작성하는 것이 가능하다.

7.1.AOP 구성 요소

AOP에는 새로운 용어가 많이 등장한다. 이 중에서 특히 AOP를 이용해서 개발하는데 필요한 다음의 주요 구성 요소들에 대해 정확한 이해가 필요하다.

7.1.1.JointPoint

Crosscutting Concerns 모듈이 삽입되어 동작할 수 있는 실행 가능한 특정 위치를 말한다. 예를 들어 메소드가 호출되는 부분 또는 리턴되는 시점이 하나의 JoinPoint가 될 수 있다. 또 필드를 액세스하는 부분, 인스턴스가 만들어지는 지점, 예외가 던져지는 시점, 등이 대표적인 JoinPoint가 될 수 있다. 각각의 JoinPoint들은 그 전후로 Crosscutting Concerns의 기능이 AOP에 의해 자동으로 추가되어져서 동작할 수 있는 후보지가 되는 것이다.

7.1.2.Pointcut

Pointcut은 어느 JoinPoint를 사용할 것인지를 결정하는 선택 기능을 말한다. AOP가 항상 모든 모듈의 모든 JoinPoint를 사용할 것이 아니기 때문에 필요에 따라 사용해야 할 모듈의 특정 JoinPoint를 지정할 필요가 있다. 일종의 JoinPoint 선정 룰과 같은 개념으로 다음과 같은 Pattern Matching 방법들을 이용하여 룰을 정의할 수 있다.

7.1.2.1.Pattern Matching Examples

  1. Basics

    • set*(..) : set으로 시작하는 모든 메소드명

    • * main(..) : return type이 any type이고, 0개 이상의 any type parameter를 가진 main 메소드

  2. Matching Type

    • java.io.* : java.io 패키지 내에 속한 모든 요소

    • org.myco.myapp..* : org.myco.myapp 패키지 또는 서브 패키지 내에 속한 모든 요소

    • Number+ : Number 또는 Number의 서브 type으로 Integer, Float, Double ..등이 이에 해당

    • !(Number+) : Number 또는 Number의 서브 type이 아닌 모든 type

    • org.xyz.myapp..* && !Serializable+ : org.xyz.myapp 패키지 또는 서브 패키지 내에 존재하면서 Serializable type이 아닌 모든 요소

    • int || Integer : int 또는 Integer type

  3. Matching Modifiers

    • public static void main(..) : 0개 이상의 any type parameter를 가진 public static void main 메소드

    • !private * *(..) : return type이 any type이고, 0개 이상의 any type parameter를 가진 모든 메소드중 modifier가 private이 아닌 메소드

    • * main(..) : modifier를 별도로 명시하지 않은 경우, default modifier가 아닌 any modifier 의미

  4. Matching Parameter

    • * main(*) : return type이 any type이고, 1개의 any type parameter를 가진 main 메소드

    • * main(*,..) : return type이 any type이고, 최소 1개의 any type parameter를 가진 main 메소드

    • * main(*,..,String,*) : return type이 any type이고, 최소 3개의 any type parameter를 가지며 끝에서 두번째 parameter type이 String인 main 메소드

  5. Matching Constructor

    • new(..) : 0개 이상의 any type parameter를 가진 constructor

    • Account.new(..) : 0개 이상의 any type parameter를 가진 Account 클래스의 constructor

AspectJ는 Pointcut을 명시할 수 있는 다양한 Pointcut Designator(지시자)를 제공한다. 이제부터 앞서 정의한 Pattern Matching 방법을 이용하여, 본격적으로 Pointcut Designator별 Pointcut 정의 방법에 대해 살펴보기로 하자.

7.1.2.2.Pointcut Designators

  1. execution 또는 call

    특정 메소드나 생성자 실행을 위한 JoinPoint를 정의하는 것으로, JoinPoint의 특정 method name, parameter types, return type, declared exceptions, declaring type, modifiers에 대한 matching이 가능하며, 단, return type pattern, method name pattern, parameter list pattern은 필수적으로 정의해야 한다. 다음은 execution, call을 이용한 pointcut 정의 예이다.

    • execution(* main(..)) : return type이 any type이고, 0개 이상의 any type parameter를 가진 main 메소드 실행시

    • call(Account.new(..)) : any type parameter를 가진 Account 클래스의 constructor 호출시

  2. get 또는 set

    특정 Field에 접근하거나 특정 Field 수정을 위한 JoinPoint를 정의한다.

    • get(Collection+ org.xyz.myapp..*.*) : Collection type의 org.xyz.myapp 패키지에 속한 any field에 대한 getter 호출시

    • set(!private * Account+.*) : Account type의 non-private field에 대한 setter 호출시

  3. handler

    Exception 핸들링을 위한 JoinPoint를 정의한다.

    • handler(DataAccessException) : matches cach(DataAccessException){...} and doesn't match catch(RuntimeException)

    • handler(RuntimeException+) : matches both

  4. within

    특정 유형에 속하는 JoinPoint를 정의하며, 주로 &&, ||, ! 등과 함께 조합된 형태로 사용된다.

    • within(*) : matches any JoinPoint

    • within(org.xyz.myapp..*) : org.xyz.myapp 패키지 내에 속하는 모든 요소

    • within(IInterface+) : IInterface type의 모든 요소

  5. withincode

    해당되는 메소드 또는 constructor 내에 정의된 코드를 위한 JoinPoint를 정의한다.

    • withincode(!void get*()) : return type이 void가 아니고 메소드명이 get으로 시작하며 parameter가 없는 메소드 내의 코드

  6. args

    입력값의 개수, type 등에 대한 JoinPoint를 정의한다.

    • call(* transfer(..)) && args(DepositAccount,CheckingAccount,*) : 메소드명이 transfer이고, 입력 인자가 2개 이상이며, 1,2번째 입력 인자의 type이 DepositAccount,CheckingAccount인 메소드 호출시

  7. this

    JoinPoint를 가진 object의 type을 정의한다. (Runtime type)

    • this(Account) : 인터페이스 Account를 구현한 클래스(Proxy)의 모든 JoinPoint

  8. target

    JoinPoint를 가진 target object의 type을 정의한다. (Runtime type)

    • call(* *(..)) && target(Account) : Account 클래스 내의 모든 메소드 호출시

Spring은 메소드 호출 부분에 대한 AOP만을 지원하므로, 위에 정의한 다양한 Pointcut Designator 중 execution, within, target, this, args만이 사용 가능하다.

7.1.3.Advice

Advice는 각 JoinPoint에 삽입되어져 동작할 수 있는 코드로 동작 시점은 pointcut에 Matching되는 JoinPoint 실행 전후이며 before, after, after returning, after throwing, around 중에서 선택 가능하다.

동작시점설명
BeforeBefore Advice는 Matching된 JoinPoint 전에 동작하는 Advice이다.
AfterAfter Advice는 동작 시점에 따라 after (finally), after returning, after throwing 으로 구분할 수 있다.
  • after returning : Matching된 JoinPoint가 성공적으로 return된 후에 동작하는 Advice이다.

  • after throwing : Exception이 발생하여 Matching된 JoinPoint가 종료된 후에 동작하는 Advice이다.

  • after (finally) : Matching된 JoinPoint 종료 후에 동작하는 Advice이며 잘 사용되지는 않는다.

Around가장 강력한 Advice로 Matching된 JoinPoint 전, 후에 동작하며 JoinPoint 실행 시점을 결정할 수 있다. 또한 다른 Advice와는 달리 입력값, target object, return 값 등에 대한 변경이 가능하다.

동작 시점별 Advice 정의 방법에 대해서는 매뉴얼 Spring >> AOP 하위의 Annotation based AOP , XML based AOP , AspectJ based AOP 를 참고하도록 한다.

7.1.4.Weaving 또는 CrossCutting

AOP가 Core Concerns 모듈의 코드를 직접 건드리지 않고 필요한 기능이 작동하도록 하는 데는 Weaving 또는 CrossCutting이라고 불리는 특수한 작업이 필요하다. Core Concerns 모듈이 자신이 필요한 Crosscutting Concerns 모듈을 찾아 사용하는 대신에 AOP에서는 Weaving 작업을 통해 Core Concerns 모듈의 사이 사이에 필요한 Crosscutting Concerns 코드가 동작하도록 엮어지게 만든다. 이를 통해 AOP는 기존의 OOP로 작성된 코드들을 수정하지 않고도 필요한 Crosscutting Concerns 기능을 효과적으로 적용해 낼 수 있다.

Weaving은 기존의 자바 언어와 컴파일러에서는 쉽게 구현할 수 있는 방법이 아니었으며 본격적인 AOP 기술이 등장한 것은 1990년대 후반 제록스 PARC 연구소에서 그레거 킥제일(Gregor Kiczales)에 의해 AspectJ가 개발되면서라고 볼 수 있다.

Weaving을 처리하는 방법은 다음과 같이 3가지가 존재한다.

Weaving 방식설명
Compiletime Weaving 별도 컴파일러를 통해 Core Concerns 모듈의 사이 사이에 Aspect 형태로 만들어진 Crosscutting Concerns 코드들이 삽입되어 Aspect가 적용된 최종 바이너리가 만들어지는 방식이다. (ex. AspectJ, ...)
Loadingtime Weaving 별도의 Agent를 이용하여 JVM이 클래스를 로딩할 때 해당 클래스의 바이너리 정보를 변경한다. 즉, Agent가 Crosscutting Concerns 코드가 삽입된 바이너리 코드를 제공함으로써 AOP를 지원하게 된다. (ex. AspectWerkz, ...)
Runtime Weaving 소스 코드나 바이너리 파일의 변경없이 Proxy를 이용하여 AOP를 지원하는 방식이다. Proxy를 통해 Core Concerns를 구현한 객체에 접근하게 되는데, Proxy는 Core Concerns 실행 전후에 Cross Concerns를 실행한다. 따라서 Proxy 기반의 Runtime Weaving의 경우 메소드 호출시에만 AOP를 적용할 수 있다는 제한점이 있다. (ex. Spring AOP, ...)

7.1.5.Aspect

Aspect는 어디에서(Pointcut) 무엇을 할 것인지(Advice)를 합쳐놓은 것을 말한다. AspectJ와 같은 자바 언어를 확장한 AOP에서는 마치 자바의 클래스처럼 Aspect를 코드로 작성할 수 있다. 다음은 모든 클래스의 main 메소드 실행(pointcut main()) 후에 "Hello from AspectJ"라는 문자열을 남기는 (after returning advice) Aspect HelloFromAspectJ의 일부이다.

public aspect HelloFromAspectJ{
	// define pointcut
	pointcut main(): execution(public static void main(String[]));
	// define advice
	after() returning : main() {
		System.out.println("Hello from AspectJ!");
	}
}

Aspect 정의에 대한 자세한 설명은 매뉴얼 Spring >> AOP 하위의 Annotation based AOP , XML based AOP , AspectJ based AOP 를 참고하도록 한다.

7.2.Annotation based AOP

다음에서는 AOP 대표적인 툴 중 @AspectJ(Annotation)을 이용하여 Aspect를 정의하고 테스트하는 방법에 대해서 다루고자 한다. @AspectJ(Annotation)은 AspectJ 5 버전에 추가된 Annotation이며, Spring 2.0 에서부터 이러한 Annotation에 대한 처리가 가능하므로, Spring 기반일 경우 별도의 Compiler나 Weaver 없이 @AspectJ(Annotation) 기반의 AOP 적용이 가능하다. 또한 Annotation을 이용하여 Aspect를 정의할 경우 별도 XML 파일에 대한 정의가 불필요하므로, Aspect 적용이 보다 간결해짐을 알 수 있을 것이다. (단, Annotation은 JAVA 5 이상에서만 정의 가능함에 유의하도록 한다.)

7.2.1.Configuration

@AspectJ(Annotation)이 적용된 클래스들을 로딩하여 해당 클래스에 정의된 Pointcut, Advice를 실행하기 위해서는 Spring 속성 정의 XML 파일에 다음과 같이 추가해주어야 한다.

<aop:aspectj-autoproxy/>

7.2.2.@Aspect 정의

@Aspect를 이용하여 특정 클래스가 Aspect임을 나타낸다. 다음 LoggingAspect 에서는 @Aspect를 이용하여 해당 클래스가 Aspect임을 나타내고 있다.

@Aspect
public class LoggingAspect {
	//...
}

7.2.3.@Pointcut 정의

@Pointcut을 이용하여 해당 Aspect를 적용할 부분을 정의한다. (Pointcut 정의시에는 Pointcut Designator와 Pattern Matching 활용 방법 을 참고한다.) 다음은 PrintStringUsingAnnotation 의 Pointcut 정의 부분이다. @Pointcut을 "execution(* *..GenericService+.*(..))"와 같이 정의하고, 해당 Pointcut에 대해 식별자로써 serviceMethod라는 메소드명을 부여하였다. 이것은 클래스명이 GenericService로 끝나는 모든 메소드의 실행 부분이 Aspect을 적용할 Pointcut임을 의미한다. 해당 Pointcut은 serviceMethod()라는 이름으로 이용 가능하다.

@Pointcut("execution(* *..GenericService+.*(..))")
	public void serviceMethod(){}

7.2.4.@Advice 정의

다음에서는 Annotation을 이용하여 동작 시점별 Advice를 정의하는 방법에 대해 살펴보기로 한다.

7.2.4.1.Before Advice

@Before를 이용하여 Before Advice를 정의한다. 다음은 Before Advice 정의 부분이다. Before Advice인 beforeLogging()는 앞서 정의한 serviceMethod()라는 Pointcut 전에 "Logging Aspect : executed "라는 문자열과 해당 Pointcut을 가진 메소드명 클래스명을 출력하는 역할을 수행한다.

@Before("serviceMethod()")
public void beforeLogging(JoinPoint thisJoinPoint) {
	Class clazz = thisJoinPoint.getTarget().getClass();
	String className = (thisJoinPoint.getTarget().getClass().getName())
			.toLowerCase();
	String methodName = thisJoinPoint.getSignature().getName();

	StringBuffer buf = new StringBuffer();
	buf.append("\n** Logging Aspect : executed " + methodName + "() in "
			+ className + " Class.");
	Object[] arguments = thisJoinPoint.getArgs();
	if (arguments.length > 0) {
		for (int i = 0; i < arguments.length; i++) {
			buf
					.append("\n*************"
							+ arguments[i].getClass().getName()
							+ "*************\n");
			buf.append(arguments[i].toString());
			buf.append("\n*******************************************\n");
		}
	} else
		buf.append("\nNo arguments\n");

	Log logger = LogFactory.getLog(clazz);
	if (logger.isDebugEnabled())
		logger.debug(buf.toString());
}

beforeLogging()는 1개의 입력 인자(JoinPoint)를 가지고 있는데 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있다. Target 정보가 불필요한 Advice인 경우에는 JoinPoint라는 입력 인자를 선언하지 않아도 된다.

7.2.4.2.AfterReturning Advice

@AfterReturning을 이용하여 AfterReturning Advice를 정의한다. 다음은 AfterReturning Advice 정의 부분으로 해당 Pointcut 실행 결과를 retVal이라는 변수에 담도록 정의하고 있다. AfterReturning Advice인 afterReturningExecuteGetMethod()는 앞서 정의한 Pointcut 후에 , "AfterReturning Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

@AfterReturning(pointcut = "serviceMethod()", returning = "retVal")
public void afterReturningExecuteGetMethod(JoinPoint thisJoinPoint,
		Object retVal) {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("AfterReturning Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}

afterReturningExecuteGetMethod()는 2개의 입력 인자(JoinPoint, Object)를 가지고 있는데 첫번째 인자는 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있으며, 두번째 인자는 해당 Pointcut의 실행 결과이다. AfterReturning Advice에서 특정 Pointcut 실행 결과를 참조해야 한다면, Advice 정의시 returning의 값을 정의하고 해당하는 메소드의 입력 인자명을 동일하게 정의해주도록 한다. 각 입력 인자는 AfterReturning Advice 정의시 필요에 따라 선택 정의할 수 있다.

7.2.4.3.AfterThrowing Advice

@AfterThrowing을 이용하여 AfterThrowing Advice를 정의한다. 다음은 transfer의 AfterThrowing Advice 정의 부분으로 해당 Pointcut 실행시 발생한 Exception 객체를 exception이라는 변수에 담도록 정의하고 있다. AfterThrowing Advice인 serviceMethod()는 앞서 정의한 Pointcut에서 Exception이 발생한 후에 Exception을 핸들링하고 Exception의 종류에 따라 Exception meesage를 출력하게된다.

@AfterThrowing(pointcut = "serviceMethod()", throwing = "exception")
public void transfer(JoinPoint thisJoinPoint, Exception exception)
		throws SalesException {
	Object target = thisJoinPoint.getTarget();
	while (target instanceof Advised) {
		try {
			target = ((Advised) target).getTargetSource().getTarget();
		} catch (Exception e) {
			LogFactory.getLog(this.getClass()).error(
					"Fail to get target object from JointPoint.", e);
			break;
		}
	}

	String className = target.getClass().getSimpleName().toLowerCase();
	String opName = (thisJoinPoint.getSignature().getName()).toLowerCase();
	Log logger = LogFactory.getLog(target.getClass());

	if (exception instanceof SalesException) {
		SalesException empEx = (SalesException) exception;
		logger.error(empEx.getMessage(), empEx);
		throw empEx;
	}
	
	if (exception instanceof BaseException) {
		BaseException baseEx = (BaseException) exception;
		logger.error(baseEx.getMessage(), baseEx);
		throw new SalesException(messageSource, "error." + className + "."
				+ opName, new String[] {}, exception);
	}		

	logger.error(messageSource.getMessage("error." + className + "."
			+ opName, new String[] {}, "no messages", Locale.getDefault()),
			exception);

	throw new SalesException(messageSource, "error." + className + "."
			+ opName, new String[] {}, exception);
}

transfer()는 2개의 입력 인자(JoinPoint, Exception)를 가지고 있는데 첫번째 인자는 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있으며, 두번째 인자는 Pointcut 실행시 발생한 Exception 객체이다. AfterThrowing Advice에서 특정 Pointcut 실행시 발생한 Exception을 참조해야 한다면, Advice 정의시 throwing의 값을 정의하고 해당하는 메소드의 입력 인자명을 동일하게 정의해주도록 한다. 각 입력 인자는 AfterThrowing Advice 정의시 필요에 따라 선택 정의할 수 있다.

7.2.4.4.After(finally) Advice

@After를 이용하여 After(finally) Advice를 정의한다. 다음은 PrintStringUsingAnnotation 의 After(finally) Advice 정의 부분이다. After(finally) Advice인 afterExecuteGetMethod()는 앞서 정의한 getMethods()라는 Pointcut 후에 "After(finally) Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

@After("getMethods()")
public void afterExecuteGetMethod(JoinPoint thisJoinPoint) {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out
			.println("After(finally) Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}      

afterExecuteGetMethod()는 1개의 입력 인자(JoinPoint)를 가지고 있는데 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있다. Target 정보가 불필요한 Advice인 경우에는 JoinPoint라는 입력 인자를 선언하지 않아도 된다.

7.2.4.5.Around Advice

@Around를 이용하여 Around Advice를 정의한다. 다음은 PrintStringAroundUsingAnnotation 의 Around Advice 정의 부분이다. Around Advice인 aroundExecuteUpdateMethod()는 updateMethods()라는 Pointcut 후에 "Around Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

@Around("updateMethods()")
public Object aroundExecuteUpdateMethod(ProceedingJoinPoint thisJoinPoint)
		throws Throwable {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("Around Advice of PrintStringUsingAnnotation");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
	// before logic
	Object retVal = thisJoinPoint.proceed();
	// after logic
	return retVal;
}   

aroundExecuteUpdateMethod()는 1개의 입력 인자(ProceedingJoinPoint)를 가지고 있는데 proceed()라는 메소드 호출을 통해 대상 Pointcut을 실행할 수 있으며, Target 클래스명, 메소드명 등과 같은 Target 정보도 포함하고 있다. 즉, Pointcut 전, 후 처리가 가능하며, Pointcut 실행 시점을 결정할 수 있다. 또한 다른 Advice와는 달리 입력값, target, return 값 등에 대해 변경이 가능하다. Target 정보가 불필요한 Advice인 경우에는 ProceedingJoinPoint라는 입력 인자를 선언하지 않아도 된다.

7.2.5.Aspect 실행

이제 테스트코드 Main.java를 이용하여 앞서 언급한 Aspect들이 정상적으로 동작하는지 확인해 보도록 하자. 다음은 테스트코드 Main.java 의 main()메소드로 실제 product에 대한 CRUD 로직을 수행함으로써 Befor Advice로 정의된 LoggingAspect가 제대로 수행되는지 확인할 수 있다.

public static void main(String[] args) throws Exception {
		Main main = new Main();

		// 1. initialize context
		main.setup();
		// 2. test
		main.manageProduct();
		// 3. close context
		main.teardown();
	}

	public void manageProduct() throws Exception {
		// 1. lookup productService, categoryService
		ProductService productService = (ProductService) context
				.getBean("productService");

		// 2. create a new product
		Product product = new Product();
		product.setProdNo("PRODUCT-99999");
		product.setProdName("example.sportsone");
		product.setProdDetail("sports one detail");
		product.setSellerId("woos41");
		product.setAsYn("Y");
		product.setManufactureDay("20081225");
		product.setSellAmount(new Long(50));
		product.setSellQuantity(new Long(50));
		productService.create(product);

		// 3. get product list
		ProductSearchVO searchVO = new ProductSearchVO();
		searchVO.setSearchCondition("0");
		searchVO.setSearchKeyword("example.sportsone");
		Page products = productService.getPagingList(searchVO);
		System.out.println("after creating a new product, product size is a '"
				+ products.getSize() + "'.");

		// 4. update a product
		product.setProdName("sportsone-update");
		product.setProdDetail("sports one detail-update");
		productService.update(product);

		// 5. get a product
		Product result = productService.get(product.getProdNo());
		System.out.println("after updating a product, product name is a '"
				+ result.getProdName() + "'.");

		// 6. remove a product
		productService.remove(product.getProdNo());

		// 7. get product list
		searchVO = new ProductSearchVO();
		searchVO.setSearchCondition("0");
		searchVO.setSearchKeyword("example.sportsone");
		products = productService.getPagingList(searchVO);
		System.out.println("after creating a new product, product size is a '"
				+ products.getSize() + "'.");
	}
}

첫번째 로직 productService.create(product); 실행시 Before Advice로 정의된 LoggingAspect 클래스가 적용되며, 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.

** Logging Aspect : executed initialize() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
No arguments
** Logging Aspect : executed initialize() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
No arguments

두번째 로직productService.getPagingList(searchVO); 실행시 Before Advice로 정의된 LoggingAspect 클래스가 적용되며, 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.

** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

after creating a new product, product size is a '1'.

7.3.XML based AOP

다음에서는 AOP 대표적인 툴 중 Spring AOP를 이용하여 XML 스키마 기반에서 Aspect를 정의하고 테스트하는 방법에 대해서 다루고자 한다. Spring 2.x 버전부터 AOP 설정을 위한 aop namespace, XML 스키마가 추가되었다.

7.3.1.Aspect 정의

<aop:config>의 하위 태그인 <aop:aspect>를 이용하여 Aspect을 정의한다. 다음 context-aspect.xml 에서는 aop namespace를 이용하여 Aspect을 정의하고 있다.

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/aop
		http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

	<bean id="methodLoggingAspect" class="anyframe.example.common.aspect.LoggingAspect" />
	<bean id="exceptionTransfer" class="anyframe.example.common.aspect.ExceptionTransfer">
			<property name="messageSource" ref="messageSource"/>
	</bean>
</beans>

Advice를 정의한 클래스를 Bean으로 정의해 두고, 해당 Bean을 <aop:config> 내의 <aop:aspect> 에서 참조하는 형태로 Aspect을 정의할 수 있다.

7.3.2.Pointcut 정의

<aop:pointcut> 내의 expression의 값에 Pointcut Designator와 Pattern Matching을 이용하여 pointcut을 정의한다. 그리고 id의 값에 식별자를 부여한다. (Pointcut 정의시에는 Pointcut Designator와 Pattern Matching 활용 방법 을 참고한다.)

<aop:pointcut id="serviceMethod" expression="execution(* *..GenericService+.*(..))" />

이것은 클래스명이 GenericService로 끝나는 모든 메소드의 실행 부분이 Aspect을 적용할 Pointcut임을 의미한다. 해당 Pointcut은 serviceMethod라는 이름으로 이용 가능하다.

7.3.3.Advice 정의 및 구현

다음에서는 XML 기반에서 동작 시점별로 Advice 정의 및 구현 방법에 대해 살펴보기로 한다.

7.3.3.1.Before Advice

<aop:before>를 이용하여 Before Advice를 정의한다. 다음은 context-aspect.xml 의 Before Advice 정의 부분이다. 앞서 정의한 serviceMethod pointcut을 참조하고 있으며, 해당 pointcut 전에 methodLoggingAspect라는 Bean의 beforeLogging() 메소드를 호출해야 함을 명시하고 있다.

<aop:before method="beforeLogging" pointcut-ref="serviceMethod"/>

다음은 Before Advice를 구현하고 있는 LoggingAspect 클래스의 일부이다. Before Advice 역할을 수행하는 beforeLogging()는 앞서 정의한 serviceMethod Pointcut 전에 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

public class LoggingAspect {
	public void beforeLogging(JoinPoint thisJoinPoint) {
		Class clazz = thisJoinPoint.getTarget().getClass();
		String className = (thisJoinPoint.getTarget().getClass().getName())
				.toLowerCase();
		String methodName = thisJoinPoint.getSignature().getName();

		StringBuffer buf = new StringBuffer();
		buf.append("\n** Logging Aspect : executed " + methodName + "() in "
				+ className + " Class.");
		Object[] arguments = thisJoinPoint.getArgs();
		if (arguments.length > 0) {
			for (int i = 0; i < arguments.length; i++) {
				buf
						.append("\n*************"
								+ arguments[i].getClass().getName()
								+ "*************\n");
				buf.append(arguments[i].toString());
				buf.append("\n*******************************************\n");
			}
		} else
			buf.append("\nNo arguments\n");

		Log logger = LogFactory.getLog(clazz);
		if (logger.isDebugEnabled())
			logger.debug(buf.toString());
	}
}

beforeLogging()는 1개의 입력 인자(JoinPoint)를 가지고 있는데 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있다. Target 정보가 불필요한 Advice인 경우에는 JoinPoint라는 입력 인자를 선언하지 않아도 된다.

7.3.3.2.AfterReturning Advice

<aop:after-returning>을 이용하여 AfterReturning Advice를 정의한다. 다음은 context-aspect.xml 의 AfterReturning Advice 정의 부분이다. 앞서 정의한 serviceMethod라는 pointcut을 참조하고 있으며, 해당 pointcut 후에 printStringAspect라는 Bean의 afterReturningExecuteGetMethod() 메소드를 호출해야 함을 명시하고 있다. 또한 해당 Pointcut 실행 결과를 retVal이라는 변수에 담도록 하고 있다.

<aop:after-returning method="afterReturningExecuteGetMethod" returning="retVal" 
     pointcut-ref="serviceMethod" />       

다음은 AfterReturning Advice를 구현하고 있는 PrintStringUsingXML 클래스의 일부이다. AfterReturning Advice 역할을 수행하는 afterReturningExecuteGetMethod()는 앞서 정의한 Pointcut 후에 , "AfterReturning Advice of PrintStringUsingXML"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

public class PrintStringUsingXML {
	// ...
	
	public void afterReturningExecuteGetMethod(JoinPoint thisJoinPoint, Object retVal) {
		Class targetClass = thisJoinPoint.getTarget().getClass();
		Signature signature = thisJoinPoint.getSignature();
		String opName = signature.getName();
	
		System.out.println("AfterReturning Advice of PrintStringUsingXML");
		System.out.println("***" + targetClass + "." + opName + "()" + "***");
	}
	
	// ...
}

afterReturningExecuteGetMethod()는 2개의 입력 인자(JoinPoint, Object)를 가지고 있는데 첫번째 인자는 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있으며, 두번째 인자는 해당 Pointcut의 실행 결과이다. AfterReturning Advice에서 특정 Pointcut 실행 결과를 참조해야 한다면, XML에 해당 Advice 정의시 returning의 값을 정의하고 해당하는 메소드의 입력 인자명을 동일하게 정의해주도록 한다. 각 입력 인자는 AfterReturning Advice 정의시 필요에 따라 선택 정의할 수 있다.

7.3.3.3.AfterThrowing Advice

<aop:throwing>을 이용하여 AfterThrowing Advice를 정의한다. 다음은 context-aspect.xml 의 AfterThrowing Advice 정의 부분이다. 앞서 정의한 serviceMethod라는 pointcut을 참조하고 있으며, 해당 pointcut 후에 exceptionTransfer Bean의 tranfer() 메소드를 호출해야 함을 명시하고 있다. 또한 해당 Pointcut 실행시 발생한 Exception을 exception이라는 변수로 해당 Advice의 입력 인자명과 동일해야 한다.

<aop:after-throwing throwing="exception" pointcut-ref="serviceMethod" method="transfer" />

다음은 AfterThrowing Advice를 구현하고 있는 ExceptionTransfer 클래스의 일부이다. AfterThrowing Advice 역할을 수행하는 transfer()는 앞서 정의한 Pointcut에서 Exception이 발생한 후에 에러메시지를 출력하는 역할을 수행한다.

public class ExceptionTransfer {

	private MessageSource messageSource;

	public void setMessageSource(MessageSource messageSource) {
		this.messageSource = messageSource;
	}	

	public void transfer(JoinPoint thisJoinPoint, Exception exception)
			throws SalesException {
		Object target = thisJoinPoint.getTarget();
		while (target instanceof Advised) {
			try {
				target = ((Advised) target).getTargetSource().getTarget();
			} catch (Exception e) {
				LogFactory.getLog(this.getClass()).error(
						"Fail to get target object from JointPoint.", e);
				break;
			}
		}

		String className = target.getClass().getSimpleName().toLowerCase();
		String opName = (thisJoinPoint.getSignature().getName()).toLowerCase();
		Log logger = LogFactory.getLog(target.getClass());

		if (exception instanceof SalesException) {
			SalesException empEx = (SalesException) exception;
			logger.error(empEx.getMessage(), empEx);
			throw empEx;
		}
		
		if (exception instanceof BaseException) {
			BaseException baseEx = (BaseException) exception;
			logger.error(baseEx.getMessage(), baseEx);
			throw new SalesException(messageSource, "error." + className + "."
					+ opName, new String[] {}, exception);
		}		

		logger.error(messageSource.getMessage("error." + className + "."
				+ opName, new String[] {}, "no messages", Locale.getDefault()),
				exception);

		throw new SalesException(messageSource, "error." + className + "."
				+ opName, new String[] {}, exception);
	}
}

transfer()는 2개의 입력 인자(JoinPoint, Exception)를 가지고 있는데 첫번째 인자는 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있으며, 두번째 인자는 Pointcut 실행시 발생한 Exception 객체이다. AfterThrowing Advice에서 특정 Pointcut 실행시 발생한 Exception을 참조해야 한다면, XML에 해당 Advice 정의시 throwing의 값을 정의하고 해당하는 메소드의 입력 인자명을 동일하게 정의해주도록 한다. 각 입력 인자는 AfterThrowing Advice 정의시 필요에 따라 선택 정의할 수 있다.

7.3.3.4.After(finally) Advice

<aop:after>를 이용하여 After(finally) Advice를 정의한다. 다음은 >cnotext-aspect.xml 의 After(finally) Advice 정의 부분이다. 앞서 정의한 getMethods라는 pointcut을 참조하고 있으며, 해당 pointcut 후에 printStringAspect라는 Bean의 afterExecuteGetMethod() 메소드를 호출해야 함을 명시하고 있다.

<aop:after method="afterExecuteGetMethod" pointcut-ref="getMethods" />        

다음은 After(finally) Advice를 구현하고 있는 PrintStringUsingXML 클래스의 일부이다. After(finally) Advice 역할을 수행하는 afterExecuteGetMethod()는 앞서 정의한 getMethods()라는 Pointcut 후에 "After(finally) Advice of PrintStringUsingXML"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

public class PrintStringUsingXML {
	// ...
	
	public void afterExecuteGetMethod(JoinPoint thisJoinPoint) {
		Class targetClass = thisJoinPoint.getTarget().getClass();
		Signature signature = thisJoinPoint.getSignature();
		String opName = signature.getName();

		System.out
				.println("After(finally) Advice of PrintStringUsingXML");
		System.out.println("***" + targetClass + "." + opName + "()" + "***");
	}
	
	// ...
}	       

afterExecuteGetMethod()는 1개의 입력 인자(JoinPoint)를 가지고 있는데 Target 클래스명, 메소드명 등과 같은 Target 정보를 포함하고 있다. Target 정보가 불필요한 Advice인 경우에는 JoinPoint라는 입력 인자를 선언하지 않아도 된다.

7.3.3.5.Around Advice

<aop:around>를 이용하여 Around Advice를 정의한다. 다음은 context-aspect.xml 의 Around Advice 정의 부분이다. updateMethods라는 pointcut을 참조하고 있으며, 해당 pointcut 후에 printStringAspect라는 Bean의 aroundExecuteGetMethod() 메소드를 호출해야 함을 명시하고 있다.

<aop:around method="aroundExecuteUpdateMethod" pointcut-ref="updateMethods" />        

다음은 Around Advice를 구현하고 있는 PrintStringUsingXML 클래스의 일부이다. Around Advice 역할을 수행하는 aroundExecuteUpdateMethod()는 updateMethods()라는 Pointcut 후에 "Around Advice of PrintStringUsingXML"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

public class PrintStringUsingXML {
	// ...
	
	public Object aroundExecuteUpdateMethod(ProceedingJoinPoint thisJoinPoint)
			throws Throwable {
		Class targetClass = thisJoinPoint.getTarget().getClass();
		Signature signature = thisJoinPoint.getSignature();
		String opName = signature.getName();

		System.out.println("Around Advice of PrintStringUsingXML");
		System.out.println("***" + targetClass + "." + opName + "()" + "***");
		// before logic
		Object retVal = thisJoinPoint.proceed();
		// after logic
		return retVal;
	}
	
	// ...
}	        

aroundExecuteUpdateMethod()는 1개의 입력 인자(ProceedingJoinPoint)를 가지고 있는데 proceed()라는 메소드 호출을 통해 대상 Pointcut을 실행할 수 있으며, Target 클래스명, 메소드명 등과 같은 Target 정보도 포함하고 있다. 즉, Pointcut 전, 후 처리가 가능하며, Pointcut 실행 시점을 결정할 수 있다. 또한 다른 Advice와는 달리 입력값, target, return 값 등에 대해 변경이 가능하다. Target 정보가 불필요한 Advice인 경우에는 ProceedingJoinPoint라는 입력 인자를 선언하지 않아도 된다.

7.3.4.Aspect 실행

이제 테스트코드 Main.java를 이용하여 앞서 언급한 Aspect들이 정상적으로 동작하는지 확인해 보도록 하자. 다음은 테스트코드 Main.java 의 main()메소드로 실제 product에 대한 CRUD 로직을 수행함으로써 Befor Advice로 정의된 LoggingAspect가 제대로 수행되는지 확인할 수 있다.

public static void main(String[] args) throws Exception {
		Main main = new Main();

		// 1. initialize context
		main.setup();
		// 2. test
		main.manageProduct();
		// 3. close context
		main.teardown();
	}

	public void manageProduct() throws Exception {
		// 1. lookup productService, categoryService
		ProductService productService = (ProductService) context
				.getBean("productService");

		// 2. create a new product
		Product product = new Product();
		product.setProdNo("PRODUCT-99999");
		product.setProdName("example.sportsone");
		product.setProdDetail("sports one detail");
		product.setSellerId("woos41");
		product.setAsYn("Y");
		product.setManufactureDay("20081225");
		product.setSellAmount(new Long(50));
		product.setSellQuantity(new Long(50));
		productService.create(product);

		// 3. get product list
		ProductSearchVO searchVO = new ProductSearchVO();
		searchVO.setSearchCondition("0");
		searchVO.setSearchKeyword("example.sportsone");
		Page products = productService.getPagingList(searchVO);
		System.out.println("after creating a new product, product size is a '"
				+ products.getSize() + "'.");

		// 4. update a product
		product.setProdName("sportsone-update");
		product.setProdDetail("sports one detail-update");
		productService.update(product);

		// 5. get a product
		Product result = productService.get(product.getProdNo());
		System.out.println("after updating a product, product name is a '"
				+ result.getProdName() + "'.");

		// 6. remove a product
		productService.remove(product.getProdNo());

		// 7. get product list
		searchVO = new ProductSearchVO();
		searchVO.setSearchCondition("0");
		searchVO.setSearchKeyword("example.sportsone");
		products = productService.getPagingList(searchVO);
		System.out.println("after creating a new product, product size is a '"
				+ products.getSize() + "'.");
	}
}

첫번째 로직 productService.create(product); 실행시 Before Advice로 정의된 LoggingAspect 클래스가 적용되며, 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.

** Logging Aspect : executed initialize() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
No arguments
** Logging Aspect : executed initialize() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
No arguments

두번째 로직productService.getPagingList(searchVO); 실행시 Before Advice로 정의된 LoggingAspect 클래스가 적용되며, 콘솔창에 다음과 같은 실행 결과를 포함하게 된다.

** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

after creating a new product, product size is a '1'.

7.4.AspectJ based AOP

다음에서는 AOP 대표적인 툴 중 AspectJ를 이용하여 Aspect를 정의하고 테스트하는 방법에 대해서 다루고자 한다.

7.4.1.시작하기 전에

AspectJ를 이용하기 위해서는 다음과 같은 사항에 대해 확인이 필요하다.

  1. AJDT(AspectJ Development Tool) 설치 Eclipse 플러그인 AJDT는 Aspect 파일을 생성하고 컴파일하기 위한 개발툴이다. 사용중인 Eclipse 내에 플러그인 AJDT(AspectJ Development Tool)가 설치되어 있지 않다면, AJDT(AspectJ Development Tool)를 다운로드 하여 사용중인 Eclipse 내에 설치하는 것이 좋다. 만약, AJDT를 이용하지 않고 Aspect를 컴파일하고자 한다면, aspectjtools-1.5.4.jar 내에 정의된 Ant task "iajc"을 이용하도록 한다. 다음은 샘플 build.xml 파일의 compile target의 내용이다.

    <target name="compile" depends="init">
       <taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
          <classpath>
            <pathelement location="${lib.dir}/aspectjtools-1.5.4.jar" />
          </classpath>
       </taskdef>
    
       <iajc verbose="true" destdir="${output.dir}" debug="on" source="1.5"
                 showweaveinfo="true" xnoinline="true">
          <sourceroots>
             <pathelement location="src/main/java" />
          </sourceroots>
          <classpath>
             <pathelement location="${lib.dir}/aspectjrt-1.5.4.jar" />
             <pathelement location="${lib.dir}/commons-logging-1.0.4.jar" />
          </classpath>
       </iajc>
    </target>        
  2. Convert to AspectJ Project 특정 프로젝트가 확장자 aj를 가진 Aspect 파일을 인식할 수 있도록 하기 위해서는 해당 프로젝트에 대한 context menu AspectJ Tools > Convert to AspectJ Project를 선택하여 해당 프로젝트에 대해 AspectJ 프로젝트의 성격을 부여해 주어야 한다.

7.4.2.Aspect 정의

확장자가 aj인 파일을 생성하고, aspect을 이용하여 Aspect 클래스를 정의한다. 다음 LoggingAspect에서는 aspect를 이용하여 해당 클래스가 Aspect임을 나타내고 있다.

public aspect LoggingAspect {
	// ...
}        

7.4.3.Pointcut 정의

pointcut을 이용하여 해당 Aspect를 적용할 부분을 정의한다. (Pointcut 정의시에는 Pointcut Designator와 Pattern Matching 활용 방법 을 참고한다.) 다음은 LoggingAspect 의 Pointcut 정의 부분이다. pointcut을 "execution(* *..GenericService+.*(..))"와 같이 정의하고, 해당 Pointcut에 대해 식별자로써 serviceMethod()라는 메소드명을 부여하였다. 이것은 클래스명이 GenericService로 끝나는 모든 메소드의 실행 부분이 Aspect을 적용할 Pointcut임을 의미한다. 해당 Pointcut은 serviceMethod()라는 이름으로 이용 가능하다.

pointcut serviceMethod(): execution(* *..GenericService+.*(..));      

7.4.4.Advice 정의

다음에서는 AspectJ 기반에서 동작 시점별 Advice를 정의하는 방법에 대해 살펴보기로 한다.

7.4.4.1.Before Advice

before()를 이용하여 Before Advice를 정의한다. 다음은 PrintStringUsingAspctJ 의 Before Advice 정의 부분이다. Before Advice는 앞서 정의한 getMethods()라는 Pointcut 전에 "Before Advice of PrintStringUsingAspctJ"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

before() : getMethods() {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("Before Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");		
}      

위에서 제시한 Before Advice는 내부에 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다.

7.4.4.2.AfterReturning Advice

after() returning()을 이용하여 AfterReturning Advice를 정의한다. 다음은 PrintStringUsingAspctJ 의 AfterReturning Advice 정의 부분으로 해당 Pointcut 실행 결과를 retVal이라는 변수에 담도록 정의하고 있다. AfterReturning Advice는 앞서 정의한 Pointcut 후에 , "AfterReturning Advice of PrintStringUsingAspctJ"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

after() returning(UserVO retVal) : getMethods() {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out
			.println("AfterReturning Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}        

위에서 제시한 AfterReturning Advice는 내부 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다. 또한, 1개의 입력 인자(UserVO)를 가지고 있는데 이는 해당 Pointcut의 실행 결과이다. AfterReturning Advice에서 특정 Pointcut 실행 결과를 참조해야 한다면, Advice 정의시 returning에 해당하는 객체를 정의하고 메소드 로직 내에서 이를 활용하면 된다. 입력 인자는 AfterReturning Advice 정의시 필요에 따라 선택 정의할 수 있다.

7.4.4.3.AfterThrowing Advice

after() throwing()을 이용하여 AfterThrowing Advice를 정의한다. 다음은 PrintStringUsingAspctJ 의 AfterThrowing Advice 정의 부분으로 해당 Pointcut 실행시 발생한 Exception 객체를 exception이라는 변수에 담도록 정의하고 있다. AfterThrowing Advice는 앞서 정의한 Pointcut에서 Exception이 발생한 후에 , "AfterThrowing Advice of PrintStringUsingAspctJ"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

after() throwing(Exception exception) : getMethods() {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out
			.println("AfterThrowing Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
}        
위에서 제시한 AfterThrowing Advice는 내부 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다. 또한, 1개의 입력 인자(Exception)를 가지고 있는데 이것은 Pointcut 실행시 발생한 Exception 객체이다. AfterThrowing Advice에서 특정 Pointcut 실행시 발생한 Exception을 참조해야 한다면, Advice 정의시 throwing에 해당하는 객체를 메소드 로직 내에서 이를 활용하면 된다. 입력 인자는 AfterThrowing Advice 정의시 필요에 따라 선택 정의할 수 있다.

7.4.4.4.After(finally) Advice

after()를 이용하여 After(finally) Advice를 정의한다. 다음은 PrintStringUsingAspctJ 의 After(finally) Advice 정의 부분이다. After(finally) Advice는 앞서 정의한 getMethods()라는 Pointcut 후에 "After(finally) Advice of PrintStringUsingAspctJ"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

after() : getMethods() {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out
			.println("After(finally) Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");		
}        

위에서 제시한 After(finally) Advice는 내부에 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다.

7.4.4.5.Around Advice

around()를 이용하여 Around Advice를 정의한다. 다음은 PrintStringAroundUsingAspctJ 의 Around Advice 정의 부분으로 다른 Advice와 다르게 Return Type 정의가 추가되어 있음을 알 수 있다. Around Advice는 updateMethods()라는 Pointcut 후에 "Around Advice of PrintStringUsingAnnotation"라는 문자열과 해당 Pointcut을 가진 클래스명, 메소드명을 출력하는 역할을 수행한다.

Object around() : updateMethods() {
	Class targetClass = thisJoinPoint.getTarget().getClass();
	Signature signature = thisJoinPoint.getSignature();
	String opName = signature.getName();

	System.out.println("Around Advice of PrintStringUsingAspctJ");
	System.out.println("***" + targetClass + "." + opName + "()" + "***");
	// before logic
	Object retVal = proceed();
	// after logic
	return retVal;		
}        

위에서 제시한 Around Advice는 내부에 정의된 JoinPoint 유형의 thisJoinPoint라는 객체를 이용하여, Target 클래스명, 메소드명 등과 같은 Target 정보를 추출하고 있다. 또한 Around Advice 내에서 proceed()라는 메소드 호출을 통해 대상 Pointcut을 실행할 수 있어, Pointcut 전, 후 처리가 가능하며, Pointcut 실행 시점을 결정할 수 있게 된다. 또한 다른 Advice와는 달리 입력값, target, return 값 등에 대해 변경이 가능하다.

7.5.AOP Examples

다양한 부분에 Aspect을 적용할 수 있다. 이 페이지를 통하여 각 적용 예를 살펴보고, 적용 방법을 상세히 소개하고자 한다. 상세한 내용을 알고자 한다면, 아래 나열된 각 항목에 대한 링크를 참고하도록 한다.

7.5.1.AOP Example - Logging

개발된 어플리케이션 테스트시 오류가 발생한 경우, 해당하는 메소드 로직 내에 입력값 확인을 위해 DEBUG 레벨의 로그를 추가하거나, System.out.println() 구문을 추가하게 되는데 이로 인해 핵심 비즈니스 로직과 섞이게 되어 코드 복잡도가 증가한다. 따라서 특정 메소드 호출시 전달하는 입력값 확인을 위한 별도 Aspect을 정의하여 활용하면 관련된 메소드 내에 입력값 확인을 위한 로직들을 제외시킬 수 있게 된다. 다음에서는 AOP의 대표적인 툴 중 @AspectJ(Annotation)를 이용하여 Logging Aspect를 생성하고 테스트해 보도록 할 것이다. Logging Aspect 적용 대상은 GenericService 이며, 모든 메소드 실행 전에 해당 메소드를 실행하기 위해 입력된 인자들의 값을 로그로 남기는 역할을 수행하게 될 것이다.

7.5.1.1.Configuration

@AspectJ(Annotation)이 적용된 클래스들을 로딩하여 해당 클래스에 정의된 Pointcut, Advice를 실행하기 위해서는 Spring 속성 정의 XML 파일에 다음과 같이 추가해주어야 한다.

<aop:aspectj-autoproxy/>

7.5.1.2.Aspect 정의

다음과 같이 Annotation과 함께 구성된 LoggingAspect라는 Aspect 클래스를 생성한다. LoggingAspect라는 GenericService로 끝나는 클래스의 모든 메소드 실행 전에 해당 메소드 정보와 입력 인자값을 로그로 남기는 역할을 수행한다.

@Aspect
public class LoggingAspect {
	@Pointcut("execution(* *..GenericService+.*(..))")
	public void serviceMethod(){}
	
	@Before("serviceMethod()")
	public void beforeLogging(JoinPoint thisJoinPoint) {
		Class clazz = thisJoinPoint.getTarget().getClass();
		String className = (thisJoinPoint.getTarget().getClass().getName())
				.toLowerCase();
		String methodName = thisJoinPoint.getSignature().getName();

		StringBuffer buf = new StringBuffer();
		buf.append("\n** Logging Aspect : executed " + methodName + "() in "
				+ className + " Class.");
		Object[] arguments = thisJoinPoint.getArgs();
		if (arguments.length > 0) {
			for (int i = 0; i < arguments.length; i++) {
				buf
						.append("\n*************"
								+ arguments[i].getClass().getName()
								+ "*************\n");
				buf.append(arguments[i].toString());
				buf.append("\n*******************************************\n");
			}
		} else
			buf.append("\nNo arguments\n");

		Log logger = LogFactory.getLog(clazz);
		if (logger.isDebugEnabled())
			logger.debug(buf.toString());
	}
}

beforeLogging() 메소드에서는 JoinPoint라는 객체를 이용하여, 해당 메소드 정보와 입력 인자값을 Target 클래스의 로를 통해 로그를 남기고 있음을 알 수 있다.

7.5.1.3.Aspect 실행

productService를 호출하여, Product CRUD 로직으로 구성된 Main.java 클래스를 실행시키면 Logging Aspect가 적용되어, 콘솔창을 통해 다음과 같은 형태의 로그를 볼 수 있다.

2009-12-01 16:33:55,468 DEBUG [anyframe.example.aop.sales.service.impl.ProductServiceImpl] 
** Logging Aspect : executed initialize() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
No arguments

2009-12-01 16:33:55,468 DEBUG [anyframe.example.aop.sales.service.impl.ProductServiceImpl] 
** Logging Aspect : executed initialize() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
No arguments

2009-12-01 16:33:55,781 DEBUG [anyframe.example.aop.sales.service.impl.ProductServiceImpl] 
** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

2009-12-01 16:33:55,781 DEBUG [anyframe.example.aop.sales.service.impl.ProductServiceImpl] 
** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

after creating a new product, product size is a '1'.
after updating a product, product name is a 'sportsone-update'.
2009-12-01 16:33:55,906 DEBUG [anyframe.example.aop.sales.service.impl.ProductServiceImpl] 
** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

2009-12-01 16:33:55,906 DEBUG [anyframe.example.aop.sales.service.impl.ProductServiceImpl] 
** Logging Aspect : executed getPagingList() in anyframe.example.aop.sales.service.impl.productserviceimpl Class.
*************anyframe.example.aop.sales.service.ProductSearchVO*************
 searchCondition - 0
 searchKeyword - example.sportsone
 pageIndex - 1

*******************************************

after creating a new product, product size is a '0'.

7.5.2.AOP Example - Exception Transfer

특정 비즈니스 로직 수행시 발생할 수 있는 Exception에 대한 로그 및 메시지 처리를 수행하기 위해 핵심 비즈니스 로직외에 Exception 처리 로직이 추가되어야 한다. 때문에 핵심 비즈니스 로직외에 매 로직마다 반복되는 try ~ catch 블럭으로 인해 코드가 복잡해진다. 만일 별도 Aspect을 통해 공통적으로 Exception들을 처리하게 하고, 각 비즈니스 로직에서 try ~ catch 블럭을 제거할 수 있다면 코드가 훨씬 간단해지고, 궁극적으로 개발자는 비즈니스 로직에만 집중할 수 있는 기반이 마련될 수 있을 것이다. 다음에서는 AOP 대표적인 툴 중 Spring AOP를 이용하여 XML 스키마 기반에서 ExceptionTransfer를 위한 Aspect를 생성하고 테스트해 보도록 할 것이다. ExceptionTransfer Aspect 적용 대상은 GenericService로 끝나는 클래스의 모든 메소드 실행시 Exception이 발생한 경우 이를 처리하기 위한 역할을 수행하게 될 것이다.

7.5.2.1.Aspect 정의

Spring 속성 정의 XML(context-xml.xml ) 파일 내에 Aspect 클래스를 Bean으로 정의한 후, 해당 Aspect에 대한 Pointcut과 Advice를 정의한다.

<aop:config>
	<aop:pointcut id="serviceMethod" expression="execution(* *..GenericService+.*(..))" />
	<aop:aspect ref="exceptionTransfer" order="1"> 
		<aop:after-throwing throwing="exception" pointcut-ref="serviceMethod" method="transfer" />					
	</aop:aspect>
</aop:config>

ExceptionTransfer는 GenericService로 끝나는 클래스의 모든 메소드 실행시 발생한 Exception에 대해 처리하는 역할을 수행한다.

7.5.2.2.Advice 구현

다음과 같이 ExceptionTransfer 라는 Aspect 클래스를 생성한다.

public class ExceptionTransfer {

	private MessageSource messageSource;

	public void setMessageSource(MessageSource messageSource) {
		this.messageSource = messageSource;
	}	

	public void transfer(JoinPoint thisJoinPoint, Exception exception)
			throws SalesException {
		Object target = thisJoinPoint.getTarget();
		while (target instanceof Advised) {
			try {
				target = ((Advised) target).getTargetSource().getTarget();
			} catch (Exception e) {
				LogFactory.getLog(this.getClass()).error(
						"Fail to get target object from JointPoint.", e);
				break;
			}
		}

		String className = target.getClass().getSimpleName().toLowerCase();
		String opName = (thisJoinPoint.getSignature().getName()).toLowerCase();
		Log logger = LogFactory.getLog(target.getClass());

		if (exception instanceof SalesException) {
			SalesException empEx = (SalesException) exception;
			logger.error(empEx.getMessage(), empEx);
			throw empEx;
		}
		
		if (exception instanceof BaseException) {
			BaseException baseEx = (BaseException) exception;
			logger.error(baseEx.getMessage(), baseEx);
			throw new SalesException(messageSource, "error." + className + "."
					+ opName, new String[] {}, exception);
		}		

		logger.error(messageSource.getMessage("error." + className + "."
				+ opName, new String[] {}, "no messages", Locale.getDefault()),
				exception);

		throw new SalesException(messageSource, "error." + className + "."
				+ opName, new String[] {}, exception);
	}
}

transfer() 메소드에서는 발생한 Exception 객체의 유형을 SalesException, BaseException, 기타로 구분하고 Exception이 발생한 Target 클래스와 메소드명을 조합한 message key를 이용하여 해당하는 메시지를 얻어낸다. 그런 후에 이 메시지를 이용하여 ERROR 레벨의 로그를 남긴 후에 SalesException으로 전환하여 throw한다.

7.5.3.AOP Example - Profiler

별도 성능 측정 툴없이도 Aspect을 통해 응답 속도가 중요시 되는 일부 메소드에 대해 개발 시점에 미리 메소드 수행에 걸리는 시간을 측정해 볼 수 있다. 따라서 개발시에 성능 저하의 요인이 되는 지점을 미리 파악하고 대처해 볼 수 있을 것이다.

다음에서는 AOP의 대표적인 툴 중 @AspectJ(Annotation)를 이용하여 SimpleProfiler Aspect를 생성하고 테스트해 보도록 할 것이다. SimpleProfiler Aspect 적용 대상은 UserService 이며, 특정 메소드(add*) 실행에 소요되는 시간을 측정하고, 이를 콘솔에 남기는 역할을 수행하게 될 것이다.

7.5.3.1.Configuration

@AspectJ(Annotation)이 적용된 클래스들을 로딩하여 해당 클래스에 정의된 Pointcut, Advice를 실행하기 위해서는 Spring 속성 정의 XML 파일에 다음과 같이 추가해주어야 한다.

<aop:aspectj-autoproxy/>

7.5.3.2.Aspect 정의

다음과 같이 Annotation과 함께 구성된 SimpleProfiler 라는 Aspect 클래스를 생성한다. SimpleProfiler는 anyframe.example 패키지 내에 속한 모든 클래스 중 클래스명이 Impl로 끝나는 모든 클래스 내의 메소드명이 add로 시작하는 메소드를 대상으로 한다. 그리고 해당 메소드의 실행 전후에 Spring에서 제공하는 StopWatch를 이용하여 메소드 실행에 소요되는 시간을 측정하고, 이를 콘솔에 남기는 역할을 수행하게 될 것이다.

@Aspect
public class SimpleProfiler {

	@Pointcut("execution(* anyframe.example.aop.service..*Impl.add*(..))")
	public void addMethods() {
	}

	@Around("addMethods()")
	public Object profile(ProceedingJoinPoint thisJoinPoint) throws Throwable {
		String className = thisJoinPoint.getSignature().getDeclaringTypeName();
		StopWatch stopWatch = new StopWatch("Profiling for [" + className + "]");
		try {
			stopWatch.start(thisJoinPoint.toShortString());
			return thisJoinPoint.proceed();
		} finally {
			stopWatch.stop();
			System.out.println(stopWatch.shortSummary());
		}
	}
}

7.5.3.3.Aspect 실행

UserService를 호출하여, 구현 로직에서 1000 milliseconds 동안 멈추도록 로직이 추가되어 있는, 신규 User 정보 등록 기능을 호출하는 SimpleProfilerAspectTest 클래스를 실행시키면 SimpleProfiler Aspect가 적용되어, 콘솔창을 통해 다음과 같은 형태의 로그를 볼 수 있다.

StopWatch 'Profiling for [anyframe.example.aop.service.UserServiceImpl]'
: running time (millis) = 1016

7.5.4.AOP Example - Design Level Assertions

개발 표준이라 함은 각종 명명 표준 및 해당 프로젝트에서 기 검증한 소프트웨어 아키텍처 스타일에 맞춰 개발 작업을 수행할 수 있도록 가이드한다. 따라서, 개발자들이 개발 초기에 겪게 되는 혼선을 줄이고 비즈니스 로직에만 집중할 수 있도록 하며, 동일한 표준을 준수한 코드에 대해서는 유지보수 및 변경이 용이하다. 대부분의 프로젝트에서는 어플리케이션을 본격적으로 개발하기에 앞서 상당한 시간을 들여 해당 프로젝트에 적합한 개발 표준을 별도 문서로 정의하고 개발자들이 이를 준수하여 개발 작업을 수행할 것을 권장하나, 제대로 지켜지지 않고 있으며 이에 대한 검증 또한 한계가 있는게 사실이다.

만일 코드 컴파일시에 개발 표준을 적용할 수 있다면, 코딩시에 손쉽게 표준에 부적합한 코드를 인식하고 수정할 수 있게 될 것이다. 이를 위해 본 문서에서는 Design Rule을 declare error/warning 문으로 구성된 Aspect로 정의하고, 부적합 사항을 찾아 수정하는 방법에 대해 알아보기로 하자.

먼저, declare error/warning 문은 다음과 같이 정의하며, Pointcut Expression에 해당하는 JoinPoint가 있을 경우 정의된 메시지를 보여준다.

@DeclareWarning ("Pointcut Expressions")
static final String varialableName = "msg...";

다음은 Anyframe 기반 어플리케이션 개발시 가장 흔하게 볼 수 있는 일부 소프트웨어 아키텍처 그림이다.

해당 어플리케이션의 패키지는 com.sds.emp로 시작하며, 프리젠테이션 레이어는 com.sds.emp.서브모듈명.web, 비즈니스 레이어는 com.sds.emp.서브모듈명.services 내에 위치한다라고 가정하자.

정의 가능한 Design Rule은 크게 Interaction Rule , Naming Rule 로 구분해 볼 수 있으며, 이제부터 위 그림을 기반으로 Design Rule을 하나씩 정의해 보도록 하자.

7.5.4.1.Interaction Rule 정의 예제

패키지 레벨, 클래스 레벨 등에서 필요한 Pointcut을 정의하고 Declare 문에서 앞서 정의한 여러 Pointcut을 조합하여 클래스간 Interaction Rule을 정의하였다. 이는 기 정의된 Pointcut을 다른 Declare 문에서 재사용하기 위함이다. 다음은 DevStandard Aspect에 정의된 Interaction Rule의 일부이다.

  1. 프리젠테이션 레이어에 속하지 않은 클래스에서 Action 또는 Form 클래스를 호출할 수 없다.

    // 패키지명이 com.sds.emp로 시작하고 중간에 web을 포함하는 모든 패키지에 속한 JoinPoint
    @Pointcut("within(com.sds.emp..web..*)")
    public void inWebPkg() {}
    
    // 클래스명이 Action 또는 Form으로 끝나는 클래스의 모든 메소드 호출하는 JoinPoint
    @Pointcut("call(* com.sds.emp..web.*Action.*(..)) && call(* com.sds.emp..web.*Form.*(..))")
    public void callToWeb() {}	
    	
    // web 패키지에 속하지 않은 클래스에서 web 패키지 내의 Action 또는 Form 클래스를 호출하는 경우 
    // 다음과 같은 Error 메시지를 보여준다.
    @DeclareError("!inWebPkg() && callToWeb()")
    static final String irMsg5 = "web 패키지에 속한 모든 클래스에 접근할 수 없습니다.";

  2. 프리젠테이션 레이어에서는 반드시 Interface를 통해 특정 서비스에 접근해야 한다.

    // 패키지명이 com.sds.emp로 시작하고 중간에 web을 포함하는 모든 패키지에 속한 JoinPoint
    @Pointcut("within(com.sds.emp..web..*)")
    public void inWebPkg() {}
    
    // 클래스명이 DAO로 끝나는 클래스의 모든 메소드 호출하는 JoinPoint
    @Pointcut("call(* com.sds.emp..services.impl.*DAO.*(..))")
    public void callToDAO() {}
    
    // 클래스명이 Impl로 끝나는 클래스의 모든 메소드 호출하는 JoinPoint
    @Pointcut("call(* com.sds.emp..services.impl.*Impl.*(..))")
    public void callToImplementation() {}
    	
    // web 패키지에서 DAO 또는 Impl 내의 메소드를 직접 호출하는 경우 다음과 같은 Error 메시지를 보여준다.
    @DeclareError("inWebPkg() && ( callToDAO() || callToImplementation())")
    static final String irMsg1 = "Action 클래스에서는 특정 서비스의 구현 클래스나 DAO 클래스에 직접 "
    + "접근할 수 없습니다.";              

  3. 특정 객체(java.sql.Connection)를 직접 사용하지 않도록 한다.

    // 패키지명이 integration 또는 unit으로 시작하는 모든 테스트 패키지에 속한 JoinPoint
    @Pointcut("within(integration..* || unit..*)")
    public void inTestPkg() {}
    
    // java.sql.Connection 클래스의 모든 메소드를 호출하는 JoinPoint
    @Pointcut("call(* java.sql.Connection.*(..))")
    public void callToConnection() {}
    
    // 테스트 패키지를 제외한 모든 패키지에서 Connection 객체를 직접 호출하는 경우 다음과 같은
    // Error 메시지를 보여준다.
    @DeclareError("callToConnection() && !inTestPkg()")
    static final String irMsg2 = "java.sql.Connection 객체에 직접 접근할 수 없습니다. "
    + "Anyframe Technical Service를 이용하세요.";              

  4. 생성자를 직접 호출하여 DAO 인스턴스를 생성할 수 없다. Dependency Injection을 통해 객체간 참조 관계를 정의해야 한다.

    // DAO 클래스의 Constructor를 호출하는 JoinPoint
    @Pointcut("call(com.sds.emp..services.impl.*DAO.new(..))")
    public void callToDAOConstructor() {}
    
    // DAO 클래스를 Constructor를 직접 호출하는 경우 다음과 같은 Error 메시지를 보여준다.
    @DeclareError("callToDAOConstructor()")
    static final String irMsg3 = "DAO 인스턴스를 직접 생성하실 수 없습니다. "
    + "객체간 참조 관계는 서비스 속성 정의 XML에 정의하여 사용하세요.";              

7.5.4.2.Naming Rule 정의 예제

패키지 레벨, 클래스 레벨 등에서 필요한 Pointcut을 정의하고 Declare 문에서 앞서 정의한 여러 Pointcut을 조합하여 Naming Rule을 정의하였다. 이는 기 정의된 Pointcut을 다른 Declare 문에서 재사용하기 위함이다. 다음은 DevStandard Aspect에 정의된 Naming Rule의 일부이다.

  1. com.sds.emp.서브모듈명.web 패키지 내에 존재하는 클래스명은 Action 또는 Form으로 끝내야 한다.

    // 패키지명이 com.sds.emp로 시작하고 중간에 web을 포함하는 모든 패키지에 속한 JoinPoint
    @Pointcut("within(com.sds.emp..web..*)")
    public void inWebPkg() {}
    
    // 메소드나 Constructor, 메소드 로직이 아닌 모든 JoinPoint 즉, 클래스 정의 부분만 해당
    @Pointcut("!(execution(* *(..)) || withincode(*.new(..)) || withincode(* *(..)))")
    public void clazz(){}
    
    // 패키지명이 com.sds.emp로 시작하고 중간에 web을 포함하는 모든 패키지에 속한 JoinPoint 중 
    // 클래스명이 Action으로 끝나는 클래스에 속한 모든 JoinPoint
    @Pointcut("within(com.sds.emp..web..*Action)")
    public void actionName() {}
    
    // 패키지명이 com.sds.emp로 시작하고 중간에 web을 포함하는 모든 패키지에 속한 JoinPoint 중 
    // 클래스명이 Form으로 끝나는 클래스에 속한 모든 JoinPoint
    @Pointcut("within(com.sds.emp..web..*Form)")
    public void formName() {}		
    
    // web 패키지에 속하면서 클래스명이 Action이나 Form으로 끝나지 않는 클래스 정의 부분이 있을 경우, 
    // 다음과 같은 Warning 메시지를 보여준다.
    @DeclareWarning ("inWebPkg() && clazz() && !(actionName() || formName())")
    static final String nrMsg2 = "web 패키지에 속한 모든 클래스의 이름은 
    Action 또는 Form으로 끝나야 합니다.";              

  2. com.sds.emp.서브모듈명.services.impl 패키지 내에 존재하는 클래스명은 Impl 또는 DAO로 끝내야 한다.

    // 패키지명이 com.sds.emp로 시작하고 중간에 services.impl을 포함하는 모든 패키지에 속한 JoinPoint
    @Pointcut("within(com.sds.emp..services.impl..*)")
    public void inImplementationPkg() {}
    
    // 메소드나 Constructor, 메소드 로직이 아닌 모든 JoinPoint 즉, 클래스 정의 부분만 해당
    @Pointcut("!(execution(* *(..)) || withincode(* *(..)) || withincode(*.new(..)))")
    public void clazz(){}
    
    // 패키지명이 com.sds.emp로 시작하고 중간에 services.impl을 포함하는 모든 패키지에 속한 JoinPoint 중 
    // 클래스명이 Impl로 끝나는 클래스에 속한 모든 JoinPoint
    @Pointcut("within(com.sds.emp..services.impl..*Impl)")
    public void implementationName() {}
    
    // 패키지명이 com.sds.emp로 시작하고 중간에 services.impl을 포함하는 모든 패키지에 속한 JoinPoint 중 
    // 클래스명이 DAO로 끝나는 클래스에 속한 모든 JoinPoint
    @Pointcut("within(com.sds.emp..services.impl..*DAO)")
    public void daoName() {}
    
    // services.impl 패키지에 속하면서 클래스명이 Impl이나 DAO로 끝나지 않는 클래스 정의 부분이 있을 경우, 
    // 다음과 같은 Warning 메시지를 보여준다
    @DeclareWarning ("inImplementationPkg() && clazz() && !(implementationName() || daoName())")
    static final String nrMsg4 = "services 패키지에 속한 모든 클래스의 이름은
    Impl 또는 DAO로 끝나야 합니다.";              

7.5.4.3.Refactoring

Eclipse 기반하에 어플리케이션을 개발하면 Design Rule을 위반한 코드를 보다 손쉽게 수정할 수 있다.

  1. Eclipse 작업 공간 내에 Problems View가 없다면, Eclipse 메뉴 Window > Show View > Problems를 선택하여 Problems view를 오픈한다.

  2. Problems View를 통해 Design Rule을 위반한 항목들을 확인한다.

  3. Problems View에서 수정할 항목을 더블 클릭하여 대상 코드로 이동한다.

  4. Design Rule을 준수한 코드로 수정함으로써 Problem을 제거한다.

    public void setUserDAO(UserDAO userDAO) {
    	this.userDAO = userDAO;
    }
    
    public Page getUserList(SearchVO searchVO) throws EmpException {
    	//UserDAO dao2 = new UserDAO();
    	try {
    		// 중략
    		return userDAO.getUserList(searchVO);
    	}
    	// 중략
    }              

7.6.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.aop.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=anyframe.example.aop.Main이라는 명령어를 실행시켜 결과를 확인한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더의 anyframe/example/aop 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 7.1. Download List

    NameDownload
    hsqldb.zip Download
    anyframe.example.aop.zip Download

  • 참고자료

8.Spring Remoting

Remoting이란 클라이언트 어플리케이션과 원격 어플리케이션에서 제공하는 서비스간의 의사소통을 말한다.

Spring Remoting에서 지원하는 원격 기술은 다음 표와 같다.

원격 기술설명
RMI(Remote Method Invocation)방화벽과 같은 네트워크 제약이 없는 상황에서 자바 기반의 서비스를 공개하거나 접근하려는 경우에 사용한다.
Hessian/Burlap방화벽과 같이 네트워크 제약이 있는 상황에서 자바 기반의 서비스를 공개하거나 접근하려는 경우에 사용한다.
HTTP Invoker방화벽과 같이 네트워크 제약이 있는 상황에서 Spring 기반의 서비스를 공개하거나 접근하려는 경우에 사용한다.
Web Services웹 서비스에 접근하여 서비스를 호출하는 경우에 사용한다. Anyframe에서는 Web Services 기능을 Spring에서 제공하는 Web Service 기능이 아닌, Apache CXF Web Service Framework에서 제공하는 기능을 이용하여 제공하고 있다. Apache CXF는 JDK 5.0만을 지원하므로 Anyframe 를 통해 Web Service 기능을 구현하려면 JDK 5.0을 설치해야 함에 유의하도록 한다. 웹 서비스에 접근하여 서비스를 호출하는 Web Services 매뉴얼 내용은 Web Services 메뉴 를 참고하도록 한다.
  • Spring Remoting 기술 별 장점 및 단점

    원격 기술장점단점
    RMI자바 객체 직렬화 매커니즘을 지원하므로 복잡한 형태의 데이터 타입을 네트워크 상에서 전송 가능함자바 대 자바 Remoting 솔루션이며 HTTP 프로토콜을 사용할 수 없음. RMI 컴파일러와 레지스트리가 별도로 필요함
    Hessian/Burlap방화벽 문제 없음자체 객체 직렬화 매커니즘을 가지고 있어서 복잡한 형태의 데이터 타입 사용이 어려움. Burlap은 자바 클라이언트만 지원함
    HTTP InvokerHTTP 기반의 Remoting 기술 사용 편의성자바 대 자바 Remoting 솔루션이며 서버와 클라이언트 모두 Spring 어플리케이션으로 구축해야 함
    EJBRemoting, 어플리케이션 보안, 트랜잭션 처리 등을 위한 J2EE 서비스무거운(heavyweight) 기술로 J2EE 컨테이너가 반드시 필요함
    Web Services플랫폼과 언어에 독립적SOAP을 사용하며 웹 서비스 구동을 위한 엔진이 필요함

8.1.RMI(Remote Method Invocation)

RMI는 JDK 1.1에서 처음으로 자바에 도입되어 자바 프로그램 사이에 통신할 수 있는 방법을 제공하였다. RMI 이전에는 CORBA를 사용하거나 소켓 프로그래밍을 해야만 했었다. 그러나 RMI 서비스를 개발하거나 접근하는 일은 복잡하여 개발하기가 불편하며 다음과 같은 일을 수행해야한다.

  • java.rmi.Remote를 상속받은 인터페이스 클래스를 작성한다.

  • UnicastRemoteObject를 상속받으며 위의 인터페이스 클래스를 구현하는 구현클래스를 작성한다.

  • RMI 컴파일러를 사용하여 클라이언트 Stub 클래스와 서버 Skeleton 클래스를 생성시킨다. (rmic –d classname)

  • RMI 레지스트리를 구동시키고 서비스를 레지스트리에 바인딩시킨다.

  • 클라이언트 코드를 이용하여 RMI 서비스를 호출하여 사용한다.

Spring의 빈 형태로 서비스를 개발한 후 Spring에서 제공하는 RmiServiceExporter를 사용하면 RMI 객체처럼 서비스 객체의 인터페이스를 쉽게 원격 서비스로 노출할 수 있는 기능을 제공한다. 즉, 위에서 언급한 RMI 서비스 개발 단계에서 수행했던 일들을 RmiServiceExporter에서 수행한다. 개발자는 RMI 서비스와 관련된 개발 작업을 비즈니스 서비스에 반영할 필요가 없으므로 비즈니스 로직에 집중하여 개발할 수 있다. 단, RMI는 통신을 위해 특정 Port를 사용하므로 방화벽을 통과하기 어렵고, 클라이언트와 서버에서 제공되는 서비스 모두 자바로 작성되어야 한다는 제약사항이 존재한다. 다음은 RMI 기능을 Server와 Client 단에서 어떻게 사용해야 하는지에 대한 사용법이다.

8.1.1.Server Configuration

서버 구현 방식은 Spring에서 제공하는 org.springframework.remoting.rmi.RmiServiceExporter 클래스를 이용하여 손쉽게 일반 Spring Bean으로 작성된 서비스를 RMI Service로 노출시킬 수 있다.

Property NameDescriptionRequiredDefault Value
serviceName서비스 이름은 서비스를 RMI Registry에 바인딩 하기 위해 사용된다.YN/A
serviceRMI 서비스로 노출시키고 싶은 Spring Bean의 id를 설정한다.YN/A
serviceInterfaceRMI 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A
registryPortRMI 등록(registry)을 위한 port를 오버라이딩하기 위해 사용된다. 작성하지 않은 경우 디폴트로 1099 port가 사용된다.N1099

8.1.1.1.Samples

다음은 RMI 서버 구현 속성 설정에 대한 예제이다. 서비스는 일반 Spring Bean 개발과 동일하며 RmiServiceExporter Bean에서 property 설정 정보를 참조하여 RMI 서비스로 노출시키고 있다.

  • Configuration

    다음은 RMI 서비스를 지원하는 RmiServiceExporter의 속성을 정의한 context-product-server.xml 의 일부이다.

    <!-- ProductService -->
    <bean id="anyframe.example.remoting.sales.ProductService"
      class="anyframe.example.remoting.sales.impl.ProductServiceImpl">
        <property name="productDAO">
            <ref bean="productDAO" />
        </property>
    </bean>
    
    <bean id="productDAO" class="anyframe.example.remoting.sales.impl.ProductDAOImpl" />
        
    <!-- Add RMI ServiceExporter -->
    <bean class="org.springframework.remoting.rmi.RmiServiceExporter">
    
        <property name="serviceName" value="ProductService" />
        <property name="service" ref="anyframe.example.remoting.sales.ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService" />
        <!-- defaults to 1099 -->
        <property name="registryPort" value="1199" />
    </bean>        

8.1.2.Client Configuration

클라이언트는 Spring에서 제공하는 org.springframework.remoting.rmi.RmiProxyFactoryBean 클래스를 사용하여 RMI Service에 접근할 수 있다.

Property NameDescriptionRequiredDefault Value
serviceUrlRMI 서비스 접근 URL 정보이다. "rmi://" + 서버ip + ":" + port 번호 + "/" + 서비스 명 (ex.rmi://localhost:1099/ProductService)YN/A
serviceInterfaceRMI 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.1.2.1.Samples

다음은 RMI 클라이언트 속성 설정에 대한 예제이다. 클라이언트는 RmiProxyFactoryBean에서 property 설정 정보를 참조하여 RMI 서비스에 접근하고 있다.

  • Configuration

    다음은 RMI 서비스에 접근하는 RmiProxyFactoryBean의 속성을 정의한 context-product-client.xml 의 일부이다.

    <!-- Add RMI Client -->
    <bean id="productServiceClient" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
      
        <property name="serviceUrl" value="rmi://localhost:1099/ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService"/>
    </bean>        

8.2.Hessian

Hessian과 Burlap은 HTTP를 통해 경량의 원격 서비스를 가능하게 하는 Caucho Technology 가 제공하는 솔루션이다.

  • RMI의 방화벽 문제 해결

  • 메모리와 저장 공간이 제한된 환경에서 사용 적합(애플릿/무선 단말기)

  • 제약 사항 - 자바 표준 직렬화 매커니즘이 아닌 자체 직렬화 매커니즘 사용으로 복합 데이터 모델 불충분

Spring을 사용하지 않는 경우에도 Hessian 서비스를 작성하는 것은 매우 쉽다. 서비스 클래스가 com.caucho.hessian.server.HessianServlet을 확장하도록 하고 노출 대상 메소드의 지시자를 public으로 설정하면 된다. Spring 기반으로 Hessian 서비스를 작성하는 경우 Dependency Injection, Spring AOP 등의 Spring의 기능을 모두 이용할 수 있으므로 Spring Remoting 기능으로 제공하고 있다. RMI 서비스 작성 시 Spring 설정 파일에 RmiServiceExporter 빈을 설정한 것과 마찬가지로 Hessian 서비스 작성시에는 HessianServiceExporter 빈을 사용한다. Property 설정이 거의 유사하지만 RmiServiceExporter빈에서 설정했던 serviceName, registryPort Property 설정은 지정하지 않는다. Hessian 서비스는 RMI 레지스트리를 갖고 있지 않으므로 서비스 명과 Port 번호 설정이 필요하지 않다.

다음은 Hessian 기능을 Server와 Client 단에서 어떻게 사용해야 하는지에 대한 사용법이다.

8.2.1.Server Configuration

서버 구현 방식은 일반 서비스 빈 개발 방식과 같으며 HessianServiceExporter 클래스를 이용하여 손쉽게 일반 Spring Bean으로 작성된 서비스를 Hessian Service로 노출시킬 수 있다. 이때 모든 public 메소드는 서비스 메소드로 노출된다.

Property NameDescriptionRequiredDefault Value
serviceHessian 서비스로 노출시키고 싶은 Spring Bean의 id를 설정한다.YN/A
serviceInterfaceHessian 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.2.1.1.Samples

다음은 Hessian 서버 구현 속성 설정에 대한 예제이다. 서비스는 일반 Spring Bean 개발과 동일하며 HessianServiceExporter Bean에서 property 설정 정보를 참조하여 Hessian 서비스로 노출시키고 있다.

  • Configuration

    다음은 Hessian 서비스를 지원하는 HessianServiceExporter의 속성을 정의한 context-product-server.xml 의 일부이다.

    <!-- ProductService -->
    <bean id="anyframe.example.remoting.sales.ProductService"
      class="anyframe.example.remoting.sales.impl.ProductServiceImpl">
        <property name="productDAO">
            <ref bean="productDAO" />
        </property>
    </bean>
    
    <bean id="productDAO" class="anyframe.example.remoting.sales.impl.ProductDAOImpl" />
        
    <!-- Add Hessian ServiceExporter -->
    <bean id="hessianProductService" 
      class="org.springframework.remoting.caucho.HessianServiceExporter">
    
        <property name="service" ref="anyframe.example.remoting.sales.ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService" />
    </bean>        

    여기서 HessianServiceExporter 빈은 Spring MVC의 컨트롤러로 작성되어 있으므로 Spring MVC의 DispatcherServlet을 web.xml 파일에 설정해야 한다. HTTP로 서비스를 제공하기 위해서 웹 어플리케이션으로 서비스를 배포하여 제공하고 있다. 다음은 서버 사이드 웹 어플리케이션의 web.xml 의 일부이다.

    <web-app>
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:/spring/context-*.xml</param-value>
        </context-param>
        ...중략...
        <listener>
            <listener-class>
                org.springframework.web.context.ContextLoaderListener
            </listener-class>
        </listener>
        ...중략...
        <servlet>
            <servlet-name>remoting</servlet-name>
            <servlet-class>
                org.springframework.web.servlet.DispatcherServlet
            </servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:/springmvc/*-servlet.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>remoting</servlet-name>
            <url-pattern>/*</url-pattern>
        </servlet-mapping>
        ...중략...
    </web-app>        

    여기서 Spring MVC의 URL Mapping 기능을 사용하여 HTTP 기반의 Hessian 컨트롤러를 호출할 수 있도록 한다. 다음은 서버 사이드 웹 어플리케이션의 hessian-servlet.xml 의 일부이다. * 패턴의 모든 URL에 대한 요청이 DispatcherServlet으로 전달된 후 urlMapping 정보에 의해 hessianProductService가 호출되어 Hessian 서비스가 제공된다.

    <bean id="urlMappingUser" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/ProductService">hessianProductService</prop>
            </props>
        </property>
    </bean>       

8.2.2.Client Configuration

클라이언트는 Spring에서 제공하는 org.springframework.remoting.caucho.HessianProxyFactoryBean 클래스를 사용하여 Hessian Service에 접근할 수 있다.

Property NameDescriptionRequiredDefault Value
serviceUrlHessian 서비스 접근 URL 정보이다. "http://" + 서버ip + ":" + port 번호 + "/" + 서비스 명 (ex.http://localhost:9002/ProductService)YN/A
serviceInterfaceHessian 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.2.2.1.Samples

다음은 Hessian 클라이언트 속성 설정에 대한 예제이다. 클라이언트는 HessianProxyFactoryBean에서 property 설정 정보를 참조하여 Hessian 서비스에 접근하고 있다.

  • Configuration

    다음은 Hessian 서비스에 접근하는 HessianProxyFactoryBean의 속성을 정의한 context-product-client.xml 의 일부이다.

    <!-- Add Hessian Client -->
    <bean id="productServiceClient" 
      class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
      
        <property name="serviceUrl" value="http://localhost:9002/ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService"/>
    </bean>        

    RMI 서비스에 접근하는 클라이언트와 마찬가지로 serviceInterface Property에는 서비스가 구현하는 인터페이스 클래스를 설정하고. serviceUrl Property에는 서비스 URL을 작성하는데 Hessian은 HTTP 기반으로 제공되므로 HTTP URL을 작성하도록 한다.

8.2.3.Hessian과 Burlap의 차이점

  • Hessian의 경우 RMI와 같이 클라이언트와 원격 시스템의 서비스 간 통신을 할때 바이너리 데이터를 사용한다.

  • Burlap의 경우 XML 기반으로 통신한다. Hessian의 바이너리 데이터에 비해 Human-readable하다. 그러나 SOAP 메시지 기반의 Remoting과 달리 Burlap의 XML 메시지는 WSDL이나 IDL과 같은 별도의 Definition Language가 없는 간단한 구조로 구성되어 있다.

  • 바이너리 데이터를 사용하는 Hessian이 네트워크 환경에서의 데이터 전송 면에서 더 효율적이다.

  • 그러나 메시지 가독성이 중요한 경우 혹은 Hessian 구현이 존재하지 않는 타 Language와 통신해야 하는 경우에는 Burlap을 사용해야 한다.

8.3.Burlap

Hessian과 Burlap은 HTTP를 통해 경량의 원격 서비스를 가능하게 하는 Caucho Technology 가 제공하는 솔루션이다.

  • RMI의 방화벽 문제 해결

  • 메모리와 저장 공간이 제한된 환경에서 사용 적합(애플릿/무선 단말기)

  • 제약 사항 - 자바 표준 직렬화 매커니즘이 아닌 자체 직렬화 매커니즘 사용으로 복합 데이터 모델 불충분

Spring을 사용하지 않는 경우에도 Burlap 서비스를 작성하는 것은 매우 쉽다. 서비스 클래스가 com.caucho.burlap.server.BurlapServlet을 확장하도록 하고 노출 대상 메소드의 지시자를 public으로 설정하면 된다. Spring 기반으로 Burlap 서비스를 작성하는 경우 Dependency Injection, Spring AOP 등의 Spring의 기능을 모두 이용할 수 있으므로 Spring Remoting 기능으로 제공하고 있다. RMI 서비스 작성 시 Spring 설정 파일에 RmiServiceExporter 빈을 설정한 것과 마찬가지로 Burlap 서비스 작성시에는 BurlapServiceExporter 빈을 사용한다. Property 설정이 거의 유사하지만 RmiServiceExporter빈에서 설정했던 serviceName, registryPort Property 설정은 지정하지 않는다. Burlap 서비스는 RMI 레지스트리를 갖고 있지 않으므로 서비스 명과 Port 번호 설정이 필요하지 않다. 다음은 Burlap 기능을 Server와 Client 단에서 어떻게 사용해야 하는지에 대한 사용법이다.

8.3.1.Server Configuration

서버 구현 방식은 일반 서비스 빈 개발 방식과 같으며 BurlapServiceExporter 클래스를 이용하여 손쉽게 일반 Spring Bean으로 작성된 서비스를 Burlap Service로 노출시킬 수 있다. 이때 모든 public 메소드는 서비스 메소드로 노출된다.

Property NameDescriptionRequiredDefault Value
serviceBurlap 서비스로 노출시키고 싶은 Spring Bean의 id를 설정한다.YN/A
serviceInterfaceBurlap 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.3.1.1.Samples

다음은 Burlap 서버 구현 속성 설정에 대한 예제이다. 서비스는 일반 Spring Bean 개발과 동일하며 BurlapServiceExporter Bean에서 property 설정 정보를 참조하여 Burlap 서비스로 노출시키고 있다.

  • Configuration

    다음은 Burlap 서비스를 지원하는 BurlapServiceExporter의 속성을 정의한 context-product-server.xml 의 일부이다.

    <!-- ProductService -->
    <bean id="anyframe.example.remoting.sales.ProductService"
      class="anyframe.example.remoting.sales.impl.ProductServiceImpl">
        <property name="productDAO">
            <ref bean="productDAO" />
        </property>
    </bean>
    
    <bean id="productDAO" class="anyframe.example.remoting.sales.impl.ProductDAOImpl" />
        
    <!-- Add Burlap ServiceExporter -->
    <bean id="burlapProductService" 
      class="org.springframework.remoting.caucho.BurlapServiceExporter">
    
        <property name="service" ref="anyframe.example.remoting.sales.ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService" />
    </bean>        

    여기서 BurlapServiceExporter 빈은 Spring MVC의 컨트롤러로 작성되어 있으므로 Spring MVC의 DispatcherServlet을 web.xml 파일에 설정해야 한다. HTTP로 서비스를 제공하기 위해서 웹 어플리케이션으로 서비스를 배포하여 제공하고 있다. 다음은 서버 사이드 웹 어플리케이션의 web.xml 의 일부이다.

    <web-app>
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/context-product-server.xml</param-value>
        </context-param>
    
        <listener>
            <listener-class>
                org.springframework.web.context.ContextLoaderListener
            </listener-class>
        </listener>
        <servlet>
            <servlet-name>remoting</servlet-name>
            <servlet-class>
                org.springframework.web.servlet.DispatcherServlet
            </servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>/WEB-INF/burlap-servlet.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>remoting</servlet-name>
            <url-pattern>/*</url-pattern>
        </servlet-mapping>
    </web-app>

    여기서 Spring MVC의 URL Mapping 기능을 사용하여 HTTP 기반의 Burlap 컨트롤러를 호출할 수 있도록 한다. 다음은 서버 사이드 웹 어플리케이션의 burlap-servlet.xml 의 일부이다. * 패턴의 모든 URL에 대한 요청이 DispatcherServlet으로 전달된 후 urlMapping 정보에 의해 burlapProductService가 호출되어 Burlap 서비스가 제공된다.

    <bean id="urlMappingUser" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/ProductService">burlapProductService</prop>
            </props>
        </property>
    </bean>

8.3.2.Client Configuration

클라이언트는 Spring에서 제공하는 org.springframework.remoting.caucho.BurlapProxyFactoryBean 클래스를 사용하여 Burlap Service에 접근할 수 있다.

Property NameDescriptionRequiredDefault Value
serviceUrlBurlap 서비스 접근 URL 정보이다. "http://" + 서버ip + ":" + port 번호 + "/" + 서비스 명 (ex.http://localhost:9002/ProductService)YN/A
serviceInterfaceBurlap 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.3.2.1.Samples

다음은 Burlap 클라이언트 속성 설정에 대한 예제이다. 클라이언트는 BurlapProxyFactoryBean에서 property 설정 정보를 참조하여 Burlap 서비스에 접근하고 있다.

  • Configuration

    다음은 Burlap 서비스에 접근하는 BurlapProxyFactoryBean의 속성을 정의한 context-product-client.xml 의 일부이다.

    <!-- Add Burlap Client -->
    <bean id="productServiceClient" 
      class="org.springframework.remoting.caucho.BurlapProxyFactoryBean">
      
        <property name="serviceUrl" value="http://localhost:9002/ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService"/>
    </bean>

    RMI 서비스에 접근하는 클라이언트와 마찬가지로 serviceInterface Property에는 서비스가 구현하는 인터페이스 클래스를 설정하고. serviceUrl Property에는 서비스 URL을 작성하는데 Burlap은 HTTP 기반으로 제공되므로 HTTP URL을 작성하도록 한다.

8.3.3.Hessian과 Burlap의 차이점

  • Hessian의 경우 RMI와 같이 클라이언트와 원격 시스템의 서비스 간 통신을 할때 바이너리 데이터를 사용한다.

  • Burlap의 경우 XML 기반으로 통신한다. Hessian의 바이너리 데이터에 비해 Human-readable하다. 그러나 SOAP 메시지 기반의 Remoting과 달리 Burlap의 XML 메시지는 WSDL이나 IDL과 같은 별도의 Definition Language가 없는 간단한 구조로 구성되어 있다.

  • 바이너리 데이터를 사용하는 Hessian이 네트워크 환경에서의 데이터 전송 면에서 더 효율적이다.

  • 그러나 메시지 가독성이 중요한 경우 혹은 Hessian 구현이 존재하지 않는 타 Language와 통신해야 하는 경우에는 Burlap을 사용해야 한다.

8.4.HTTP Invoker

HTTP Invoker는 HTTP를 통해 경량의 원격 서비스를 가능하게 하는 Spring에서 제공하는 Remoting 기능이다. 앞서 소개한 RMI와 Hessian/Burlap의 경우 아래와 같은 단점이 있는데 이러한 단점을 보완해주는 기능을 HTTP Invoker가 가지고 있다.

  • RMI 단점: RMI의 경우 자바의 표준 객체 직렬화를 사용하지만 방화벽을 통과하기가 어렵다.

  • Hessian/Burlap의 단점: 방화벽을 쉽게 통과하지만, 자체적인 객체 직렬화 매커니즘을 사용하여 복잡한 형태의 객체 직렬화 시 문제가 발생할 수 있다.

HTTP Invoker는 RMI와 Hessian/Burlap의 단점을 보완해준다. Spring에서 제공하는 Remoting 기능으로 HTTP를 통해 Remoting 기능을 수행하며 자바의 표준 직렬화 매커니즘을 사용한다. 단, Spring에서만 제공하는 Remoting 기술로 클라이언트와 원격 서비스 모두 Spring 어플리케이션 이어야 한다.

다음은 HTTP Invoker 기능을 Server와 Client 단에서 어떻게 사용해야 하는지에 대한 사용법이다.

8.4.1.Server Configuration

서버 구현 방식은 일반 서비스 빈 개발 방식과 같으며 HttpInvokerServiceExporter 클래스를 이용하여 손쉽게 일반 Spring Bean으로 작성된 서비스를 HTTP Invoker Service로 노출시킬 수 있다. 이때 모든 public 메소드는 서비스 메소드로 노출된다.

Property NameDescriptionRequiredDefault Value
serviceHTTP Invoker 서비스로 노출시키고 싶은 Spring Bean의 id를 설정한다.YN/A
serviceInterfaceHTTP Invoker 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.4.1.1.Samples

다음은 HTTP Invoker 서버 구현 속성 설정에 대한 예제이다. 서비스는 일반 Spring Bean 개발과 동일하며 HttpInvokerServiceExporter Bean에서 property 설정 정보를 참조하여 HTTP Invoker 서비스로 노출시키고 있다.

  • Configuration

    다음은 HTTP Invoker 서비스를 지원하는 HttpInvokerServiceExporter의 속성을 정의한 context-remoting.xml 의 일부이다.

    <bean id="remotingProductService" 
      class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
            <property name="service" ref="productService" />
            <property name="serviceInterface" value="anyframe.example.remoting.sales.service.ProductService" />
        </bean>      

    여기서 HttpInvokerServiceExporter 빈은 Spring MVC의 컨트롤러로 작성되어 있으므로 Spring MVC의 DispatcherServlet을 web.xml 파일에 설정해야 한다. HTTP로 서비스를 제공하기 위해서 웹 어플리케이션으로 서비스를 배포하여 제공하고 있다. 다음은 서버 사이드 웹 어플리케이션의 web.xml 의 일부이다.

    <web-app id="WebApp_ID" version="2.4"
        xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
        <display-name>sample-web</display-name>
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                classpath:/spring/context-*.xml
            </param-value>
        </context-param>
        
        ...중략...
    
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener
            </listener-class>
        </listener>
        
        <servlet>
            <servlet-name>action</servlet-name>
            <servlet-class>
                org.springframework.web.servlet.DispatcherServlet
            </servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:/springmvc/*-servlet.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
        
        <servlet-mapping>
            <servlet-name>action</servlet-name>
            <url-pattern>*.do</url-pattern>
        </servlet-mapping>
        
        <!-- remoting-configuration-START -->  
        <servlet>
            <servlet-name>remoting</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:/springmvc/remoting-servlet.xml</param-value>
            </init-param>
        </servlet>    
        
        <servlet-mapping>
            <servlet-name>remoting</servlet-name>
            <url-pattern>/remoting/*</url-pattern>
        </servlet-mapping>    
        <!-- remoting-configuration-END -->
        ...중략...
    </web-app>

    여기서 Spring MVC의 URL Mapping 기능을 사용하여 HTTP 기반의 HTTP Invoker 컨트롤러를 호출할 수 있도록 한다. 다음은 서버 사이드 웹 어플리케이션의 remoting-servlet.xml 의 일부이다. * 패턴의 모든 URL에 대한 요청이 DispatcherServlet으로 전달된 후 urlMapping 정보에 의해 remotingProductService가 호출되어 HTTP Invoker 서비스가 제공된다.

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
            <property name="mappings">
                <props>
                    <prop key="/ProductService">remotingProductService</prop>
                </props>
            </property>
        </bean>

8.4.2.Client Configuration

클라이언트는 Spring에서 제공하는 org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean 클래스를 사용하여 HTTP Invoker Service에 접근할 수 있다.

Property NameDescriptionRequiredDefault Value
serviceUrlHTTP Invoker 서비스 접근 URL 정보이다. "http://" + 서버ip + ":" + port 번호 + "/" + 서비스 명 (ex.http://localhost:9002/ProductService)YN/A
serviceInterfaceHTTP Invoker 서비스로 노출되는 서비스의 인터페이스 클래스를 패키지정보와 함께 작성한다.YN/A

8.4.2.1.Samples

다음은 HTTP Invoker 클라이언트 속성 설정에 대한 예제이다. 클라이언트는 HttpInvokerProxyFactoryBean에서 property 설정 정보를 참조하여 HTTP Invoker 서비스에 접근하고 있다.

  • Configuration

    다음은 HTTP Invoker 서비스에 접근하는 HttpInvokerProxyFactoryBean의 속성을 정의한 context-remoting.xml 의 일부이다.

    <bean id="productServiceClient"
        class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl"
            value="http://localhost:8080/anyframe.example.remoting/remoting/ProductService" />
        <property name="serviceInterface"
            value="anyframe.example.remoting.sales.service.ProductService" />
    </bean>

    Hessian/Burlap 서비스에 접근하는 클라이언트와 마찬가지로 serviceInterface Property에는 서비스가 구현하는 인터페이스 클래스를 설정하고. serviceUrl Property에는 서비스 URL을 작성하는데 HTTP Invoker라는 이름에서도 알 수 있듯이 HTTP 기반으로 제공되므로 HTTP URL을 작성하도록 한다. HTTP 방식으로 원격 서비스를 호출할 때 HTTP 클라이언트를 선택할 수 있다. 기본적으로 HttpInvokerProxyFactoryBean은 J2SE HTTP 클라이언트를 사용하지만, httpInvokerRequestExecutor 프라퍼티를 셋팅하여 Apache Commons HttpClient를 사용할 수도 있다.

    <!-- Add HTTP Invoker Client -->
    <bean id="productServiceClient" 
          class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
        <property name="serviceUrl" value="http://localhost:8080/ProductService" />
        <property name="serviceInterface" value="anyframe.example.remoting.sales.ProductService"/>
        <property name="httpInvokerRequestExecutor" ref="httpClient"/>    
    </bean>
    
    <bean id="httpClient" 
          class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"> 
        <property name="httpClient"> 
               <bean class="org.apache.commons.httpclient.HttpClient"> 
                      <property name="connectionTimeout" value="2000"/> 
               </bean> 
        </property> 
    </bean>

  • Test Case

    다음은 앞서 정의한 속성 설정 파일들을 기반으로 하여 HTTP Invoker 서비스에 접근하는 예제인 ProductController.java 코드의 일부이다.

    public class HttpInvokerSpringSupportTest extends RemotingSpringTestCase {
    
    @Controller
    public class ProductController {
        @Resource(name = "productServiceClient")
        private ProductService productService;
    
        /**
         * get a product detail.
         * 
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @RequestMapping("/remotingGetProduct.do")
        public ModelAndView get(HttpServletRequest request) throws Exception {
    
            String prodNo = request.getParameter("prodNo");
    
            if (!StringUtils.isBlank(prodNo)) {
                Product gettedProduct = (Product) productService.get(prodNo);
    
                gettedProduct.setImageFile("/upload/"
                        + gettedProduct.getImageFile());
                request.setAttribute("product", gettedProduct);
            }
    
            return new ModelAndView(
                    "/WEB-INF/jsp/remoting/sales/product/viewProduct.jsp");
        }
    
        /**
         * display product list
         * 
         * @param request
         * @param response
         * @return
         * @throws Exception
         */
        @RequestMapping("/remotingListProduct.do")
        public ModelAndView list(HttpServletRequest request,
                ProductSearchVO searchVO) throws Exception {
    
            String pageParam = (new ParamEncoder("productList")
                    .encodeParameterName(TableTagParameters.PARAMETER_PAGE));
            String pageParamValue = request.getParameter(pageParam);
            int pageIndex = StringUtil.isNotEmpty(pageParamValue) ? (Integer
                    .parseInt(pageParamValue)) : 1;
            searchVO.setPageIndex(pageIndex);
    
            Page resultPage = productService.getPagingList(searchVO);
    
            request.setAttribute("search", searchVO);
            request.setAttribute("productList", resultPage.getList());
            request.setAttribute("size", resultPage.getTotalCount());
            request.setAttribute("pagesize", resultPage.getPagesize());
            request.setAttribute("pageunit", resultPage.getPageunit());
    
            return new ModelAndView(
                    "/WEB-INF/jsp/remoting/sales/product/listProduct.jsp");
        }
    }

8.5.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.remoting.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.remoting를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.remoting를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.remoting를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 8.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.remoting.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

9.Annotation

Spring XML 만을 독립적으로 사용할 경우 때때로 방대하고 복잡한 속성 파일들로 인해 시스템 개발 및 유지보수의 지연을 초래할 가능성이 높아진다. 이러한 문제점을 해결하기 위해 Spring Framework에서는 별도 XML 정의없이도 사용 가능한 annotation 지원에 주력하고 있는 실정이다. Spring 2.0에서는 @Transactional, @Required, @PersistenceConetxt/@PersistenceUnit과 같은 Transaction 관리 또는 Persistence 관리 영역에 대한 annotation들을 지원했다면 Spring 2.5부터는 Bean 또는 Dependency 정의 등과 같이 Spring 속성 정의 XML과 직접적으로 관련된 annotation들을 선보이고 있다. 본 문서에서는 annotation 사용 용도를 Bean Management, Dependency Injection, Life Cycle로 구분하고 각각의 경우에 따른 사용법에 대해 상세히 살펴보도록 한다.

기본적으로, Annotation은 JDK 1.5 이상에서 활용이 가능하며, Spring Container가 Annotation을 인식할 수 있도록 하기 위해서는 속성 정의 XML 파일 내에 다음과 같은 정의가 추가되어야 함에 유의해야 한다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemalLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/contxt/spring-context-2.5.xsd">
        <context:annotation-config/>	                            
</beans>

  • XML vs. Annotation

    다음은 특정 서비스를 구성하는 구현 클래스, DAO 클래스, 속성 정의 XML에 대해 XML을 이용하는 경우와 Annotation을 이용하는 경우로 나누어 비교해 본 그림이다.

9.1.Bean Management

Stereotype Annotation을 사용하면 Spring Framework의 컨테이너에 의해 관리되어야 하는 Bean들을 정의할 수 있다. 일반적으로 Parent Stereotype Annotation인 @Component 를 활용하면 모든 Bean에 대한 정의가 가능하다. 그러나 Spring Framework에서는 레이어별로 구성 요소를 구분하여 다음과 같은 Annotation을 사용할 것을 권장하고 있고, 향후 지속적으로 레이어별 특성을 반영할 수 있는 속성들을 추가해 나아갈 예정이다.

  • @Service

    비즈니스 로직을 처리하는 클래스를 정의하는데 사용한다.

  • @Controller

    프리젠테이션 레이어를 구성하는 Controller 클래스를 정의하는데 사용하며, Spring MVC 기반인 경우에 한해 활용 가능하다.

  • @Repository

    데이터 접근 로직을 처리하는 클래스를 정의하는데 사용하며, 퍼시스턴스 레이어에서 발생한 Exception에 대한 Translation이 지원된다.

본 문서에서는 위에서 나열한 annotation을 사용하는 방법에 대해서 자세히 살펴보도록 한다.

9.1.1.Auto Detecting

Stereotype Annotation을 사용하여 Bean을 정의하면 XML에 따로 Bean 정의를 명시하지 않아도 Spring Container가 Bean을 인식하고 관리할 수 있다. 단, 자동 인식이 되기 위해서는 서비스 속성 정의 XML 내에 <context:component-scan /> 을 정의해 주어야 한다. 이 설정을 추가하면 Spring Container는 클래스패스 상에 존재하는 클래스들을 스캔하여 Stereotype Annotation이 정의된 클래스들 Bean으로 인식하고 자동으로 등록한다.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemalLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/contxt/spring-context-2.5.xsd">
        <context:component-scan base-package="anyframe.example" />	                            
</beans>

<context:component-scan />을 정의한 경우 Annotation 인식을 위한 설정 <context:annotation-config/> 을 별도로 추가하지 않아도 된다.

다음은 서비스 레이어의 구성 요소인 ProductServiceImpl 클래스에 대해 @Service라는 Stereotype Annotation을 사용한 예이다.

@Service
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource
        ProductDao productDao;
}

위 예제에서는 해당 클래스의 클래스명(소문자로 시작)이 Bean name으로 셋팅되어 해당 Bean을 찾을 때 productServiceImpl 이라는 문자열을 사용해야 한다.

ProductService service = (ProductService) context.getBean("productServiceImpl");       

해당 Annotation에 속성을 부여하면, 원하는 Bean name을 지정하는 것 또한 가능하다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource
        ProductDao productDao;
}

이 경우에 해당되는 Bean을 찾기 위해서는 속성으로 정의한 Name을 활용해야 한다.

ProductService service = (ProductService) context.getBean("productService");

9.1.2.Using Filters to customize scanning

<context:component-scan>의 여러 속성들을 이용하면 검색 대상의 범위를 조정하여 자동으로 검색되어 Bean으로 등록되는 클래스들을 filtering 할 수 있다. base-package 속성은 <context:component-scan> 내에 정의 가능한 속성으로 검색 대상 패키지를 정의하는 용도로 사용된다. 이외에도 <context:component-scan>은 하위 element로 <context:include-filter>, <context:exclude-filter>를 가질 수 있는데, 다양한 Filter Type(type)에 해당하는 표현식(expression)을 정의함으로써 이에 해당하는 클래스들을 포함 또는 제외시킬 수가 있다. 다음은 <context:include-filter>, <context:exclude-filter> 사용 예이다.

<context:component-scan base-package="anyframe.example">
    <context:include-filter type="regex" expression=".*Stub.*Repository"/>
    <context:exclude-filter type="annotation"           expression="org.springframework.stereotype.Repository"/>
</context:component-scan>

정의 가능한 Filter Type은 4가지이며, 다음과 같다.

Filter TypeExample Expressions
annotation org.example.SomeAnnotation
assignable org.example.SomeClass
regex org\.example\.Default.*
aspectj org.example..*Service+

참고

@Controller, @Service, @Repository가 적용된 클래스를 auto detection하는 디폴트 설정을 사용하지 않고자 하는 경우에는 <context:component-scan />태그에 use-default-filters="false" 속성을 추가하면 된다.

9.1.3.Scope Definition

Spring Framework에서는 Bean의 인스턴스 생성 메커니즘에 따라 5가지 Scope 을 제공하는데 이러한 Bean Scope을 정의하기 위해서는 다음과 같이 @Scope을 사용하도록 한다.

@Scope("prototype")
@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource
        ProductDao productDao;
}       

9.2.Dependency Injection

특정 Bean의 기능 수행을 위해 다른 Bean을 참조해야 하는 경우 사용하는 Annotation으로는 @Autowired 또는 @Resource가 있다.

  • @Autowired

    Spring Framework에서 지원하는 Dependency 정의 용도의 Annotation으로, Spring Framework에 종속적이긴 하지만 정밀한 Dependency Injection이 필요한 경우에 유용하다.

  • @Resource

    JSR-250 표준 Annotation으로 Spring Framework 2.5.* 부터 지원하는 Annotation이다. 특정 Framework에 종속되지 않은 어플리케이션을 구성하기 위해서는 @Resource를 사용할 것을 권장한다. @Resource를 사용하기 위해서는 클래스패스 내에 jsr250-api.jar 파일이 추가되어야 함에 유의해야 한다.

@Autowired와 @Resource를 사용할 수 있는 위치는 다음과 같이 약간의 차이가 있으므로 필요에 따라 적절히 사용하면 된다.

  • @Autowired : 멤버변수, setter 메소드, 생성자, 일반 메소드에 적용 가능

  • @Resource : 멤버변수, setter 메소드에 적용가능

@Autowired나 @Resource를 멤버변수에 직접 정의하는 경우 별도 setter 메소드는 정의하지 않아도 된다.

9.2.1.@Resource

@Resource annotation은 Bean name을 지정하여 Dependency Injection을 하고자 하는 경우에 사용한다. @Resource는 name이라는 속성을 가지고 있어서, Spring Container가 @Resource로 정의된 요소에 injection하기 위한 Bean을 검색할 때, name 속성에 지정한 이름을 검색할 Bean Name으로 사용한다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        MessageSource messageSource;
        @Resource (name="productDao")
        ProductDao productDao;

명시적으로 name 속성에 이름을 지정하지 않는 경우, 검색할 Bean Name은 다음과 같은 규칙을 따른다.

  • @Resource가 멤버 변수에 정의되었을 때 : 멤버 변수의 이름

  • @Resource가 setter 메소드에 정의되었을 때 : 해당 setter 메소드의 이름에서 'set'을 제외한 이름(첫 글자는 소문자)

    예) setFoo(...) --> 'foo'

해당하는 Bean Name으로 injection할 Bean을 찾지 못했을 경우에는 @Autowired 처럼 Bean의 type으로 검색한다.

@Resource를 이용하면 BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, MessageSource 인터페이스와 하위 인터페이스들을 별도 설정 없이 바로 사용 가능하다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Resource
        ApplicationContext context;
}

9.2.2.@Autowired

서로 다른 Bean 간의 Dependency 정의를 위한 또 다른 Annotation인 @Autowired는 Spring에 종속적이긴 하지만, 적용할 수 있는 위치가 @Resource보다 다양하고, 정밀한 Dependency Injection이 필요한 경우에 유용하다.

다음은 @Autowired를 사용한 예이다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Autowired
        ProductDao productDao;
}

@Autowired 적용 위치 별로 사용 예를 들면 다음과 같다.

  • 생성자 및 멤버 변수

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            @Autowired
            ProductDao productDao;
            MessageSource messageSource;
    		
            @Autowired
            public ProductServiceImpl(MessageSource messageSource) {
                    this.messageSource = messageSource;
            }
    }
    위의 예제와 같이 @Autowired를 사용하면 ProductServiceImpl 클래스가 생성될 때 Spring Container에 의해서 MessageSource 타입의 Bean이 생성자의 argument로 자동으로 injection 된다. 또한 productDao 멤버변수에도 @Autowired가 적용되어 있으므로 ProductDao 타입의 Bean이 자동 injection된다.

  • setter 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            @Autowired
            public void setProductDao(ProductDao productDao) {
                    this.productDao = productDao;
            }
    }
    Spring Container에 의해서 자동으로 setProductDao() 메소드가 호출되어 ProductDao 타입의 Bean이 productDao 멤버변수로 injection된다.

  • 일반 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            MessageSource messageSource;
            @Autowired
            public void prepare(ProductDao productDao, MessageSource messageSource) {
                    this.productDao = productDao;
                    this.messageSource = messageSource;
            }
    }
    @Resource 와는 달리 위의 prepare()와 같은 일반 메소드에도 @Autowired를 적용함으로써 Spring Container에 의한 Dependency Injection 처리를 할 수 있다. 위의 예제에서는 ProductDao 타입의 Bean이 productDao로, MessageSource 타입의 Bean이 messageSource로 injection된다

  • 배열이나 Collection 형태의 멤버변수와 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            @Autowired
            Category[] categories;
    }
    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
            ProductDao productDao;
            Set<Category> categories;
            @Autowired
            public void setCategories(ProductDao productDao, Set<Category> categories) {
                    this.productDao = productDao;
                    this.categories = categories;
            }
    }
    위 예제 소스의 경우, Spring Container에 등록된 Category 타입의 Bean들이 모두 categories 배열(또는 collection)에 injection된다.

  • Map(key=Bean Name, value=Bean 객체) 형태의 멤버변수와 메소드

    @Service("productService")
    public class ProductServiceImpl extends GenericServiceImpl<Product, String>
    		implements ProductService {
           ProductDao productDao;
           Map<String, Category> categories;
           @Autowired
           public void setCategories(ProductDao productDao, Map<String, Category> categories) {
                    this.productDao = productDao;
                    this.categories = categories;
            }
    }
    위 예제 소스의 경우, Spring Container에 등록된 Category 타입의 Bean들이 Bean name이 key로, Bean 객체가 value인 쌍으로 모두 categories Map에 injection된다.

기본적으로 @Autowired가 적용된 참조 관계는 반드시 해당 빈이 존재해야 하지만, required 속성을 false로 설정하는 경우에는 해당되는 Bean을 찾지 못하더라도 에러가 발생하지 않는다.

@Service
public UserService implements UserService {
        @Autowired(required=false)
        private UserDAO userDAO;
}

또한, @Resource에서 설명했던 바와 같이 @Autowired도 BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, MessageSource 인터페이스와 하위 인터페이스들을 별도 설정 없이 바로 사용할 수 있게 해준다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Autowired
        ApplicationContext context;
}

9.2.3.@Qualifier

기본적으로 @Autowired는 type-driven injection 형태로 동작하여, @Autowired가 정의되었을 경우 Spring Container가 해당 Bean을 찾을 때 객체의 type을 기준으로 검색을 한다. 이와 같은 경우 동일한 객체 type의 Bean이 여러 개 검색되었을 때 injection 대상이 되는 Bean을 결정하기 위한 세밀한 제어를 요하는데, 이 때 @Qualifier를 사용할 수 있다.

다음은 @Qualifier를 사용한 예이다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product, String>
		implements ProductService {
        @Autowired
        @Qualifier("sports")
        Category sportsCategory;
}

위와 같이 정의하면 "sports"라는 qualifier 속성 값이 정의된 Bean이 sportsCategory 멤버변수로 injection된다.

위의 @Qualifier에 의해 연결될 Bean은 다음과 같이 정의할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
        				
        <context:annotation-config/>

        <bean class="anyframe.example.domain.Category">
                <qualifier value="sportsCategory"/>
                <!-- inject any dependencies required by this bean -->
        </bean>
        <bean class="anyframe.example.domain.Category">
                <qualifier value="livingCategory"/>
                <!-- inject any dependencies required by this bean -->
        </bean>

        <bean id="productService" class="anyframe.example.annotation.sales.service.impl.ProductServiceImpl"/>
</beans>
    	

9.2.4.@Resource vs. @Autowired

@Resource와 @Autowired을 비교하면 다음과 같다.

Annotation@Resource@Autowired
Injection 방식name-matching injectiontype-driven injection
사용가능한 위치멤버변수, setter 메소드멤버변수, setter 메소드, 생성자, 일반 메소드

9.3.LifeCycle Annotation

IoC의 Life Cycle 에서 설명한 바와 같이 Bean의 LifeCycle은 Initializaion ->Activation -> Destruction으로 구성되어 있으며, LifeCycle 메소드를 정의하는 경우 컨테이너 기동시 또는 종료시 필요한 로직을 수행할 수 있게 된다. Bean을 초기화 또는 소멸화 하는 시점에 별도 작업이 필요한 경우 기존에는 InitializingBean과 DesposableBean 인터페이스를 상속하거나, Bean 정의시 명시적으로 초기화 메소드나 소멸화 메소드를 별도로 지정해야 했다. 그러나, 다음과 같은 Annotation을 사용하면 XML 정의 또는 별도 인터페이스 상속없이 Bean의 LifeCycle 관리가 가능해진다.

9.3.1.@PostConstruct

JSR-250 표준 Annotation으로 Bean 초기화시 필요한 작업을 담은 메소드에 대해 정의한다. @PostConstruct를 사용하기 위해서는 클래스패스 내에 jsr250-api.jar 파일이 추가되어 있어야 한다.

@PostConstruct
// 메소드명은 자유롭게 정의할 수 있다.
public void initialize() {
        // ...
}      

9.3.2.@PreDestroy

JSR-250 표준 Annotation으로 Bean 소멸시 필요한 작업을 담은 메소드에 대해 정의한다. @PreDestroy를 사용하기 위해서는 클래스패스 내에 jsr250-api.jar 파일이 추가되어 있어야 한다.

@PreDestroy
// 메소드명은 자유롭게 정의할 수 있다.
public void dispose() {
        // ...
}       

9.3.3.Combining lifecycle mechanisms

앞에서 설명한 바와 같이, Spring 2.5에서 bean lifecycle을 관리할 수 있는 방법은 다음과 같이 세가지가 있다.

  • InitializingBean과 DisposableBean callback 인터페이스 이용

  • 사용자가 작성한 초기화/소멸화 메소드를 XML에서 init-method/destroy-method 속성을 이용하여 정의

  • @PostConstruct와 @PreDestroy annotation 이용

위의 3가지 방법이 동시에 존재하는 경우(예를 들어, 3가지 방법이 각각 정의된 클래스가 Parent-child 관계를 가지는 경우), 실행되는 순서는 다음과 같다.

Initialization 메소드

  1. @PostConstruct를 이용하여 정의한 메소드

  2. InitializingBean 인터페이스의 afterPropertiesSet() 메소드

  3. XML에서 init-method 속성으로 정의된 초기화 메소드

Destroy 메소드

  1. @PreDestroy를 이용하여 정의한 메소드

  2. DisposableBean 인터페이스의 destroy() 메소드

  3. XML에서 destroy-method 속성으로 정의된 소멸화 메소드

9.4.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.annotation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 9.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.annotation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

IV.Spring MVC

Spring MVC는 Spring에서 제공하는 웹 프레임워크로 MVC(Model, View, Controller) 패턴 기반의 Model2 아키텍쳐를 사용한다. 또한 Spring MVC는 Controller, Handler mappings, ModelAndView, view resolver, view 등의 구성 요소를 가지며 요청을 할당해주는 Front Controller 역할로써 DispatchServlet을 사용한다. 기본적인 핸들러는 Controller Interface이며 Controller는 public abstract ModelAndView handleRequest(HttpServletRequest httpservletrequest, HttpServletResponse httpservletresponse) 메소드를 제공한다. Controller 자체로도 application 컨트롤러로 사용될 수 있지만 AbstractController, AbstractCommandController, MultiActionController등의 컨트롤러 구현체를 사용하여 사용자가 보다 의도에 맞게 컨트롤러를 구현할 수 있다. 또한 Struts에서는 ActionForm과 같은 클래스를 이용해 입력 폼 데이터를 전달 하지만 Spring MVC는 이러한 입력 폼 데이터를 transfer object로 바로 바인딩 할 수 있다. Spring MVC는 Spirng 프레임워크의 한 모듈로써 Spring IoC 컨테이너와 완벽하게 통합이 되어 Spring의 또 다른 기능과의 연계가 용이하다.

Spirng MVC 웹 프레임워크는 다음과 같은 특징을 가진다.

  • 역할 분리가 명확하다. controller, validator, command 객체, 폼 객체, model 객체, DispatcherServlet, handler mapping, view resolver 등 특화된 객체에 의해 역할을 수행할 수 있다.

  • 다양한 컨트롤러 인터페이스를 제공하기 때문에 시나리오에 맞는 컨트롤러를 선택하여 사용할 수 있다.

  • business 객체를 command 또는 폼 객체로 재사용할 수 있다.

  • application 레벨에서 데이터를 바인딩 하고 validation 에러를 체크할 수 있다.

  • 간단한 URL 기반 설정으로 다양한 handler mapping과 view resolution이 가능하다.

  • 모델이 맵으로 구성되기 때문에 여러 view 기술과의 연계가 쉽다.

  • 데이터 바인딩이나 테마 사용을 위한 spring 태그를 제공한다.

  • JSP의 입력 폼을 보다 쉽게 만들 수 있는 form 태그를 제공한다.

10.Architecture

Spring MVC는 MVC 패턴 기반의 Model2아키텍처를 사용하며 Model, View, Controller 컴포넌트로 구성된다.

  • Model : Model Component를 만드는 다양한 방법을 직접 제공하지 않는다. 대신 EJB (Enterprise Java Beans), JDO (Java Data Objects), JavaBeans, ORM (Object to Relational Mapping framework ) 등 여러 기술들을 이용해 구현된 어떤 Model Component 에도 접근 가능하다. 또한 폼 입력 필드 값을 폼 객체 없이 모델 객체로 바인딩 할 수 있는데 이 때 모델 객체 attribute로 정의된 타입에 따라 자동 매핑된다. 단, attribute명과 입력 필드 명이 일치 해야한다.

  • View : 표준 JSP 나 Spring MVC에서 제공하는 tag library 를 사용하여 View Component를 제작 한다. Spring MVC에서는 별도의 bean, html, logic 태그는 제공하지 않으며 표준JSP 태그인 JSTL을 사용하는것을 권장한다. Component의 재사용, 관리 노력의 절감, 에러 감소를 위해 Application-Specific Custom tag, Image Rendering Component 등 다른 기술의 채택을 고려할 수 있다.

  • Controller : DispatcherServlet이 Front Controller 역할을 담당하고 있으며 모든 Request의 Flow를 제어한다.

FrontController 역할을 하는 DispatcherServlet의 요청 처리 workflow는 아래의 그림과 같다.

위 workflow에서 볼 수 있듯이 모든 요청이 통과하는 곳은 Front Controller이며 Spring MVC에서는 DispatcherServlet이 이 Front Controller 역할을 한다. DispatcherServlet은 모든 요청을 하나의 받은 뒤 Handler Mapping을 통해 각각의 요청을 처리할 컨트롤러에 넘긴다. 컨트롤러에서 요청을 처리한 뒤 ModelAndView 객체를 다시 DispatcherServlet에게 넘기면 DispatcherServlet은 ModelAndView 객체의 view 이름과 ViewResolver를 사용하여 해당 응답을 보여줄 view에게 Model 객체들을 넘기고 출력할 view를 만들게 된다. 그 후 다시 DispatcherServlet에게 제어권을 넘기면 Reponse에 방금 만들어낸 view을 실어 보낸다.

11.Configuration

Spring MVC를 이용하여 웹 애플리케이션을 개발하기 위해서는 기본 FrontController 역할을 하는 DispatcherServlet등록을 위해 web.xml 파일과 Spring MVC의 구성 요소를 정의하고 WebApplicationContext로 로드시킬 xml파일(ex> action-servlet.xml)을 작성해야 한다. 자세한 내용은 각각의 링크를 참조하도록 한다.

11.1.web.xml 작성

Spring MVC를 사용하기 위해서는 web.xml 파일에 DispatcherServlet을 등록하고 Spring MVC의 구성요소들이 작성되어 있는 XML 파일의 WebApplicationContext들을 로드할 수 있도록 해당 파일 위치를 지정해 줘야 한다. 작성 방법은 아래와 같다.

11.1.1.DispatcherServlet 등록

다음은 web.xml 파일에 DispatcherServlet 설정한 모습이다.

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

위와 같이 정의할 경우 servlet의 이름은 action이 되게 되고 DispatchserServlet은 servlet의 이름에 따른 XML 파일로부터 WebApplicationContext를 로드한다. 이 때 servlet 이름이 action이므로 기본적으로 web contents 폴더의 WEB-INF 폴더 하위의 action-servlet.xml 파일로 부터 applicationContext를 로드하게 된다. 그 다음엔 DispatchserServlet에 의해 처리될 요청을 URL로 지정해야 한다.

<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

위와 같이 설정할 경우 URL의 확장자가 ".do"인 모든 URL에 대한 요청은 DispatcherServlet이 처리하게 된다.

11.1.2.Spring 설정 파일 위치 등록

위에서 언급하였듯이 기본적으로 DispatcherServlet은 ${servlet-name}-servlet.xml 파일로 부터 WebApplicationContext를 로드하게 되는데 이를 특정 다른 위치의 다른 파일 또는 다중의 파일을 정의하기 위해서는 아래와 같이 <servlet> 내의 <init-param>으로 contextConfigLocations 속성을 사용하여 지정해 준다.

<init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /config/springmvc/common-servlet.xml, /config/springmvc/user-servlet.xml
    </param-value>
</init-param>

11.2.action-servlet.xml 작성

Spring MVC의 DispatcherServlet은 WebApplicationContext를 가지고 있고 context Hierarchy는 다음 그림과 같다.

또한 WebApplicationContext에 설정할 수 있는 특별한 bean들은 다음과 같다.

Bean type설명
Controllers각종 컨트롤러들
Handler mappings요청된 URL을 처리할 컨트롤러와 매칭
View resolversView를 결정
Locale resolver국제화 지원하기 위해 사용자의 locale 알아냄
Theme resolver테마 사용할 때 사용
Multipart file resolver폼에서 파일 업로드 할 때 사용
Exception resolver특정 예외가 발생할 때의 보여줄 view 등록

이러한 bean들을 action-servlet.xml 파일에 정의하여 사용하게 된다.

11.2.1.action-servlet.xml 설정

web.xml 설정이 끝나면 위에서 설명한 WebApplicationContext 요소들을 action-servlet.xml파일에 정의해줘야 한다. 이 페이지에서는 Handler Mapping과 View Resolver를 정의하는 방법에 대해 알아보고 다른 요소(Controller , Locale Resolver , Multipart File Resolver , Exception Resolver )들에 대해서는 각각의 상세 페이지에서 설명하도록 한다.

11.2.1.1.Handler Mapping

Cotroller와 URL을 매핑하기 위해서 Spring MVC에서는 여러가지의 Handler Mapping을 지원한다. 또한 Handler Mapping에 요청된 URL경로를 입력할 때 /**/*.do 와 같이 "*" 기호를 사용하여 경로를 지정하는것이 가능하다.

  • BeanNameUrlHandlerMapping

    BeanNameUrlHandlerMapping은 bean 이름과 URL을 Mapping한다. 다음은 BeanNameUrlHandlerMapping을 정의한 common-servlet.xml 파일의 일부이다.

    <bean id="beanNameUrlHandlerMapping"
        class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
    </bean>
    
    <bean name="/login.do"
        class="anyframe.sample.springmvc.web.controller.basic.LoginController">
    </bean>
    

    아무런 HandlerMapping도 정의하지 않으면 default로 BeanNameHandlerMapping이 적용된다.

    위와같이 정의되어 있는 Controller bean이 있다면 "/login.do"라는 요청이 들어왔을 때 class로 정의되어있는 LoginController에서 해당 요청을 처리하게 된다.

  • SimpleUrlHandlerMapping

    SimpleUrlHandleMapping은 매핑에 대한 정보를 각각의 Controller에서 설정하는 것이 아니라 하나의 저장소에서 관리하는 것이다. 사용자는 컨트롤러를 빈으로 정의해 주고 각각의 요청을 핸들링 하게 될 컨트롤러 빈의 id를 mappings 프로퍼티를 사용하여 지정해 준다. 다음은 위의 BeanNameUrlHandlerMapping 예시를 SimpleUrlHandlerMapping을 이용해 나타낸 것이다.

    <bean id="simpleUrlHandlerMapping"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /login.do = loginController
            </value>
        </property>
    </bean>
    
    <bean id="loginController"
        class="anyframe.sample.springmvc.web.controller.basic.LoginController">
    </bean>
    

    또한 SimplerUrlHandlerMapping을 사용할 경우 매핑 정보를 빈 설정 파일이 아닌 별도의 파일에서 관리하는 것이 가능하다. 예는 다음과같다.

    <bean id="simpleUrlHandlerMapping" 
         class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">
                <property name="location">
                    <value>/mapping.properties</value>
                </property>
            </bean>
        </property>
    </bean>

    다음은 위에서 정의된 mapping.properties파일의 내용이다.

    /login.do = loginController

  • Intercepting requests

    handler mapping에는 interceptor를 정의할 수 있으며 해당 handler mapping에 의해 처리되는 요청은 정의한 interceptor가 적용되게 된다. 이러한 interceptro는 요청을 가로채서 요청이 들어오기 전, 들어온 후, 완료된 후에 특정 작업을 추가할 수 있다. interceptor 클래스는 org.springframework.web.servlet.HandlerInterceptor 클래스를 상속 받아 생성하고 preHandle(), postHandle(), afterCompletion() 메소드를 구현하여 각 시점에 따라 로직을 추가할 수 있다.

    다음은 LoginInterceptor.java 파일의 일부이다.

    public class LoginInterceptor extends HandlerInterceptorAdapter{
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                            Object hadler) throws Exception {
              if(request.getSession().getAttribute("userId") != null)
                    return true;
                else {
                    response.sendRedirect("login.jsp");
                    return false;
                }
        }
    }

    위의 예에서는 preHandle() 메소드를 오버라이딩 하여 요청이 들어오기 전에 해당 로직을 수행하게 된다. session에 userId 값이 존재할 경우 true를 리턴하고 요청을 처리하게 될 것이고, userId값이 존재 하지 않는다면 login.jsp 페이지를 출력하게 될것이다. 다음은 빈 설정파일에 interceptor를 설정한 user-servlet.xml 파일의 일부이다.

    <bean id="simpleUrlHandlerMapping"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /userForm.do = userController
            </value>
        </property>
        <property name="interceptors" ref="loginInterceptor"/>
        <!-- 여러개의 handler mapping이 정의되어 있을시에 order를 정의하여 우선순위를 부여할 수 있다.
            숫자가 작을수록 높은 우선순위를 갖는다.-->
        <property name="order">
            <value>1</value>
        </property>
    </bean>
    
    <bean id="loginInterceptor" class="anyframe.sample.springmvc.web.interceptor.LoginInterceptor" />
    
    위와 같이 interceptor 클래스를 빈으로 설정하고 handler mapping에서 해당 빈을 참조하여 interceptor를 적용시킬 수 있다.

11.2.1.2.View Resolver

핸들러(controller)는 요청을 처리 한 뒤 ModelAndView 객체를 넘겨준다. 이 때 이 객체에 view의 이름을 같이 넘겨 주는데 이 이름으로 실제 view를 찾아 주는 역할을 하는 것이 View Resolver이다. Spring MVC에서 제공하는 View Resolver들은 다음과 같다.

ViewResolver설명
AbstractCachingViewResolverView 들을 caching하는 기능 제공
XmlViewResolverViewResolver 의 구현체로 XML파일 사용(/WEB-INF/views.xml 을 기본 설정파일로 사용)
ResourceBundleViewResolverViewResolver 의 구현체로 리소스 파일 사용(views.properties 를 기본 리소스 파일로 사용)
UrlBasedViewResolverViewResolver 의 구현체로 특별한 맵핑 정보 없이 의미상 view 이름을 URL로 사용(View 이름과 실제 view 자원과의 이름이 같을 때 사용)
InternalResourceViewResolverUrlBasedViewResolver 를 상속 받았으며 InternalResourceView(Servlet, JSP)를 사용
VelocityViewResolver/FreeMarkerViewResolverUrlBasedViewResolver 를 상속 받았으며 VelocityView 와 FreeMarkerView를 사용

사용하려는 기술에 따라 위와같은 View Resolver를 적절히 선택해야 한다.

  • JSP 사용

    <bean id="viewResolver"
          class="org.springframework.web.servlet.view.UrlBasedViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    

    viewResolver는 사용자에게 보여줄 view를 생성할 때, prefix와 suffix를 지정해 줄 수 있다. 만약 controller에서 넘겨준 modelAndView 값이 index이고 prefix를 "/jsp/", suffix를 ".jsp"라고 정의 했다면 이 viewResolver는 "/jsp/index.jsp"를 찾게 된다.

  • JSTL 사용

    다음은 JstlView가 설정되어 있는common-servlet.xml 의 일부이다.

    <bean id="jstlViewResolver"
        class="org.springframework.web.servlet.view.UrlBasedViewResolver">
        <!-- view class for jstl -->
        <property name="viewClass"
            value="org.springframework.web.servlet.view.JstlView" />
        <property name="order" value="1" />
    </bean>
    

    만약 JSTL 태그를 사용한다면 viewClass를 JstlView로 지정해 준다. JstlView도 요청을 JSP에 전달한다.

12.Controller

컨트롤러는 MVC중 C(Controller)에 해당하며 사용자의 요청을 처리한 후 적절한 뷰를 찾아 뷰에 필요한 모델 객체를 넘겨 준다. Spring MVC에서는 다양한 Controller Interface의 구현체를 제공한다. 구현체의 대표적인 몇 가지를 살펴보도록 한다.

12.1.AbstractController

다양한 컨트롤러들의 가장 기반이 되는 구현체는 AbstractController이다. AbstractController에서는 사용자의 요청을 처리하기 위해서 handleRequestInternal 메소드를 구현해줘야 하며 구현 예는 다음과 같다.

    public class addProductViewController extends AbstractController {
        
      	private ProductService productService;
		private CategoryService categoryService;

		public void setProductService(ProductService productService) {
			this.productService = productService;
		}

		public void setCategoryService(CategoryService categoryService) {
			this.categoryService = categoryService;
		}
        //override handleRequestInternal() method.
        protected ModelAndView handleRequestInternal(HttpServletRequest request,
                HttpServletResponse response) throws Exception {
            request.setAttribute("categoryList", categoryService
				.getDropDownCategoryList());
			request.setAttribute("product", new Product());

			return new ModelAndView("/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp");
    }
}

위와 같이 작성할 경우 해당 컨트롤러는 요청 처리 후 ModelAndView를 FrontController인 DispatcherServlet으로 리턴하고 view 이름은 /jsp/user/userInformResult.jsp이다 view에서는 users라는 이름으로 Controller에서 생성된 UserVO객체를 넘겨줄 수 있다.

12.2.MultiActionController

웹 애플리케이션을 개발하면서 하나의 페이지가 추가될 때마다 새로운 컨트롤러를 추가해야하는 번거로움을 없애기 위해 MultiActionController를 사용할 수 있다. 이 컨트롤러는 하나의 컨트롤러에 여러개의 핸들러 메소드를 정의 할 수 있다. 이렇게 여러개의 핸들러 메소드를 정의하기 위해서는 다음과 같이 methodNameResolver를 추가해 줘야 한다.

<bean name="/foundationProduct.do" class="anyframe.example.foundation.sales.web.ProductController">
	<property name="productService" ref="foundationProductService"/>
	<property name="categoryService" ref="foundationCategoryService"/>
	<property name="idGenenrationService" ref="idGenerationService"/>
	<property name="methodNameResolver" ref="paramResolver" />
</bean>	

위에서 참조하는 ProductController 클래스는 다음과 같다. 위 설정파일에 통하면 /foundationProduct.do?method=addView 요청은 아래의 addView() 메소드를 /foundationProduct.do?method=add 요청은 add() 메소드를 호출하게 된다.

public class ProductController extends MultiActionController {
	private ProductService productService;
	private CategoryService categoryService;

	public void setProductService(ProductService productService) {
		this.productService = productService;
	}

	public void setCategoryService(CategoryService categoryService) {
		this.categoryService = categoryService;
	}

	/**
	 * display add product form.
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws Exception
	 */
	public ModelAndView addView(HttpServletRequest request,
			HttpServletResponse response) throws Exception {
		request.setAttribute("categoryList", categoryService
				.getDropDownCategoryList());
		request.setAttribute("product", new Product());

		return new ModelAndView("/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp");
	}

	/**
	 * add product
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws Exception
	 */
	public ModelAndView add(HttpServletRequest request,
			HttpServletResponse response) throws Exception {

		Product product = new Product();
		bind(request, product);
		product.setAsYn(request.getParameter("_asYn"));
		
		if(product.getAsYn()==null || product.getAsYn().equals(""))
			product.setAsYn("N");

		product.setSellerId("test");
		productService.create(product);

		return new ModelAndView("/foundationProduct.do?method=list");
	}
}
MultiActionController에서 제공하는 bind 메소드는 폼에서 받은 입력 파라미터들을 지정한 도메인 객체로 바인딩 시켜준다. 이 때, 입력 파라미터의 이름과 도메인 객체의 필드 이름이 일치하는 데이터에 한해서 자동으로 바인딩 시켜주게 된다. 핸들러 메소드의 리턴 타입은 ModelAndView, Map, String, void가 있다.

12.3.AbstractCommandController

Spring MVC의 Command 컨트롤러는 HttpServletRequest로 받아온 파라미터를 동적으로 특정한 데이터 객체로 바인드 할 수 있다. Spring에서는 데이터 객체가 Struts의 ActionForm과 같이 framework-specific 인터페이스를 구현하지 않아도 된다. 그 중 대표적인 command 컨트롤러에는 AbstractCommandController가 있다. AbstractCommandController는 특정 객체로 request의 파라미터를 바인딩 할 수 있다. 폼 기능은 제공하지 않지만 validation을 할 수 있으며 바인딩 객체를 사용하여 원하는 일을 할 수 있다. 이러한 AbstractCommandController를 사용한 클래스의 예는 다음과 같다.

public class AddProductController extends AbstractCommandController{

    private ProductService productService;

	public void setProductService(ProductService productService) {
		this.productService = productService;
	}
    
    //setting command class for data binding
    public AddProductController() {
        setCommandClass(Product.class);
    }
    

    //override handle() method.
    protected ModelAndView handle(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException exception) 
            throws Exception {
        
        //data binding using command object
        Product product = (Product)command;
        product.setAsYn(request.getParameter("_asYn"));
		
		if(product.getAsYn()==null || product.getAsYn().equals(""))
			product.setAsYn("N");
			
        product.setSellerId("test");
		productService.create(product);

		return new ModelAndView("/foundationProduct.do?method=list");
    }
}

입력 파라미터를 도메인 객체에 바인딩 시킬 때 생성자에서 정의해준 commandClass와 도메인 객체는 같은 타입이어야 한다.

12.4.SimpleFormController

Spring MVC에서는 사용자가 보다 쉽게 입력 폼을 구현하기 위해 SimpleFormController를 지원한다. 이 컨트롤러는 입력 화면에 대한 정보와 작업이 성공적으로 완료했을 때 이동하게될 URL정보를 설정할 수 있으며 입력 폼 화면에 필요한 데이터들을 전달하기 위해 formBackingObject() 메소드를 지원한다. 또한 데이터 유효성을 체크하는중에 에러가 발생할 경우 입력 폼 화면으로 이동하며 에러메시지를 출력해줄 수도 있다. Validator 관련 사항은 본 매뉴얼 Validator를 참고한다. SimpleFormController를 작성하는 방법은 다음과 같다.

public class ProductController extends SimpleFormController {

   	private ProductService productService;

	public void setProductService(ProductService productService) {
		this.productService = productService;
	}

    // setting command class for data binding
    public ProductController() {
        setCommandClass(Product.class);
        setCommandName("product");
        setValidator(new ProductValidator());
        setFormView("/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp");
        setSuccessView("/foundationProduct.do?method=list");
    }

    // override onSubmit() method.
    protected ModelAndView onSubmit(Object command) throws Exception {

        // data binding using command object
        Product product = (Product) command;

        product.setAsYn(request.getParameter("_asYn"));
		
		if(product.getAsYn()==null || product.getAsYn().equals(""))
			product.setAsYn("N");
			
		product.setSellerId("test");
		productService.create(product);

		return new ModelAndView("/foundationProduct.do?method=list");
    }
    protected Map formBackingObject(HttpServletRequest request)
            throws Exception {
        request.setAttribute("categoryList", categoryService
				.getDropDownCategoryList());
		Product product = new Product();
		return product;
    }
}

위 코드에서 볼수 있듯이 생성자를 통해 폼 입력 화면인 formView, 입력 화면에 대한 작업이 완료되었을 때 보여줄 successView, 폼 입력 값을 받아온 command 객체를 바인딩할 클래스를 지정해주는commandClass, validation 체크를 위한 validator 설정을 할 수 있다. 생성자에서 셋팅해주는 이러한 속성들은 모두 Controller 빈을 정의할 때 XML 파일에 선언적으로 아래와 같이 정의할 수 있다.

<bean name="/foundationProduct.do" class="anyframe.example.foundation.sales.web.ProductController">
	<property name="productService" ref="foundationProductService"/>
	<property name="commandClass" value="anyframe.example.domain.Product"/>
	<property name="commandName" value="product"/>
	<property name="validator" value="anyframe.example.validator.ProductValidator" />
	<property name="formView" value="/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp" />
	<property name="successView" value="/foundationProduct.do?method=list" />
</bean>
여기서 commandName으로 지정된 "users"는 JSP의 spring form 태그의 commandName과 같아야 하며 그 값은 위의 formBckingObject에서 return된 객체의 값이 자동으로 셋팅된다. 다음은 JSP페이지 spring form 태그의 commandName지정 모습이다. form 태그의 자세한 사항은 본 매뉴얼 Tag Library 부분을 참조한다.
<form:form commandName="users">
위 컨트롤러에서 구현한 formBackingObject()메소드는 입력폼에 필요한 데이터를 전달해 주는 역할을 하며 onSubmit 메소드는 핸들러 메소드로 요청을 처리하는 역할을 한다. 이러한 SimpleFormController는 다양한 폼 기능 뿐만 아니라 중복 폼 서브밋 방지기능을 제공한다. 하지만 SimpleFormControllef를 사용하여 중복 폼 서브밋 방지 기능을 구현하기에는 설정과 방법이 복잡하므로 Anyframe 에서는 SimpleFormController와 MultiActionController를 확장한 AnyframeFormController 를 사용하는 것을 권장 한다.

12.5.UrlFilenameViewController

단지 HTML이나 JSP화면을 출력해주는 정적인 페이지로의 포워딩을 해주기 위해 별도의 컨트롤러 작성 없이 Spring MVC에서 제공하는 UrlFilenameViewController를 사용한다. 이러한 정적인 페이지로의 이동은 다음과 같이 간단하게 정의할 수 있다.

<bean id="urlMapping"
    class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <value>
            /index.do=jspViewController
        </value>
    </property>
</bean>
<bean id="jspViewController" 
      class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>

UrlFilnameViewController를 사용하게 되면 요청된 url에서 확장자를 뺀 부분을 view 이름으로 가지고 viewResolver에 따른 view 결정을 한다. 그러므로 중복된 view 이름이 없도록 주의해야한다. 위 같이 정의할 경우에는 index를 view 이름으로 하여 viewResolver에 의해 결정된 view를 출력해 주게 되는 것이다.

12.6.ParameterizableViewController

정적인 페이지로의 포워딩을 해주기 위해 위에서 설명한 UrlFilenameViewController를 이용할 수 있지만 url을 가지고 view 이름을 결정하므로 url과 view 이름이 같아야 한다. 이렇게 url로 view 이름을 정하지 않고 사용자가 정한 view 이름으로 포워딩 해줄 때 ParameterizableViewController를 사용한다. org.springframework.web.servlet.mvc.ParameterizableViewController를 Controller Class로 정의한 다음 "viewName"이라는 property에 출력해줄 view 이름을 정의한다.

<bean name="/addProductView.do"
    class="org.springframework.web.servlet.mvc.ParameterizableViewController">
    <property name="viewName" value="/WEB-INF/jsp/foundation/sales/product/viewProduct.jsp" />
</bean>

13.View

Spring MVC는 JSP에서 보다 쉽게 데이터를 출력할 수 있도록 Tag Library를 제공하며 여러 View 기술(Velocity, Freemarker, Tiles 등)과의 연계 방법을 제시한다. 여기서는 Spring Form Tag와 Tiles 연계 방안에 대해 설명하도록 한다.

13.1.Tag library

Spring MVC에서는 입력 폼 구현을 보다 쉽게 구현하기 위해 Spring Form Tag를 제공한다. 이는 태그에서 command 객체, controller 참조 데이터로의 접근이 가능하다. Spring Form tag의 사용 방법은 매우 간단하며 예제를 중심으로 각 tag에 대한 내용을 살펴본다.

13.1.1.configuration

Spring Form Tag를 사용하기 위해서는 spring-form.tld 파일이 필요하고 이는 spring-webmvc-2.5.2.jar 파일에 포함되어 있다. 이 폼 태그를 사용하기 위해서는 JSP 페이지에 taglib을 추가해줘야한다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

13.1.2.form

<form>은 데이터 바인딩을 위해 태그 안에 바인딩 path를 지정해 줄 수 있다. path에 해당되는 값은 도메인 모델의 Bean 객체를 의미한다. 사용예는 다음과 같다.

<form:form commandName="user">
userId : <form:input path="userId" />
</form:form>

또한 Spring Form Tag를 이용하기 위해서는 각각의 입력 path값에 매칭될 트랜스퍼 오브젝트를 지정해 줘야 하는데 <form>안에 commandName 속성으로 다음과 같이 지정해 줄 수 있다.

<% request.setAttribute("user", sample.services.UserVO())%>

이러한 commandName의 기본값은 "command"이며 input값들과 매칭될 트랜스퍼 오브젝트를 request값으로 세팅해 줘야한다. 이 값은 SimpleFormController를 사용할 경우 FormBackingObject()메소드에서 지정해 줄 수도있다.

protected Object formBackingObject(HttpServletRequest request)
    throws Exception {
    UserVO vo=new UserVO();
    request.setAttribute("user",vo);
    return new UserVO();
}

13.1.3.input

HTML의 <input>의 value가 text인 것을 기본 value로 갖는다. 이 태그의 예는 위의 <form> 예에서 볼 수 있다.

13.1.4.checkbox

다음은 <checkbox>의 예이다. 마찬가지로 path에 트랜스퍼 오브젝트의 bean name을 매핑시켜주고 label속성을 이용하면 jsp페이지로 보여질 이름을 설정할 수 있다.

<form:checkbox path="hobby" value="listeningMusic" label="음악감상"/>
<form:checkbox path="hobby" value="study" label="공부"/>

※ 위 코드는 아래와 같은 화면을 출력한다.

13.1.5.checkboxes

위의 <checkbox>는 각각의 항목에 대해 작성해줘야 하지만 <checkboxes>를 사용하면 items속성을 이용해서 한줄로 나타내줄 수 있다. 이러한 items에 들어갈 값은 컨트롤러의 formBackingObject()메소드에서 Array, List, Map형태의 것들로 넘겨 줄 수 있다. Map의 key와 value쌍으로 넘겨줄 경우 key는 태그의 value값이 되고 value는 label명이 된다. (단, Array나 List로 넘길 경우 label은 value와 같은 값을 가지게 된다.) 다음은 그 예이다.

protected Object formBackingObject(HttpServletRequest request)
    throws Exception {
    UserVO vo=new UserVO();
    Map interest =new HashMap();
    interest.put("reading", "독서 ");
    interest.put("listeningMusic", "음악감상");
    interest.put("study", "공부");
    request.setAttribute("interest",interest);
    request.setAttribute("user",vo);
    return new UserVO();
}

<tr>
    <td>hobby :</td>
    <td><form:checkboxes path="hobby" items="${interest}" /></td>
</tr>

※ 위 코드는 아래와 같은 화면을 출력한다.

13.1.6.radiobutton

다음은 <radiobutton>의 예이다. <radiobutton> 또한 label 속성을 이용하여 label명을 설정해 줄 수 있다.

<tr>
    <td>Sex:</td>
    <td>Male: <form:radiobutton path="sex" value="M" label="남자"/> <br/>
        Female: <form:radiobutton path="sex" value="F" label="여자"/> </td>
</tr>

13.1.7.radiobuttons

다음은 <radiobuttons>의 예이다. items 속성의 사용방법은 위의 <checkboxes>와 동일하다.

<tr>
    <td>Sex:</td>
    <td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>

13.1.8.password

다음은 <password>의 예이다.

<tr>
    <td>password :</td>
    <td><form:password path="password" /></td>
</tr>

※ 위 코드는 아래와 같은 화면을 출력한다.

13.1.9.select

<select>도 위의 <checkboxes>나 <radiobuttons>처럼 items 속성을 이용하여 formBackingObject에서 넘겨주는 값으로 자동 매핑 시켜줄 수 있다.

protected Object formBackingObject(HttpServletRequest request)
    throws Exception {
    UserVO vo=new UserVO();
    Map address =new HashMap();
    address.put("seoul","서울");
    address.put("daegu","대구");
    address.put("busan","부산");
    request.setAttribute("address",address);
    request.setAttribute("user",vo);
return new UserVO();
}

<tr>
    <td>주소</td>
    <td><form:select path="address" items="${address}" /></td>
</tr>

※ 위 코드는 아래와 같은 화면을 출력한다.

일반적인 <option>와 함께 아래와 같이 사용할 수도 있다.

13.1.10.option

다음은 <option>의 사용 예이다.

<tr>
    <td>주소</td>
    <td><form:select path="address">
        <form:option value="seoul" label="서울" />
        <form:option value="daegu" label="대구" />
        <form:option value="busan" label="부산" />
    </form:select></td>
</tr>

13.1.11.options

다음은 <options>의 사용예이다.

<tr>
    <td>주소</td>
    <td><form:select path="address">
        <form:options items="${address}" />
    </form:select></td>
</tr>

13.1.12.textarea

다음은 <textarea>의 사용 예이다.

<td>Note :</td>
<td><form:textarea path="comment" rows="3" cols="20"></form:textarea></td>

13.1.13.hidden

다음은 <hidden>의 사용 예이다.

<form:hidden path="userId" />

13.1.14.errors

Spring MVC는 validatior에서 얻어진 메시지를 JSP페이지에서 쉽게 출력할 수 있도록 Spring Form 태그의 <form:errors>를 제공한다. 이는 생성한 validator를 통해 입력값의 유효성 체크 후 에러 메시지를 출력해주는데 자세한 사항은 본 매뉴얼 Spring MVC validator - form:errors 태그 사용 방법 을 참고한다.

13.1.15.sample

13.1.15.1.입력 화면

다음은 입력 화면 작성 예인 userForm.jsp 파일의 일부이다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<form:form commandName="users" name="form" enctype="multipart/form-data">
    <table>
    <tr><td colspan="3"><center><strong>
        <spring:message code="title.user.form"></spring:message>
    </strong></center><br/><br/></td></tr>
        <tr>
            <td> Name :</td>
            <td><form:input path="userName" />(required)</td>
            <td><form:errors path="userName" /></td>
        </tr>
        <tr>
            <td>password :</td>
            <td><form:password path="password" />(required, 6자이상입력)</td>
            <td><form:errors path="password" /></td>
        </tr>
        <tr>
            <td>confirm password :</td>
            <td><form:password path="confirmPassword" />(위의 password와 동일해야함)</td>
            <td><form:errors path="confirmPassword" /></td>
        </tr>
        <tr>
            <td>sex :</td>
            <td><form:radiobutton path="sex" value="M" label="남자" /> 
            <form:radiobutton path="sex" value="F" label="여자" /></td>
        </tr>
        <tr>
            <td>address :</td>
            <!-- items 속성을 사용하여 컨트롤러의 
            formbackingObject()에서 넘겨준  Map 형태의 객체를 받아 출력해준다. -->
            <td><form:select path="address" items="${address}"/>
        </tr>
        <tr>
            <td>hobby :</td>
            <td><form:checkboxes path="hobby" items="${hobby}" /></td>
        </tr>
        <tr>
            <td>Note :</td>
            <td><form:textarea path="comment" rows="3" cols="20"></form:textarea></td>
        </tr>
    </table>
    <a href="javascript:fncGetUser();">submit</a>
</form:form>

13.1.15.2.Controller 클래스

다음은 Form에서 사용할 객체를 셋팅해주는 UserController.java 파일의 FormBackingObjet()메소드와 요청 처리 결과를 모델객체에 셋팅해서 view로 넘겨주는 onSubmit()메소드의 일부이다.

public class UserController extends SimpleFormController {

    ... 중략...
    // setting command class for data binding
    public UserController() {
        setCommandClass(UserVO.class);
        //form tag에서 사용할 commandName 된다. 
        setCommandName("users"); 
        setFormView("/jsp/user/userForm.jsp");
    }

    // override onSubmit() method.
    protected ModelAndView onSubmit(Object command) throws Exception {
        // data binding using command object
        UserVO userVO = (UserVO) command;

        // call business service
        userVO = userService.getUser(userVO);
        // setting view name
        ModelAndView mav = new ModelAndView("/jsp/user/getUser.jsp");
        //view에 "userVO"라는 모델 객체를 넘겨준다. 
        mav.addObject(userVO);
        // return a ModelAndView object.
        return mav;
    }

    protected Object formBackingObject(HttpServletRequest request)
            throws Exception {
        Map address = new HashMap();
        address.put("seoul", "서울");
        address.put("daegu", "대구");
        address.put("busan", "부산");

        Map hobby = new HashMap();
        hobby.put("reading", "독서");
        hobby.put("listeningMusic", "음악감상");
        hobby.put("study", "공부");
        
        request.setAttribute("address", address);
        request.setAttribute("hobby", hobby);
        
        //commandName인 "users"에 return값이 셋팅된다. 
        return new UserVO();
    }

}

13.1.15.3.출력 화면

다음은 EL문을 사용한 데이터 출력을 작성한getUser.jsp 파일의 일부이다.

<tr><td>User Name : </td><td>${userVO.userName}</td></tr>
<tr><td>User Password : </td><td>${userVO.password}</td></tr>
<tr><td>User Address : </td><td>${userVO.address}</td></tr>
<tr><td>User hobby : </td><td>${userVO.hobby}</td></tr>

위의 JSP 코드처럼 Expression Language(JSP 2.0에서 지원)를 사용하여 Controller에서 넘겨준 "userVO"라는 이름의 모델 객체의 값을 출력할 수 있다.

13.2.Tiles

Spring MVC에서는 Tiles1, Tiles2를 각각 지원하는 viewClass를 제공한다. 본 매뉴얼에서는 Tiles2와의 연계 방안에 대해 설명할 것이며 기본적인 viewResolver에 대한 내용은 Spring MVC >> Configuration의 viewResolver 정의 부분을 참고한다. Spring MVC 2.5와 Tiles2를 연계하기 위해서는 JDK1.5 이상, Tiles 2.0.X, Commons BeanUtils, Commons Digester, Commons Logging이 필요하다. Tiles2를 연계하기 위해서는 아래와 같은 절차를 따른다.

  • Tiles view class 정의

  • TilesConfigurer 정의

  • Tiles definition 파일 작성

13.2.1.Tiles view class 정의

viewResolver 정의 부분에서 간략하게 설명했듯이 Tiles 2를 이용하기 위해서는 UrlBasedViewResolver를 정의한 후 viewClass를 아래 코드와 같이 org.springframework.web.servlet.view.tiles2.TilesView로 정의해줘야 한다.

<bean id="tilesViewResolver"
    class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass"
        value="org.springframework.web.servlet.view.tiles2.TilesView" />
</bean>

13.2.2.TilesConfigurer 정의

Tiles 매핑 관련 정보가 작성되어 있는 tiles definition 파일의 위치를 정의해줘야 하는데 이 때 TilesConfigurer를 아래와 같이 정의해 준다.

<bean id="tilesConfigurer"
    class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/tiles-def.xml</value>
        </list>
    </property>
</bean>

위와 같이 정의할 경우 /WEB-INF/tiles-def.xml 파일을 로드하여 각 view 이름에 맞는 tiles view를 리턴해 준다.

13.2.3.Tiles definition 파일 작성

Tiles를 사용하기 위해서는 실제 Controller에서 리턴된 view 이름을 토대로 페이지에 출력해줄 tiles attribute를 정의해주는 tiles definition을 정의해 줘야한다. (위의 tilesConfigurer 위치로 정의한 tiles-def.xml 파일) 다음은 tiles definition 정의 예이다.

<definition name="template" template="/sample/layouts/layout.jsp">
    <put-attribute name="header" value="/sample/layouts/top.jsp" />
    <put-attribute name="body" value="/sample/layouts/welcome.jsp" />
    <put-attribute name="footer" value="/sample/layouts/left.jsp" />
</definition>
<definition name="listCategory" extends="template">
    <put-attribute name="body" value="/sample/category/listCategory.jsp" />
</definition>

먼저 Layout을 정의한 jsp 페이지를 정의한다. 해당 layout.jsp 페이지에서 기본적으로 사용할 페이지 구성 요소(위의 예에선 header, body, footer)들을 정의한 후 다른 view들은 미리 정의된 template이라는 definition을 extends하여 body만 설정하여 사용할 수 있다. 위의 예에서 listCategory라는 이름의 view가 리턴될 경우 "/sample/layouts/layout.jsp" 페이지의 레이아웃으로 header에는 "/sample/layouts/top.jsp" body는 "/sample/category/listCategory.jsp", footer는 "/sample/layouts/left.jsp"이 될 것이다. JSP에서 tiles 구성 요소를 넣을 때는 아래와 같이 tiles taglib을 정의한 후 <tiles:insertAttribute> 태그를 이용하여 사용한다.

<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles"%>
...중략...
<tr>
    <td colspan="2"><tiles:insertAttribute name="header" /></td>
</tr>
<tr>
    <td valign="top"><tiles:insertAttribute name="footer" /></td>
    <td valign="top"><tiles:insertAttribute name="body" /></td>
</tr>

name attribute에 들어갈 이름은 tiles definition 파일의 name attribute의 이름이 된다.

14.File Upload

Spring MVC는 파일 업로드 기능을 지원하기 위하여 Commons 파일 업로드COS 파일업로드 라이브러리를 지원한다. Anyframe 에서는 commons 라이브러리를 사용할 것이다. commons 라이브러리를 사용하기 위해서는 commons-fileupload-x.x.jar 파일과 commons-io-x.x.jar파일이 필요하다. 이는 Anyframe 배포 라이브러리에 포함되어 있다.

COS 라이브러리 사용시 주의할 점

COS 라이브러리를 사용하기 위해서는 해당 라이센스 를 반드시 확인한다. 파일 업로드 기능을 구현하기 위해서는 먼저 빈 설정 파일에 다음과 같이 MultipartResolver를 정의해야한다.

<bean id="multipartResolver"
    class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize">
        <value>10000000</value>
    </property>
</bean>           

또한 해당 컨트롤러의 property로 파일의 업로드 위치를 지정해주고 컨트롤러에서 setter injection을 통해 지정된 파일 업로드 위치를 불러올 수 있다. 사용예는 다음과 같다.

<bean id="helloworldCommandController"
    class="sample.web.controller.HelloworldCommandController">
    <property name="destinationDir" value="C:/Temp/fileupload/temp" />
    <property name="helloworldService" ref="helloworldService" />
</bean>

파일 업로드를 위해 JSP파일의 입력 폼 타입을 file로 지정하고 form의 enctype을 multipart/form-data로 지정한다.

<body>
    <form name="fileForm" action="file.do" method="post" enctype="multipart/form-data">
    파일  : <input type="file" style="width:400" name="file"><br/>
    <input type="submit" value="upload" />
    </form>
</body>

Spring MVC에서는 파일 업로드를 위해 MultipartFile이라는 객체 타입을 제공한다.

private MultipartFile file;
private Long size;
private String name;
private String filePath;

다음은 파일 업로드를 위해 Controller를 구현한 모습이다.

public class HelloworldCommandController extends AbstractCommandController {
    
    private File destinationDir;
    
    /** 
     * 파일업로드를 위한 빈 설정의 property로 지정해준 
     * destinationDir setter injection
     */
     public void setDestinationDir(File destinationDir) {
            this.destinationDir = destinationDir;
        }
    
    ...중략...
    
    protected ModelAndView handle(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException exception)
            throws Exception {
        
        //전달 받은 Request값을 MultipartHttpServletRequest로 바인딩 시킨다.
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        
        //request의 "file"을 찾아 file객체에 세팅한다.
        MultipartFile file = multipartRequest.getFile("file");
        String fileName = file.getOriginalFilename();
        File destination = File.createTempFile("file", fileName, destinationDir);
        
        //파일카피
        FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(destination));
        
        //새로운 파일 속성 세팅
        HelloVO vo = (HelloVO) command;
        vo.setFilePath(destination.getAbsolutePath());
        vo.setName(file.getOriginalFilename());
        vo.setSize(file.getSize());
        vo.setFile(file);
        helloworldService.getMessage1(vo);
        return new ModelAndView("result", "message", vo);
    }
}

위와 같이 간단한 파일 업로드를 실행시켜 볼 수 있다. 위의 예제는 화면에서 입력 받은 객체를 MultipartFile타입으로 받았기 때문에 별다른 바인딩 작업이 필요하지않았다. 하지만 화면에서 입력받은 파일을 String 타입으로 바인딩하려면 StringMultipartEditor, byte 타입의 배열로 바인딩 하려면 ByteArrayMultipartEditor를 사용하여 Contoller에 다음과 같이 initBinder 메소드를 오버라이드하여 구현해 줄 수 있다.

  • StringMultipartEditor

    protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
       throws ServletException {
       binder.registerCustomEditor(String.class, new StringMultipartFileEditor());
    }
    
  • ByteArrayMultipartEditor

    protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
        throws ServletException {
        binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor());
    }
    

15.Internationalization

Spring MVC에서는 Presentation Layer에서 사용자의 Local에 따른 국제화를 위해 여러가지 Locale Resolver를 한다. Request가 들어오면 DispatcherServlet은 Locale resolver에 의해 사용자의 Local을 알아내게 되며 RequestContext.getLocale() 메소드를 사용해서 Locale을 확인할 수 있다.

15.1.다국어 지원 기능

Spring MVC는 다국어를 지원하기 위하여 Locale Resolver를 가지고 있으며 특정 Locale Resolver를 정의하지 않을 경우 디폴트로 AcceptHeaderLocaleResolver를 이용한다. 또한 사용자들이 원하는 언어를 직접 선택할 수 있도록 구현해야 한다면 CookieLocaleResolver 또는 SessionLocaleResolver를 이용하여 구현하도록 한다. 웹 어플리케이션의 화면에 출력해줄 메세지 리소스를 추출하기 위해 Spring MVC에서는 MessageSource를 제공하며 이러한 MessageSource에서 추출한 메시지를 화면에 출력해 줄 수있는 tag 라이브러리를 제공한다. 사용 방법은 아래와 같다.

  • Step 1 : properties 파일 작성

    각각 언어에 따른 properties파일을 생성하고 출력할 메시지를 작성한다. PropertiesEditor 이클립스 플러그인을 사용하면 쉽게 작성할 수 있다. 다음은 message-user_ko.properties , message-user_en.properties 파일의 일부이다.

    • 한글용 (message-user_ko.properties)

      title.user.form = 당신의 정보를 입력하세요.

    • 영어용, default용 (message-user.properties, message-user_en.properties)

      title.user.form = Input your information

  • Step 2 : MessageSource 정의

    다음은 messageSource가 정의되어 있는 context-user.xml 파일의 일부이다.

    <bean id="messageSource"
       class="org.springframework.context.support.ResourceBundleMessageSource">
       <property name="basenames">
          <list>
             <!-- properties파일의 이름을 등록한다. 
                                등록되 있지 않을 시에 디폴트로 message.properties파일을 찾는다.-->
             <value>message-user</value>
          </list>
       </property>
    </bean>

  • Step 3: JSP 파일 작성

    JSP파일에서 등록한 message를 출력하기 위해서 Spring에서 제공하는 태그라이브러리를 등록한 userForm.jsp 파일의 일부이다.

    <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

    다음과 같이 <spring:message> 태그를 사용하여 메시지를 출력할 수 있다.

    <spring:message code="title.user.form"></spring:message>

    이러한 spring:message 태그의 속성은 다음과 같다.

    속성설명
    arguments부가적인 인자를 넘겨줌. 콤마로 구분된 문자열, 객체 배열, 객체 하나를 넘김.
    argumentSeparator넘겨줄 인자들의 구분자 설정. 기본값은 콤마.
    code룩업할 메시지의 키 지정. 지정하지 않으면 text에 입력한 값 출력.
    htmlEscapehtml 기본 escapse 속성 오버라이딩. 기본값 false.
    javaScriptEscape기본값 false
    messageMessageSourceResolvable 인자로 Spring MVC validation을 거친 errors의 메시지를 쉽게 보여줄 때 사용
    scope결과 값을 변수에 지정할 때 변수의 scope 지정 (page, request, session, application)
    text해당 code로 가져온 값이 없을 때 기본으로 보여줄 문자열. 빈 값이면 null 출력.
    var결과 값을 이 속성에 해당한 문자열에 바인딩 할 때 사용. 빈 값이면 그냥 JSP에 뿌려줌.

15.1.1.Locale Resolver를 이용한 Locale 변경

Locale Resolver를 사용하여 locale을 바꾸고 싶을 때는 setter Injection을 통해 정의한 Locale Resolver를 injection 한 후 setLocale()메소드를 통해 locale을 변경해 줄 수 있다. 또한 resolveLocale(request)메소드를 사용하여 현재 request에 셋팅되어 있는 Locale을 알아낼 수 있다.

public class UserController extends MultiActionController {

    LocaleResolver localeResolver= null;
    
    //setter injection
    public void setLocaleResolver(LocaleResolver localeResolver){
        this.localeResolver = localeResolver;
    }

    protected ModelAndView changeLocale(HttpServletRequest request, HttpServletResponse response) 
        throws Exception {
        //request parameter "locale"에 사용자가 설정한 locale을 가지고 온다.(ex> en, ko)
        Locale locale = new Locale(request.getParameter("locale"));
        //localeResolver에 locale 셋팅
        localeResolver.setLocale(request, response, locale);
        //셋팅된 locale 확인
        System.out.println("current locale from locale resolver ====== " + 
            localeResolver.resolveLocale(request));
        return new ModelAndView("/jsp/result.jsp");
    }

...생략...

15.1.2.LocaleChangeInterceptor를 이용한 Locale 변경

HandlerMapping에 interceptor를 등록하여 특정 locale의 요청을 가로채서 특정 파라미터에 넘어 온 값으로 locale을 지정할 수 있다. 속성 정의 파일 내의 LocaleChaneInterceptor 정의 예는 다음과 같다.

<bean id="localeResolver"
      class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
      
<bean id="localeChangeInterceptor"
      class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="locale"/>
</bean>

<bean id="urlMapping"
      class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/list.do=getUserListController</value>
    </property>
</bean>

여기서는 모든 /list.do 요청을 가로채서 "locale"이라는 request의 파라미터로 locale을 알아낼 수 있다.

15.2.Locale Resolver

위의 다국어 지원 예에서 처럼 Spring MVC에서는 Locale Resolver를 사용하여 Locale을 얻어올 수 있으며 이러한 Locale Resolver에는 아래와 같은 것들이 있다.

15.2.1.AcceptHeaderLocaleResolver

사용자의 브라우져에서 보내진 request의 헤더에 accept-language부분에서 Locale을 읽어들인다. 사용자의 OS locale을 나타낸다.

<bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver" />

AcceptHeaderLocaleResolver는 setLocale() method를 이용한 locale 변경이 불가능하다.

15.2.2.CookieLocaleResolver

사용자의 쿠키에 설정된 Locale을 읽어 들인다. 다음과 같은 속성을 설정할 수 있다.

속성기본값설명
cookieNameclassname + LOCALE쿠키 이름
cookieMaxAgeInteger.MAX_INT쿠키 살려둘 시간. -1로 해두면 브라우저를 닫을 때 없어짐
cookiePath/Path를 지정해 주면 해당 하는 path와 그 하위 path에서만 참조
<bean id="localeResolver"
    class="org.springframework.web.servlet.i18n.CookieLocaleResolver" >
    <property name="cookieName" value="clientlanguage"/>    
    <property name="cookieMaxAge" value="100000"/>
    <property name="cookiePath" value="web/cookie"/>
</bean>

15.2.3.SessionLocaleResolver

requst가 가지고 있는 session으로 부터 locale 정보를 가져온다.

<bean id="localeResolver"
    class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />

15.2.4.FixedLocaleResolver

사용자가 특정한 Locale을 지정할 수 있으며 setLocale()메소드를 지원하지 않는다.

<bean id="fixedLocaleResolver"
    class="org.springframework.web.servlet.i18n.FixedLocaleResolver">
    <property name="defaultLocale" value="en"/>
</bean>

16.Validator

Spring MVC에서는 Validator를 구현하기 위해 ValidatorUtils를 이용한 입력 필드의 값 존재 여부의 validation 체크나 Errors 객체를 통해 에러 메시지를 출력해주는 validation 체크 방법을 지원한다. 이것은 jsp 페이지에서 form:errors를 태그를 통해 간단히 에러 메시지를 출력할 수 있으며 SimpleFormController를 사용하면 입력 페이지에 에러 메시지를 출력해 줄 수 있다.

16.1.Validator 생성

  • ValidatorUtils 사용

    필수 입력 필드의 validation을 체크하고 에러메시지를 출력 할 수 있도록 지원한다. 간단히 ValidatorUtils를 사용하여 구현할 수 있다. 다음은 validator를 구현한 UserValidator.java의 일부이다.

    public class UserValidator implements Validator {
    
        public boolean supports(Class clazz) {
            return UserVO.class.isAssignableFrom(clazz);
        }
    
        public void validate(Object object, Errors errors) {
    
            // validationUtils를 이용하여 입력값이 비었는지 체크
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName",
                    "required", new Object[] { "userName" }, "Enter your name");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password",
                    "required", new Object[] { "password" }, "Enter your password");>
    

    ValidationUtils 클래스에 대한 API는 여기를 참고한다.

  • Property 파일 사용

    validation을 체크할 때 Errors 객체를 사용하여 해당 필드에 대해 에러 메시지를 출력할 수 있는데 그 예는 다음과 같다.

    public class UserValidator implements Validator {
        public boolean supports(Class clazz) {
            return HelloVO.class.isAssignableFrom(clazz);
        }
    
        public void validate(Object object, Errors errors) {
            
            HelloVO helloVO = (HelloVO) object;
            if (helloVO.getPassword().length() < 6)
                errors.rejectValue("password", "error.password.tooshort");
    
            if (!helloVO.getPassword().equals(helloVO.getConfirmPassword()))
                errors.rejectValue("confirmPassword", "error.confirm");
        }
    }
    

    해당 조건에 만족하지 않는 입력 값이 있으면 properties 파일에 미리 정의된 error.password.tooshort, error.confirm등의 메시지가 출력될 것이다. messageSource 정의 방법은 Spring MVC 다국어 지원 기능 을 참고하고 errors 클래스의 API는 여기 를 참고한다.

16.2.Validator 등록

위에서 처럼 생성한 validator를 컨트롤러에도 추가해 주어야 한다. 다음은 UserController 빈 정의가 작성되어 있는user-servlet.xml 파일의 일부이다.

<bean name="/getUser.do"
    class="anyframe.sample.springmvc.web.controller.basic.UserController">
    <property name="userService" ref="userService" />
    <property name="validator" ref="userValidator"/>
</bean>
<bean name="userValidator" class="anyframe.sample.springmvc.web.validator.UserValidator"/>

16.3.form:errors 태그 사용

validatior에서 얻어진 메시지를 JSP페이지에서 쉽게 출력할 수 있도록 Spring MVC에서 제공하는 태그 라이브러리 중 form 태그의 form:errors태그를 사용할 수 있다. 이 태그를 사용하기 위해서는 다음과 같은 절차를 따른다.

  • 태그 라이브러리 등록

    spring의 form 태그 라이브러리를 사용하기 위해서는 spring-form.tld파일이 필요하고 이는 spring-webmvc-2.5.2.jar 파일에 포함되어 있다. 이 폼 태그를 사용하기 위해서는 JSP 페이지에 taglib을 추가해줘야한다.

    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

  • form:form 태그 사용

    일단 form 태그를 사용하려면 commandName 속성을 지정해야 하는데 이 이름은 JSP 페이지에서 사용되는 commandName과 일치 해야 하면 commandClass와 같은 타입의 객체여야 한다. commandName에 특정 이름을 부여하지 않으면 기본 값은 command이다. 이 값은 SimpleFormController를 사용할 경우 컨트롤러의 fomBackingObject() 메소드를 통해 JSP 페이지에 넘겨줄 값을 셋팅해 줄 수 있다. 자세한 방법은 본 매뉴얼 >> SimpleFormController 부분을 참고한다. form 태그는 여러가지 폼 입력 태그들을 갖는다. 각 타입에 따라 text타입은 <form:input> 기타 나머지 타입은 <form:password>와 같은 형태로 나타난다. 기타 폼 태그의 자세한 사항은 본 매뉴얼 Tag Library를 참고한다. 이러한 입력 폼 태그는 path라는 속성에 이름을 지정해주는데 이는 반드시 도메인 객체의 필드 이름과 같아야 한다. 또한 form 태그에는 웹단에서 validator에서 발생시킨 에러 메시지를 출력해주는 <form:errors>태그를 갖고 이 태그 역시 path값을 지정해 줘야한다. path값에 "*" 값을 주게 되면 commandClass에 해당되는 모든 attribute에 대한 error 메시지를 출력한다. 다음은 form:errors 태그가 정의되어 있는getUser.jsp 파일의 일부이다.

    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
    <tr>
        <td> Name :</td>
        <td><form:input path="userName" />(required)</td>
        <td><form:errors path="userName" /></td>
    </tr>
    <tr>
        <td>password :</td>
        <td><form:password path="password" />(required, 6자이상입력)</td>
        <td><form:errors path="password" /></td>
    </tr>
    

17.Exception Handling

Spring MVC에서는 선언적인 Exception Handling을 위해서 ExceptionResolver를 제공한다. 이는 Exception의 종류에 따라 Exception 페이지를 지정해 줄 수 있다. 또한 Controller단에서 try~catch문을 이용하여 발생한 exception에 대한 메시지를 입력 뷰에 다시 출력해 줄 수도 있다.

17.1.특정 error 페이지로 이동하여 에러 메시지 출력

Spring MVC에서는 Exception Handling을 위한 HandlerExceptionResolvers를 제공한다.이는 특정 exception이 발생했을 때 특정 페이지로 이동시킬 수 있다. 일단, 사용자 exception을 정의해준다.

public class UserException extends RuntimeException {
    public UserException(){
        super();
    }
    public UserException(String message){
        super(message);
    }
}

이렇게 정의된 사용자 exception을 Controller 단, 또는 Service 단에서 exception을 throw할 수 있다. 다음은 UserException을 throw한 예이다.

//입력된 userName이 "test"가 아닐경우 UserException을 throw해준다.
if(!a.equals("test"))
    throw new UserException(new String(messageSource.getMessage("error.exception.user"
                                , new String[]{}, Locale.getDefault())));

exception을 throw할 때 messageSource를 사용하여 properties파일에 정의된 "error.exception.user"키값에 대한 메시지를 출력한다. exception 발생 후 포워딩 될 페이지 정보를 매핑하기 위해서 다음과 같이 HandlerExceptionResolvers를 정의해 준다.

<bean id="exceptionResolver" 
        class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <prop key="sample.services.UserException">userError</prop>
        </props>
    </property>
    <property name="exceptionAttribute" value="sampleException"/>
    <property name="defaultErrorView" value="error"/>
</bean>

위 같이 정의할 경우 Controller 내에서 sample.services.UserException이 발생할 경우 viewResolver에 의해 userError라는 view를 찾게되고 그 view에 에러 메시지를 출력하게 된다. 발생한 Exception은 "sampleException" 이름으로 userError.jsp 페이지에 전달하도록 설정하였다. 만약 exceptionAttribute 속성을 사용하지 않았을 때의 디폴트 값은 "exception"이다. 마지막으로 설정한 defaultErrorView 속성은 앞에서 매핑한 Exception외의 다른 에러가 발생할 경우 error.jsp에 에러메시지를 출력하도록 설정하였다. 간단한 Expression Language를 이용하여 발생한 에러 메시지를 출력할 수 있다.

<h3>${simpleException.message}</h3>

17.2.에러 페이지에 에러 메시지 출력

UI 계층의 에러 처리 부분에서 위와같은 방법을 사용하게 되면 사용자가 입력했던 값이 모두 사라지게 되는 불편함이 생기게 된다. 이러한 불편을 해소하기 위해서 입력 폼 페이지에서 에러 메시지를 출력하고 사용자가 입력한 값을 유지해 줘야하는데 이는 컨트롤러에서 exception을 직접 처리 해줘야한다. 처리 예는 다음과 같다.

protected ModelAndView onSubmit(HttpServletRequest request,
        HttpServletResponse response, Object command,
        BindException exception) throws Exception {
    ...중략...
    HelloVO vo = (HelloVO) command;
    ...중략...
    try{
        helloworldService.getMessage(vo);
        }catch (UserException e){
            ModelAndView mav = new ModelAndView(getFormView());
            mav.addObject("user",vo);
            mav.addObject("userException",e);
            request.setAttribute("userException",e);
            return mav;
        }
    return new ModelAndView(getSuccessView(),"vo",vo);
}
protected Object formBackingObject(HttpServletRequest request)
    throws Exception {
	request.setAttribute("user",new sample.services.HelloVO());
	return new HelloVO();
}

위의 코드를 보면 getMessage()메소드를 호출할때 UserException을 try~catch로 직접 처리 한 다음 이 exception과 사용자가 입력한 데이터를 ModelAndView를 이용하여 전달하고 있다. 이렇게 전달한 error 메시지는 JSP 파일에서 jstl 태그를 사용하여 출력할 수 있다.

<c:if test="${not empty userException}">
<h3><font color="red">Error : 
<c:out value="${userException.message}"/></font></h3>
</c:if>

17.3.Presentation Layer에서 message key를 이용한 locale 변경

Business Layer에서 BaseException이 발생하였을 때 messageKey를 파라미터로 넘겨주면 Presentation Layer에서 그 messageKey를 받아와 원하는 Locale에 맞게 메시지를 조작할 수 있다.

17.3.1.Business Layer의 BaseException 발생

public class UserService implements ApplicationContextAware{
    
    private static Log logger = LogFactory.getLog(UserService.class);
    private MessageSource messageSource;
    
    public UserVO getUser(UserVO userVO) throws Exception{
        logger.debug("\n=============== UserService is called ===============\n");
        throw new BaseException(messageSource,"error.test.message"
                    , new Object[]{}, "default message");
        //return userVO;
    }
    public void setApplicationContext(ApplicationContext applicaionContext)
            throws BeansException {
       this.messageSource = applicaionContext;
        
    }
}

위에서 "error.test.message"라는 key 값을 파라미터로 넘겨 주었다.

17.3.2.Presentation Layer에서 꺼낸 message key 값에 새로운 Locale로 셋팅

 try {
    // call business service

    userVO = userService.getUser(userVO);
    // setting view name
    ModelAndView mav = new ModelAndView("/jsp/user/getUser.jsp");
    mav.addObject(userVO);
    // return a ModelAndView object.
    return mav;
} catch (BaseException e) {
    //발생한 BaseException에서 getMessageKey() 메소드를 통해 message key를 추출한다. 
    String messageKey = e.getMessageKey();
    System.out.println("\n messageKey ==========" + messageKey
        + "============\n");
    //추출한 messageKey를 가지고 ENGLISH 로케일로 다시 셋팅해 주었다. 
    throw new BaseException(messageSource.getMessage(messageKey,
        new String[] {}, Locale.ENGLISH));
}

18.Spring Integration

Anyframe 은 Spring MVC를 기반으로 구성되어 있으므로 Spring 프레임워크의 다른 모듈과의 연계가 용이하다. 일반 웹 애플리케이션을 개발할 때 Business Layer의 Business Logic을 이용하여 요청을 처리하게 되는데 이 때 Business Layer를 연계하기 위한 방법은 다음과 같다.

18.1.Listener 등록과 Spring 설정 파일 목록 위치 정의

Spring MVC에서는 DispatcherServlet을 사용하여 WebApplicationContext를 로드하게 된다. 이때 Presentation Layer에서 사용할 Business Layer의 서비스 bean들을 ContextLoaderListener 등록 후 contextConfigLocation으로 Spring 설정 파일 위치를 지정해줌으로써 Presentation Layer에서 Business 서비스 bean들을 호출하여 사용할 수 있다. 다음은 설정 예인web.xml 파일의 일부이다.

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>
		classpath:/spring/context-*.xml
	</param-value>
</context-param>
&lt;!--리스너 등록 --&gt;
&lt;listener&gt;
    &lt;listener-class&gt;
        <emphasis role="bold">org.springframework.web.context.ContextLoaderListener</emphasis>
    &lt;/listener-class&gt;
&lt;/listener&gt;

18.2.Dependency Injection을 통한 Business Service 호출

위와 같이 Listener를 등록하고 Spring 설정 파일 위치를 지정해 주었으면 일반 서비스 호출과 같이 Dependency Injection을 사용하여 Business Service를 호출할 수 있다. 먼저 다음user-servlet.xml 파일에서 처럼 해당 controller bean 정의 부분에서 사용할 서비스 dependency를 정의한다.

<bean name="/foundationCategory.do" class="anyframe.example.foundation.sales.web.CategoryController">
	<property name="categoryService" ref="foundationCategoryService"/>
	<property name="methodNameResolver" ref="paramResolver" />
	<property name="success_get" value="/WEB-INF/jsp/foundation/sales/category/viewCategory.jsp" />
</bean>	

dependency를 정의한 후에 컨트롤러 클래스에서 Dependency Injection을 통해 Business Service를 사용할 수 있다. 다음은 Setter Injection을 통해 Business Service를 호출한 UserController.java 파일의 일부이다.

public class CategoryController extends AnyframeFormController {
    private CategoryService categoryService;

    public void setCategoryService(CategoryService categoryService) {
        this.categoryService = categoryService;
    }
    
    /**
     * get a category detail.
     * @param request
     * @param response
     * @return 
     * @throws Exception
     */
    public ModelAndView get(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
 		
       String categoryNo = request.getParameter("categoryNo");

       if (!StringUtils.isBlank(categoryNo)) {
       		Category gettedCategory = categoryService.get(categoryNo);        	
        	request.setAttribute("category", gettedCategory);        	
       }
        
       return new ModelAndView(this.getSuccess_get());
    }  
}
Spring IoC 컨테이너 Dependency Injection에 대한 자세한 사항은 본 매뉴얼 Dependencies를 참고한다.

18.3.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.foundation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-task-2.0.10.jar 파일이 있어야 한다.)

    표 18.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.foundation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

  • 참고자료

19.Annotation based Spring MVC

Spring XML 만을 독립적으로 사용할 경우 때때로 방대하고 복잡한 속성 정의 파일들로 인해 시스템 개발 및 유지보수의 지연을 초래할 가능성이 높아진다. 이러한 문제점을 해결하기 위해 Spring Framework에서는 별도 XML 정의없이도 사용 가능한 annotation 지원에 주력하고 있는 실정이다. SpringMVC기반의 Controller 구현을 위해서 Spring에서 제공하는 annotation의 종류와 그 사용법에 대해서 상세히 살펴보도록 한다. Annotation을 사용하여 SpringMVC기반의 Controller를 작성하면, Controller라는 인터페이스를 상속받거나 그 외 Spring에서 제공하는 Controller 구현체들을 상속받지 않아도 된다. 따라서 Servlet API와는 독립적으로 작성할 수 있다는 장점이 있다. (단, annotation은 JAVA 5 이상에서만 사용가능함에 유의하도록 한다.) 본 문서에서는 annotation에 대한 일반적인 내용보다는, annotation을 사용하여 어떻게 Spring MVC의 각 구성요소들을 구현하는지 알아보도록 한다. Annotation에 대한 보다 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고하기 바란다.

19.1.Configuration

Annotation을 사용하여 SpringMVC 기반의 웹어플리케이션을 구현하기 위해서는 속성 정의 XML에 추가되어야 할 설정들이 있다.

19.1.1.Handler 설정

@RequestMapping annotation를 처리하는 default 클래스는 다음과 같다.

  • DefaultAnnotationHandlerMapping

  • AnnotationMethodHandlerAdapter

앞에서 BeanNameUrlHandlerMapping이나 SimpleUrlHandleMapping에서 처럼, 위의 DefaultAnnotationHandlerMapping을 사용할 경우에도 Interceptor를 정의할 수 있는데, DefaultAnnotationHandlerMapping에 Interceptor를 정의하면 모든 Request URL이 Interceptor 영향을 받게 되는 불편함이 발생한다. 이 때, 특정 URL에만 Interceptor를 정의하고자 하는 경우에 SelectedAnnotationHandlerMapping 을 사용하여 다음과 같이 설정할 수 있다. common-servlet.xml 파일 예이다.

<bean id="annotationHandlerMapping"
    class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="order" value="1" />
    <property name="interceptors" ref="loginInterceptor" />
</bean>

<!-- 특정URL에만 Interceptor를 적용하기 위해 사용
※ 참고 
http://www.scottmurphy.info/spring_framework_annotation_based_controller_interceptors-->
<bean id="selectedAnnotationHandlerMapping"
    class="org.springplugins.web.SelectedAnnotationHandlerMapping">
    <!-- order 값이 작은 것이 우선적으로 적용된다. -->
    <property name="order" value="0" />
    <property name="urls">
        <list>
            <value>/updateCategory.do</value>
        </list>
    </property>
    <property name="interceptors">
        <list>
            <ref bean="authorizationInterceptor" />
        </list>
    </property>
</bean>

19.1.2.Component Scan 설정

@Controller annotation으로 정의된 컨트롤러 클래스를 사용하기 위해서는 <context:component-scan/> 을 속성 정의 XML에 추가해 주어야 한다. <context:component-scan/>에 대한 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고하기 바란다.

19.1.2.1.Using Filters to customize scanning

<context:component-scan/>은 해당 클래스패스 내에 @Component, @Service, @Repository, @Controller annotation 이 적용된 클래스를 모두 찾아서 Spring 컨테이너가 관리하는 컴포넌트로 등록하도록 하는 설정이다. 이와 같은 디폴트 설정으로 stereotype annotation을 Auto Detecting하여 사용 시, 비즈니스 레이어와 프레젠테이션 레이어에 중복으로 <context:component-scan/>을 설정하는 경우 다음과 같은 문제가 발생할 수 있다.

  • Auto Detecting으로 야기되는 문제점

    • Annotation이 적용된 컴포넌트 클래스가 비즈니스 레이어의 Application Context와 프레젠테이션 레이어의 WebApplication Context에 중복하여 등록된다.

    • 비즈니스 레이어의 Application Context와 프레젠테이션 레이어의 WebApplication Context는 Parent-child 관계이며 일반적으로 AOP 설정은 비즈니스 레이어에서 관리한다.

    • 따라서 Proxy 기반의 Spring AOP는 비즈니스 레이어의 Application Context에 등록된 컴포넌트에만 적용된다.

    • WebApplication Context에 등록된 비즈니스 레이어에 해당하는 컴포넌트는 AOP가 적용되지 않는다.

    • 이로 인해 WebApplication Context에서는 AOP가 적용되지 않은 비즈니스 컴포넌트를 먼저 참조하여 Spring AOP가 동작하지 않을 문제점이 발생한다.

    이와 같은 문제를 방지하기 위해서 비즈니스 레이어(Application Context)에서 관리되어야하는 컴포넌트와 프레젠테이션 레이어(Web Application Context)에서 관리되어야하는 컴포넌트를 구분할 필요가 있다.

    다음은 프레젠테이션 레이어에서 @Controller annotation이 적용된 클래스만 WebApplication Context에 등록하는common-servlet.xml 파일의 설정 예이다.

    <!-- use-default-filters="false"로 설정하고 include-filter를 사용했기 때문에 
          WebApplicationContext에는 stereotype @Contoller Bean 만 등록된다. -->
    <context:component-scan base-package="anyframe.sample.springmvc" 
                                                      use-default-filters="false">
        <context:include-filter type="annotation" 
            expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

    위의 예와 같이 <context:component-scan>하위에 <context:include-filter>나 <context:exclude-filter>를 추가하면 컨테이너에 의해 검색될 대상의 범위를 조정할 수 있다. filter에 대한 자세한 내용은 본 매뉴얼 >> Spring >> Annotation 을 참고 바란다.

19.2.Controller

사용자가 Spring MVC의 컨트롤러를 작성하려면 AbstractController나 SimpleFormController 등 Spring에서 제공하는 컨트롤러 클래스를 상속받아야만 했다. Spring 2.5 이상에서는 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 annotation을 사용하여 컨트롤러를 구현할 수 있다. 본 문서에서는 annotation을 사용하여 Spring MVC 컨트롤러를 작성하는 방법에 대해서 알아본다.

  • @Controller : 컨트롤러 클래스 정의

  • @RequestMapping : HTTP Request URL을 처리할 컨트롤러 클래스 또는 메소드 정의

  • @RequestParam : HTTP Request에 포함된 파라미터 참조 시 사용

  • @ModelAttribute : HTTP Request에 포함된 파라미터를 Model 객체로 바인딩함, @ModelAttribute의 'name'으로 정의한 Model객체를 다음 View에서 사용 가능

  • @SessionAttributes : Session에 저장할 Model attribute를 정의

19.2.1.@Controller

특정 클래스에 @Controller annotation을 적용하면 다른 클래스를 상속받거나 Servlet API를 사용하지 않아도 해당 클래스가 컨트롤러 역할을 수행하도록 해준다.

다음은 @Controller를 사용하여 작성한 ProductController 클래스 파일의 일부이다.

@Controller
public class ProductController {
    // 중략
}

19.2.2.@RequestMapping

@RequestMapping annotation은 컨트롤러 클래스나 메소드가 특정 HTTP Request URL을 처리하도록 매핑하기 위해서 사용한다. 그래서 클래스 선언부에 @RequestMapping을 적용할 수도 있고(이하 Type-Level), 클래스의 메소드에 @RequestMapping을 적용할 수도 있다(이하 Method-Level). Type-Level의 @RequestMappign에 URL path를 정의한 경우, Method-Level의 @RequestMapping에서는 Type-Level의 URL path를 상속받는다.

@Controller
@RequestMapping("/listProduct.do")
public class ProductController {
    // 중략    
    @RequestMapping
    public ModelAndView list(HttpServletRequest request, ProductSearchVO searchVO) 
                                                                    throws Exception {
        // 중략
        Page resultPage = productService.getPagingList(searchVO);
        
        mnv.addObject("productList", resultPage.getList());
        // 중략

        return mnv;
    }
}

@RequestMapping은 구현하는 컨트롤러 종류에 따라 아래와 같은 방식으로 사용할 수 있다.

  • Form Controller 구현

  • Multi-action Controller 구현

기존에 SimpleFormController와 같은 Controller 클래스를 상속받아서 컨트롤러를 작성할 때는, 상위클래스에 정의된 메소드를 override하여 구현하기 때문에 입력 argument 타입과 return 타입이 이미 정해져있다. 이에 반해 @RequestMapping을 적용하여 작성하는 핸들러 메소드는 다양한 argument 타입과 return 타입을 사용할 수 있다.

19.2.2.1.Form Controller 구현

  • 클래스 선언부에 @RequestMapping을 사용하여 처리할 Request URL Mapping

  • 메소드에는 @RequestMapping의 'method', 'params'와 같은 상세 속성 정보를 정의하여 Request URL의 Mapping을 세분화

위와 같이 작성하면 기존에 SimpleFormController를 상속받아 작성하였던 폼을 처리하는 컨트롤러를 구현할 수 있다. 다음은 폼 처리 컨트롤러를 작성한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {

    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView addProductView() {
        // 중략
        return mnv;
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addProduct(HttpServletRequest request, @ModelAttribute("product")
        Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        return "/listProduct.do";
    }
}

19.2.2.2.Multi-action Controller 구현

@RequestMapping annotation을 사용하여 여러 HTTP Request를 처리할 수 있는 Multi-action 컨트롤러를 구현할 수 있다.

  • 메소드에 Request URL을 Mapping한 @RequestMapping을 정의

다음은 Multi-action 컨트롤러를 구현한ProductController 의 예이다.

@Controller
public class ProductController {

    @RequestMapping("/listProduct.do")
    public ModelAndView getProductList() {
        // 중략
        return mnv;
    }

    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo")
    String productNo, ModelMap model) {
        // 중략
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }    
}
        

@RequestMapping annotation에는 다음과 같은 상세 속성 정보를 부여할 수 있다.

nameDescription
value "value='/getProduct.do'"와 같은 형식의 매핑 URI이다. 디폴트 속성이기 때문에 value만 정의하는 경우에는 'value='은 생략할 수 있다. 예 : @RequestMapping(value = {"/addProduct.do", "/updateProduct.do" }) 위의 경우 "/addProduct.do", "/updateProduct.do" 두 URL 모두 처리한다.
method GET, POST, HEAD 등으로 표현되는 HTTP Request method에 따라 requestMapping을 할 수 있다. 'method=RequestMethod.GET' 형식으로 사용한다. method 값을 정의하지 않는 경우 모든 HTTP Request method에 대해서 처리한다. 예 : @RequestMapping(method = RequestMethod.POST). 이 경우 value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.
params HTTP Request로 들어오는 파라미터 표현이다.'params={"param1=a", "param2", "!myParam"}' 로 다양하게 표현 가능하다. 예 : @RequestMapping(params = {"param1=a", "param2", "!myParam"}) 위의 경우 HTTP Request에 param1과 param2 파라미터가 존재해야하고 param1의 값은 'a'이어야 하며, myParam이라는 파라미터는 존재하지 않아야한다. 또한, value 값은 클래스 선언에 정의한 @RequestMapping의 value 값을 상속받는다.

19.2.2.3.Supported argument types

@RequestMapping을 사용하여 작성하는 핸들러 메소드는 다음과 같은 타입의 입력 argument를 순서에 관계없이 정의할 수 있다. 단, validation results를 입력 argument로 받을 경우에는 해당 command 객체 바로 다음에 위치해야한다.

  • Servlet API의 Request와 Response 객체

    ServletRequest 또는 HttpServletRequest 등을 메소드 내부에서 직접 사용해야 하는 경우

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product
                                             , BindingResult result, SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
           "product.error.exist", new String[] {product.getProductNo() },
            localeResolver.resolveLocale(request));
       }

  • Servlet API의 Session

    HttpSession 객체를 메소드 내부에서 사용하는 경우 예 : user 정보와 같은 global session attribute를 사용할 때

    @RequestMapping("/login.do")
    protected ModelAndView handleRequestInternal(HttpSession session, 
    @RequestParam("userId") String userId) throws Exception {
    	session.setAttribute("userId", userId);
    	return new ModelAndView("/index.jsp");
    }

  • java.util.Locale

    현재 request의 locale을 사용할 경우

    @RequestMapping(params = "param=add")
    public String addProduct(Locale locale, Product product, BindingResult result
                                                                  , SessionStatus status) throws Exception {
        // 중략
        String message = messageSource.getMessage(
        			"product.error.exist", new String[] {product.getProductNo()}, locale);
       }

  • java.io.InputStream 또는 java.io.Reader

    Request의 content를 직접 처리할 경우 (Servlet API가 제공하는 raw InputStream/Reader)

    @RequestMapping(params = "param=add")
    public String addProduct(InputStream is, Product product, BindingResult result
                                                                , SessionStatus status) throws Exception {
            // 중략
            for(int totalRead = 0; totalRead < totalBytes; totalRead += readBytes) {
                readBytes = is.read(binArray, totalRead, totalBytes - totalRead);
                // 중략
            }
        
        // 중략
    }

  • java.io.OutputStream 또는 java.io.Writer

    Response의 content를 직접 처리할 경우(Servlet API가 제공하는 raw OutputStream/Writer)

    @RequestMapping(params = "param=add")
    public String addProduct(OutputStream os, Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();    
        byte[] content = outStream.toByteArray();
        os.write(content);
        os.flush();
        // 중략
    }

  • @RequestParam annotation이 적용된 argument

    ServletRequest.getParameter(java.lang.String name)와 같은 역할 수행

    @RequestMapping("/deleteProduct.do")
    public String deleteProduct(@RequestParam("productNo") String productNo) {
        productService.deleteProduct(productNo);
        return "/listProduct.do";
    }

  • java.util.Map 또는 org.springframework.ui.Model 또는 org.springframework.ui.ModelMap

    Web View로 데이터를 전달해야 하는 경우 위 타입의 argument를 정의하고, 메소드 내부에서 View로 전달할 데이터를 추가함

    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, Map map) {
        Product product = productService.getProduct(productNo);    
        
        map.put("product", product);
        
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, Model model) {
        Product product = productService.getProduct(productNo);
        
       model.addAttribute("product", product);
        
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
    @RequestMapping("/getProduct.do")
    public String getProduct(@RequestParam("productNo") String productNo, ModelMap modelMap) {
        Product product = productService.getProduct(productNo);
        
       modelMap.addAttribute("product", product);
        return "/WEB-INF/jsp/annotation/sales/product/viewProduct.jsp";
    }
  • Command 또는 form 객체

    HTTP Request로 전달된 parameter를 binding한 객체로 다음 View에서 사용 가능하고 @SessionAttributes를 통해 session에 저장되어 관리될 수 있다. @ModelAttribute annotation을 이용하여 사용자 임의로 이름을 부여할 수 있다.

    @RequestMapping("/addProduct.do")
    public String updateProduct(Product product, SessionStatus status) throws Exception {
        // 여기서 'product'가 Command(/form) 객체이다.
        return "/listProduct.do";
    }

    @RequestMapping("/addProduct.do")
    public String updateProduct(@ModelAttribute("updatedProduct") Product product, 
        SessionStatus status) throws Exception {
        // 여기서 'updatedProduct'라는 이름의 'product'객체가 Command(/form) 객체이다.
        return "/listProduct.do";
    }
  • org.springframework.validation.Errors 또는 org.springframework.validation.BindingResult

    바로 이전의 입력파라미터인 Command 또는 form 객체의 validation 결과 값을 저장하는 객체로 해당 command 또는 form 객체 바로 다음에 위치해야 함에 유의하도록 한다.

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                                                                 , SessionStatus status) throws Exception {
            
        new ProductValidator().validate(product, result);
        if (result.hasErrors()) {
            return "/WEB-INF/jsp/annotation/sales/product/productForm.jsp";
        } else {
            // 중략
            return "/listProduct.do";
        }
    }

  • org.springframework.web.bind.support.SessionStatus

    폼 처리가 완료되었을 때 status를 처리하기 위해서 argument로 설정. SessionStatus.setComplete()를 호출하면 컨트롤러 클래스에 @SessionAttributes로 정의된 Model객체를 session에서 지우도록 이벤트를 발생시킨다.

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                                                , SessionStatus status) {
        // 중략
        productService.addProduct(product);
        status.setComplete();
        return "/listProduct.do";
    }

19.2.2.4.Supported return types

@RequestMapping을 이용한 핸들러 메소드는 다음과 같은 리턴타입을 가질 수 있다.

  • ModelAndView 객체

    View와 Model 정보를 모두 포함한 객체를 리턴하는 경우.

    @RequestMapping(params = "param=addView")
    public ModelAndView addProductView() {
        ModelAndView mnv = 
                 new ModelAndView("/WEB-INF/jsp/annotation/sales/product/productForm.jsp");
        mnv.addObject("product", new Product());
        return mnv;
    }

  • Map

    Web View로 전달할 데이터만 리턴하는 경우.

    @RequestMapping("/productList.do")
    public Map getProductList() {
        List productList = productService.getProductList();
        ModelMap map = new ModelMap(productList);//productList가 "productList"라는 이름으로 저장됨.
        return map;
    }

    여기서 View에 대한 정보를 명시적으로 리턴하지는 않았지만, 내부적으로 View name은 RequestToViewNameTranslator에 의해서 입력된 HTTP Request를 이용하여 생성된다. 예를 들어DefaultRequestToViewNameTranslator 는 입력된 HTTP Request URI를 변환하여 View name을 다음과 같이 생성한다.

    http://localhost:8080/anyframe-sample/display.do     -> 생성된 View name : 'display'
    http://localhost:8080/anyframe-sample/admin/index.do -> 생성된 View name : 'admin/index'

    위와 같이 자동으로 생성되는 View name에 'jsp/'와 같이 prefix를 붙이거나 '.jsp' 같은 확장자를 덧붙이고자 할 때는 아래와 같이 속정 정의 XML(xxx-servlet.xml)에 추가하면 된다.

    <bean id="viewNameTranslator" 
      class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator">
          <property name="prefix" value="jsp/"/>
          <property name="suffix" value=".jsp"/>
    </bean>

  • Model

    Web View로 전달할 데이터만 리턴하는 경우Model 은 Java-5 부터 추가된 인터페이스이다. 기본적으로 ModelMap과 같은 기능을 제공한다. Model 인터페이스의 구현클래스에는 BindingAwareModelMapExtendedModelMap 이 있다. View name은 위에서 설명한 바와 같이 RequestToViewNameTranslator에 의해 내부적으로 생성된다.

    @RequestMapping("/productList.do")
    public Model getProductList() {
        List productList = productService.getProductList();
        ExtendedModelMap map = new ExtendedModelMap();
        map.addAttribute("productList", productList);
        
        return map;
    }

  • String

    View name만 리턴하는 경우.

    @RequestMapping(value = {"/addProduct.do", "/updateProduct.do" })
    public String updateProduct(Product product, SessionStatus status) throws Exception {
    
        // 중략
        
        return "/listProduct.do";
    }

  • void

    메소드 내부에서 직접 HTTP Response를 직접 처리하는 경우. 또는 View name이 RequestToViewNameTranslator에 의해 내부적으로 생성되는 경우

    @RequestMapping("/addView.do")
    public void addView(HttpServletResponse response) {
        // 중략
        // response 직접 처리
    }

    @RequestMapping("/addView.do")
    public void addView() {
        // 중략
        // View name이 DefaultRequestToViewNameTranslator에 의해서 내부적으로 'addView'로 결정됨.
    }

19.2.3.@RequestParam

@RequestParam annotation은 HTTP Request parameter를 컨트롤러 메소드의 argument로 바인딩하는데 사용되며ServletRequest.getParameter(java.lang.String name) 와 같은 역할을 한다. 다음은 @RequestParam annotation의 사용 예이다.

@RequestMapping("/updateProduct.do")
public String updateProduct(@RequestParam("productNo") String productNo,
    @RequestParam("sellAmount") int sellAmount, 
    @RequestParam("realImageFile") MultipartFile picturefile) {
    // 중략
    return "/listProduct.do";
}

@RequestParam을 적용한 파라미터는 반드시 HTTP Request에 존재해야 한다. 그렇지 않은 경우 다음과 같이 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

org.springframework.web.bind.MissingServletRequestParameterException:
                         Required java.lang.String parameter 'productNo' is not present

그러나 아래와 같이 @RequestParam의 required 속성을 false로 설정할 경우 HTTP Request에 파라미터가 존재하지 않아도 Exception이 발생하지 않는다.

@RequestMapping("/deleteProduct.do")
public String deleteProduct(@RequestParam(value="productNo", required="false")String productNo){
    // 중략
}

19.2.4.@ModelAttribute

@ModelAttribute는 컨트롤러에서 다음과 같이 두 가지 방법으로 사용할 수 있다.

  • 메소드 자체에 정의

    입력 폼 페이지에서 출력해 줄 reference data를 전달하고자 할 때. SimpleFormController의 referenceData() 메소드와 같은 역할

  • 메소드의 입력 argument에 정의

    메소드의 argument로 입력된 Command 객체에 이름을 부여하고자 할 때.

다음은 위에서 설명한 두가지 방법으로 @ModelAttribute를 사용한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {
    // 메소드 자체에 정의
    @ModelAttribute("categoryList")
    public List populateCategoryList() throws Exception {
        return categoryService.getCategoryList();
    }
    
    // 메소드의 입력 argument에 정의
    @RequestMapping(params = "param=add")
    public String addProduct(@ModelAttribute("updatedProduct")
        Product product, BindingResult result, SessionStatus status) throws Exception {
        // 중략
    }
}

19.2.5.@SessionAttributes

@SessionAttributes는 session에 저장하여 관리할 model attribute를 정의할 때 사용한다. @SessionAttributes에 정의하는 attribute의 이름은 해당 컨트롤러 클래스안에서 사용되는 model attribute의 이름과 같아야 한다.

다음은 @SessionAttributes를 사용하여 session에 저장하여 관리할 model을 정의한 예이다.

@Controller
@RequestMapping("/product.do")
@SessionAttributes(value = {"product", "category"})
public class EditProductController {
    // 중략
}

19.3.Dependency Injection

컨트롤러 클래스에서 기능 수행을 위해 다른 Bean을 참조해야 하는 경우 @Autowired 또는 @Resource annotation을 사용한다. @Resource와 @Autowired annotation에 대한 자세한 설명은 본 매뉴얼 >> Spring >> Annotation 부분을 참고하기 바란다.

다음은 컨트롤러 클래스에서 @Resource annotation을 사용한 EditProductController 의 예이다.

@Controller
@RequestMapping("/product.do")
public class EditProductController {
    @Resource(name = "productService")
    ProductService productService;

    @Resource
    CategoryService categoryService;

    @Resource
    MessageSource messageSource;

    @Resource
    LocaleResolver localeResolver;
    // 중략
}

19.4.Double Submit Prevention

Spring MVC에서는 double submit을 방지하기 위해 AbstractFormController를 제공하고있고, 폼 컨트롤러 구현 시에 사용하는 SimpleFormController 또한 AbstractFormController를 상속받았기 때문에 double submit 방지가 가능하다. XML 기반의 double submit 방지 기능 적용 방법은 본 매뉴얼 >> Spring MVC >> Extension >>Double Submit 부분을 참고한다. 본 문서에서는 Annotation을 사용하여 다른 클래스를 상속받지 않고도 double submit 방지 기능을 구현하는 방법에 대해서 자세히 알아본다.

19.4.1.Annotation을 이용한 Double Submit 방지

annotation을 이용한 Double Submit 방지는 다음과 같은 원리로 구현된다.

  • Double submission을 방지하고자 하는 form 객체를 model로 저장

    다음 예제와 같이 ModelAndView, ModelMap 등을 이용하여 저장한다.

    @RequestMapping(params = "param=addView")
    public ModelAndView addProductView() {
        ModelAndView mnv = 
                  new ModelAndView("/WEB-INF/jsp/annotation/sales/product/productForm.jsp");
        mnv.addObject("product", new Product());
        return mnv;
    }

  • 저장한 model을 @SessionAttributes로 정의

    다음 예제와 같이 컨트롤러 클래스 선언부에 @SessionAttributes("product")로 정의한다.

    @Controller
    @RequestMapping("/product.do")
    @SessionAttributes("product")
    public class EditProductController {
            // 중략
    }

  • 컨트롤러 메소드에서 폼 처리 완료 후 Session status 변경

    @RequestMapping(params = "param=add")
    public String addProduct(HttpServletRequest request, Product product, BindingResult result
                                   , SessionStatus status) throws Exception {
        productService.addProduct(product);
        status.setComplete();
        return "/listProduct.do";    
    }
  • status.setComplete()는 session에서 저장된 model을 삭제하는 이벤트 발생

  • 사용자가 같은 버튼을 여러번 클릭하는 경우와 같이, 여러 thread가 동시에 Session에 접근할 수도 있기 때문에 반드시 AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정

    <bean id="annotationHandlerAdaptor"
        class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="synchronizeOnSession" value="true" />
    </bean>

  • 따라서, 이후에 다시 submit 요청이 온 경우 session에 저장된 model이 삭제되었기 때문에 아래와 같이 org.springframework.web.HttpSessionRequiredException발생

    org.springframework.web.HttpSessionRequiredException: 
          Session attribute 'dept' required - not found in session

19.5.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.annotation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.annotation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 19.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.annotation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

V.Spring MVC Extensions

이 장에서는 Spring MVC를 확장한 Anyframe 의 주요 기능 및 사용법을 포함한다. 이는 개발자가 xml 설정 기반의 웹 애플리케이션을 개발할 때 보다 쉽게 개발할 수 있도록 하기 위함이며 Annotation을 활용한 Spring MVC의 기본 기능 사용 방법은 본 매뉴얼 Spring MVC >> Annotation 부분을 참고한다. 또한, 이러한 Anyframe 에서의 Spring MVC 확장 포인트와 함께 JasperReports와의 연계 방안을 제시한다. 목록은 아래와 같다.

20.Controller

Anyframe 에서는 정적인 페이지 출력을 위한 ForwardController와 다중 메소드 정의, double submit 방지 기능을 제공하는 AnyframeFormController, MiPlatform과의 연계를 위한 AnyframeMiPController 세 개의 확장 컨트롤러를 제공한다. 각각에 대한 내용은 아래와 같다.

20.1.ForwardController

웹 애플리케이션 구성 시 단순히 정적인 페이지 출력만을 위한 요청시에 별도의 컨트롤러 클래스 개발 없이 Anyframe 에서 제공하는 ForwardController를 사용하여 view 이름 설정만으로 원하는 view를 출력해 줄 수 있다.

<bean id="addUserViewController"
    class="anyframe.web.springmvc.controller.ForwardController">
    <property name="viewName" value="/jsp/adduser.jsp"/>
</bean>

※ Anyframe 3.2.0 이후 버전 부터 해당 클래스는 삭제될 예정이다. 같은 기능의 구현을 위해서는 org.springframework.web.servlet.mvc.ParameterizableViewController로 class를 지정하여 사용한다. ParameterizableViewController의 자세한 사항은 본 매뉴얼 >> Spring MVC >> Controller >> ParameterizableViewController 를 참조한다.

20.2.AnyframeFormController

Anyframe 에서는 Spring MVC의 double submit 방지 기능을 제공하는 SimpleFormController의 기능과 다중 메소드 정의가 가능한 MultiActionController의 기능을 합한 AnyframeFormController를 제공한다. 이 페이지에서는 AnyframeFormController를 사용하는 기본적인 방법을 설명하고 double submit 방지 기능에 대해서는 본 매뉴얼 Spring MVC Extensions >> Double Submit 부분을 참고한다. AnyframeFormController를 상속받아 컨트롤러 클래스를 작성할 때의 사용법은 MultiActionController를 사용하여 구현할 때와 크게 다르지 않다. 하지만 double submit 방지 기능 구현을 위한 showNewForm 속성이 추가되었으며 formBackingObject 메소드를 구현해야한다. 다음은 AnyframeFormController의 구현 예인 UserController.java 파일의 일부이다.

public class UserController extends AnyframeFormController {

    UserService userService = null;

    // setter injection of userSerivce
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public UserController() {
        //commandClass, commandName setting
        setCommandClass(UserVO.class);
        setCommandName("users"); 
    }

	public ModelAndView getUser(HttpServletRequest request, HttpServletResponse response) 
            throws Exception {

        UserVO userVO = new UserVO();
        // data binding using command object
        bind(request,userVO); 

        // call business service
        userVO = userService.getUser(userVO);
        // setting view name
        ModelAndView mav = new ModelAndView(this.getSuccess_get());
        mav.addObject(userVO);
        
        // return a ModelAndView object.
        return mav;
    }

    protected Object formBackingObject(HttpServletRequest request)
            throws Exception {
        Map address = new HashMap();
        address.put("seoul", "서울");
        address.put("daegu", "대구");
        address.put("busan", "부산");

        Map hobby = new HashMap();
        hobby.put("reading", "독서");
        hobby.put("listeningMusic", "음악감상");
        hobby.put("study", "공부");
        
        request.setAttribute("address", address);
        request.setAttribute("hobby", hobby);
        return new UserVO();
    }
}

위의 코드에서 볼 수있듯이 bind()메소드를 이용한 data binding, methodNameResolver를 이용한 핸들링 메소드 정의 부분은 MultiActionController를 상속 받아 구현하였을 때와 같다. 그러므로 Controller 빈을 정의할 때 methodNameResolver 또한 정의해 줘야한다.

bean name="/getUser.do"
    class="anyframe.sample.springmvc.web.controller.extensions.UserController">
    <property name="userService" ref="userService" />
    <property name="methodNameResolver" ref="paramResolver"/>
</bean>

또한, 리턴되는 view 이름을 자바 클래스에 넣지 않고 xml 파일에 빼내어 작성하고 있는데 매번 setter injection을 통해 값을 추출해 내야하는 번거로움을 없애기 위해 AnyframeFormController는 success_addView, success_add, success_get, success_update, success_list, success_delete의 attribute에 대해 setter, getter가 정의되어 있다. 이에 사용자는 알맞은 attribute를 사용하여 view 이름을 정의할 수 있으며 다른 이름으로 정의할 시에 setter injection을 통해 추출할 수 있다. 위와 같이 AnyframeFormController는 MultiActionController의 기능을 사용할 수 있으며 SimpleFormController의 double submit 방지 기능도 사용할 수 있다. 새로운 폼을 출력할 때 formBackingObject 메소드를 오버라이드 해서 폼에 필요한 데이터를 넘겨줄 수 있다. 해당 요청에 대해서는 showNewForm 값을 true로 설정해 준다.

bean name="/userForm.do"
    class="anyframe.sample.springmvc.web.controller.extensions.UserController"&gt;
    &lt;property name="userService" ref="userService" /&gt;
    &lt;property name="formView" value="/jsp/user/userForm.jsp"/&gt;
    &lt;<emphasis role="bold">property name</emphasis>="<emphasis role="bold">showNewForm</emphasis>" <emphasis
          role="bold">value</emphasis>="<emphasis role="bold">true</emphasis>"/&gt;
    &lt;property name="sessionForm" value="true"/&gt;     
&lt;/bean&gt;

double submit 방지 기능의 자세한 설명은 여기 를 참고한다.

핸들러 메소드가 하나만 필요한 경우에는?

AnyframeFormController를 사용하여 methodNameResolver 사용 없이 하나의 메소드만 사용할 때는 process() 메소드를 오버라이드 하여 구현한다.

20.3.AnyframeMiPController

MiPlatform을 사용하여 개발 시, Client UI Component에서 조회/저장 이벤트가 발생하면 호출하는 Controller 클래스는 Business Service를 실행하여 결과값을 XML로 변환하여 전송한다. Anyframe은 개발자 편의를 위하여 AnyframeMiPController를 제공하며 개발자는 복잡한 변환로직을 신경쓰지 않고 개발이 가능하다.

public abstract class AnyframeMiPController extends AnyframeFormController {
<!-- 중략 -->
  public ModelAndView process(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        VariableList inVl = null;
        DatasetList inDl = null;
        VariableList outVl = null;
        DatasetList outDl = null;

        PlatformRequest platformRequest =
            new PlatformRequest(request, defaultCharset);
        PlatformResponse platformResponse =
            new PlatformResponse(response, defaultEncodeMethod, defaultCharset);

        try {

            platformRequest.receiveData();

            inVl = platformRequest.getVariableList();
            inDl = platformRequest.getDatasetList();
            outVl = new VariableList();
            outDl = new DatasetList();
            
            getLogger().debug(this.getClass().getName() + "." + "operate()" + " started");            
            operate(request, inVl, inDl, outVl, outDl);            
            getLogger().debug(this.getClass().getName() + "." + "operate()" + " ended");            
            setResultMessage(outVl, 0, "save successed");

        } catch (Exception e) {
            setResultMessage(outVl, -1, e.getMessage());
        } finally {
            platformResponse.sendData(outVl, outDl);
        }
        return null;
    }
    
    public abstract void operate(HttpServletRequest request, VariableList inVl,
            DatasetList inDl, VariableList outVl, DatasetList outDl)
            throws Exception;     
<!-- 중략 --> 
}

AnyframeMiPController는 AnyframeFormController의 process 메소드를 오버라이드 하고 있고 operate 메소드를 호출한다. 개발자가 AnyframeMiPController를 상속하여 User Defined Controller를 개발 할 경우 실제 코딩은 operate 메소드 내부에 구현하면 된다.

  • operate 메소드 내 이용변수 설명

    변수 타입변수명설명
    VariableListinVlClient에서 GET방식으로 전송한 parameter를 포함
    VariableListoutVlClient로 전송하는 VariableList
    DatasetListinDlClient에서 POST방식으로 전송한 Dataset XML를 포함
    DatasetListoutDlClient로 전송하는 DatasetList
  • Page navigation

    MiPlatform이용 시 화면이동은 발생하지 않으며 조회/저장 이벤트에 해당하는 결과인 Dataset XML만 전송한다. 개발자가 User Defined Controller에서 operate 메소드 구현 시 화면 이동을 위한 View Name값은 null로 설정한다. 그리고 개발자가 AnyframeMiPController를 상속하여 Controller를 개발할 때는 operate 메소드 내부에 Business Service를 실행하여 결과값을 반환하도록 구현한다.

21.View

Anyframe 에서는 개발자들이 보다 view 개발을 쉽게 할 수 있도록 Custom tag library를 제공한다. 이런 Custom tag library에는 Spring의 message 태그를 utf-8/euc-kr의 인코딩된 한글 메시지를 위해 확장한 Anyframe message 태그와 페이지 네비게이션을 JSP단의 java 코드 없이 태그로 개발할 수 있는 Page Navigator 태그가 있다.

21.1.Tag library

Anyframe 에서는 개발자들이 자바 코드를 사용하지 않고 보다 쉽게 JSP 페이지를 구현할 수 있도록 다음과 같은 Anyframe Tag Library를 제공한다.

21.1.1.Page Navigator Tag

Anyframe 에서는 Page 처리에 대한 구현이 편리하도록 page 관련 Tag Library인 Page Navigator Tag를 제공한다. 이 태그를 사용하면 리스트 화면을 출력할 때 Tag Library를 사용하여 간단히 Page Navigator를 출력해줄 수 있다. 이 태그를 사용하기 위해 JSP의 상단에 다음과 같이 anyframe-pagenavigator.tld 파일을 taglib으로 지정해 준다.

<%@ taglib uri='/WEB-INF/anyframe-page.tld' prefix='anyframe' %>

prefix를 'anyframe'으로 정의할 경우 아래와 같이 태그를 사용할 수 있다.

<anyframe:pagenavigator linkUrl="javascript:fncGetUserList(2);" 
       pages="<%=resultPage%>" formName="listForm"
    firstImg="sample/images/ct_btn_pre.jpg" 
    prevImg="sample/images/ct_btn_pre01.jpg" 
    lastImg="sample/images/ct_btn_next.jpg" 
    nextImg="sample/images/ct_btn_next01.jpg" />

anyframe을 prefix로 하는 태그로 tag name은 pagenavigator이다 . 이 때 pages라는 attribute는 반드시 anyframe.common.Page 타입의 객체를 설정해줘야 함에 유의하도록 한다.

21.1.2.Message Tag

Spring Tag Library에서 언급했듯이 다국어 지원에 따른 메시지를 출력해주기 위해 Spring MVC에서 기본적인 <message> 태그를 제공한다. 하지만 이는 한국어 출력에 대해서 유니코드로 인코딩된 한국어 메시지 파일만을 출력하도록 지원하고 있다. 이에 Anyframe 에서는 유니코드가 아닌 UTF-8이나 EUC-KR로 인코딩된 한글 메시지를 출력해주기 위해 Spring MVC의 <message> 태그를 확장한 Anyframe Message Tag를 제공한다. 이러한 Anyframe Message Tag를 사용하기 위해서는 taglib으로 anyframe-message.tld 파일을 다음과 같이 등록해 준다.

<%@ taglib uri='/WEB-INF/anyframe-message.tld' prefix='anyframe' %>

prefix를 'anyframe'으로 정의할 경우 아래와 같이 태그를 사용할 수 있다.

<anyframe:message code="error.get.userList"/>

위와 같이 tag name은 message로 정의해야 하며 <message> tag가 같는 attribute는 Spring MVC의 <message> tag와 같으며 사용 방법도 동일하다. 이 때 "code" attribute로 정의한 "error.get.userList"를 Key 값으로, messageSource에 등록된 .propertis 파일에 작성된 해당 키 값의 문자열을 읽어오게 될 것이다.

22.Double Submit Prevention

Spring MVC에서는 double submit을 방지하기 위해 AbstractFormController를 제공하고, 폼 기능 구현할때 사용하는 SimpleFormController 또한 AbstractFormController를 상속받아 위와같은 처리가 가능하다. Anyframe에서는 이 같은 double submit 방지 기능 시 messageSource에서 추출한 메시지를 지정해주기 위해서 SimpleFormController를 확장한 SessionFormController를 제공한다. 하지만 AbstractFormController를 상속받는 모든 컨트롤러에서는 메소드 이름을 통한 다중 메소드 정의를 사용할 수 없으며 사용 방법 또한 복잡하다. 이러한 SimpleFormController의 제약에 따라 Anyframe 에서는 SimpleFormController와 MultiActionController의 기능을 합한 AnyframeFormController 를 확장하여 제공한다. AnyframeFormController를 사용하여 xml 설정을 통해 double submit 방지 기능을 구현할 수 있다. Annotation을 이용한double submit 방지 기능 적용 방법은 본 매뉴얼 >> Spring MVC >> Annotation >> Double Submit 부분을 참고한다. AnyframeFormController를 이용한 double submit 방지 기능의 적용은 다음과 같은 절차를 따른다.

22.1.property 정의하기

double submit 방지 기능의 원리는 기능을 적용할 페이지로 가는 요청을 수행할 때 폼 정보를 세션에 저장하고 페이지 출력 후 submit이 발생하면 session에 폼 정보가 있는지를 확인한 후 없으면 에러메시지를 출력하고 있으면 세션의 form 정보를 없애고 정상 수행을 한다. 이러한 적용을 위해 폼 중복 서브밋을 적용할 페이지의 전 후 요청에 sessionForm 속성을 true로 지정하며, 페이지 출력을 위한 요청에는 폼 정보를 세션에 저장하기 위해 showNewForm 속성을 true로 지정해줘야 한다.

다음은 showNewForm 및 sessionForm 속성 정의가 되어있는 user-servlet.xml 파일의 일부이다.

<!-- 폼 submit 요청 -->
<bean name="/getUser.do"
    class="anyframe.sample.springmvc.web.controller.extensions.UserController">
    <property name="userService" ref="userService" />
    <property name="formView" value="/jsp/user/userForm.jsp"/>
    <!-- 다중 메소드 정의를 위한 methodNameResolver-->
    <property name="methodNameResolver" ref="paramResolver"/>
    <property name="success_get" value="/jsp/user/getUser.jsp"/>
    <property name="sessionForm" value="true"/>
</bean>
<!-- 페이지 출력 요청-->
<bean name="/userForm.do"
    class="anyframe.sample.springmvc.web.controller.extensions.UserController">
    <property name="userService" ref="userService" />
    <property name="formView" value="/jsp/user/userForm.jsp"/>
    <property name="showNewForm" value="true"/>
    <property name="sessionForm" value="true"/>
</bean>

22.2.AnyframeFormController를 상속받아 컨트롤러 클래스 구현하기

AnyframeFormController의 사용 방법은 double submit 방지 기능을 적용할 페이지 전, 후의 요청에 따라 구현해야 하는 메소드가 달라진다. 메소드 구현 방법은 다음과 같다.

22.2.1.페이지 출력 요청 (Ex. /userForm.do)

double submit 방지 기능을 구현할 페이지로 이동하기 위해서는 위에서 언급한 바와 같이 sessionForm과 showNewForm 속성을 true로 설정해야한다. 여기서 showNewForm을 true로 설정하게 되면 별도의 method 정의와 상관없이 formBackingObject 메소드를 호출하게 되고 이는 반드시 구현해야한다. 이 메소드 안에는 페이지 출력시 필요한 데이터를 넘겨주며 매핑될 도메인 객체를 생성하여 리턴해 준다. 이 메소드를 정의함에 따라 비로소 세션에 폼 정보를 저장할 수 있고, 리턴된 도메인 객체 값도 넘겨줄 수 있다. 다음은 formBackingObject 메소드가 정의된 UserController.java 파일의 일부이다.

  protected Object formBackingObject(HttpServletRequest request)
            throws Exception {
    //폼에서 필요한 데이터 셋팅
    Map address = new HashMap();
    address.put("seoul", "서울");
    address.put("daegu", "대구");
    address.put("busan", "부산");

    Map hobby = new HashMap();
    hobby.put("reading", "독서");
    hobby.put("listeningMusic", "음악감상");
    hobby.put("study", "공부");
    
    request.setAttribute("address", address);
    request.setAttribute("hobby", hobby);
    
    //commandClass 생성
    return new UserVO();
}

22.2.2.폼 submit 요청(Ex. /getUser.do)

폼 submit 후 수행될 요청은 위에서 본바와 같이 sessionForm 속성을 true로 정의해 주면 된다. 기타 구현방법은 MultiActionController 와 같다.

public ModelAndView getUser(HttpServletRequest request, HttpServletResponse response) throws Exception {

    UserVO userVO = new UserVO();
    // data binding using command object
    bind(request,userVO);

    // call business service
    userVO = userService.getUser(userVO);
    // setting view name
    ModelAndView mav = new ModelAndView(this.getSuccess_get());
    mav.addObject(userVO);
    
    // return a ModelAndView object.
    return mav;
}

22.3.messageSource 추가하기

AnyframeFormController를 사용하여 double submit이 발생하였을 때 에러메시지를 출력해 줘야하는데 이 메시지는 anyframe.springmvc.ext-x.x.x.jar 파일의 anyframe/web/springmvc/messages/springmvc.properties 파일에 정의되어 있다. 이 때 사용하는 key값은 common.msg.invalidtoken.error이다. 그러므로 이 resource properties 파일을 context-common.xml의 messageSource 빈 정의 부분에 해당 properties 파일을 설정해야한다.

<bean
   class="org.springframework.context.support.ReloadableResourceBundleMessageSource"
   id="messageSource">
   <property name="basenames">
     <list>
     <value>anyframe/web/springmvc/messages/springmvc</value>
     </list>
   </property>
</bean>

또한 Anyframe에서 제공해주는 기본 메시지가 아닌 다른 문자열을 지정해 주고 싶으면 common.msg.invalidtoken.error 키 값으로 properties 파일을 작성한 후 MessageSource 빈 설정 부분에 위와 같이 파일을 추가하면 된다.

22.4.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.foundation.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.foundation를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-task-2.0.10.jar 파일이 있어야 한다.)

    표 22.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.foundation.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

  • 참고자료

23.JasperReports Integration

는 오픈 소스로 다양한 컨텐츠를 PDF, HTML, XLS, CSV 파일 등으로 출력하는 리포팅 툴이다. 전체적으로 자바로 쓰여졌으며, 다양한 어플리케이션에서 사용되어 다양한 포맷으로 컨텐츠를 생성할 수 있다. XML 포맷으로 리포트 디자인을 작성하여 컴파일 한 후 RDBMS와 JDBC를 통해 데이터를 바인딩함으로써 최종적으로 다양한 리포트을 생성할 수 있다.

JasperReports 특징을 살펴보면 다음과 같다. :

  • 다양한 Reporting Output 형태(PDF, HTML, XLS, CSV 등) 제공

  • 다양한 Data Source를 이용하여 Reporting 데이터를 구성할 수 있음

  • 다양한 레이아웃으로 Reporting이 가능하며 Chart 및 Graphical한 화면 요소 제공

  • Graphical한 Report Designer 제공(여러 종류의 유/무료 툴 사용 가능)

아래의 그림은 XML 포맷의 리포트 디자인 과정부터 최종적으로 JasperRepoting Engine으로부터 최종 결과 리포트를 생성하는 과정을 한 눈에 볼 수 있도록 표현하고 있다.

Jasper XML(jrxml) 파일을 생성하는 툴을 제공함으로써 개발자는 비즈니스 컴포넌트 개발 후 쉽게 리포팅 기능을 구현할 수 있다. 또한 이 매뉴얼에서 설명하는 JasperReports (http://jasperforge.org/projects/jasperreports) 기능은 Spring MVC와 통합된 형태로 예제가 설명되어 있다. Anyframe 에서 제공하는 Reporting 기능은 기본적으로 JasperReports 기능을 모두 제공하나 Spring MVC와 통합된 형태로 기능이 제공되고 있기 때문에 Spring MVC를 통해서 제공되지 않은 JasperReports 일부 기능이 있을 수 있다. 이 경우 필요하다면 Spring MVC를 확장하여 구현 가능하다.

예를 들어 보면 다음과 같은 경우들이 포함된다. :

  • HTML 파일로 Reporting 하는 경우 - 이미지 파일을 html 내에서 디스플레이 해주기 위해서 Spring MVC에서 제공해주는 뷰 클래스를 확장하였다.

Anyframe 를 설치하여 Reporting 기능을 이용할 경우에는 이 매뉴얼에서 가이드 하는 방법대로 사용하면 위의 기능을 추가 코딩 없이 바로 사용할 수 있다.

23.1. User's Guide

아래 내용을 보고 JasperReports를 설치한 후, 직접 JRXML 파일을 디자인하고 샘플 어플리케이션을 실행시켜보도록 하자.

23.1.Installation

다음은 JasperReports 설치 순서로서 다운로드설치환경 그리고 Report Designer 설치 3가지 영역으로 구분하여 설명을 진행한다.Spring MVC와 통합된 형태로 JasperReports 기능을 사용하므로 Anyframe 의 Spring과 Spring MVC가 이미 설치되었다는 가정 하에서 아래 설치 과정을 진행하도록 하겠다. 그러므로 Anyframe 을 설치한 이후에 JasperReports를 설치하도록 한다.

23.1.1.다운로드

다음은 JasperReports 기능을 사용하기 위해서 다운로드 받아야 하는 기본적인 파일들이다.

파일명설명
JasperAssistant_3.1.1_Eclipse3.x.zip[필수] JasperReports Report Designer Eclipse Plugin 설치 파일(Download Evaluation Version)
AdbeRdr920_ko_KR.exe[필수] PDF 파일 디스플레이를 위한 Adobe Reader 9.2 (한국어 버전) 설치 파일
anyframe.example.jasper.zip[선택] anyframe JasperReports 예제 파일

23.1.2.설치 환경

JasperReports를 활용한 Sample Application은 아래와 같은 환경에서 테스트되었다. 아래의 SW에 대한 설치 방법은 이 매뉴얼에서는 생략한다. 이 다음 단계를 진행하기 전에 아래 3가지는 모두 개발 환경에 설치되어야 한다. 이 매뉴얼 내에서는 JDK 1.5 버전 기준으로 설명하고 있다.

  • JDK 1.5

  • Web Container - Tomcat 6.0

  • DataBase - HsqlDB 1.8.0.10

  • Eclipse 3.5.0

본 문서에서는 기본적으로 Tomcat6.0eclipse 3.5.0 (WTP 포함) 을 기준으로 설치 가이드를 진행한다.

이하 문서에서 [Eclipse Home] 이라함은 Anyframe에서 제공한 eclipse 기반 툴셋의 루트를 지칭한다. Tomcat6.0 서버는 적절한 위치에 설치한 후 Eclipse Server Runtime 으로 등록하여야 한다. 위의 다운로드 테이블의 내용 중 [필수]라고 표시된 파일들을 모두 다운로드 받았다면, AdbeRdr920_ko_KR.exe 파일을 실행시켜서 설치하도록 한다.

23.1.3.Report Designer 설치

JasperReports 보고서 XML 파일 작성을 용이하게 하기 위해서 여러 종류의 툴이 제공된다. 무료 툴(iReport)과 유료 툴(JasperAssistant) 등 여러가지가 있으므로 원하는 툴을 선택하여 작성할 수 있는데, 여기서는 JasperAssistant를 사용 하는 것으로 한다.

[참고]

* iReport : http://jasperforge.org/projects/ireport (free)

* JasperAssistant : http://www.jasperassistant.com (미 구매 시 21일 evaluation version 사용 가능)

현재 JasperAssistant Eclipse plugin 3.1.1 버전을 사용하고 있다. JasperAssistant의 버전 업데이트는 빈번하게 발생하고 있으므로 최신 버전을 다운로드 받아서 직접 설치를 수행하고자 한다면 아래 사이트를 참고한다.

* Homepage : http://www.jasperassistant.com

* Download : http://www.jasperassistant.com/download.html

JasperAssistant를 설치하기 위해서는 2가지 방법이 사용될 수 있다. 하나는 Eclipse Update Site 기능을 이용하는 방법이고, 나머지 하나는 직접 Eclipse Plugin 형태의 배포 파일을 다운로드 받아서 Eclipse에 복사해 넣는 방법이다. 여기서는 배포 파일을 다운로드 받아서 Eclipse에 복사해 넣는 방법으로 설치하는 방법을 설명하고 있다.

[Eclipse Home] 폴더에 JasperAssistant_3.1.1_Eclipse3.x.zip 압축 파일을 풀어서 설치하도록 한다. Eclipse를 사용 중이었다면 종료시킨 후 재기동 시키도록 한다.

23.1.3.1.Configuration

Eclipse Plugin인 JasperAssistant를 설치한 후, 사용하기 위해서는 아래와 같이 몇 가지 Configuration 작업을 해야 한다. Configuration 창을 열기 위해서는 Window ->Preferences ->JasperAssistant 메뉴를 선택한다.

  • License Information - 상용툴인 JasperAssistant를 구매하였다면, 이 화면에서 License Key를 입력해넣도록 한다.

  • Console - [Default 사용] JasperAssistant console 창 설정 변경

  • Designer - [Default 사용] Designer Editor 설정 변경

  • Data Sources - 필수 설정 으로 반드시 Report Designer를 사용하기 전에 설정해야 한다. Report 대상 데이터 소스에 접근하기 위한 정보를 입력한다. 이때, 총 4가지 타입의 데이터 소스(Empty Data Source, Database Data Source, XML Data Source, Custom Data Source) 형태를 제공하는데 이 매뉴얼에서는 Database Data Source 설정 방법에 대해서만 언급한다. HSQLDB에 접근하기 위해서 아래와 같이 Driver, URL, hsqldb jar 파일 위치 등의 정보를 입력하도록 한다.

  • Export - [Default 사용] Export 설정 변경(File > Report Export.. 기능 실행 시 사용됨)으로 Report Designer 사용 시 반드시 사용되지는 않지만, Export 기능을 사용하게 된다면 PDF, Excel, HTML 파일 등의 실행 파일의 위치를 명시해줘야 한다. 각각의 Preview 항목에 C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe, C:\Program Files\Internet Explorer\iexplore.exe 으로 작성한다.

JasperAssistant의 설치는 완료되었다. JasperAssistant를 활용하여 Report 파일을 만드는 방법은 Report Designer 매뉴얼 부분을 참고하도록 한다.

23.2.Report Designer

JasperAssistant는 JasperReports를 위한 Visual Report Designer 중 하나인 상용 툴이다. Eclipse Plugin 형태로 제공되며 직관적인 Graphical한 인터페이스로 jrxml 파일 작성을 용이하게 도와준다. 여기서는 예제를 어떻게 작성하였는지에 대해서 설명할 것이다. 예제내에 카테고리별 상품 목록을 조회하는 부분을 JasperReports를 이용하여 HTML, PDF, Excel, CSV 등 다양한 형태로 Reporting할 수 있다. JasperAssistant를 이용하여 카테고리별 상품 등록 현황을 조회하는 Reporting 부분을 jrxml로 작성하고, JasperReports 엔진을 통해서 다양한 UI 형태로 보여주고 있는 것이므로, 먼저 JasperAssistant를 사용하여 jrxml 파일을 작성하는 방법부터 살펴보도록 한다. 만약 JasperAssistant 사용 방법에 대해서 이 예제를 통한 방법이 아닌, 전반적인 사용 방법이 궁금하다면 JasperAssistant 사이트에 올려진 온라인 매뉴얼 문서 나 압축된 PDF 매뉴얼 문서 를 참고하도록 한다.

또한 Flash로 만들어진 JasperAssistant 사용 방법 데모를 보려면 여기(Demo) 를 참고하도록 한다. 간단한 Report 작성하는 방법을 화면과 설명을 통해 쉽게 알려주고 있으므로 처음 JasperAssistant를 접하게 되는 사용자들에게 도움이 된다.

23.2.1.목표 결과물

다음은 이 매뉴얼을 통해 작성된 카테고리별 상품 등록 현황 디자인 파일을 JasperAssistant의 Preview 기능을 사용하여 디자인(jrxml) 작성 결과를 확인해 본 모습이다. 이처럼, 카테고리별 상품 등록 현황을 테이블(Table) 형태와 파이 차트(Pie Chart) 형태로 보여지도록 디자인(jrxml) 파일을 작성해보자.

23.2.2.디자인 파일(JRXML) 작성

위와 같은 목표 결과물을 만들어 내기 위해서는 Database에 저장된 카테고리별 상품의 목록을 조회하여 다양한 UI로 Reporting 할 수 있도록 jrxml 파일을 작성해야 한다.

여기서 작성된 예제는 아래와 같은 환경에서 테스트되었다. 이 다음 단계를 진행하기 전에 아래 항목들은 모두 개발 환경에 설치되어야 한다.

  • Eclipse 3.5.0

  • JasperAssistant 3.1.1

  • DataBase - HsqlDB 1.8.0.10

23.2.2.1.Step 1 : Open JasperAssistant Perspective

JasperAssistant는 Report 작성을 위해서 Eclipse Perspective를 제공한다. 이 Perspective은 Report 작성 시 최적의 Eclipse Workbench 레이아웃 및 필요한 모든 뷰를 제공한다. JasperAssistant Perspective를 열기 위해서는 Window ->Open Perspective ->Other... ->JasperAssistant 메뉴를 선택한다.

23.2.2.2.Step 2 : Create a new Report

이제 새로운 Report를 작성해보도록 하자. 우선 src/webapp/WEB-INF/reports 폴더에 신규 Report 파일(jrxml)을 생성한다. 아래 화면들은 카테고리별 상품 등록 현황 Report 파일을 위해서 작업한 내용들이다. 만약 추가적으로 새로운 Report 파일을 작성하고자 한다면 아래 내용을 참고하여 작성하면 된다.

  • Select a wizard - Report wizard를 선택한다. 이때 JasperAssistant Perspective 화면이라면 좌측에 Navigator 뷰가 나타날 것이다. src/main/webapp/WEB-INF/jsp/jasper/report 폴더를 마우스로 선택한 상태에서 File > New 메뉴를 선택하면 Report 라는 하위 메뉴가 보이게 된다. 이 메뉴를 이용하면 바로 아래 단계로 이동하게 된다. 혹은 File > New > Other... 메뉴를 선택하면 아래 그림과 같은 화면이 나오게 되고, 이때 JasperAssistant > Report wizard를 선택하도록 한다.

  • New Report Wizard : Create a new report - 신규 Report 파일을 생성한다. 이때 파일 확장자를 jrxml으로 작성하도록 한다. 카테고리별 상품 등록 현황 Report 파일명은 testReport.jrxml로 하도록 하고, anyframe.example.jasper/src/main/webapp/WEB-INF/jsp/jasper/report 폴더 하위에 Report 파일이 저장되고 있음을 확인한다.

  • New Report Wizard : Select a data source- Report에 사용될 Data가 저장된 data source를 선택한다. Preferences 항목에 정의된 Data Source들이 목록으로 보여진다. 제공되는 샘플은 HSQL DB를 사용하므로 목록 중에 HSQL DB를 선택하도록 한다. Data Source를 추가하고자 하면, Window ->Preferences ->JasperAssistant ->Data Source 메뉴에서 추가하면 된다.

  • New Report Wizard : Specify the SQL query - Report에 사용될 Data를 얻어오는 SQL 쿼리를 작성한다. 카테고리별 상품 등록 목록을 조회하여 보여줄 Report를 작성할 것이므로 SQL 쿼리에 다음과 같이 입력하도록 한다.

    select category.CATEGORY_NO, category.CATEGORY_NAME, product.PROD_NO, product.PROD_NAME, product.SELLER_ID 
    from PRODUCT product, CATEGORY category
    where product.CATEGORY_NO = category.CATEGORY_NO
    order by category.CATEGORY_NO

  • New Report Wizard : Select the Fields - Report에 사용될 Field를 선택한다. DB로부터 얻어온 Table의 컬럼을 모두(CATEGORY_NAME, CATEGORY_NO, PROD_NAME, PROD_NO, SELLER_ID) 선택하도록 한다.

신규 Report 파일(jrxml) 생성 작업이 완료되었다. 아래 단계를 통해서 디자인 작업을 계속 진행하도록 한다.

23.2.2.3.Step 3 : Design a report using Palette

Step 2에서 생성한 Report 파일을 JasperAssistant Editor를 이용하여 Open하면, 우측에 Design/Preview Tab과 Palette가 나타난다.

이 Palette 상의 Elements와 Text Fields들을 이용하여 Report 파일을 디자인한다. 우선 Report XML 파일의 구조을 살펴보도록 한다. Report 파일은 section으로 구성되어 있는데 Section이란 height 별로 각각에 고유한 이름을 가지고 일정한 영역을 가지는 Report 파일의 화면을 나누는 개념이다. Section은 Band라는 이름으로 불리우는데 현재 JasperReports 사용 시 혼용해서 사용되고 있다. 현재 JasperAssistant Editor 메뉴 상에서 사용되는 이름은 Band이므로 앞으로는 Band라고 통일해서 부르도록 한다. Band는 가로방향으로 나뉘어 각각에 고유의 이름이 명명된 일정한 영역을 뜻한다. 아래 그림은 Band 구조를 보여주고있다. 기본적으로 Title, Last Page Footer, Summary Band를 제외한 나머지 Band들은 Detail Band를 기준으로 상하로(Header와 Footer) 구분되어 짝을 이루고 있고, 각각의 Band는 고유의 목적(기능)이 있다.

Band 명설명
Background band보고서의 배경 설정을 할 수 있다.
Title band가장 먼저 보여주는 band로 보고서 전체 페이지중 단 한번만 출력된다. 주로 보고서의 Title을 기재하는데 사용된다.
Page header band한번 정의되면 보고서 전체 페이지의 헤더 부분에 똑같은 위치와 크기로 페이지마다 반복 출력 된다.
Page footer band한번 정의되면 보고서 전체 페이지의 하단 부분에 똑같은 위치와 크기로 페이지마다 반복 출력 된다.
Column header band각 Detail band의 column 항목의 상단 영역으로 사용된다. 이 영역 역시 페이지마다 출력 된다.
Column footer band각 Detail band의 column 항목의 하단 영역으로 사용된다. 이 영역 역시 페이지마다 출력 된다.
Group header band사용자가 임의로 만든 band로 0개 이상을 만들수 있다. Detail band 상단에 위치한다.
Group footer band사용자가 임의로 만든 band로 0개 이상을 만들수 있다. Detail band 하단에 위치한다.
Detail band가장 핵심이 되는 band이다. Detail Band는 주로 실제 Query 를 이용해 수집된 데이터들을 나열해 출력할 수 있도록 하는 Band로 모든 Band들이 Detail Band를 중심으로 구성되고 설계된다고 해도 과언이 아니다. 여기서 Query를 이용해 수집된 데이터는 Detail Band 뿐만 아니라 다른 Band 에서도 사용할 수 있지만, Detail Band와 틀린 점은 Detail Band에서는 수집된 한 개 이상의 모든 Rows를 출력할 수 있지만 Detail Band를 제외한 다른 Band 에서는 첫 번째 Row에 있는 Column 값들만 출력할 수 있다.
Last Page Footer다른 일반 Page footer와 달리 보고서의 맨 마지막 페이지에 특별한 결과를 나타내고자 할 때 사용하는 Band로 Title Band처럼 보고서 전체 페이지중 딱 한번만 출력할 수 있다.
Summary bandReport footer Band라고도 하며 보고서 전체 페이지중 맨 마지막 페이지에 출력되는 Band로 주로 총 합계 등을 나타내고자 할 때 사용한다.

샘플 예제인 카테고리별 상품 등록 현황 Report 파일은 Title, Page Header, Column Header, Detail 을 사용하여 작성되었다. testReport.jrxml파일을 신규 생성하면, Default로 Detail band만 존재한다. 그러므로 추가되는 band는 Add Band 메뉴를 이용하여 추가시키도록 한다.

  • Title band 작성

    testReport.jrxml 파일이 Open되었을때 좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨)을 선택 후 마우스 우측 버튼을 클릭하여 Add Band > Add Title band 메뉴를 선택하도록 한다. 또는 Editor에서 점(dot)가 찍혀있는 않은 상하좌우 공란을 선택한 후 마우스 우측 버튼을 클릭하면 Add Band 메뉴를 사용할 수 있다. 우측 Editor 화면에 Title band가 생성되고 Palette의 Elements 항목 중 Static Text를 드래그앤드롭 하여 Title band 위에 놓는다. Static Text Element의 Properties 항목 중 Static Text > Text Property의 Value에 카테고리별 상품 등록 현황 이라고 입력해 넣는다. 또한 Reporting 시 보기 좋게 하기 위해서 아래와 같이 Property의 Value를 설정하도록 한다.

    PropertyValue설명
    Static Text > Text카테고리별 상품 등록 현황타이틀 명
    Text Font > Font Size12타이틀 글자 크기 설정
    Text Font > PDF EmbeddedtruePDF 형태로 Reporting 하는 경우 한글 지원하기 위해 설정
    Text Font > PDF EncodingUniKS-UCS2-HPDF 형태로 Reporting 하는 경우 한글 지원하기 위해 설정하는데 글자 배열 방식에 따라 UniKS-UCS2-H(horizontal) 혹은 UniKS-UCS2-V(vertical)를 선택
    Text Font > PDF Font NameHYGoThic-MediumPDF 형태로 Reporting 하는 경우 한글 지원하기 위해 설정하며, 이때 다른 글자체 선택 가능함

    Editor 기능을 이용하여 Static Text Box의 크기를 크게 하고 글자를 가운데 정렬시켜 놓는다.

  • Column Header band 작성

    testReport.jrxml파일이 Open되었을때 좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨)을 선택 후 마우스 우측 버튼을 클릭하여 Add Band > Column Header band 메뉴를 선택하도록 한다. 그런 다음 Palette의 Elements 중 Static Text 항목을 드래그앤드롭하여 [상세현황], Category Name, Product No, Product Name, Seller Id 이라는 헤더 정보를 작성한다. 이 하위에 실제 카테고리별 상품 등록 현황이 해당 필드에 맞게 Reporting 될 것이다. Editor의 툴바에서 제공하는 기능을 이용하여 굵은 글씨로 변경시키고, 테두리 선(Toggle Border 기능 사용)을 만들도록 한다. 작성하고 나면 다음과 같이 페이지 헤더 부분이 완성된 것을 확인할 수 있다. (PDF 출력을 위하여 Title band 작성을 참고하여 속성을 부여한다.)

  • Detail band 작성

    Default로 생성되는 band이므로 새로 band를 추가할 필요 없다. testReport.jrxml파일이 Open되었을때 좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨) 하위의 Fields 항목을 열어보면 위 Step 3에서 선택했던 Field들이 나온다. 이 Field들을 드래그앤드롭하여 Detail band 영역으로 가지고 오면 아래와 같이 $F{필드명} 형태로 보이게 된다. 실제 Data Source로부터 데이터를 받아와서 이 위치에 Reporting 하게 된다. 테두리 선 등 필요한 작업을 Editor의 툴바 기능을 이용해서 수행한다.(PDF 출력을 위하여 Title band 작성을 참고하여 속성을 부여한다.)

    Text Field 입력 란 박스의 Padding 값을 설정하기 위한 Property 설명과 해당 화면이다.

    PropertyValue설명
    Text > Box(2,2,2,2) : 직접 입력 불가능Properties 탭에서 Text > Box 우측의 [...] 버튼을 클릭하면 아래와 같이 설정할 수 있는 팝업창이 나온다.

    Box Settings 팝업창에서 Padding 항목을 수정해주면 된다. 여기서는 Shared padding settings 항목을 선택하고 Padding 값을 2 px로 주었다.

  • Page Header band 작성

    testReport.jrxml파일이 Open되었을때 좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨)을 선택 후 마우스 우측 버튼을 클릭하여 Add Band > Page Header band 메뉴를 선택하도록 한다. 이 Page Header band 영역에는 파이 차트를 추가해넣는다. 우측에서 Palette의 Elements 중 Chart를 클릭한 후 band 영역을 다시 클릭한다. 그러면 다음과 같이 어떤 타입의 차트를 선택할 지 묻는 팝업창이 나온다. 여기서 Pie를 선택하도록 한다. 만약 클릭으로 Chart를 선택하지 않고 위에서처럼 드래그앤드롭 형태로 Chart를 갖고 오면 Default로 Area 타입의 차트가 적용된다. 그러므로 차트 타입을 선택하기 위해서는 클릭으로 Chart Element를 선택하도록 한다.

    좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨)하위의 Summary > Chart_숫자 > Pie Dataset 항목을 선택하면 Properties 뷰에서 Dataset에 대한 설정을 할 수 있다.

    PropertyValue설명
    Pie Dataset > Key Expression$F{CATEGORY_NAME}파이 차트 작성 시 키가 되는 필드 정보를 입력
    Pie Dataset > Value Expression$V{CategoryGroup_COUNT}Number 형 데이터로 변경 가능한 데이터 필드 정보만 입력 가능함
    Pie Dataset > Label Expression 공란으로 두면 기본적으로 Key = Value 형태로 디스플레이됨

    좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨)하위의 Group 항목을 선택한 후 마우스 우측 버튼을 클릭하여 Add Group 을 선택하여 다음과 같이 Group 을 추가한다.

    PropertyValue설명
    Group > ExpressionExpression $F{CATEGORY_NO}그룹 조건을 입력
    Group > NameCategoryGroup그룹 조건명을 입력 (Pie Dataset > Value Expression 과 일치)

    좌측에 Outline 뷰에서 루트 항목(기본적으로 Unnamed라고 표기됨)하위의 Summary > Chart_숫자 항목을 선택한 후 Properties 뷰에서 Mode를 변경시켜 준다.

    PropertyValue설명
    Common > ModeOpaque파이 차트를 HTML로 Reporting 시 Mode를 Transparent로 하면 차트 이미지 주위로 불필요한 바탕색이 생기므로 이를 없애기 위해서 Opaque를 선택함 PDF, Excel 등 타 Reporting 형태일 경우에는 상관없음

    파이 차트 작성까지 모두 끝내고 나서 Editor 상의 Design Tab을 통해 보면 다음과 같은 모습으로 보인다. Preview Tab으로 이동하지 않으면 파이 차트 상에 데이터가 들어가 있는 형태를 볼 수 없으며, 에러 발생 시에도 확인이 불가능하므로 Design Tab에서의 작업이 모두 끝나면 다음 단계인 Preview 를 진행시킨다.

23.2.2.4.Step 5 : Preview Report

디자인 Report 파일 작성이 모두 완료되었다면 JasperAssistant에서 제공되는 Preview 기능을 사용하여 제대로 작성되었는지를 확인해본다. 만약 Report 파일에 에러가 존재한다면 Preview 기능을 수행시킬 때 확인해볼 수 있을 것이다. Preview 결과가 이 매뉴얼 상단의 목표 결과물과 동일한지 확인해본다.

이로써 JasperAssistant를 활용한 jrxml 파일 작성이 완료되었다. 이제 샘플 웹 어플리케이션에서 해당 Reporting 파일을 어떻게 호출해서 보여주는지 확인해본다.

23.3.Configuration

JasperReports와 Spring MVC를 연계하기 위해서는 다음과 같이 2가지 XML 파일에 대해서 설정해야 한다.

  • web.xml 작성하기

  • jasper-servlet.xml 작성하기

이 매뉴얼상에 언급된 소스 코드들은 모두 Eclipse에 Import한 anyframe-sample-web 프로젝트에 예제로 들어있다.

23.3.1.web.xml 작성하기

anyframe.example.jasper/src/main/webapp/WEB-INF 폴더 하위의 web.xml에 아래 내용을 추가해 넣는다. 기존의 Anyframe 관련 설정은 그대로 두고 JasperReports 관련한 설정만 더 추가한 것이다.

jasperaction 서블릿의 클래스인 DispatcherServlet은 Spring MVC 클래스로 url이 /reports/ pattern으로 요청되면 모두 jasperaction 서블릿을 통해 서비스 되도록 설정하고 있다. image 서블릿은 HTML 형태로 Reporting 하는 경우 image 파일을 처리해주기 위해서 필요한 서블릿이다. HTML 형태로 Reporting 하지 않는다면 설정할 필요가 없다.

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <!-- in classpath, jasper-servlet.xml exists -->
        <param-value>classpath:/springmvc/*-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
  
<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

<!-- jasper-configuration-START -->   
<servlet>
    <servlet-name>image</servlet-name>
    <servlet-class>net.sf.jasperreports.j2ee.servlets.ImageServlet</servlet-class>
</servlet>	

<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>/reports/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>image</servlet-name>
    <url-pattern>/reports/image</url-pattern>
</servlet-mapping>
<!-- jasper-configuration-END -->

23.3.2.jasper-servlet.xml 작성하기

action 서블릿의 클래스인 DispatcherServlet이 사용하는 빈 설정파일에 jasper-servlet.xml 파일을 등록해주도록 한다.

  • context:component-scan - annotaion 설정을 통해 컨트롤러 상단에 정의된 @RequestMapping 설정에 따라 /testReport.html, /testReport.pdf 등등의 형태로 URL 요청 시 지정된 reportController 빈을 호출한다. (urlMapping 을 동록하여 동일한 처리를 수행할 수도 있다.)

  • ParameterizableViewController - /jasperProduct.do 형태로 URL이 요청된 경우 /WEB-INF/jsp/jasper/listReport.jsp 페이지를 수행시킨다.

  • reportViewResolver - views.properties 형태로 URL이 요청된 경우 해당 Controller 클래스의 viewReport 메소드를 수행시킨다.

  • reportView - 가장 핵심이 되는 빈으로 Controller로부터 뷰 이름을 알아낸 결과 reportView인 경우, ExtendedJasperReportsMultiFormatView 클래스를 실행시킨다. reportView 빈의 Property 설명은 다음과 같다.

    PropertyValue설명
    url/WEB-INF/reports/testReport.jasperjrxml 파일을 JasperAssistant Preview를 이용하여 컴파일한 결과 파일
    jdbcDataSourcedataSource현재 HSQL DB를 사용하는 dataSource 빈 사용
    exporterParameters - ...IMAGES_URIimage?image=HTML Reporting 시 이미지 파일을 출력하기 위해서 web.xml에 정의한 image 서블릿을 사용하도록 설정
    exporterParameters - ...CHARACTER_ENCODINGeuc-krReporting 시 한글 깨짐을 방지하기 위해서 설정

<context:component-scan base-package="anyframe.example.jasper" 
    use-default-filters="false">
<context:include-filter type="annotation"
    expression="org.springframework.stereotype.Controller" />
</context:component-scan>	

<bean name="/jasperProduct.do" 
        class="org.springframework.web.servlet.mvc.ParameterizableViewController">
    <property name="viewName" value="/WEB-INF/jsp/jasper/listReport.jsp" />
</bean>	 

<bean id="reportViewResolver" 
        class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="order" value="1"/>
    <property name="basename" value="jasper.views"/>
</bean>     
   
<bean id="reportView" 
        class="anyframe.web.springmvc.jasperreports.ExtendedJasperReportsMultiFormatView">
    <property name="url" value="/WEB-INF/jsp/jasper/report/testReport.jasper"/>
    <property name="jdbcDataSource" ref="dataSource"/>
    <property name="exporterParameters">
        <map>
            <entry key="net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI" 
                    value="image?image="/>
            <entry key="net.sf.jasperreports.engine.JRExporterParameter.CHARACTER_ENCODING" 
                    value="euc-kr"/>
        </map>
    </property>
</bean>

23.4.Controller

Anyframe 에서 제공하는 Reporting 기능은 기본적으로 JasperReports 기능을 모두 제공하나 Spring MVC와 통합된 형태로 기능이 제공되고 있기 때문에 Spring MVC를 통해서 제공되지 않은 JasperReports 일부 기능이 있다. 이 때 개발자는 뷰 클래스를 확장하거나, Controller 클래스에 부가 작업을 해줘야 하는데 아래의 경우의 경우 Anyframe 에서 이미 확장하여 제공하므로 개발자의 추가 작업 부담을 줄일 수 있다.

  • HTML Reporting- 개발자 추가 작업 일부 필요

23.4.1.HTML Reporting

이미지 파일을 html 내에서 디스플레이 해주기 위해서 Spring MVC에서 제공해주는 뷰 클래스를 확장하고, Controller 클래스에 부가 작업을 해줘야 한다. Spring MVC에서 제공해주는 뷰 클래스를 확장한 것은 Anyframe 을 통해 제공될 것이므로, 개발자는 신경쓸 필요가 없다. 그러나 Controller 클래스에 부가 작업을 해야 하는 것은 개발자가 개발할 Controller내에 request 정보를 Model 객체 내에 저장시켜주는 작업으로 아래에 보여지는 코드와 같이 작성하도록 한다.

  • JasperReportsMultiFormatView 확장 - renderReport 메소드 내에서 아래와 같이 request로부터 세션 객체를 얻어낸 후, 객체 내 어트리뷰트로 populatedReport 값을 저장한다. 이는 후에 HTML Reporting 시 이미지 파일을 디스플레이하기 위해 필요하다.(내용을 참고만 한다. 실제 개발자가 할 일은 없다.)

    protected void renderReport(JasperPrint populatedReport, Map model,\
            HttpServletResponse response) throws Exception {
        // Check for request object and set image servlet
        if (model.containsKey("requestObject")) {
            HttpServletRequest request = (HttpServletRequest) model
                    .get("requestObject");
            request.getSession().setAttribute(
                    ImageServlet.DEFAULT_JASPER_PRINT_SESSION_ATTRIBUTE,
                    populatedReport);
    }

  • Controller 추가 작업 - 위에서와 같이 model 객채 내에서 "requestObject" 값을 얻어오기 위해서는 Controller 클래스의 Model 객체에 request 객체를 저장해주는 작업이 필요하다. 카테고리별 상품 등록 현황 조회 결과 Reporting을 위한 ReportContoller 클래스의 예는 다음과 같다.

    @Controller
    public class ReportController extends MultiActionController {
    	@RequestMapping("/testReport.*")
        public ModelAndView viewReport(HttpServletRequest request,
                                       HttpServletResponse response) 
        throws Exception {
        ... 중략
          Map model = new HashMap();
          model.put("format", format);
          model.put("requestObject", request);
            
          return new ModelAndView("reportView", model);
        }
    }

  • JSP 추가 작업 - JSP 상에서 Report 를 호출하기 위한 예는 다음과 같다. testReport.html 또는 testReport.pdf 형태로 호출한다.

    <table class="table" border="1">
        <tr>
            <td class="ct_list_b"><b>Report Style</b></td>
            <td class="ct_list_b"><b>Report</b></td>
        </tr>
        <tr class="ct_list_pop">
            <td align="center">HTML</td>
            <td>
                <a href="${ctx}/reports/testReport.html">
                    <font color="blue">
                        <u>Show Product Report</u>
                    </font>
                </a>                
            </td>
        </tr>
        <tr class="ct_list_pop">
            <td align="center">PDF</td>
            <td>
                <a href="${ctx}/reports/testReport.pdf">
                    <font color="blue">
                        <u>Show Product Report</u>
                    </font>
                </a>
            </td>
        </tr>
    </table>
    

23.5.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.jasper.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, anyframe.example.jasper/src/main/webapp 폴더의 index.jsp를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Run on Server 를 클릭한다. 그리고 실행 결과를 확인한다.

    표 23.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.jasper.zipDownload

VI.Spring Web Flow

일반적인 웹 어플리케이션을 개발 할 때 페이지 처리 흐름을 제어 하기 위해서는 복잡하고 반복적인 코드가 들어가게 된다. 이 때, Spring Web Flow를 사용하면 선언적인 Flow Definition 파일을 작성함으로써 보다 쉽게 페이지 흐름을 제어할 수 있다. 이번 장에서는 Spring Web Flow에 대한 소개 및 Flow Definition 파일 작성 방법과 Spring Web Flow를 사용한 웹 어플리케이션 개발 방법에 대해 알아본다.

24.configuration

Spring Web Flow를 사용하게 위해서는 다음과 같은 기본 설정과 Spring MVC와의 연계를 위한 부가 설정이 필요하다.

24.1.기본 설정

24.1.1.FlowRegistry 정의

Spring Web Flow를 실행 시키기 위해 Flow Definition 파일이 있는 위치를 FlowRegistry에 등록하고 각각의 Flow Definition 파일에 대해 Flow ID를 부여한다. 필요에 따라 base-path를 지정해 줄 수도 있다.

<webflow:flow-registry id="flowRegistry" base-path="/WEB-INF/jsp/webflow">
	<webflow:flow-location path="/sales/product/addProduct-flow.xml"
		id="webflowAddProduct" />
</webflow:flow-registry>
위와 같이 정의할 경우 base-path를 포함하여 /WEB-INF/jsp/webflow/sales/product/addProduct-flow.xml 파일을 Flow Registry에 등록하게 된다.

24.1.1.1.Flow Registry 정의 방법

Flow Registry를 정의 하는 방법은 아래와 같다.

  • path를 이용한 Flow Definition 파일 위치 직접 지정 Flow Definition 파일의 위치를 직접 정의하여 Flow Registry에 등록 할 수 있다.

    <webflow:flow-registry id="flowRegistry">
    	<webflow:flow-location path="/WEB-INF/jsp/webflow/sales/product/addProduct-flow.xml"
    		id="webflowAddProduct" />
    </webflow:flow-registry>

  • pattern을 이용한 위치 지정 pattern을 이용하여 여러 개의 Flow Definition 파일을 한꺼번에 등록할 수 있다.

    <webflow:flow-registry id="flowRegistry"
    	base-path="/WEB-INF/jsp/webflow/sales">
    	<webflow:flow-location-pattern value="/**/*-flow.xml"/>
    </webflow:flow-registry>
    위와 같이 정의할 경우 /WEB-INF/jsp/webflow/sales 하위 모든 폴더의 -flow.xml로 끝나는 모든 파일을 Flow Registry에 등록 하게 된다.

24.1.1.2.Flow ID 생성

Flow Registry 설정 시 path를 이용하여 id 요소에 id를 지정해 줄 경우 해당 id로 Flow ID를 생성하지만 별도의 id를 정의하지 않았을 때나 pattern을 이용하여 Flow Registry를 정의했을 때에는 pase-path 설정 유무에 따라 아래와 같이 Flow ID가 생성된다. Flow ID는 실제 Flow를 실행시키게 되는 요청명이 되므로 어떠한 Flow ID로 생성되는지 개발자는 인지하고 있어야할 것이다.

  • base-path를 정의할 경우 base-path를 정의한 경우에는 실제 파일의 경로에서 base-path와 파일명을 제외한 문자열이 Flow의 ID가 된다.

    <webflow:flow-registry id="flowRegistry" base-path="/WEB-INF/jsp/webflow"/>>
    	<webflow:flow-location path="/sales/product/addProduct-flow.xml"
    		id="webflowAddProduct" />
    </webflow:flow-registry>
    위와 같이 정의할 경우 Flow의 ID는 sales/product가 된다. pattern을 사용하였을 경우에도 마찬가지다. 그러나 이 때, 폴더 명이 ID가 되므로 하나의 폴더에는 하나의 Flow Definition 파일만 존재해야 한다.

  • base-path를 정의하지 않을 경우 base-path를 정의하지 않을 경우에는 Flow Definition 파일의 이름에서 확장자를 제외한 파일 이름이 Flow ID가 된다.

    <webflow:flow-registry id="flowRegistry">
    <webflow:flow-location-pattern value="/WEB-INF/jsp/webflow/sales/**/*-flow.xml"/>
    </webflow:flow-registry>
    위에서 정의한 pattern과 일치하는 파일이 category-flow.xml, product-flow.xml 이라고 할 떄 각각의 Id는 category-flow, product-flow가 된다.

생성된 flow id를 확인할 수 있는 방법은?

개발자가 flow의 id를 따로 지정안해 줄 경우 생성된 flow id는 log4j.xml 파일에서 org.springframework.webflow logger를 DEBUG 레벨로 정의 하면 확인할 수 있다. 다음은 이렇게 정의하였을 때 출력되는 로그의 일부이다.

[/WEB-INF/jsp/webflow/sales/category/category-flow.xml]' under id 'category'
 [/WEB-INF/jsp/webflow/sales/category/category-flow.xml]' under id 'category-flow'

24.1.2.FlowExecutor 정의

Flow를 실행 시키기 위해서 FlowExecutor 배포하여야 한다.

<webflow:flow-executor id="flowExecutor" /> 

24.2.Spring MVC와 연계하기 위한 설정

Spring Web Flow는 Spring MVC, JSP, Faces, Portlet 등의 환경에서 사용이 가능하다. 이 절에서는 Spring MVC와의 연계 방법에 대해 알아보게 될 것이며 기본적으로 이를 위해서는 DispatcherServlet이 정의되어 있고 Spring Web Flow로 구현될 모든 요청이 DispatcherServlet을 servlet으로 사용해야 한다.

24.2.1.FlowHandlerAdaptor 정의

Spring MVC 환경에서 Flow가 핸들링 될 수 있도록 해주기 위해 FlowHandlerAdaptor를 정의한다.

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
	<property name="flowExecutor" ref="flowExecutor" />
</bean>
속성 중 flowExcutor는 위에서 정의한 flowExcutor의 id를 참조하게 된다.

24.2.2.FlowHandlerMapping 정의

요청에 대한 Flow를 매핑시켜 주기위해 FlowHandlingMapping을 정의 한다.

<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
	<property name="order" value="0" />
	<property name="flowRegistry" ref="flowRegistry" />
</bean>
위에서 정의한 FlowHandlerMapping은 Spring MVC의 BeanNameHandlerMapping, SimpleHandlerMapping과 같이 interceptor, order등의 속성을 줄 수 있다. 속성 중 flowRegistry는 위에서 위에서 정의한 flowRegistry의 id를 참조 하며 실제 요청을 처리하게될 flow 파일을 찾게되는 저장소이다.

특정 URL에만 Interceptor를 적용해야 할 경우에는?

특정 URL에만 Interceptor를 적용해야 할 경우 Spring Web Flow에서 제공하는 FlowController를 이용하여 정의할 수 있다.

<bean 
	class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
	<property name="order" value="0"/>
	<property name="mappings">
		<value>
			/webflowProduct.do=flowController
		</value>
	</property>
	<property name="interceptors" ref="loginInterceptor" />
</bean>
<bean id="flowController" class="org.springframework.webflow.mvc.servlet.FlowController">
	<!-- 필수 -->
	<property name="flowExecutor" ref="flowExecutor"/>
</bean>
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
	<!-- order는 SimpleUrlHandlerMapping 보다 낮게 준다. -->
	<property name="order" value="1" />
	<property name="flowRegistry" ref="flowRegistry" />
</bean>
<bean id="loginInterceptor" class="common.LoginInterceptor" />
FlowController는 flowExecutor를 속성으로 가질 수 있으며 이를 통해 Flow가 요청을 처리할 수 있게 된다. 위와 같이 정의할 경우 "/webflowProduct.do"라는 요청에 대해서만 loginInterceptor를 적용하게 된다.

24.2.3.Spring MVC의 ViewResolver 지정

Spring MVC를 사용할 때 정의한 ViewResolver를 사용하기 위해 flow-builder-services를 등록하고 정의한 viewResolver를 참조하도록 한다. 정의하는 방법은 아래와 같은 Step을 따른다.

  1. flowRegistry에 flow-builder-services 속성 추가

    <webflow:flow-registry id="flowRegistry"
    	flow-builder-services="flowBuilderServices">
    	<webflow:flow-location path="/sample/product/addProduct-flow.xml"
                                                      		id="addProduct" />
    </webflow:flow-registry>

  2. flowBuilderServices 정의

    <webflow:flow-builder-services id="flowBuilderServices"
    view-factory-creator="mvcViewFactoryCreator" development="true" />

  3. mvcViewFactoryCreater 정의

    <bean id="mvcViewFactoryCreator"
    	class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
    	<property name="viewResolvers" ref="tilesViewResolver" />
    </bean>

25.플로우 정의

flow가 시작되어 종료될 때 까지의 재사용 가능한 범위가 Flow 정의 파일의 단위가 된다. 이러한 flow를 정의하기 위해서 작성해야할 요소에 대해 알아보도록 한다.

25.1.필수 요소

view-state, transition, end-state 세 요소를 통해 기본적이 view navigation을 구성할 수 있다.

25.1.1.view-state

view-state는 flow의 step을 정의하며 해당 view를 출력해주는 역할을 한다.

<view-state id="addProductView" model="product"
	view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp">
</view-state>
위와 같이 정의할 수 있으며 view-state의 id는 플로우내에서 유일해야 한다. model attribute로 model 객체를 정의해 줄 수 있으며 해당 변수에 대한 선언은 <var>를 이용해서 할 수 있다.
<var name="product" class="domain.Product" />
또한, view를 사용하여 출력해 줄 view 이름을 정의해 줄 수 있으며 정의해 주지 않을 시에는 view-state의 id가 view 이름이 된다. 위의 코드에서 view 이름을 따로 정의해 주지 않았다면 출력해 줄 view 이름은 addProductView가 된다. 플로우 정의 파일에 여러 개의 view-state가 정의되어있다면 플로우가 시작될 때 실행되는 view-state는 맨 처음에 정의되어 있는 view-state가 된다.

25.1.2.transition

화면에서 일어난 event에 의해 다음으로 진행해야할 view-state에 대해 정의해준다.

<view-state id="confirmAddProduct"
	view="/WEB-INF/jsp/webflow/sales/product/reviewProduct.jsp">
	<transition on="revise" to="addProductView" />
	<transition on="confirm" to="backtolist" history="invalidate">
		<evaluate expression="foundationProductService.add(product)" />
	</transition>
	<transition on="cancel" to="backtolist" />
</view-state>
event id에 따라 위와 같이 각각의 view-state로 분기할 수 있다. 이 때, on에는 event id를 to에는 분기시킬 view-state id를 정의해 준다.

view에서 event id는 어떻게 지정해 줄까?

Spring MVC와 연동하여 화면에서 어떠한 event가 발생하였을 때의 event id를 JSP 페이지에서 정해주게 된다. 보통 폼이 존재하는 JSP 페이지에서 처리할 때 "_eventId"라는 키 값에 해당하는 value 값이 event id가 되며 아래와 같이 정의할 수 있다.

<input type="submit" value="save"/>
<input type="hidden" name="_eventId" value="success" />
submit 타입의 input 태그에 "_eventId"에 transition의 on에 속하는 속성을 줄 수도 있다.
<input type="submit" name="_eventId_success" value="save" />
단순한 HTML 링크 형식의 event일 경우 에는 아래와 같이 <a href ... > 태그를 사용하여 event id를 정해줄 수 있다.
<a href="${flowExecutionUrl}&_eventId=success">success</a>
위에서 사용한 Expression Language인 flowExecutionUrl을 사용하여 현재의 flowExcutionUrl을 가지고 올 수 있다.

25.1.3.end-state

플로우를 종료시키기 위해 end-state를 정의해준다. 플로우는 반드시 end-state로 종료되어야한다.

<end-state id="cancel" />

25.2.메소드 호출

플로우에서는 대부분 화면 이동에 대한 로직이 들어가게 된다. 그러나 아래와 같은 시점에서 Business Service를 호출하거나 다른 클래스의 메소드를 호출할 수 있다.

  • 플로우가 시작될 때

    <on-start>
    	<evaluate expression="..." />
    </on-start>

  • state에 진입할 때

    <view-state id="addProductView" model="product"
    	view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp">
    	<on-entry>
    		<evaluate expression="..." />
    	</on-entry>
    	....
    </view-state>

  • view가 출력될 때

    <view-state id="addProductView" model="product"
    	view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp">
    	<on-render>
    		<evaluate expression="..." />
    	</on-render>
    	....
    </view-state>

  • transition이 실행될 때

    <view-state id="addProductView" model="product"
    	view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp">
    	<transition on="add" to="confirmAddProduct">
    		<evaluate expression="..." />
    	</transition>
    	....
    </view-state>

  • state가 끝날 때

    <view-state id="addProductView" model="product"
    	view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp">
    	....
    	<on-exit>
    		<evaluate expression="..." />
    	</on-exit>
    </view-state>

  • flow가 끝날 때

    <on-end>
    	<evaluate expression="..." />
    </on-end>

보통 Spring Bean으로 등록된 클래스의 메소드를 실행 시킬 수 있으며 이 경우 해당 bean을 Autowired 방식으로 찾게 된다. 또한, 일반 클래스의 메소드도 variable로 정의한 후 호출해서 쓸 수 있다.

25.2.1.evaluate

action을 실행시키기 위해서는 위에서 언급한 시점에서 <evaluate>를 사용하면 된다.

<evaluate expression="foundationProductService.save(product)" />
<var name="list" class="java.util.ArrayList" />
<evaluate expression="list.add(product)" />
위에서 처럼 Spring Bean으로 정의되지 않은 클래스를 varialer로 정의하여 호출할 때에는 variable로 정의된 클래스가 플로우 요청 간 인스턴스의 상태를 유지하기 위해서 java.io.Serializable를 구현하여야 한다. 메소드 실행 후 결과값을 받아야 한다면 result를 사용한다.
<evaluate expression="foundationProductService.get(prodNo)" result="flowScope.product" />

25.3.Transition Decision

view-state에서 transition을 결정 할 때 단순히 사용자의 입력 값을 받아 처리할 수도 있지만 어떠한 action을 실행시킨 후 그 결과에 따라 transition을 결정할 수도 있다. 이 때, 사용할 수 있는것이 action-state와 decision-state이다.

25.3.1.action-state

action-state를 사용하면 action을 실행시킨 후 리턴 값에 의해 다음으로 진행될 transition을 정할 수 있다.

<action-state id="checkUserLogin">
	<evaluate expression="userService.isUserLogin(userId)"/>
	<transition on="yes" to="getCategory"/>
	<transition on="no" to="backtolist"/>
</action-state>
위와 같이 정의할 경우 userService의 isUserLogin() 메소드를 실행 시킨 후 리턴 값이 true일 경우 event id는 yes가 되어 getCategory로 이동하게 되고 false일 event id가 no가 되어 backtolist로 이동하게 될 것이다. action 수행 후 리턴 값에 매핑 되는 event id는 아래와 같다.
메소드 리턴 타입event id
java.lang.StringString 값
java.lang.Booleanyes(true일 경우), no(false일 경우)
java.lang.EnumEnum 이름
그 밖의 타입success

25.3.2.decision-state

decision-state는 action-state와 같은 역할을 하지만 if/else 문을 사용할 수 있다. 위에서 정의한 코드를 decision-state로 정의하면 아래와 같은 코드가 된다.

<decision-state id="checkUserLogin">
	<if test="UserService.isUserLogin(userId)" then="getCategory" else="backtolist" />
</decision-state>

25.4.Expression Language

Spring Web Flow에서는 Expression Language(EL)을 통해 모델 객체에 접근하거나 action을 실행시킬 수 있다. 이 때, Unified EL을 사용하며 현재 Spring Web Flow에서는 구현체로 jboss-el을 사용하고 있다. 개발자는 이러한 EL문을 사용하여 client가 입력한 input 데이터나 request parameter에 담겨온 데이터들에 접근할 수 있고 flowScope 같이 플로우 내부에서 사용하는 데이터에 접근할 수 있다. 또한, Spring Bean으로 정의된 class를 호출할 수도 있다.

25.4.1.Special EL variables

다음은 flow 파일 내에서 유용하게 사용할 수 있는 변수들이다.

  • flowScope : flow 내에서 사용할 수 있는 변수에 할당할 수 있다. flowScope은 플로우가 요청을 처리하는 동안 존재 하게 되며 플로우가 호출될 때 생성되며 플로우가 응답을 반환하고 나면 파괴된다.

    <evaluate expression="foundationProductService.get(prodNo)" result="flowScope.product"/>

  • viewScope : view에서 사용할 수 있는 변수에 할당할 수 있다. view 상태가 시작되서 존대하는 동안 지속된다. 또한 외부에서는 참조할 수 었다.

    <evaluate expression="foundationProductService.get(prodNo)" result="viewScope.product"/>

    view에서는 viewScope에 있는 변수를 어떻게 접근 할까?

    jsp에서 viewScope에 선언되어 있는 변수를 꺼내어 쓰고 싶을 때 JSP 내부에서 Expression Language를 사용하면 쉽게 사용할 수 있다.

    ${product}

  • requestScope : request 내에서 사용할 수 있는 변수에 할당할 수 있다. 플로우가 요청을 처리하는 동안에 존재하며 플로우가 호출될 때 생성되고 플로우가 응답을 반환하면 파괴된다.

    <evaluate expression="foundationProductService.get(prodNo)" result="requestScope.product"/>

  • flashScope : 플로우가 실행되는 전반에 걸쳐 필요한 변수에 할당할 수 있다. 플로우가 시작되어 종료될 때까지 존재한다.

    <evaluate expression="foundationProductService.get(prodNo)" result="flashScope.product"/>

  • conversationScope : 최상위 플로우가 시작될 때 할당되며 최상이 플로우가 종료될 때 파괴된다. 그러므로 최상위 플로우가 가지고 있는 subflow에서도 conversationScope으로 설정된 변수에 접근할 수 있다.

    <evaluate expression="foundationProductService.get(prodNo)" result="conversationScope.product"/>

  • requestParameters : client단에서 request parameter로 넘어온 값들에 대해 접근할 수 있다.

    <set name="flowScope.userId" value="requestParameters.userId" />
    위와 같이 정의할 시 Java에서 HttpServletRequest의 getParameter("userId")한 것과 같다.

Spring Web Flow에서 제공하는 Expression Langugae에 대한 더 많은 정보는 Spring Web Flow Reference를 참고 한다.

26.View

위에서 살펴본 view-state에서 view 속성을 정의해 주면 사용자가 원하는 view를 출력해 줄 수 있다. 이 때, view 속성을 지정하지 않을 경우에 view-state의 id가 view 이름이 된다.

<view-state id="confirmAddProduct" view="/WEB-INF/jsp/webflow/sales/product/reviewProduct.jsp">
view는 아래와 같은 방법으로 정의할 수 있다.
  • 플로우로부터의 상대 경로로 지정 : 플로우의 working directory로 부터 상대적인 경로로 view를 지정할 수 있다.

    <view-state id="confirmAddProduct" view="reviewProduct.jsp">

  • 절대 경로로 지정 : webapp의 루트 디렉토리부터의 경로로 view를 절대 경로로 지정할 수 있다.

    <view-state id="confirmAddProduct" view="/WEB-INF/jsp/webflow/sales/product/reviewProduct.jsp">

  • 논리적인 이름으로 지정 : Spring MVC의 viewResolver를 사용하는 것과 같이 논리적 이름을 지정하여 viewResolver에 의해 view를 찾게 할 수 있다.

    <view-state id="confirmAddProduct" view="confirmAddProductView">

26.1.model 바인딩

사용자가 입력한 데이터를 model 객체로 바인딩하기 위해 Spring Web Flow에서는 model 속성을 정의하여 쓸 수 있다.

<var name="product" class="domain.Product" />
<view-state id="getProduct" model="product"
	view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp">
위와 같이 정의할 경우 사용자가 입력한 데이터의 parameter 이름과 model 객체의 attribute의 이름과 일치하면 자동으로 바인딩이 된다. model 객체는 하나만 정의할 수 있으며 이렇게 정의된 model 객체에 대해 validation 체크를 할 수 있다. 보통 model 속성만 정의할 경우 해당 model 객체에 대한 모든 public attribute들이 바인딩되게 된다. <binder>를 사용하면 특정 attribute에 대해서만 바인딩 시킬 수 있다.
<binder>
	<binding property="userId" />
	<binding property="userName" />
</binder>

26.2.view backtracking

사용자는 어플리케이션을 이용할 때 브라우저의 뒤로가기 버튼을 이용하여 이미 끝난 view-state나 transition으로 되돌아 갈 수 있다. history 정책에 대해 Spring Web Flow에서는 history 속성 설정만으로 제어할 수 있으며 history에 대한 설정이 없을 시에는 기본적으로 backtracking이 허용된다.

26.2.1.discard

해당 뷰에 대해 backtracking을 방지 한다.

<transition on="confirm" to="backtolist" history="discard">
	<evaluate expression="foundationProductService.add(product)" />
</transition>

26.2.2.invalidate

이전에 출력되었던 모든 뷰에 대한 backtracking을 방지 한다.

<transition on="confirm" to="backtolist" history="invalidate">
	<evaluate expression="foundationProductService.add(product)" />
</transition>

27.Subflow

플로우내에서 하위 플로우를 호출하려고 할 때 subflow를 이용한다. 이 때, 하위의 flow가 결과를 리턴할 때 까지 상위 플로우는 대기하게 된다.

27.1.subflow-state

먼저 subflow를 호출하게될 state에서 subflow-state를 정의하고 실행시킬 subflow의 id를 지정해 준다. 아래의 코드는 상위 플로우 정의 부분의 일부이다.

<subflow-state id="viewCategory" subflow="viewCategorySubFlow">
	<!-- transition 정의 -->
	<transition ... />
</subflow-state>
위에서 정의한것 처럼 viewCategory state에 오게 되면 viewCategorySubFlow라는 ID를 가진 플로우를 subflow로 실행시키게 된다.

27.2.input

subflow에 전달해줄 객체가 있으면 input을 사용한다.

<subflow-state id="viewCategory" subflow="viewCategory">
	<!-- subflow에서 필요한 data를 input 값으로 정의  -->
	<input name="categoryNo" value="product.category.categoryNo"/>
	<!-- transition 정의 -->
	<transition ... />
</subflow-state>
상위 플로우에서 전달해준 input값을 전달받기 위해서는 하위 플로우에서도 input을 정의해주어야 한다. 아래의 코드는 하위 플로우 정의 부분의 일부이다.
<!-- 상위 플로우에서 받게 될 input값 정의  -->
<input name="categoryNo"/>
<view-state id="viewCategory"
		view="/WEB-INF/jsp/webflow/sales/category/viewCategoryForFlow.jsp">
	<on-render>
		<evaluate 
			expression="foundationCategoryService.get(categoryNo)" 
			result="category"/>
		<set name="viewScope.category" value="category"/>
	</on-render>
	<transition ... />
</view-state>

27.3.output

하위 플로우에서 플로우를 진행시킨 후에 상위 플로우에 전달해 줄 객체가 있을 경우는 output으로 정의한다.

<end-state id="submit">
	<!-- 상위플로우에 전달해 줄 data를 output으로 셋팅 -->
	<output name="category" value="category" />
</end-state>
input과 마찬가지로 하위 플로우에서 전달받은 output 객체를 상위 클래스에서도 output으로 정의해줘야 한다.
<output name="category" />

28.플로우 상속

플로우 파일을 작성할 때 한 플로우에서 다른 플로우를 상속받아 구현할 수 있으며 flow 레벨과 state 레벨로 가능하다. 보통 global transition이나 global exception을 parent로 정의하여 사용한다. child 플로우에서 parent 플로우를 상속받을 경우 해당 element는 override되는 것이 아니라 merge되게 된다. 단, bean-import, evaluate, exception-handler, persistence-context, render 속성에 대해서는 merge가 불가능하다.

28.1.flow 레벨 상속

flow 레벨 상속은 여러개의 플로우 상속이 가능하다. 이 때 콤마(,)로 구분하여 정의한다.

<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow  
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd" parent="webflowParent1, webflowParent2">

28.2.state 레벨 상속

state 레벨 상속은 한 개의 state에서 한 개의 state만 상속받을 수 있다. 또한, child state와 parent state는 같은 타입이어야 한다. 예를 들어 child state가 view-state 이면 parent state도 view-state여야 한다. flow id와 state id간의 구분자는 "#"이다.

<view-state id="getProduct" model="product"
		view="/WEB-INF/jsp/webflow/sales/product/viewProduct.jsp" parent="webflowParent#stateParent">
		...
</view-state>

29.Validator

Spring Web Flow에서는 Validator를 구현하여 지정된 model 객체에 대한 프로그램적인 Validation을 수행할 수 있다. Validator를 구현하는 방법에는 model 객체에 validate 메소드를 구현하는 방법과 validator 클래스 및 메소드를 구현하는 방법이 있다.

29.1.model 객체 내에 validate 메소드 구현

model 객체 내에 validate 메소드를 구현하여 해당 model 객체에 대한 validation을 수행할 수 있다. 이 때, 메소드의 이름은 일정한 룰에 따라 정의해줘야 하는데 그 이름은 "validate${state}"가 되며 ValidationContext를 입력 argument로 받게 된다. 예를 들어, "domain.Category"라는 타입의 model 객체를 갖는 "addCategoryView" state에 대한 validate 메소드는 아래와 같이 정의할 수 있다.

public class Category implements java.io.Serializable {
//....
       public void validateAddCategoryView(ValidationContext context){
               MessageContext messages = context.getMessageContext();
               if(categoryName.length() <=3)
                      messages.addMessage(new MessageBuilder().error().source("categoryName").
                defaultText("카테고리명은 4자 이상이어야 합니다. ").build());
       }

29.2.validator 클래스 및 메소드 구현

위와 같이 model 객체 내에 validate를 정의할 경우 각각의 model 객체가 validate 메소드를 포함하고 있는 모습이 된다. Validator 클래스를 따로 작성하여 Validation 체크 부분에 대한 코드를 따로 관리할 수 있다. 이 때, 클래스 명은 "${model}Validator"가 되며 state에 따라 수행되게될 메소드의 이름은 "validate${state}"가 된다. 또한 입력 argument는 model 객체와 ValidationContext가 된다.

@Component
public class CategoryValidator {
	public void validateGetCategory(Category category, ValidationContext context) {
		MessageContext messages = context.getMessageContext();
		if (category.getCategoryName().length() <= 3)
		       messages.addMessage(new MessageBuilder().error().source(
				"categoryName").code("category.length.error").build());
		}
	}
위에 코드에서 볼 수 있듯이 Validator 클래스를 @Component로 정의하여 Bean으로 등록하여 scan할 수 있도록 한다.

29.3.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.webflow.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.webflow를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.webflow를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.webflow를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-task-2.0.10.jar 파일이 있어야 한다.)

    표 29.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.webflow.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

  • 참고자료

VII.Struts

Apache Struts Framework는 Java web application을 개발하기 위한 open-source framework이다. Struts는 software application의 separate concerns중 하나인 Model-View-Controller(MVC) architecture를 기반으로 web application을 개발할 수 있도록 도와주고 있다. Struts에서는 Controller를 ActionServlet형태로 제공하고, JSP Taglib를 사용하여 View 레이어를 구현하도록 가이드하고 있다. 또한 Spring Framework의 Web Struts에서는(spring-webmvc-struts.jar), Spring Framework을 기반으로 비즈니스 레이어를 구성할 경우 Struts의 WebApplicationContext에 Spring Bean으로 등록된 비즈니스 레이어의 인터페이스에 접근할 수 있는 환경을 제공한다.

Struts의 특징은 다음과 같다.

  • MVC architecture를 따르고 있기 때문에 역할 분리가 명백하다.

    Model과 View를 연결하는 Controller인, ActionServlet은 입력된 HTTP Request에 따라 Action클래스를 실행하고, Action클래스는 Model에 해당하는 비즈니스 레이어나 Database관련 로직을 수행한다. 그리고 Model과 View사이의 data 전달을 위한 ActionForm 클래스를 활용할 수 있다.

  • JSP로 구현하는 View 개발을 편리하게 도와주는 Taglib를 제공한다.

    Struts에서 기본으로 제공하는 Taglib에는 Bean, HTML, Logic, Nested가 있다.

  • Configuration 설정으로 Exception Handling이 가능하다.

    struts-config.xml파일의 exception handling을 이용하여 Exception종류에 따른 Exception page를 간편하게 설정할 수 있다.

  • Configuration 설정으로 권한처리가 가능하다.

    특정 URL에 권한을 부여하여 허가된 사용자만이 해당 URL에 접근이 가능하도록 설정할 수 있다.

  • Controller에서 Validation Check가 가능하다.

    사용자의 입력값을 View가 아닌 Controller에서 Validation Check를 할 수 있다.

  • MessageResource를 이용한 국제화(I18N)기능을 지원한다.

    사용자 Locale정보에 따라 다양한 언어로 web page 출력이 가능하다.

30.Architecture

Struts의 Architecture는 Model-View-Controller 디자인 패턴을 기반으로 하고 있다. 따라서 Model과 View를 분리함으로써 Single domain model 기반의 다양한 프리젠테이션을 구성할 수 있는 환경을 제공한다.

30.1.Controller Structure

J2EE 의 front controller 패턴을 따라 하나의 컨트롤러를 사용한다. 따라서 Controller를 통해 보안, 국제화, 로깅과 같은 서비스를 집중해서 처리할 수 있는 이점을 제공하며, 다음과 같은 역할을 수행한다.

  • 클라이언트로 부터 들어오는 Request를 가로채는 역할

  • 특정 비즈니스 로직에 각 Request를 매핑하는 역할

  • 현재 상태와 비즈니스 로직 처리 결과를 바탕으로 뷰를 결정하여 클라이언트에 보여주는 역할

30.2.Request의 흐름

다음 그림은 Struts를 이용해 프리젠테이션 레이어를 구성할 때의 Architecture이다.

클라이언트로부터 Request가 전달되면 다음과 같은 순서로 실행된다.

  • 웹 어플리케이션이 시작되면 web.xml에 설정된 servlet정보를 이용해 Struts관련 설정 파일(struts-config.xml)이 로딩된다.

  • 이 때 struts-config.xml에 정의된 RequestProcessor가 들어온 Request에 따라 Action 매핑을 결정한다.

  • struts-config.xml에 설정된 URL과 Action 매핑 정보에 따라 해당 Action클래스의 execute() 메소드를 실행 한다.

  • Action의 execute() 메소드에서는 비즈니스 레이어와 연계하여 비즈니스 로직을 호출한다.

  • 비즈니즈 로직 수행 결과에 따라서 ActionForward를 리턴하고 Controller는 리턴값에 따라 적절한 View로 forwarding한다.

31.Configuration

Struts를 사용하기 위해서는 기본적인 환경 설정이 필요하다. 먼저 웹 어플리케이션의 배포지시자인 web.xml 파일에 <servlet>설정 등이 필요하고 Struts의 Controller가 어떤 Action을 실행할 것인지 어떤 화면으로 이동할 것인지 등에 대한 설정 정보를 설정하는 struts-config.xml이 필요하다. 본 문서에서는 Struts를 사용해 웹 어플리케이션을 개발하기 위한 최소 조건과 Struts에서 많이 사용하는 기능에 대한 환경 설정에 대해서 중점적으로 다루겠다.

31.1.web.xml

웹 어플리케이션의 배포 지시자(Deployment Descriptor)로 J2EE 환경에서 웹 어플리케이션이 어떻게 배포되어야 하는지를 기술하는 파일이다. XML 구문으로 기술되며 웹 어플리케이션 root 바로 아래 서브 디렉토리인 WEB-INF에 위치한다. 본 문서에서는 ActionServlet을 확장한 Anyframe의 DefaultActionServlet을 사용 할 경우 web.xml 작성법을 중심으로 설명하겠다.

ActionServlet에 대한 내용은 Apache Struts User Guide 를 참조하기 바란다.

  • servlet, servlet-mapping 설정

  • taglib 설정

31.1.1.servlet, servlet-mapping 설정

31.1.1.1.<servlet>설정

  • org.apache.struts.action.ActionServlet 또는 서브 클래스를 <servlet-class>에 등록

  • 해당 Servlet을 통해 로드되어 할 Struts 속성 정의 파일 목록 정의

  • Servlet 초기화에 필요한 속성 정의

다음은 <servlet>하위에 <init-param>으로 정의할 수 있는 초기화 파라미터들이다.

NamePurpose / Default Value
configDefault application의 struts 설정 파일이 있는 상대(module-relative)경로를 나타낸다. Default value는 /WEB-INF/struts-config.xml이다.
config/sub1Sub-application을 사용할 때, config/와 sub-application의 prefix를 사용하여 기술한다. 예로, hello라는 prefix를 가진 sub-application이 있다면, config/hello라는 이름으로 <init-param>을 기술해야 한다. 또한, sub-application이 여러 개 있다면 그 각각에 대해서 <init-param>을 기술해야 한다. (여기서, config와 config/sub1, config/sub2 등을 각각 하나의 module이라고 부른다.)
convertNull강제로 forms의 property 값들을 null로 populate한다. 예를 들어, convertNull 값이 true이면, java.lang.Integer type의 property들은 디폴트 값으로 0이 아닌 null이 셋팅된다. convertNull의 디폴트 값은 false이다.
chainConfigaction에서 definition명으로 forwarding할 때 설정을 해야 한다. Struts 1.3에서 새롭게 추가된 내용이므로 Struts Tiles를 이용할 시 주의해야 한다. 디폴트 값은 org/apache/struts/chain/chain-config.xml이다.
configFactoryModuleConfig interface의 implementation을 생성하기 위한 ModuleConfigFactory의 Java class name이다. (Struts 1.3 이상)
debugServlet의 Logging 수준을 결정한다. 디폴트 값은 0이고, 0~6 사이의 수를 넣을 수 있다. 6이 가장 많은 양의 logging 정보를 출력할 것이다.

31.1.1.2.<servlet-mapping>설정

  • Request의 URL 패턴을 <servlet>과 매핑

31.1.1.3.Samples

다음은 Struts를 기반으로 구성된 웹 어플리케이션의 web.xml 파일에서 <servlet>, <servlet-mapping>을 설정한 예제이다.

*.do URL 패턴의 request에 대하여 front controller 서블릿인 ActionServlet이 처리하도록 설정되어 있으며, 초기화 파라미터로 해당 웹 어플리케이션에서 사용하는 struts-config 설정 파일이 복수개로 등록되어 있다.(','로 복수개 설정) convertNull의 초기화 파라미터 설정도 확인할 수 있다. load-on-startup 설정은 서블릿 엔진이 시작될 때 로드될 우선 순위를 지정한 값이다.

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
        <init-param>
        <param-name>config</param-name>
        <param-value>
            /config/struts/struts-config.xml,
            /config/struts/struts-config-login.xml
        </param-value>
    </init-param>
    <init-param>
        <param-name>debug</param-name>
        <param-value>3</param-value>
    </init-param>
    <init-param>
        <param-name>convertNull</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>0</load-on-startup>
</servlet>
	
<servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
</servlet-mapping>

31.1.2.taglib 설정

31.1.2.1.JSP에서의 설정

Struts 1.3에서는 Servlet 2.3과 2.4 모두 tag library의 설정이 간단해 졌다. struts-taglib.jar파일을 /WEB-INF/lib에 복사한 후 아래 sample과 같이 사용할 tag에 대한 정의를 JSP에 추가하면 된다.

31.1.2.2.Samples

다음은 'bean', 'html', 'logic' tag library를 사용하는 경우 JSP에 어떻게 정의해야 하는지를 보여주는 예제이다.

<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
<%@ taglib uri="http://struts.apache.org/tags-logic" prefix="logic"%>

31.2.struts-config.xml

Struts 기반의 웹 어플리케이션을 위한 배포 지시자로 Model, View, Controller를 함께 연결시켜 주는 주된 설정 파일이다. 본 문서에서는 자주 사용하는 attribute들에 대해서만 소개하고 자세한 내용은Apache Struts User Guide 와 struts-config online DTDDoc 를 참고한다.

  • 코드에서 설정 정보 분리

  • Struts DTD : struts-config_1_3.dtd를 준수

  • Action, ActionForm, JSP 간의 매핑 정보 저장

  • 하나 이상의 configuration file 사용 가능

  • 서브 모듈 별 설정 파일 정의 가능

Struts configuration 파일의 설정 요소

31.2.1.controller

31.2.1.1.<controller>설정

  • Request를 받은 ActionServlet은 실제 처리를 RequestProcessor에게 위임함

  • 각각의 sub-application에 대한 controller를 다르게 설정하여 모듈별로 분리할 수 있음

  • org.apache.struts.action.RequestProcessor 또는 서브 클래스를 <controller>에 등록

다음은 <controller>의 attribute들이다.

NameDescription
bufferSize파일 업로드 시 사용하는 입력 버퍼의 크기. 이 속성은 선택적으로 사용할 수 있다. 디폴트 값 : 4096
className컨트롤러 정보를 포함한 Config 빈을 구현하는 클래스의 full name. org.apache.struts.config.ControllerConfig를 상속받은 클래스이어야 한다. 디폴트 값 : org.apache.struts.config.ControllerConfig
contentTypeResponse 결과를 보낼 때 사용하는 컨텐트 타입. 이 속성은 선택적으로 사용. 이 속성에 지정한 값이 있더라도 액션이나 JSP 페이지에서 지정한 컨텐트 타입이 우선한다. 디폴트 값 : text/html
forwardPattern

/로 시작하는 (contextRelative 속성이 false일 경우) context-relative URL과 <forward>의 'path'속성이 어떻게 매핑되는지에 대한 대체 패턴. 이 값은 아래 항목의 조합으로 이루어진다.

디폴트 값 : $M$P

  • $M – Module의 prefix값으로 대체

  • $P – 선택된 <forward>의 'path' 속성값으로 대체

  • $$ - URL 에서 $ 표시 그대로 표현됨

  • $x(x는 위에 정의되지 않은 다른 문자) – 추후 사용을 위해 예약됨

inputForward<action>의 input attribute를 최종 URL로 사용할 forward 또는 global-forward의 이름이길 원하면 true로 설정한다. false 이면 sub application의 상대경로로 간주한다. 디폴트 값 : false
locale사용자의 locale정보를 사용자 세션에 저장할지 여부 디폴트 값 : true
maxFileSize파일 업로드 시 파일의 최대 용량으로 K,M,G 단위를 붙여 사용한다. 디폴트 값 : 250M
multipartClassMultipart 요청에 대한 처리를 담당하는 클래스의 전체 이름. 파일 업로드 시 사용. 디폴트 값 : org.apache.struts.upload.CommonsMultipartRequestHandler
nocacheResponse에서 HTTP 헤더를 'nocache'로 설정 할 지에 대한 Boolean 값. Optional 디폴트 값 : false
pagePattern

커스텀 태그를 사용하는 페이지의 page 속성이 컨텍스트 상대 URL 경로에 매핑 방법에 대한 대체 패턴. 이 속성의 값은 아래 항목들로 구성된다.

디폴트 값 : $M$P

  • $M – Module 접두어에 의한 대체

  • $P – 선택한 forward 요소의 path 속성에 의한 대체

  • $$ - URL 에서 $ 표시 그대로 표현됨

  • $x(x는 위에 정의되지 않은 다른 문자) – 추후 사용을 위해 예약됨

processorClass사용자의 요청을 처리할 클래스의 full name. 여기에 지정한 클래스는 org.apache.struts.action.RequestProcessor 클래스의 하위 클래스가 된다. 디폴트 값 : org.apache.struts.chain.ComposableRequestProcessor
tempDir파일 업로드 시 사용할 임시 디렉터리. Optional 디폴트 값 : Servlet container가 제공하는 디렉터리

31.2.1.2.Samples

다음은 struts-config.xml 파일에서 controller 설정에 대한 예제이다.

<controller
        contentType="text/html;charset=utf-8" locale="true" nocache="true"
        processorClass="org.apache.struts.action.RequestProcessor"/>

31.2.2.message-resources

31.2.2.1.<message-resources>설정

  • 메시지 리소스 번들과 관련된 사항들을 정의

  • 국제화 지원(I18N)

  • Application에 여러 개의 <message-resources>를 등록해 사용 할 수 있음

  • 개별 Module은 각각이 사용하는 resource bundle을 정의할 수 있음

  • Application에 여러 개의 resource bundle을 사용하려고 할 때는 key값을 설정해 줘야 함

다음은 <message-resources>의 attribute들이다.

NameDescription
classNamemessage-resources의 정보를 포함하는 설정 빈을 구현하는 클래스 이름. 여기에 지정한 클래스는 org.apache.struts.config.MessageResourcesConfig 클래스의 하위 클래스여야 한다.Optional. 디폴트 값 : org.apache.struts.config.MessageResourcesConfig
factory사용할 MessageResourcesFactory 클래스의 이름. 디폴트 값 : org.apache.struts.util.PropertyMessageResourcesFactory
key메시지 리소스 번들이 저장될 ServletContext 속성. 이 속성은 optional. 디폴트 값 : org.apache.struts.action.MESSAGE
null정의되지 않은 메시지 키가 사용된 경우 이를 MessageResources의 하위 클래스에서 어떻게 처리할지 나타내는 Boolean 값. false로 설정하면 null이 아닌 "???keyname???" 과 같은 문자열을 되돌려준다. 디폴트 값 : true
parameter리소스 번들의 이름. 예를 들어, 리소스 번들 이름이 ApplicationResources.properties 이면 이 속성의 값은 ApplicationResources가 된다. 이 속성은 반드시 정의해야 한다. 만일 리소스 번들이 패키지화되어 있다면 해당 패키지의 이름을 포함한 전체 이름을 지정해야 한다.

31.2.2.2.Samples

다음은struts-config.xml 파일에서 message-resources 설정에 대한 예제이다.

<message-resources
        parameter="message.message-productmgmt"/>

31.2.3.plug-in

31.2.3.1.<plug-in>설정

  • Struts Application 이 구동 시에 동적인 자원을 처리하게 해주는 강력한 기능임

  • org.apache.struts.action.PlugIn 인터페이스를 구현하는 자바 클래스를 생성한 후 설정 파일에 plug-in 요소를 추가하여 사용 가능

  • plug-in 에 대한 예 : validator, tiles 등

다음은 <plug-in>의 attribute이다.

NameDescription
classNamePlug-In 클래스의 전체 이름을 나타내며, 해당 클래스는 반드시 org.apache.struts.action.PlugIn 인터페이스를 구현해야 한다.

31.2.3.2.Samples

다음은 struts-config.xml 파일에서 plug-in 설정에 대한 예제이다.

<plug-in
        className="org.apache.struts.tiles.TilesPlugin"> 
    <set-property property="definitions-config" value="/WEB-INF/struts-tiles-defs.xml"/>
</plug-in>

31.2.4.form-beans

31.2.4.1.<form-beans>설정

  • Action 수행에 사용되는 form bean의 정보 설정

  • <form-bean>하위에 여러 개의 <form-bean> 구성 가능

  • <form-bean>은 하위에 여러 개의 <form-property>를 가질 수 있음

다음은 <form-bean>의 attribute들이다.

NameDescription
classNameForm bean들의 configuration 정보를 담고 있을 객체이다.반드시 org.apache.struts.config.FormBeanConfig를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.config.FormBeanConfig
dynamicForm bean의 type attribute가 org.apache.struts.action.DynaActionForm이거나 org.apache.struts.action.DynaActionForm을 상속 받아 구현한 클래스이면, 이 attribute는 true여야 한다. 디폴트 값 : false
name이 form bean의 이름이고, 다른 form bean들과 구분될 수 있는 identifier이다. [required]
type이 form bean의 구현 클래스를 나타낸다. 이 클래스는 ActionForm의 서브클래스여야 한다. [required]

다음은 <form-property>의 attribute들이다.

NameDescription
classNameForm property들의 configuration 정보를 담고 있을 객체이다. 반드시 org.apache.struts.config.FormPropertyConfig 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.config.FormPropertyConfig
initial이 property의 initial 값을 나타내고, 문자로 표현된다.
nameForm bean이 사용하는 이 property의 이름이다. [required]
sizeForm property의 type attribute가 array일 경우에, 그 array elements의 개수를 나타낸다.
type

Form bean이 사용하는 이 property의 type이다. 다음은 DynaActionForm에서 지원하는 type이다.

  • java.math.BigDecimal

  • java.math.BigInteger

  • boolean

  • byte

  • char

  • java.lang.Class

  • double, int, long, short

  • java.lang.String

  • java.sql.Data

  • java.sql.Time

  • java.sql.Timestamp

31.2.4.2.Samples

다음은 struts-config.xml 파일에서 form-beans 설정에 대한 예제이다.

<form-beans>
    <form-bean
        name="productForm"
        type="anyframe.example.struts.sales.web.form.ProductForm">
    </form-bean>
    <form-bean 
        name="employeeForm"
        type="org.apache.struts.action.DynaActionForm">
        <form-property name="name" type="java.lang.String"/>
        <form-property name="age" type="int"/>
        <form-property name="department" type="java.lang.String" initial="2"/>
        <form-property name="flavorIDs" type="java.lang.String[]"/>
    </form-bean>
</form-beans>

31.2.4.3.DynaActionForm

Struts 어플리케이션에서 각 Form에 대한 별개의 실재하는 ActionForm 클래스를 관리하는 것은 많은 시간을 요한다. DynaActionForm을 사용하게 되면, 위의 두 번째 form-bean 설정과 같이 struts-config.xml 상에 bean의 프로퍼티, 타입, 디폴트 값을 나열함으로써 ActionForm을 직접 작성하지 않아도 된다. 하지만 설정이 복잡하며 성능이 저하되는 문제점이 존재한다.

31.2.5.action-mappings

31.2.5.1.<action-mappings>설정

  • 컨트롤러가 요청을 받았을 때 어떤 Action 인스턴스를 실행할 것인가에 대한 설정 정보

  • <action-mappings>하위에 <action>을 이용해서 여러 개의 action 설정 가능

  • <action> : 특정 request URI와 대응하는 Action 매핑 정의

31.2.5.2.<action>의 주요 attribute

  • path : 확장자를 제외한 "/"로 시작하는 경로명

  • type : action클래스의 이름

  • scope : form bean이 저장되어 있는 context의 scope

  • name : action과 연결된 form bean의 name

  • role : Action 객체에 접근할 수 있는 권한을 설정

  • input : form bean에서 validation error가 발생한 경우 되돌아 가거나 상황을 표시할 수 있는 경로

다음은 <action>의 attribute들이다.

NameDescription
attributeForm bean에 접근하기 위한, request-scope 또는 session-scope attribute의 name 값이다. 사용할 form bean을 다른 attribute의 이름으로 사용하고자 할 때 사용한다. Form bean이 name attribute에 기술되어 있을 때에만 기술될 수 있다.
classNameAction들의 configuration 정보를 담고 있을 객체이다. 반드시 org.apache.struts.config.ActionMapping 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.config.ActionMapping
forward요청된 request를 Action 클래스 대신하여 수행할 resource(*.do, *.jsp 등)의 상대(module-relative) 경로를 나타낸다. [required: 반드시 forward, include, type attribute 중의 하나만 기술되어야 한다.]
include요청된 request를 Action 클래스 대신하여 수행할 resource(*.do, *.jsp 등)의 상대(module-relative) 경로를 나타낸다. [required: 반드시 forward, include, type attribute 중의 하나만 기술되어야 한다.]
inputForm bean에서 validation error가 발생했을 때, 이를 나타낼 resource(*.do, *.jsp 등)의 상대(module-relative) 경로를 가리킨다. Form bean이 name attribute에 기술되어 있을 때에만 기술될 수 있다. [required: form bean이 name attribute에 기술되어 있고 validation error들을 리턴할 경우]
name이 action 매핑 사용하는 form bean의 이름을 나타낸다.
pathSubmit된 request의 상대(module-relative)경로를 나타낸다. 이 attribute는 반드시 "/"으로 시작해야 하고, filename의 확장자 없이 기술되어야 한다. 예를 들어, "/main.do"은 적절한 path attribute의 기술 방법이 아니다. 왜냐하면 이미 do라는 확장자가 action 매핑에 사용되고 있는 것을 알고 있기 때문에, "/main"이라고만 기술하는 것이 옳다. [required]
parameterAction 객체에 특별한 어떤 값을 넘겨주기 위한 설정 parameter이다. 현 Action 클래스에서는 이 attribute를 이용하지 않고 있기 때문에, 값을 넣는다 해도 처리되지 않는다. 만약 이 attribute를 사용하고자 하면, Action 클래스의 서브클래스를 만들어 구현해야 한다.
prefixRequest parameter name을 form bean property name에 매치시키는 데 사용되는 prefix를 나타낸다. Form bean이 name attribute에 기술되어 있을 때에만 설정할 수 있다.
rolesAction 객체에 접근할 수 있는 권한을 설정한다. 여러 role 이름들은 콤마(,)로 구분하여 쓸 수 있다. 예를 들어, "admin, master, user"라고 써주면 admin, master, user의 세 가지 권한 중 어느 한가지 권한이라도 가진 사용자는 이 action을 사용할 수 있게 된다.
scope이 action이 사용하는 form bean이 저장되어 있는 context의 scope를 나타낸다. request 또는 session. 디폴트 값 : session
suffixRequest parameter name을 form bean property name에 매치시키는 데 사용되는 suffix를 나타낸다. Form bean이 name attribute에 기술되어 있을 때에만 설정할 수 있다.
type요청된 request를 수행할 Action 클래스를 나타낸다. 이 클래스는 org.apache.struts.action.Action의 서브클래스여야 한다. [required: 반드시 forward, include, type attribute 중의 하나만 기술되어야 한다.]
unknown설정 파일에 정의되지 않은 request를 처리하는 default action 매핑인지 여부를 나타낸다. 요청된 request를 수행할 action 매핑 객체가 없을 경우에, unknown이 true로 설정된 action 매핑 객체에게 이 request를 넘겨 처리하게 한다. 각각의 module마다 unknown이 true인 action 매핑은 하나만 있을 수 있다. 디폴트 값 : false
validateForm bean에서 validation을 수행할지 여부를 나타낸다. 이 값이 true이면, form bean의 validate 메소드가 실행된다. 디폴트 값 : true
cancellableStruts 1.3에 추가된 attribute로 Struts 1.2.9에서 <set-property>로 설정했던 것이 1.3부터 바뀌었다. Cancel Process를 사용하기 위해서 설정해야 한다.

31.2.5.3.Samples

다음은 struts-config-login.xml 파일에서 action-mappings 설정에 대한 예제이다.

<action-mappings>
    <action
        path="/login"
        type="anyframe.sample.struts.web.action.LoginAction"
        name="userForm"
        scope="request"
        input="/basic/login.jsp">
    <exception key="error.password.mismatch" path="/basic/login.jsp"  
            type="javax.security.auth.login.FailedLoginException" />
    <forward name="success" path="/basic/main.jsp" />
    </action>
	...
</action-mappings>

'/login.do' 의 request에 대해 LoginAction 이 처리하도록 매핑되어 있으며, 이 때 action에 연결된 form bean은 UserForm 이다. request scope 동안 form bean 이 유지되며 forward 경로는 Action클래스에서 "success"라는 이름으로 forward name을 세팅 했기 때문에 /basic/main.jsp로 forwarding한다. exception 발생 시 /basic/login.jsp로 돌려진다.

<action>의 작성은 개발자가 반드시 숙지해야할 부분으로, request의 처리를 담당하는 Action을 매핑하고 페이지 네비게이션을 제어하는 등 웹 어플리케이션 개발의 중요한 작업이다.

31.2.6.global-forwards

31.2.6.1.<global-forwards> 설정

  • 실제 forward 또는 redirect 할 수 있는 URI를 논리적인 이름으로 맵핑

  • <global-forwards>하위에 <forward>를 이용해서 여러 개의 URI 매핑 설정

  • 하나의 <forward>는 하나의 논리적인 이름을 module-relative 또는 context-relative URI 경로로 매핑함 (URI 경로를 직접 사용하는 것 보다 logic 내부적으로 정해진 이름을 사용함으로써, view로부터 controller와 model을 분리)

  • 모든 action에서 사용할 수 있는 global level의 forward를 정의

forward 의 우선순위

<forward>은 전역(global) level과 action level 에서 정의될 수 있는데 action level 에서 선언된 것이 더 우선순위가 높다. 다음은 <forward>의 attribute들이다.

NameDescription
classNameForward들의 configuration 정보를 담고 있을 객체이다. org.apache.struts.config.ActionForward 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.config.ActionForward
name현재 forward의 이름이고, 다른 forward들과 구분될 수 있는 identifier이다. [required]
pathForward 또는 redirect할 resource(*.do, *.jsp 등)의 상대(module-relative or context-relative)경로를 나타낸다. [required]
redirectRequestProcessor가 이 forward에 대해 redirect할 필요가 있을 때, true로 설정한다. 디폴트 값 : false

31.2.6.2.Samples

다음은 struts-config.xml 파일에서 global-forwards 설정에 대한 예제이다.

<global-forwards>
    <forward name="login" path="/login.jsp"/>
    <forward name="main" path="/main.do" redirect="true"/>
</global-forwards>

32.Controller

Controller는 MVC의 Model과 View사이의 중계자 역할을 한다. Struts의 Controller는 사용자의 Request를 처리하고 리소스의 초기화 등을 담당하고 있는 ActionServlet, 사용자 Request로 부터 ActionForm 객체를 생성하고 Action클래스의 execute()를 실행하는 RequestProcessor, 그리고 비즈니스 로직을 호출하고 성공과 실패에 대한 forward 정보를 설정하는 Action클래스로 이루어져있다.

32.1.ActionServlet

ActionServlet은 Struts에서 Controller역할을 담당하는 주요한 클래스이다.

32.1.1.ActionServlet의 역할

  • 사용자의 요청을 받는 단일 진입점의 역할 (Front Controller)

  • 리소스의 초기화와 clean-up 을 담당

32.1.2.초기화 프로세스

ActionServlet의 init() 메소드에서 다음과 같이 초기화 절차가 진행된다.

  • Struts 내부에서 사용되는 에러나 경고에 사용되는 메세지 초기화

  • web.xml에서 init-param으로 정의된 정보(debug, config, detail 등)들을 초기화

  • web.xml에서 설정한 servlet-mapping 정보 초기화

  • ServletContext에 ActionServlet 객체 저장

  • init-param의 'config'에 의해 정의된 디폴트 모듈 정보(정의되지 않은 경우 '/WEB-INF/struts-config.xml')를 이용하여 ApplicationConfig 객체를 생성

  • struts-config.xml에 메세지 리소스 관련 설정이 있을 경우 초기화

  • struts-config.xml에 DataSource가 설정되어 있을 경우 초기화

  • struts-config.xml에 정의된 Plug-In정보를 초기화

32.1.3.실행 시(ActionServlet 인스턴스가 HTTP Request를 받을 때)

  • Request의 prefix에 따라 해당되는 서브 어플리케이션을 찾음

  • RequestProcessor를 찾아 process() 메소드 호출

32.1.4.ShutDown 프로세스

  • RequestProcessor의 destroy() 메소드 호출

  • <plug-in>에 의해 정의된 값이 존재하는 경우 해당 destroy() 메소드 호출

  • <data-sources>에 의해 정의된 값이 존재하는 경우 해당 close() 메소드 호출

32.2.RequestProcessor

RequestProcessor는 개발자가 필요에 따라 Request 처리에 대한 내용을 확장할 수 있도록 Struts 1.1부터 제공되기 시작했다. 이로 인해 ActionServlet과 RequestProcessor 클래스가 분리되어 어플리케이션 모듈별로 각각의 RequestProcessor를 가질 수 있다는 장점이 있다. RequestProcessor는 기존에 ActionServlet이 제공하던 기본적인 기능에 더하여 확장 가능한 다양한 메소드를 제공한다.

Anyframe 에서는 RequestProcessor를 상속받아 DefaultRequestProcessor로 확장한 클래스를 제공한다.

32.2.1.RequestProcessor의 역할

  • Request로부터 데이터를 받아서 ActionForm을 생성

  • Action의 execute() 메소드실행

  • ActionForm 전달

  • Configuration에 정의된 대로 forward나 redirect 수행

  • 어플리케이션의 configuration정보 유지

32.2.2.process() 메소드의 Request 처리 절차

  • processMultipart(): HTTP Request의 content type이 multipart/form-data일 경우 새로운 Request Wrapper 생성

  • processPath(): Request의 URI에서 ActionMapping처리를 위한 "path" 값을 추출

  • processLocale(): Request로 부터 Locale 정보를 추출하여 session에 저장

  • processContent(): Request의 content type과 encoding 정보를 설정

  • processNoCache(): struts-config.xml에서 <controller>의 nocache 값이 true로 설정되었을 경우 HTTP Response의 header에 브라우저의 cache를 사용하지 않도록 설정

  • processPreprocess(): Request가 처리되기 전에 수행되어야 하는 작업이 있을 경우 RequestProcessor를 상속받아서 확장하는 클래스에서 이 메소드를 override하여 구현

  • processMapping(): 앞에서 추출한 "path"정보를 이용하여 ActionMapping 검색

  • processRoles(): 사용자가 현재 Request를 수행할 수 있는 Role을 가지고 있는지 확인

  • processActionForm(): ActionMapping에 설정된 ActionForm이 존재할 경우, 설정 파일에서 정의한 scope(session 또는 request)에서 ActionForm을 검색하고, 없을 경우 새로운 ActionForm을 생성하여 해당 scope에 저장

  • processPopulate(): HTTP Request 파라미터들을 ActionForm에 저장

  • processValidate(): 설정 파일에서 'validate' 값이 true로 설정된 경우, ActionForm의 validate() 메소드를 호출.

  • processForward(): 설정 파일에서 <action>에 forward나 include 속성이 정의되어 있는 경우, RequestDispatcher의 forward(), include() 메소드를 호출

  • processActionCreate(): Request에 해당하는 Action 객체 생성. 최초에 한번 생성된 후 ServletContext에 저장되어 재사용 됨

  • processActionPerform(): Action 객체의 execute() 메소드 호출

  • processActionForward(): Action 객체의 execute() 메소드의 리턴 값인 ActionForward에 해당하는 URL로 forward

32.2.3.Sample

아래는 RequestProcessor 를 struts-config.xml의 <controller>로 설정한 예이다.

<controller
      contentType="text/html;charset=UTF-8" debug="3" locale="true"
      nocache="true"
      processorClass="org.apache.struts.action.RequestProcessor"/>

위의 processorClass 에는 필요에 따라 RequestProcessor를 확장한 Controller를 설정할 수 있다. Anyframe 에서는 RequestProcessor 를 확장한 DefaultRequestProcessor 를 제공한다.

32.3.Action

Action 클래스는 비지니스 로직을 호출하고 성공과 실패에 대한 적절한 forward 정보를 설정한다.

32.3.1.Action의 역할

  • Action의 execute()는 클라이언트의 요청과 Business Logic을 연결

  • Action은 MVC 구조에서 Controller와 Model사이를 이어주는 역할

32.3.2.Action의 구현

  • 하나의 인스턴스가 그 action으로 매핑된 모든 request를 처리하므로 execute() 메소드를 thread-safe 메소드로 구현해야 함

  • Action 클래스에서 실행하는 Business Logic은 한 업무 처리에만 관련이 있어야 함

  • execute()는 항상 ActionForward를 리턴해야함

  • 비즈니스 로직을 호출하는 로직만 존재해야 함, 비즈니스 로직은 모델 객체에 구현해야 함

    Action 인스턴스는 단일 인스턴스로 모든 클라이언트 요청이 같은 인스턴스를 공유하므로 특정 클라이언트의 정보를 Action 클래스의 멤버변수로 저장하는 것은 잘못된 구현이다. 클라이언트의 정보가 필요한 경우 request나 session에 저장하도록 하고 특정 클라이언트의 상태를 나타내기 위해서는 execute() 메소드내에서 지역변수를 사용하면 각 스레드별로 영향을 끼치지 않는다. Action 클래스의 작성은 개발자가 반드시 숙지해야 할 부분으로, request로 전달된 사용자 입력 데이터를 추출하여 비지니스 로직을 호출하고 그 결과를 다시 클라이언트로 포워드하는 중요한 작업이다.

32.3.3.Sample

  • Action 클래스의 작성 예

    아래는 로그인에 필요한 사용자 입력 값을 받아 체크한 후 Session에 저장하는 LoginAction.java 의 소스코드이다.

    public class LoginAction extends Action{
    
        public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) 
            throws Exception {
    			
            UserForm userForm = (UserForm) form;
    		
            String userId = userForm.getUserId();
            String password = userForm.getPassword(); 
    		
            //사용자 Id, Password 체크
            if ((userId != null && userId.equals("anyframe"))
                && (password != null && password.equals("anyframe"))) {
    			
                //로그인 성공시 Session에 사용자 정보를 저장한다.
                Set principals = new HashSet();
                Set credentials = new HashSet();
    			
                //사용자의 이름과 권한을 저장한다.
                principals.add(new TypedPrincipal("Anyframe", TypedPrincipal.USER));
    
                principals.add(new TypedPrincipal("ADMIN", TypedPrincipal.GROUP));
    
                Subject subject = new Subject(false, principals, credentials,
                            credentials);
    
                HttpSession session = request.getSession();
    
                session.setAttribute("subject", subject);
        
            } else {
                throw new FailedLoginException();
            }
            return (mapping.findForward("success"));
        }
    }

    execute()메소드가 호출되면 ActionForm에 저장된 userid 와 password 값을 받아오고 사용자 인증이 성공하면(위에서는 비지니스 서비스없이 임시로 anyframe/anyframe 에 대해 ADMIN 사용자로 체크함) Subject객체를 Session에 설정한 후 sucess로 지정된 ActionForward를 리턴한다. 인증에 실패할 경우에는 FailedLoginException을 발생시켜 struts-config.xml에 설정된 Exception처리 설정을 따른다.

  • Action 매핑 예

    아래는 위의 LoginAction.java의 매핑 정보를 설정한 struts-config-login.xml 의 일부이다.

    <action
        path="/login"
        type="anyframe.sample.struts.web.action.LoginAction"
        name="userForm"
        scope="request"
        input="/basic/login.jsp">
        <exception key="error.password.mismatch" path="/basic/login.jsp" 
            type="javax.security.auth.login.FailedLoginException" />
        <forward name="success" path="/basic/main.jsp" />
    </action>

  • Spring Bean 형태의 비즈니스 서비스 Invoke

    Spring Framework에서는 Struts와의 통합을 위해 spring-webmvc-struts의 ActionSupport(내부적으로 Struts의 Action을 extends하고 있음)를 제공하고 있다. Struts의 Action클래스 대신 ActionSupport 클래스를 extends하면 Spring Framework Bean형태의 비즈니스 서비스에 쉽게 접근이 가능하다. 사용방법은 다음과 같다.

    public class SampleAction extends ActionSupport {
        public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {
    		
            ApplicationContext ctx = getWebApplicationContext();
            BusinessService businessService = 
                    (BusinessService)ctx.getBean("businessService");
     		//중략 
            return mapping.findForward("success");
        }
    }

32.4.ActionForward

32.4.1.ActionForward의 역할

  • JSP 페이지나 서블릿 같은 웹 리소스의 논리적인 추상화

  • 물리적인 리소스에 대한 정보는 설정 파일에 저장

Action 클래스에서 execute() 메소드는 ActionForward 객체를 리턴한다. ActionForward는 리소스를 감싸서 어플리케이션과 물리적인 리소스를 분리하며 설정 파일에 forward의 name, path, redirect 속성과 같은 요소들로 정의하고 코드에는 포함하지 않는다. RequestDispatcher는 redirect 요소의 값에 따라 ActionForward의 포워드나 리다이렉트를 실행하게 된다. Action에서 ActionForward를 반환할 때 설정 파일에 미리 정의된 ActionForward를 알아내기 위해 일반적으로 ActionMapping을 사용한다. 다음 코드는 ActionMapping을 사용하여 논리적 이름에 근거해 ActionForward를 찾는 방법을 보여준다.

return mapping.findForward("success");

32.5.Actions Package

Struts 에는 어플리케이션에서 쉽게 통합할 수 있는 out-of-the-box 형태의 Action 클래스 5개를 포함하고 있으며 이를 이용해 개발 시간을 단축할 수 있다.

32.5.1.org.apache.struts.actions 패키지에 미리 정의되어 있는 Action

  • org.apache.struts.actions.ForwardAction

  • org.apache.struts.actions.IncludeAction

  • org.apache.struts.actions.DispatchAction

  • org.apache.struts.actions.LookupDispatchAction

  • org.apache.struts.actions.SwitchAction

다음 그림은 Struts 의 org.apache.struts.actions 패키지에 해당하는 클래스들의 간단한 클래스 다이어그램이다.

32.5.2.org.apache.struts.actions.ForwardAction

단순히 포워딩만 실행하는 경우 Action 클래스를 직접 구현하지 않고 struts에서 미리 구현된 ForwardAction을 사용한다. 이 Action 클래스는 파라미터 속성에 정의된 URI로 포워드를 실행한다.

<action
    path="/loginView"
    type="org.apache.struts.actions.ForwardAction"
    parameter="/basic/login.jsp">
</action>

request url이 loginView.do이면 /basic/login.jsp로 포워드하게 된다. Action 클래스를 통하지 않고 JSP를 직접 호출하는 것은 바람직하지 못하다. MVC 구조에서 Controller의 책임을 위반할 뿐만 아니라 Struts에서 처리하는 중간 단계를 건너뛰게 되므로 문제가 발생할 수 있다.(ex. 리소스 번들에서 올바른 메시지를 가져오지 못함)

32.5.3.org.apache.struts.actions.IncludeAction

ForwardAction과 비슷하며 기존에 있던 서블릿 기반 컴포넌트들을 스트럿츠 기반 웹 어플리케이션과 쉽게 통합하기 위해 제공하는 것이다.

<action
    input="/subscription.jsp"
    name="subscriptionForm"
    path="/saveSubscription"
    parameter="/path/to/processing/servlet"
    scope="request"
    type="org.apache.struts.actions.IncludeAction" />
</action>

32.5.4.org.apache.struts.actions.DispatchAction

기능마다 각각의 Action 클래스를 만들게 되면 클래스 수가 많아져서 관리하기가 어렵다. Struts에서는 관련된 기능을 하나의 Action으로 모을 수 있는 DispatchAction을 제공한다. 예를 들면 add, update, view 와 같은 여러 기능을 하나의 Action(extends DispatchAction) 안에 메소드로 구현하면 (cf. 일반 Action에서는 execute() 메소드 구현 ) 설정 파일의 parameter 속성값으로 지정한 문자열 키값(아래에서는 "method"로 지정하였음. 다른 문자열을 써도 됨.)으로 메소드를 호출할 수 있게 된다. 아래는 DispatchAction을 extends한 ShoppingCartAction.java 의 소스코드이다.

public class ShoppingCartAction extends DispatchAction {

    public ActionForward add (
        ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception{
	    // TODO : add 기능 관련 로직
        return mapping.findForward("add");
    }

    public ActionForward update (
    ...
    // TODO : update 기능 관련 로직
    return mapping.findForward("update");
    }

    public ActionForward search (
    ...
    // TODO : search 기능 관련 로직
    return mapping.findForward("search");
    }

    public ActionForward view (
    ...
    // TODO : view 기능 관련 로직
    return mapping.findForward("view");
    }
}

ShoppingCartAction에 대한 action 매핑 정보를 설정한 struts-config-dispatch.xml 의 일부이다.

<action
    path="/shoppingCart"
    type="anyframe.sample.struts.web.action.ShoppingCartAction"
    name="dispatchForm"
    parameter="dispatchMethod"
    scope="request">
    <forward name="add" path="/dispatchActionView.do" />
    <forward name="list" path="/dispatchActionView.do" />
    <forward name="update" path="/dispatchActionView.do" />
    <forward name="delete" path="/dispatchActionView.do" />
</action>

32.5.5.org.apache.struts.actions.LookupDispatchAction

DispatchAction과 비슷하나 메소드를 찾을 때 parameter 속성에서 찾는 것이 아니라 리소스 번들에 키 값을 이용해 찾게 된다. LookupDispatchAction을 사용하게 되면 하나의 HTML Form에 submit 버튼이 여러 개 있는 경우 더 쉽게 처리할 수 있다.

32.5.6.org.apache.struts.actions.SwitchAction

이 클래스는 하나의 어플리케이션 모듈에서 다른 모듈로 스위칭을 지원하고 어플리케이션의 리소스를 컨트롤한다. prefix, page 파라메터를 통하여 어플리케이션 모듈을 전환할 수 있다. 사용 예는 다음과 같다.

<action path="/toModule"
          type="org.apache.struts.actions.SwitchAction"/>
/toModule.do?prefix=/moduleB&page=/index.do

디폴트 모듈로 전환하고자 하는 경우 prefix를 null String으로 설정하여 호출할 수 있다.

/toModule.do?prefix=&page=/index.do

33.View

View 는 클라이언트가 모델의 상태를 보기 위해 사용하는 창(window)이다. 하나의 모델은 여러 창, 즉 뷰를 포함할 수 있으며 클라이언트가 어떤 view를 통해 모델을 보느냐에 따라 화면이 달라진다. 표시 방법으로 XML, XSLT, SOAP, HTML 등 다양한 방법을 택할 수 있으며 Anyframe 에서는 주로 자바 코드와 태그로 View를 구성하며 JSP를 기반으로 클라이언트에 동적인 컨텐츠를 제공하게 된다.

View의 역할

  • 사용자 입력 데이터 수용 및 데이터 표시

  • 입력 데이터 검증

  • 에러처리

  • 국제화

자주 사용되는 View 형태

  • HTML : 브라우저를 통해 최종사용자가 보는 페이지이다.

  • JSP Custom Tag : Struts 어플리케이션에서 매우 중요한 역할을 한다, 필수는 아님.

  • JavaScript and StyleSheet : 사용을 금지하지 않으며 적절히 사용하면 효과적인 view를 만들 수 있다.

  • MessageResource Bundle : Localization 기능을 제공하며 유지보수 시간을 절약할 수 있다.

  • Multimedia file

View의 구성 요소는 JSP 및 관련 기술과 ActionForm이 있으며 해당 내용은 아래와 같다.

  • JSP

    • 서버에서 동적 웹 컨텐츠를 생성하는 자바 플랫폼 기술, Servlet Container 상에서 수행되는 서버 스크립트 언어

    • 서블릿으로 변환되어 수행되며 최초 요청 시 해당 페이지에 대한 컴파일 된 서블릿 인스턴스는 한번만 생성되므로 서버 자원을 효율적으로 사용

    • 강력한 이식성, 빠른 수행속도, 프리젠테이션 로직과 비즈니스 로직 분리, 컴포넌트의 재사용, 커스텀 태그의 사용으로 인한 개발 편의성 등이 장점

  • Javascript

    • 넷스케이프에서 만든 인터프리터 형 스크립트 언어

    • 자바스크립트 코드는 HTML 페이지 내에 삽입될 수 있으며, 클라이언트 측인 웹 브라우저에 의해 해석됨

    • 객체 지향 모델로써 구조화된 문서를 표현하는 표준 형식인 DOM(Document Object Model) 객체를 다루고 프로그램으로 조작(manipulate)함

  • CSS

    • Cascading Style Sheet는 마크업 언어(HTML, XHTML, XML)가 실제 표시되는 방법을 기술하는 언어로 W3C의 표준임

    • 스타일 정보의 수정 시 홈페이지 전체에서 이에 해당하는 요소들이 한꺼번에 반영됨

    • 각기 다른 사용자 환경에서도 동일한 형태의 문서를 제공

    DHTML : 정적 마크업 언어인 HTML, 클라이언트 기반 스크립트 언어(ex. Javascript)와 스타일 정의 언어인 CSS를 조합하여 대화형 웹 사이트를 제작하는 기법을 의미함

ActionForm

웹 어플리케이션에서 사용자의 입력을 받을 때 페이지에는 텍스트 박스, 버튼 등과 같은 컴포넌트들이 HTML 의 폼 요소 내에 포함되어 있고 사용자가 버튼을 누르게 되면 필드 내에 있는 값들이 HTTP request와 함께 서버로 submit 된다. 서버 어플리케이션은 request에서 이 입력 값들을 꺼내어 올바른 데이터를 입력했는지 validation을 수행하고 나서 실제 비지니스를 수행하기 위해 Action으로 데이터를 넘기게 된다. 만일 입력 데이터가 validation rule을 통과하지 못한 경우 에러 메시지를 설정하여 입력 페이지로 돌아가게끔 처리해야 한다. 이처럼 요청에서 입력 값을 꺼내어 검증을 수행하고 실패에 대한 에러 메시지를 출력하는 등의 기능을 직접 구현하는 것은 쉬운 일이 아니다. 또한 이런 작업은 전체 어플리케이션 내에서 반복해서 일어나므로 재사용하는 것이 좋다. 이러한 작업들을 해주는 것이 org.apache.struts.action.ActionForm 클래스 이다.

  • ActionForm의 역할

    • 요청에서 입력 값을 꺼내어 검증 수행, 실패에 대한 에러 메시지를 출력하는 등의 일련의 처리 과정을 재사용

    • ActionForm은 클라이언트의 입력 값을 Action으로 전달하고, 결과를 되돌려줄 수 있음

    • 입력 데이터들을 검증하는 동안 상태를 보관하는 버퍼로 동작

    • 확실하지 않은 입력 값들을 검증 룰을 통해 세밀하게 조사하기 전까지 비즈니스 계층 밖에 위치하도록 해주는 firewall 역할

    • ActionForm을 화면 표시 데이터로 설정하여 HTML 폼의 입력 필드를 쉽게 표시할 수 있음

    Html 입력 Form으로 부터 받은 parameter 들은 자동으로 ActionForm 객체에 채워진다. 검증을 위한 validate() 메소드와 parameter가 ActionForm에 채워지기 전에 초기화 하는 reset() 메소드를 구현해야 한다. struts-config.xml 에 ActionForm 에 대한 <form-beans> 정의가 필요하다. ActionForm은 Model의 부분이 아니다. 비즈니스 처리를 수행하기 위한 Model 영역은 Controller / View 와 완전히 분리하여야 하며 직접 비즈니스 계층으로 전달해서는 안되고 ValueObject 나 Parameters 같은 형태의 Data Transfer Object를 생성하여 전달하도록 해야 한다.

  • ActionForm의 단점

    • 어플리케이션 개발자가 ActionForm의 서브클래스를 직접 구현해야 함

    • 많은 수의 클래스가 생겨날 수 있어서 유지보수 관리 어려움

    • validate() 메소드를 구현하려면 DynaActionForm을 상속받아 직접 구현해야함. Validator 프레임워크를 이용하는 것이 좋음.

  • DynaActionForm

    • 실제 구현 클래스들을 만들 필요가 없음

    • Property는 configuration파일에서 설정

    • validate() 메소드를 구현하려면 DynaActionForm을 상속받아 직접 구현해야함. Validator 프레임워크를 이용하는 것이 좋음.

  • ActionForm의 scope

    • ActionForm 객체가 저장되어 유지되는 context의 scope를 나타낸다.

    • session, request 2가지 레벨이 있다.

    • struts-config.xml 의 <action>의 scope 속성으로 설정한다. default는 session이다. session scope일 경우 ActionForm의 제거에 유의해야 한다.

  • ActionForm의 LifeCycle

    1. 액션의 매핑 정보를 확인하고 ActionForm이 설정되어 있는지 검사한다.

    2. 액션에 ActionForm이 설정되어 있다면, 폼 빈의 설정 정보에서 action요소의 name속성을 찾는데 사용한다.

    3. 이미 만든 ActionForm 인스턴스가 있는지 검사한다.

    4. ActionForm 인스턴스가 적합한 scope에 있고 요청에 필요한 타입과 같다면 재사용한다.

    5. ActionForm 인스턴스가 적합한 scope내에 없다면 새로운 인스턴스를 만들고 action 요소의 scope 속성에 따른 scope에 저장한다.

    6. ActionForm 인스턴스의 reset() 메소드를 호출한다.

    7. 요청 파라미터의 이름에 따른 ActionForm의 setter 메소드를 통해서 요청 파라미터의 값을 ActionForm에 입력한다. (populate 라고 한다.)

    8. 마지막으로 validate 속성이 "true"로 설정되어 있다면 ActionForm 인스턴스의 validate()메소드를 수행하고 검증과정에 에러가 있다면 에러들을 반환한다.

    다음은 ActionForm 작성의 예를 보여주는 UserForm.java 의 일부 소스코드이다.

    public class UserForm extends ActionForm{
    	
    	private String userId;
    	
    	private String password;
    	
    	public String getUserId() {
    		return userId;
    	}
    	
    	public void setUserId(String userId) {
    		this.userId = userId;
    	}
    	
    	public String getPassword() {
    		return password;
    	}
    	
    	public void setPassword(String password) {
    		this.password = password;
    	}
    	
    	public void reset(ActionMapping mapping, HttpServletRequest request) {
            this.password = null;
            this.userId = null;
        }
    
    	public ActionErrors validate(ActionMapping mapping,
                                     HttpServletRequest request) {
            ActionErrors errors = new ActionErrors();
            if ((userId == null) || (userId.length() < 1))
                errors.add("userId", new ActionMessage("error.userid.required"));
            if ((password == null) || (password.length() < 1))
                errors.add("password", new ActionMessage("error.password.required"));
            return errors;
        }
    }
    

    화면에서 입력받을 요소에 대한 attribute를 정의하고 해당 getter/setter 메소드를 작성한다. 또한 검증을 위한 validate 메소드와 초기화를 위한 reset 메소드를 구현해야 한다.

  • ActionErrors 사용하기

    위 ActionForm 소스의 validate 메소드에서 ActionErrors 객체를 반환하는 것을 보았다. 여기서는 ActionErrors의 사용에 대해 알아본다.

    • ActionErrors는 어플리케이션에서 발견한 에러를 하나 이상 캡슐화한다.

    • request에 저장된 ActionErrors는 이후 JSP 에서 custom tag를 통해 사용자들에게 에러 메시지로 보여진다.

    다음은 ActionMessage 생성의 예이다.

    ActionMessage message = new ActionMessage("global.error.login.requiredfield", "email");

    위의 첫번째 인자는 리소스 번들 내의 키 중 하나와 일치하는 문자열이고, 두번째 인자는 메시지를 위한 parameter이다. 아래는 리소스 번들 내의 관련 메시지 정의이다.

    global.error.login.requiredfield=The {0} field is required.

    {0} 부분에는 email 이란 글자가 찍혀 표시된다. 위의 형태 외에도 복수개의 메시지 파라메터를 처리할 수 있는 몇가지 생성자 유형이 더 있다. ActionMessage는 ActionForm의 validate 에서만 생성할 수 있는 것은 아니다. 예를 들어, Action에서 호출한 비즈니스 처리에서 예외가 발생했고 이를 사용자에게 알리기 위한 에러 메시지를 추가하려고 할 때도 ActionMessage를 사용할 수 있다. JSP 에서 Taglib를 이용해 메세지로 ActionErrors를 보여주는 예는 다음과 같다.

    <%@ page contentType="text/html; charset=euc-kr" %>
    <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
    
    <html:html>
        <head>
            <title>Error Page</title>
        </head>
        <body>
            <html:errors/>
        </body>
    </html:html>

33.1.Taglib

Struts Framework 는 몇몇 종류의 태그들을 포함하고 있으며 이 Tag Library 기능을 이용하면 프리젠테이션 계층을 더 쉽게 제어할 수 있고 재사용이 용이하다. 제공하는 Tag library를 사용하여 JSP 페이지에서 자바 코드를 일체 사용하지 않고도 개발이 가능하다.

33.1.1.1.Taglib의 특징

Tag library의 필요성
  • GUI 제작시 재사용을 통해 생산성 향상

  • scripting 요소의 제거로 개발자와 디자이너간 역할 분담에 도움을 줌

  • 전체 업무 영역에서 많이 사용되는 공통 기능을 커스텀 태그로 구현하면 생산성 향상에 도움이 될 수 있음

Tag library의 구성요소
  • Tag Handler : Tag 가 어떤 식으로 동작하는지 정의하는 클래스, javax.servlet.jsp.tagext.Tag 인터페이스를 구현한 javax.servlet.jsp.tagext.TagSupport 나 BodyTagSupport를 상속(extends)하여 구현함

  • Tag Library Descriptor (TLD) : Tag Handler 클래스로 구현한 Custom Tag 들에 대한 XML 형식의 기술문서

  • taglib 지시자 (JSP 페이지 내에서) : JSP 페이지에서 해당 Tag Library를 사용하기 위한 지시자

Tag library의 종류
  • Struts Tag Library : HTML, Logic, Bean, Nested

  • JSTL : core, fmt, xml, sql

  • Jakarata taglibs

  • 해당 프로젝트에 맞게 작성한 Custom Tag Library

33.1.1.2.Struts Taglib

Struts Tag library의 종류는 아래와 같다.

  • HTML tag : HTML 입력 폼을 작성하거나 HTML 기반 사용자 인터페이스를 작성하는데 일반적으로 쓰이는 태그

  • Logic tag : 조건 처리, Collection 객체를 loop을 돌면서 출력, 흐름 제어 등에 쓰이는 태그

  • Bean tag : 자바 빈과 관련 프로퍼티들에 접근하는 데 이용되는 태그. 변수의 기술을 통해 쉽게 접근할 수 있는 새 빈을 정의할 수 있음

  • Template tag : layout를 공유하는 동적인 JSP 템플릿을 작성할 때 유용하게 쓸 수 있는 태그

  • Nested tag : Struts 태그들을 중첩해서 사용할 수 있게 해줌

사용법은 다른 Tag Library와 같다. 아래는 JSP에 taglib 선언한 예이다.

<%@ taglib
        uri="http://struts.apache.org/tags-bean"
        prefix="bean"%>

많은 경우 Tag Library 들은 자바빈즈와 함께 사용된다. 자바 빈은 HTML 폼의 입력 필드에 대응하는 프로퍼티들을 포함하는 ActionForm일 수도 있지만 Value Object들도 사용할 수 있다.

HTML

다음은 HTML Tag Library의 태그들에 대한 설명이다.

NameDescription
baseHTML의 <base>를 표시한다.
buttonbutton 입력 필드를 표시한다.
cancelcancel 버튼을 표시한다.
checkboxcheckbox 입력필드를 표시한다.
errors모든 일련의 에러 메시지를 조건적으로 표시한다.
filefile 선택 입력 필드를 표시한다.
formHTML <form>을 정의한다.
frameHTML의 <frame>을 정의한다.
hiddenhidden 필드를 표시한다.
htmlHTML의 <html>을 표시한다.
imgHTML의 <img>를 표시한다.
javascriptvalidator 플러그 인이 로딩한 validation-rule에 기반한 javascript를 표시한다.
linkHTML의 앵커나 하이퍼링크를 표시한다.
messages모여진 일련의 메시지들을 조건적으로 표시한다.
multibox멀티플 checkbox 입력 필드를 표시한다.
optionselect의 option을 표시한다.
optionsselect의 option들의 집합을 표시한다.
optionsCollectionselect의 option들의 집합을 표시한다.
passwordpassword 입력필드를 표시한다.
radioradio 버튼 입력 필드를 표시한다.
resetreset 버튼 입력 필드를 표시한다.
rewriteURI를 표시한다.
select<select>를 표시한다.
submitsubmit 버튼을 표시한다.
text"text" 타입의 입력 필드를 표시한다.
textareatextarea 입력 필드를 표시한다

다음은 HTML 의 link, password 태그의 예이다.

<tr>
    <td colspan="4" align="center">
    <html:link page="/html-link.do?doubleProperty=321.321&longProperty=321321">
        Double and long via hard coded changes
    </html:link>
    </td>
</tr>

<html:password property="password"
          size="15" maxlength="16" redisplay="false" />
Logic

다음은 Logic Tag Library의 태그들에 대한 설명이다.

NameDescription
empty요청한 변수가 null 또는 빈 문자열인 경우 이 태그의 바디 컨텐츠를 수행한다.
equal요청한 변수가 지정한 값과 같을 경우 이 태그의 바디 컨텐츠를 수행한다.
forwardActionForward 엔트리를 통해 지정한 페이지로 포워드를 수행한다.
greaterEqual요청한 변수가 지정한 값보다 크거나 동일한 경우 이 태그의 바디 컨텐츠를 수행한다.
greaterThan요청한 변수가 지정한 값보다 큰 경우…
iterate지정한 컬렉션으로 이 태그 내의 바디 컨텐츠를 반복한다
lessEqual요청한 변수가 지정한 값보다 작거나 동일한 경우
lessThan요청한 변수가 지정한 값보다 작을 경우
match지정한 값이 요청한 변수의 부분 문자열에 일치하는 경우
messagesNotPresent지정한 메시지가 이 요청에 없는 경우
messagesPresent지정한 메시지가 이 요청에 있는 경우
notEmpty요청한 변수가 null도, 빈 문자열도 아닌 경우
notEqual요청한 변수가 지정한 값과 동일하지 않은 경우
notMatch지정한 값이 요청한 변수의 부분 문자열에 일치하지 않는 경우 이 태그의 바디 컨텐츠를 수행한다.
notPresent지정한 값이 이 Request에 없는 경우
present지정한 값이 이 Request에 있는 경우
redirectHTTP Redirect를 표시한다.

다음은 notEmpty, iterate 태그의 예이다.

<logic:notEmpty name="userSummary" property="addresses">
<!—이 부분은 address Collection 의 모든 객체들을 돌며 반복 출력하는 logic 태그로 구성하면 됨 -->
</logic:notEmpty>

<logic:iterate id="address" name="usersSummary" property="addresses">
<!—address 객체를 테이블 형태로 출력한다. -->
</logic:iterate>
Bean

다음은 Bean Tag Library의 태그들에 대한 설명이다.

NameDescription
cookie지정한 요청 쿠키의 값에 근거해 변수를 정의한다
define지정한 빈 프로퍼티의 값에 근거해 변수를 정의한다
header지정한 요청 헤더의 값에 근거해 변수를 정의한다
include동적인 어플리케이션 요청의 응답을 로드해 빈으로 이용할 수 있도록 한다
message응답이 되는 국제화된 메시지 문자열을 표시한다
page지정한 아이템을 빈으로써 페이지 문맥에서 꺼낸다
parameter지정한 요청 파라미터의 값에 근거해 변수를 정의한다
resource웹 어플리케이션의 자원을 로드 해 빈으로 이용할 수 있도록 한다
sizeCollection 또는 Map의 요소의 갯수를 포함한 빈을 정의한다.
struts지정한 Struts 내부 설정 객체를 빈으로 꺼낸다.
write지정한 빈 프로퍼티의 값을 표시한다.

다음은 message, write 태그의 예이다.

<td><bean:message key="global.user.firstName"/>:</td>

위와 같이 사용하면 global.user.firstName 에 해당하는 메시지를 가져와 보여준다.

<td>Hello <bean:write name="user" property="firstName"/>:</td>

위와 같이 사용하면 user 라는 빈에서 firstName을 꺼내 Hello 옆에 붙여준다.

Nested

한 태그를 다른 태그에 중첩하여 사용하고자 할 경우 적용할 수 있다. Struts에서 지원하는 현재 태그와 매칭되는 HTML Nested Tag, Logic Nested Tag, Bean Nested Tag가 존재하며 사용 방법은 원래 태그와 같다.

33.1.1.3.JSP Standard Tag Library

  • JSR52, JSP Standard Tag Library 스펙

  • 어떤 컨테이너에서도 사용 가능한 표준 태그 집합을 정의

  • core, fmt, xml, sql 태그가 있음

  • Servlet 2.3, JSP 1.2 이상을 지원하는 컨테이너 필요(Tomcat 을 비롯하여 대부분 지원함)

JSTL은 데이터의 포맷, 반복 처리, 조건 처리 등 전형적인 프리젠테이션 레이어를 위한 표준 구현을 제공하기 때문에, JSP 작성자들이 어플리케이션 개발에 집중하는데 도움이 되며 일반적인 기능을 커스텀 태그 라이브러리의 표준 세트로 패키징했기 때문에 JSP 작성자들이 스크립팅 엘리먼트에 대한 필요를 줄이고 관련된 관리 비용을 피할 수 있도록 한다. 이에 반해 pure 자바 코드에 비해 시스템 리소스를 많이 사용하며 극한 부하 상황에서는 2~3배의 성능 저하가 발생할 수 있으므로 성능이 이슈가 되는 경우 사용에 유의하도록 한다.

Struts Bean, Logic 태그들은 JSTL로 바꾸어 더 쉽게 사용할 수 있다. 다음은 JSTL Core 태그 중 조건 분기 및 Collection loop 처리의 예이다.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
..

<!-- 테이블의 리스트 반복부  -->
<c:choose>                                      	
    <c:when test="${page.totalCount <= 0}">
        <tr class="ct_list_pop">
            <td colspan="11" align="center">::: 조회된 사용자 정보가 없습니다. :::</td>	
        </tr>
        <tr>
            <td colspan="11" bgcolor="D6D7D6" height="1"></td>
        </tr>
    </c:when>
    <c:otherwise>
        <c:forEach var="userVO" items="${page.list}" varStatus="status">
            <tr class="ct_list_pop">
                <td align="center">
                    <c:out value="${status.count + ((page.currentPage - 1) * pageSize) }"/>
                </td>
                <td></td>
                <td align="left">
                    <a href="javascript:fncGetUser('<c:out value="${userVO.userId}"/>');">
                        <c:out value="${userVO.userId}"/>
                    </a>
                </td>
                <td></td>
                <td align="left"><c:out value="${userVO.userName}"/></td>
                <td></td>
                <td align="center" style="padding-right:3px;"><c:out value="${userVO.ssn}"/>
                </td>
                <td></td>
                <td align="center"><c:out value="${userVO.cellPhone}"/></td>
                <td></td>
                <td align="left"><c:out value="${userVO.email}"/></td>		
            </tr>
            <tr>
                <td colspan="11" bgcolor="D6D7D6" height="1"></td>
            </tr>
        </c:forEach>
    </c:otherwise>
</c:choose>
..

33.1.1.4.기타 Taglib

Jakarata taglibs

JSP Standard Tag Library (JSTL) 의 구현인 Standard Taglib 1.1(JSTL 1.1 - Servlet 2.4, JSP 2.0 이상)을 비롯한 많은 태그 라이브러리들을 활용할 수 있다.

Custom Tags
  • Tag interface를 구현한 클래스를 만들고 XML 형식의 tag library descriptor (TLD) 파일을 제공해야 함

  • 보통 관련된 Custom Tag의 묶음인 Tag Library 형태로 제공됨

  • 전체 업무 영역에서 많이 사용되는 공통 기능을 커스텀 태그로 직접 구현

예를 들면 코드 테이블로부터 코드리스트를 조회하여 select box 형식으로 표출하는 Custom Tag 를 적용하면 전체 업무의 생산성 향상에 도움이 될 것이다. Anyframe 에서 제공하는 pagenavigator 태그도 다중 행의 리스트성 자료의 페이지 처리 기능을 돕기 위해 직접 구현한 커스텀 태그이다.

33.2.Tiles

JSP Page의 Layout을 구성하는 방법에는 몇 가지가 있다. 대표적인 것이 include 지시어를 사용하여 중복되는 코드를 줄여주는 방법이다. 그러나 여전히 한계가 존재하며 이를 해결하기 위한 더 나은 접근법은 템플릿 중심 아키텍처를 적용하는 것이다. Tiles 는 이를 지원하는 templating system으로 웹 어플리케이션의 유저 인터페이스를 단순화 하기위해 만들어졌다. Struts 에는 Plug-in 형태로 내장되어 있다. Struts 없이도 Tiles를 독립적으로 적용할 수 있으며 이는 Apache Tiles 프로젝트 (http://tiles.apache.org/)에서 확인할 수 있다.

33.2.1.Page Layout 구성 방법

33.2.1.1.구성 방법

  • JSP based approach : 페이지에 삽입할 기능이 많아질수록 복잡해짐. 소규모 어플리케이션에만 적합

  • include 지시어 사용 : 중복되는 코딩 부분의 재사용. 여전히 페이지에서 컨텐츠와 레이아웃이 혼재됨.

  • Template based approach : 페이지의 물리적인 영역을 은닉화하는 방법 제공. 컨텐츠와 레이아웃의 분리

    위 그림은 Tiles를 적용하여 페이지 레이아웃을 구성한 예이다.

33.2.2.Tiles 설치

  • struts-tiles.jar을 WEB-INF/lib 디렉토리에 복사한다.

  • web.xml파일의 Action Servlet 정의 부분에 다음과 같이 추가한다.

<init-param>
    <param-name>chainConfig</param-name>
    <param-value>org/apache/struts/tiles/chain-config.xml</param-value>
</init-param> 
  • struts-config.xml의 plug-in에 TilesPlugin을 다음과 같이 등록한다.

<plug-in className="org.apache.struts.tiles.TilesPlugin">
    <set-property
        property="definitions-config"
        value="/WEB-INF/tiles-defs.xml"/>
</plug-in>
  • Tiles를 사용하는 JSP에 다음과 같은 코드를 추가 한다.

 <%@ taglib
      uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>

33.2.3.Tiles 사용

33.2.3.1.Tiles 적용 시 고려점

  • Tiles 단독으로도 사용이 가능하다.

  • Tiles plug-in은 Tiles definitions를 사용할 때만 필요하다. (plug-in 설정 없이도 Tiles 라이브러리를 사용할 수 있음)

  • Tiles definition은 JSP로 정의할 수도 있으나 일반적으로 layout과 Tiles definition (xml) 을 별도로 지정한다.

  • Tiles는 기본적 Template 으로 많은 Layout을 제공하지만 개발에 들어가기 전에 Layout 에 대한 충분한 준비가 필요하다.

33.2.3.2.Tiles Tag Library의 속성

다음은 Tiles Tag Library의 태그들의 목록과 간단한 설명이다.

AttributeDescription
addelement를 추가
definitiontitles component 정의
get<template:put>을 통해 JSP로 전달된 자원을 얻는다.
getAsStringTile/Component/Template 속성값을 JspWriter에 출력
importAttribute정의된 context안에 타일의 속성을 추가한다.
initComponentDefinitions정의 팩토리(definitions factory)를 초기화한다.
InsertJSP 페이지 내에서 동적 템플릿을 추가
put<template:insert>태그 내에서 템플릿에 삽입할 자원을 지정
putList속성으로 전달할 리스트를 선언
useAttributeJSP 페이지에서 속성 값을 사용한다.

보통 모든 페이지에서 적용되는 헤더나 저작 관련 내용들을 한곳에 모아 미리 Layout를 정의해둔다. 이를 Definition 이라 하는데 JSP 나 XML 로 만들 수 있으며 예는 다음과 같다.

33.2.4.Tiles Layout 정의

33.2.4.1.JSP 로 레이아웃을 정의한 예

다음은 storefront-defs.jsp 라는 jsp 에 tile definition을 정의한 예이다.

<%@ taglib
        uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>
<tiles:definition id="storefront.default"
        pgae="/layouts/storefrontdefaultLayout.jsp" scope="request">
    <tiles:put name="header" value="/common/header.jsp"/>
    <tiles:put name="menubar" value="/common/menubar.jsp"/>
    <tiles:put name="copyright" value="/common/copyright.jsp"/>
</tiles:definition>

definition들을 이용하려면, 타일 컴포넌트들이 definition 에 접근할 수 있어야 한다. 다음은 definition을 사용하는 JSP 의 예이다. include 를 사용하여 storefront-defs.jsp 를 참조하고 있다.

<%@ taglib uri="http://struts.apache.org/tags-tiles" prefix="tiles" %>
<%@ include file="../common/storefront-defs.jsp" %>

<tiles:insert beanName="storefront.default" beanScope="request">
    <tiles:put name="body-content" value="../security/sigin-body.jsp"/>
</tiles:insert>

33.2.4.2.XML 로 레이아웃을 정의한 예

위에서는 tile definition 을 jsp 에 설정한 예를 보였지만, xml 로 정의하고 struts-config.xml 에 plug-in으로 정의해 놓고 쓰는 것이 일반적이다. 다음은 tiles-defs.xml의 일부분이다. index 라는 definition을 기본으로 하여 extends 해서 사용함으로 반복된 코딩을 줄여준다.

<!-- Doc index page description  -->
<definition name="index" path="/layout.jsp">
    <put name="title"  value="Anyframe Sample" />
    <put name="header" value="/header.jsp" />
    <put name="menu"   value="/menu.jsp" />
    <put name="body"   value="/body.jsp" />
    <put name="footer" value="/bottom.jsp" /> 
</definition>

<!-- view order information page description  -->  
<definition name="list" extends="index">
    <put name="body"   value="/user/listUser.jsp" />
</definition>

Tiles를 사용하는 경우 action-mapping 에서는 forward path를 Tiles definition에 정의된 definition name으로 주어야 한다.

<action
    name="userForm"
    path="/empListUser"
    type="com.sds.emp.view.action.user.GetUserListAction"
    scope="request"
    validate="false"
    roles="admin,user">
    <forward name="success" path="list"  />
</action>

forward 의 path 부분에 tiles 의 Definition name 이 들어가면 '/'를 사용하지 않음에 유의한다.

34.Internationalization

국제화(Internationalization, I18N)란 미리 다양한 언어와 지역을 지원하도록 소프트웨어를 설계하고 쉽게 리엔지니어링을 할 수 있게 지원하는 일련의 과정이다.

34.1.Internationalization의 특징

34.1.1.Internationalization의 필요성

  • 코드 수정 없이 지원하는 언어를 추가

  • 텍스트 요소들, 메시지들, 이미지 소스들과 소스코드의 분리

  • 날짜,시간,숫자, 통화 등과 같은 문화에 종속적인 데이터들을 언어와 위치에 따라 달리 포맷팅

  • 비 표준 문자 집합들을 지원

  • 어플리케이션을 새로운 언어와 지역에 쉽고 빠르게 적용

어플리케이션이 국제화를 지원한다고 해서 모든 언어와 지역에서 곧바로 쓸 수 있는 것을 의미한다는 것은 아니다. 국제화를 지원한다는 것은 언어나 지역에 영향을 받는 부분과 영향을 받지 않는 코드를 분리하여 쉽게 지역화될 수 있게 만들었다는 것을 의미한다.

34.1.2.지역 (Locale)

  • 관습과 문화, 언어를 공유하는 영역을 의미함

  • 지역화(L10N, Localization)는 제대로 국제화된 어플리케이션을 특정 지역에 맞게 만드는 일련의 과정

국제화 지원 시 위의 특징들 중 일부만 골라서 구현하는 것은 별 의미가 없다. 모두 지원하든지 지원하지 않든지 둘중의 하나이다. cf.) Anyframe 에서는 모든 국제화 문제를 고려할 수는 없고 Core Class 인 java.util.Locale, java.util.ResourceBundle 위주로 논의되었다. Anyframe 에서 제공하는 리소스들은 텍스트와 이미지에 초점이 맞추어져 있다. MessageResource Bundle은 ProperyResourceBundle Class 의 규약에 따라 만들어야 한다. 확장자 .properties 의 텍스트 파일로, 메시지들은 key=value와 같은 형식으로 작성해야 함은 이미 말한 바 있다. 또한 클래스처럼 찾아오기 때문에 클래스패스 상에 작성해야 한다. MessageResource Bundle 이 여러 개 있을 경우 Struts는 번들 중 하나에서 메시지를 찾을 때 기본 이름과 지역을 사용하여 이름이 가장 가깝게 일치하는 번들을 찾는다. 찾는데 실패하면 기본 번들을 사용한다. 다음은 메시지 정의의 예이다.

# error message
common.msg.authorization.error=You can not access this page.
..
# text resource
common.ui.title=eMarketPlace
..

다음은 ActionMessage 생성 시 메시지 키를 설정하는 예이다.

erros.add(ActionErrors.GLOBAL_ERROR, new ActionMessage("common.msg.authorization.error"));

다음은 Bean 태그 라이브러리의 MessageTag 를 사용하여 해당 메시지 키에 대한 실제 텍스트 value를 표시하는 예이다.

<head>
    <title><bean:message key="common.ui.title"/></title>
</head>

34.2.Internationalization Sample

다음은 사용자의 Locale정보에 의해 JSP페이지에 Message가 다른 언어로 보여지는 예이다.

34.2.1.Sample

  • JSP

    JSP화면에서 해당 언어를 클릭하면 MessageResource Bundle에 등록되어 있는 msg.internationalization키에 대한 값이 "Internationalization"과 "국제화"로 바뀌는 것을 보여주는 internationalization.jsp 이다.

    <%@ page contentType="text/html; charset=utf-8" %>
    <%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>
    <%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
    <html>
    <head>
    <title>Internationalization Sample</title>
    </head>
    <body>
        <strong>Change Language | 언어 변경 </strong><br/>
        <html:link action="/internationalizationSample?language=en">English | 영어</html:link><br/>
        <html:link action="/internationalizationSample?language=ko">Korean | 한국어</html:link><br/>
        <bean:message key="msg.internationalization"/>
    </body>
    </html>
    

  • Action

    아래는 위의 JSP화면에서 해당 언어를 클릭했을 때 Action에서 Session에 사용자 Locale정보를 설정하는 예를 보여주는 InternationalizationAction.java 의 일부이다.

    public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) 
            throws Exception {
        
        HttpSession session = request.getSession();
        Locale locale = getLocale(request);
        
        String language = null;
        String country = null;
        
        try {
               language = (String)
                 PropertyUtils.getSimpleProperty(form, "language");
               country = (String)
                 PropertyUtils.getSimpleProperty(form, "country");
           } catch (Exception e) {
              e.printStackTrace();
           }
    
           if ((language != null && language.length() > 0) &&
               (country != null && country.length() > 0)) {
              locale = new java.util.Locale(language, country);
           } else if (language != null && language.length() > 0) {
              locale = new java.util.Locale(language, "");
       }
           session.setAttribute(Globals.LOCALE_KEY, locale);
    
           return mapping.findForward("success");
    }
    

    위 InternationalizationAction.java는 Struts에서 제공하고 있는 struts-examples-1.3.10의 LocaleAction을 수정한 것으로 화면에서 parameter값으로 보내온 language 값을 Globals.LOCALE_KEY 이름으로 Session에 저장한다.

35.Validator

사용자 입력 값을 검증하는 여러가지 방법 중에서 일반적으로 많이 사용하고 있는 방법이 JSP페이지 내에서 javascript 함수를 이용해 사용자 입력 값을 검증하는 방법이다. 하지만 이런 방법으로는 완전한 검증을 할 수 없기 때문에 Sever측에서 다시 한번 검증하는 것이 필요하다. Validator는 Struts 1.1부터 배포된 프레임워크로 ActionForm에 대한 유효성 검사를 편리하게 도와주고 있다. 본 문서에서는 Validator를 사용하기 위한 플러그인 등록 및 Struts에서 제공하고 있는 validator-rule에 대해서 소개하기로 한다.

Validator 설정 및 사용 방법

35.1.Plug-in 등록

35.1.1.struts-config.xml에 plug-in 등록

Validator를 사용하기 위해서는 struts-config.xml에 org.apache.struts.validator.ValidatorPlugIn을 등록해야 한다. ValidatorPlugIn은 property로 pathname을 가지며 pathname의 값으로는 Struts에서 기본으로 제공하고 있는 검증 규칙이 정의되어 있는 validator-rules.xml과 사용자가 검증한 ActionForm에 대한 검증 규칙을 정의한 xml파일을 세팅한다.

35.1.2.Samples

다음은 Validator Plug-in을 설정한 struts-config-validator.xml 의 일부분이다.

<plug-in className="org.apache.struts.validator.ValidatorPlugIn">
    <set-property property="pathnames"
        value="/org/apache/struts/validator/validator-rules-compressed.xml,
        /WEB-INF/validator/validation-sample.xml" />
</plug-in>

Struts에서는 일반적으로 많이 사용하고 있는 검증 규칙에 대한 정의가 포함된 validator-rules.xml과 validator-rules-compressed.xml을 struts-core-1.3.x.jar파일에 같이 배포하고 있다. validation-sample.xml은 검증하고자 하는 ActionForm에 대한 formset 설정 이 포함되어 있다.

35.2.Validator Rules

35.2.1.Struts Validator Rules 기본 기능

Struts에서 기본적으로 많은 검증 규칙을 제공하고 있기 때문에 사용자가 검증규칙을 새로 작성하는 번거로운 작업이 많이 줄어들었다. 다음은 validation rule 설정의 예이다.

<validator name="minlength"
    classname="org.apache.struts.validator.FieldChecks"
    method="validateMinLength"
    methodParams="java.lang.Object,
            org.apache.commons.validator.ValidatorAction,
            org.apache.commons.validator.Field,
            org.apache.struts.action.ActionErrors,
            javax.servlet.http.HttpServletRequest"
        epends="required"
        msg="errors.minlength">
    <javascript><![CDATA[function validateMinLength(form) {
    // javascript를 사용하고자 할 경우 여기에 작성하면 된다.
    }]]>
    </javascript>
</validator>

name 은 unique 해야 하며 다른 rule에서 참조할 때도 name을 사용하게 된다. classname과 method는 실제 validation 로직이 들어있는 클래스와 메소드의 이름이다. depends 는 validator rule 간의 우선순위이다. 위의 예는 minlength를 체크하기 전에 required 라는 validation rule을 먼저 수행한다는 뜻이다. depends="required,integer"라고 되어있다면 null 인지 체크하고, Integer 인지 체크하고 그 다음에 minlength를 체크하겠다는 뜻이다. msg 는 validation error 가 발생했을 때 뿌려줄 메시지를 resource bundle 에서 가져올 때 사용할 key 값이다.

기본적으로 다음의 값들을 사용한다.

# Struts Validator Error Messages 
errors.required={0} is required.
errors.minlength={0} can not be less than {1} characters.
errors.maxlength={0} can not be greater than {1} characters.
errors.invalid={0} is invalid.
errors.byte={0} must be a byte.
errors.short={0} must be a short.
errors.integer={0} must be an integer.
errors.long={0} must be a long.
errors.float={0} must be a float.
errors.double={0} must be a double.
errors.date={0} is not a date.
errors.range={0} is not in the range {1} through {2}.
errors.creditcard={0} is an invalid credit card number.
errors.email={0} is an invalid e-mail address.

다음은 validator-rules.xml에 포함되어 있는 검증 규칙에 대한 설명이다.

NameDescription<var-name>
required입력 값이 반드시 존재해야 한다.-
validwhen다른 필드의 값을 비교한다.test
minlength입력 값의 최소 글자수를 제한한다.minlength
maxlength입력 값의 최대 글자수를 제한한다.maxlength
mask설정한 regular expression을 만족 해야한다.mask
dateDate형태로 변환 가능해야 한다.datePatternStrice 또는 datePattern
creditCard신용카드 번호 규칙에 만족해야 한다.-
emaile-mail 규칙에 만족해야 한다.-

35.3.ActionForm

35.3.1.ValidatorForm의 상속

Validator를 이용할 때는 ActionForm이 아닌 org.apache.struts.validator.ValidatorForm을 상속 받아야 한다. ValidatorForm은 유효성 검증에 필요한 validate() 메소드를 포함하고 있으며 검증 실패 시 ActionErrors에 error message를 셋팅해서 리턴한다.

35.3.2.Samples

다음은 ValidatorForm을 상속받은 ValidatorSampleForm.java 의 일부분이다.

    public class ValidatorSampleForm extends ValidatorForm{

    private String userName;
    
    private String userId;
    
    private String password;
    
    private String phoneNumber;
    
    private String email;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
    //중략
    }
}

35.4.formset 설정

35.4.1.formset 설정 방법

validator-rules.xml에 등록된 검증 규칙과 ActionForm과의 매핑을 하기 위해서는 formset을 정의해야 한다. 다음은 <form>의 attribute들이다.

AttributeDescription
propertyActionForm subclass의 property에 해당한다.
depends하나 이상의 Validation rule들을 지정한다
pageMulti-page ActionForm에서 페이지 번호를 지정할 때 사용
indexedListPropertyActionForm 에서 Collection을 return하는 property name

35.4.2.Sample

다음은 ValidatorSampleForm에 Struts에서 제공한 검증규칙을 적용하기 위해 설정한 validation-sample.xml 의 일부분이다.

<form name="validatorSampleForm">
    <field property="userName" depends="required,minlength,maxlength">
        <arg key="validator.sample.userName" position="0"/>
        <arg name="minlength" key="${var:minlength}" resource="false" position="1"/>
        <arg name="maxlength" key="${var:maxlength}" resource="false" position="1"/>
        <var>
            <var-name>minlength</var-name>
            <var-value>4</var-value>
        </var>
        <var>
            <var-name>maxlength</var-name>
            <var-value>10</var-value>
        </var>
    </field>
        <!-- 중략  -->
    <field property="email" depends="email">
        <arg key="validator.sample.email" position="0"/>
    </field>
</form>

<form>의 name attribute에 struts-config.xml에 등록된 ActionForm의 form name을 등록하고 <form의 하위의 <field>에 ActionForm의 유효성 검사를 할 attribute에 대한 설정을 등록한다. 위의 Sample에서는 userName은 필수 입력값에, 4자리 이상 10자리 이하로 설정을 했고 email은 e-mail 주소 검증 규칙을 적용했다. 유효성 검증에 실패하면 arg 값을 에러 메시지의 arguements로 설정하여 돌려주게 된다. 기본으로 arg0-arg3 요소는 message resources에서 해당 key 값으로 찾게되고. resource 속성이 false로 설정되면 message resources에서 찾지 않고 해당 값을 바로 돌려준다. validation.xml은 크게 global 과 formset 으로 나누는데 global은 file 내에서 상수로 사용될 값들을 정의한다. formset은 다시 constant 와 form으로 나누는데 constant는 global과 같으며 form 은 validation을 적용할 ActionForm 에 매핑된다. 참고로 formset 에는 language와 country라는 attribute 가 있어서 다국어 지원이 가능하다.

35.5.Action 매핑 설정

35.5.1.struts-config.xml의 Action 매핑 설정

RequestProcessor가 들어온 Request 정보를 이용해 ActionForm을 생성할 때 유효성 검증을 실행하기 위해서는 struts-config.xml의 <action>에 validate="true"를 설정해야 한다.

35.5.2.Sample

다음은 struts-config.xml의 <action>에 Validator를 적용하기 위한 설정을 보여주는 struts-config-validator.xml 의 일부이다.

<action
    path="/validatorSample"
    type="anyframe.sample.struts.basic.ValidatorSampleAction"
    validate="true"
    cancellable="true"
    scope="request"
    input="/basic/validatorSample.jsp" 
    name="validatorSampleForm">
    <forward name="success" path="/basic/validatorSuccess.jsp"></forward>
</action>

36.Exception Handling

Action클래스의 execute() 메소드에서 Exception이 전달되었을 때 실행하는 ExceptionHandler를 설정할 수 있다. struts-config.xml에 개별 Action에 대한 ExceptionHandler를 설정할 수도 있고(action level), <global-exceptions>을 이용해 application의 모든 Action에서 발생하는 Exception에 대한 ExceptionHandler를 설정(global level)할 수도 있다. 양쪽 모두에 ExceptionHandler가 정의되었을 때 action level에 선언된 것이 더 우선순위가 높다.

36.1.Global Level Exception Handling

36.1.1.Global Level Exception Handling의 특징

  • 에러나 Exception 처리를 정의하는 선언적인 방법

  • 모든 action에서 사용할 수 있는 global level Exception 처리를 정의

  • 하위로 여러 개의 <exception>을 가질 수 있음

다음은 <exceptions>의 attribute들이다.

NameDescription
bundleException의 handler 클래스가 사용하는 message resources bundle에 대한 servlet context attribute의 name 값이다. 디폴트 값 : org.apache.struts.Globals.MESSAGES_KEY의 값
classNameException들의 configuration 정보를 담고 있을 객체이다. 반드시 org.apache.struts.config.ExceptionConfig 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.config.ExceptionConfig
handler이 exception이 발생할 때, 이 exception을 처리하는 클래스를 나타낸다. 즉, handler 클래스는, 어떤 exception이 발생하면, 적절한 error message('key' attribute)와 함께 적절한 페이지('path' attribute)로 forward 해주는 클래스이다. 반드시 org.apache.struts.action.ExceptionHandler 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.action.ExceptionHandler
key이 exception이 발생할 때, message resource bundle에서 찾아낼 error message의 key 값이다. [required]
path이 exception이 발생할 때, forward할 resource(*.do, *.jsp 등)의 상대(module-relative)경로를 나타낸다.
scopeActionError 객체에 접근할 context의 scope를 나타낸다. request 또는 session. 디폴트 값 : request
typeException Handling을 수행할 exception의 type을 나타낸다. [required]

36.1.2.Samples

다음은 struts-config.xml 파일에서 global-exceptions 설정에 대한 예제이다.

<global-exceptions>
    <exception key="global.exception.message" 
        path="/basic/globalException.jsp" 
        type="java.lang.Exception" 
        handler="org.apache.struts.action.ExceptionHandler" />
</global-exceptions>

Action 클래스의 execute() 메소드에서 Exception이 발생하면 ExceptionHandler에서 exception message key값(global.exception.message)을 이용해 Resource Bundle에서 Exception Message를 세팅한 후 path에 설정된 /basic/globalException.jsp로 forwarding한다.

36.2.Action Level Exception Handling

36.2.1.Action Level Exception Handling의 특징

  • 개별 Action에 대한 Exception Handling이 가능

  • Action Level Exception이 정의되어 있지 않을 경우 Global Level Exception 적용

  • <action>하위에 <exception>으로 정의

다음은 <exception>의 attribute들이다.

NameDescription
bundleException의 handler 클래스가 사용하는 message resources bundle에 대한 servlet context attribute의 name 값이다. 디폴트 값 : org.apache.struts.Globals.MESSAGES_KEY의 값
classNameException들의 configuration 정보를 담고 있을 객체이다. 반드시 org.apache.struts.config.ExceptionConfig 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.config.ExceptionConfig
handler이 exception이 발생할 때, 이 exception을 처리하는 클래스를 나타낸다. 즉, handler 클래스는, 어떤 exception이 발생하면, 적절한 error message('key' attribute)와 함께 적절한 페이지('path' attribute)로 forward 해주는 클래스이다. 반드시 org.apache.struts.action.ExceptionHandler 또는 이를 상속 받은 클래스여야 한다. 디폴트 값 : org.apache.struts.action.ExceptionHandler
key이 exception이 발생할 때, message resource bundle에서 찾아 낼 error message의 key 값이다. [required]
scopeActionError 객체에 접근할 context의 scope를 나타낸다. request 또는 session. 디폴트 값 : request
typeException Handling을 수행할 exception의 type을 나타낸다. [required]

36.2.2.Samples

다음은 struts-config-login.xml 파일에서 <action> 하위의 <exception> 설정에 대한 예제이다.

<action
    path="/login"
    type="anyframe.sample.struts.web.action.LoginAction"
    name="userForm"
    scope="request"
    input="/basic/login.jsp">
    <exception key="error.password.mismatch" path="/basic/login.jsp" 
        type="javax.security.auth.login.FailedLoginException" />
    <forward name="success" path="/basic/main.jsp" />
</action>

Global Level Exception 설정과 달리 모든 Action에서 발생하는 Exception이 아닌 LoginAction의 execute() 메소드에서 발생하는 Exception에 대한 처리를 담당한다. FailedLoginException이 발생했을 경우 /basic/login.jsp로 forwarding되고, 이 외 다른 Exception이 발생할 경우 Global Level Exception에 설정된 error page로 forwarding된다.

36.3.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.struts.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.struts를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.struts를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.struts를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-task-2.0.10.jar 파일이 있어야 한다.)

    표 36.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.struts.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

VIII.Struts Extensions

Struts Extensions은 Apache Struts Framework를 실제 프로젝트에서 보다 편리하게 사용하기 위해 추가적인 기능들을 제공한다. Role기반의 인증 및 관리 기능 등이 추가 된 Struts의 TilesRequestProcessor를 확장한 DefaultRequestProcessor , 선언적인 Synchronized Token 처리 기능 등이 추가된 AbstractActionSupport 외에 다양한 Controller클래스와 Exception 처리를 위한 DefaultExceptionHandler 등을 제공하고 있다. 그 밖에 Ria Solution인 MiPlatform을 이용해 화면 개발 시 공통적인 로직을 담당하고 있는 AnyframeMiPAction클래스가 포함되어 있다.

37.Controller

Struts의 Controller는 크게 ActionServlet, RequestProcessor, Action 클래스로 구성된다. Anyframe 에서는 Struts Controller의 기본 기능과 Business Service연계, 로깅, 인증 및 권한 처리, Exception 처리 등을 위한 확장 기능을 제공하고 있다. 각각에 대한 내용은 아래와 같다.

37.1.DefaultActionServlet

<servlet-class>는 org.apache.struts.action.ActionServlet을 extends한 anyframe.web.struts.action.DefaultActionServlet으로 정의한다. <init-param>은 ActionServlet이 제공하는 기본 기능과 Character Encoding을 설정 할 수 있다. Character Encoding 속성은 DefaulActionServlet에서 확장한 것으로 정의된 Message Resource Bundle을 읽어들일 때, 적용할 Character Encoding을 설정하기 위한 것인다. 다음은 DefaultActionServlet을 <servlet>으로 설정한 web.xml 의 일부이다.

<servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>
        anyframe.web.struts.action.DefaultActionServlet
    </servlet-class>
    <init-param>
        <param-name>config</param-name>
            <param-value>
            /config/struts/struts-config.xml,
            /config/struts/struts-config-login.xml,
            /config/struts/struts-config-dispatch.xml,
            /config/struts/struts-config-token.xml,
            /config/struts/struts-config-exception.xml,
            /config/struts/struts-config-authorization.xml
        </param-value>
    </init-param>
    <init-param>
        <param-name>character-encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
    <init-param>
        <param-name>debug</param-name>
        <param-value>3</param-value>
    </init-param>
    <init-param>
        <param-name>detail</param-name>
        <param-value>3</param-value>
    </init-param>
    <init-param>
        <param-name>convertNull</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>0</load-on-startup>
</servlet>

37.2.DefaultRequestProcessor

anyframe.web.struts.action.DefaultRequestProcessor는 Struts의 TilesRequestProcessor를 extends하고 있다. 따라서, DefaultRequestProcessor를 Struts 속성 정의 파일에 controller로써 설정한 경우에는 반드시 plug-in으로 TilesPlugin를 등록해 주어야 한다. TilesPlugin을 plug-in으로 등록하는 방법은 Struts Tiles의 Tiles 설치 를 참고한다. 아래는 struts-config.xml 의 일부로, DefaultRequestProcessor를 <controller> 내에 정의하고 있다.

<controller contentType="text/html;charset=utf-8"
    locale="true" 
    processorClass="anyframe.web.struts.action.DefaultRequestProcessor"/>

37.2.1.DefaultRequestProcessor 기능

  • role 기반의 인증 및 관리 기능

  • web.xml에서 설정한 character-encoding 값 적용

  • Locale 정보를 Session에 org.apache.struts.action.LOCALE이란 key 값으로 저장

아래는 인증 및 권한 처리를 수행하고 있는 DefaultRequestProcessor의 processRoles 메소드 구현 로직의 일부이다.

protected boolean processRoles(HttpServletRequest request,
            HttpServletResponse response, ActionMapping mapping)
            throws IOException, ServletException {
	// [public] Is this action protected by role requirements?
	String roles[] = mapping.getRoleNames();
	if ((roles == null) || (roles.length < 1)) {
	    return (true);
	}
	
	Subject _subject = null;
	
	HttpSession session = request.getSession();
	_subject = (Subject) session.getAttribute("subject");
	
	if (_subject == null) {
	    log.debug("#AuthenticationException is encounted");
	    
	    ExceptionConfig config = mapping.findException(AuthenticationException.class);
	    
	    if(config == null ){
	    	mapping.findException(Exception.class);
	    }
	    
	    AuthenticationException ae = new AuthenticationException(config.getKey(), request
	            .getRequestURI());
	// 중략 ...

위의 소스 코드에서 보듯이 Struts 속성 정의 파일의 특정 action 매핑 정보에 role이 부여되었을 때 Session에 저장된 Subject에서 사용자 인증 정보를 확인한 후, 인증되지 않았을 경우 AuthenticationException을 throw한다. Exception 메시지의 key는 Struts 속성 정의 파일 <exception> 내에 정의된 AuthenticationException에 대한 key와 동일하며, 정의되지 않았을 경우엔 java.lang.Exception에 대한 key와 동일하다. 아래는 struts-config-exception.xml 파일의 일부로 AuthenticationException을 Global Level Exception으로 등록한 예이다.

<global-exceptions>
		<exception key="common.msg.authentication.error" 
				path="/WEB-INF/jsp/struts/common/error.jsp"
			type="anyframe.web.struts.util.AuthenticationException" 
				handler="anyframe.web.struts.util.DefaultExceptionHandler" />
	</global-exceptions>

메시지리소스로 등록하는 메시지 properties 파일에는 common.msg.authentication.error 의 메시지 키에 대하여

common.msg.authentication.error=Authentication Fail 
		- You are not logon or Session expired. Please try re-logon. - {0}.
common.msg.authorization.error=You can not access this page. - {0}.
..
과 같이 메시지 파일이 등록되어 있음을 가정한다.

37.3.AbstractActionSupport

anyframe.web.struts.action.AbstractActionSupport 클래스는 다음과 같은 주요 기능을 제공한다.

  • Spring 기반의 Anyframe 서비스와의 손쉬운 연동 지원

  • 선언적인 Synchronized Token 처리

  • 공통 Exception 처리

따라서, 각 Action 클래스는 AbstractActionSupport를 상속받아 구현하되, process 메소드를 오버라이드하여 비즈니스 레이어와 연계하여 클라이언트의 요청을 처리하는 로직을 담도록 한다. 위와 같은 주요 기능을 제공하는 AbstractActionSupport의 execute 메소드는 다음과 같이 구현되어 있다.

public ActionForward execute(ActionMapping mapping, ActionForm form,
		HttpServletRequest request, HttpServletResponse response)
		throws Exception {

    ActionForward forward = null;

    try {
        preProcess(mapping, form, request, response);
        getLogger()
                .debug(this.getClass().getName() + ".process() Started!");
        forward = process(mapping, form, request, response);
        getLogger().debug(this.getClass().getName() + ".process() Ended!");
        forward = postProcess(mapping, form, request, response, forward);
    } catch (InvalidTokenException tokenException) {
        forward = processInvalidTokenException(mapping, form, request,
                response, tokenException);
    } catch (RuntimeException uncheckedException) {
        forward = processUnCheckedException(mapping, form, request,
                response, uncheckedException);
    } catch (Exception checkedException) {
        getLogger().debug("\n Action Support Exception catch!!");
        forward = processCheckedException(mapping, form, request, response,
                checkedException);
    } finally {
        forward = processFinally(mapping, form, request, response, forward);
    }
    return forward;
}

다음 목록에 제시된 메소드들은 AbstractActionSupport 클래스 내에 구현된 메소드들로써, execute 메소드의 로직을 수행하기 위해 적절한 순서에 따라 호출된다.

  • preProcess : AbstractActionSupport 클래스를 상속받은 Action 클래스의 process 메소드 수행 전에 호출되는 메소드로서 Action 매핑 정보(validateToken, resetToken)에 기반하여 Token의 유효성을 체크한다. 해당 Action을 수행하기 위한 preCondition이 필요할 경우 이 메소드를 오버라이드하면 된다.

  • process : abstract 메소드이다. 따라서, AbstractActionSupport를 상속받은 하위 Action 클래스에서 반드시 구현해야 하며, process 메소드 내에는 비즈니스 레이어와 연계하여 클라이언트의 요청을 처리하는 로직을 담는다.

  • postProcess : AbstractActionSupport 클래스를 상속받은 Action 클래스의 process 메소드 수행 후 호출되는 메소드로서 Action 매핑 정보(saveToken)에 기반하여 Token을 생성한다. 해당 Action을 수행하기 위한 postCondition이 필요할 경우 이 메소드를 오버라이드하면 된다.

  • processInvalidTokenException : Synchronized Token 사용시 Token이 유효하지 않을 경우에 대한 처리 로직을 담고 있다. "요청이 올바르지 않습니다."라는 메시지를 담은 ActionMessage를 생성하고, InvalidTokenException을 throw한다.

  • processUnCheckedException : preProcess(), process(), postProcess() 수행시 RunTimeException이 발생한 경우, 해당 Exception을 throw한다. UnCheckedException 발생시 별도 처리 로직이 필요한 경우 이 메소드를 오버라이드하면 된다.

  • processCheckedException : preProcess(), process(), postProcess() 수행시 Exception이 발생한 경우, 해당 Exception을 throw한다. CheckedException 발생시 별도 처리 로직이 필요한 경우 이 메소드를 오버라이드하면 된다.

  • processFinally : AbstractActionSupport 클래스 execute 메소드의 finally 구문에서 호출되는 메소드이다. finally 구문에서 별도 처리 로직이 필요한 경우 이 메소드를 오버라이드하면 된다.

위에서 제시한 AbstractActionSupport의 기본 제공 기능 이외에 각 Action 클래스에서 처리해야 할 공통 기능이 필요할 경우, AbstractActionSupport를 상속받은 클래스를 생성하고, 해당 클래스에서 필요한 기능을 추가하도록 한다. 그리고 각 Action 클래스는 AbstractActionSupport를 상속받은 클래스를 상속받아 구현하도록 한다.

37.3.1.Action Sample

다음은 AbstractActionSupport를 상속받아 구현한 LoginAction.java 이다.

public class LoginAction extends AbstractActionSupport {
	
    public Log getLogger() throws Exception {
        return LogFactory.getLog(this.getClass().getName());
    }
	
    public ActionForward process(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        AuthenticationService authenticationService = 
                (AuthenticationService) getService("authenticationService");

        UserForm userForm = (UserForm) form;
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userVO, userForm);

        Subject subject = authenticationService.authenticate(userVO);

        HttpSession session = request.getSession();

        session.setAttribute("subject", subject);
        return (mapping.findForward("success"));
    }
}

위의 소스코드에서는 LoginAction 클래스에서 개별 Logger를 사용하기 위해 AbstractActionSupport의 getLogger 메소드를 오버라이드하고 있다.

37.4.DefaultDispathActionSupport

anyframe.web.struts.action.DefaultDispathActionSupport은 앞서 언급한 AbstractActionSupport를 상속받아 구현한 클래스로써 Struts에서 기본으로 제공하는 DispatchAction과 동일한 기능을 제공한다.

37.4.1.Action Sample

다음은 DefaultDispatchActionSupport를 상속받아 구현한 ProductAction.java 이다.

public class ProductAction extends DefaultDispatchActionSupport {

    public ActionForward get(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {
            
        // TODO : 단건 조회 기능 관련 로직
			
        return mapping.findForward("success_get");
			
	}

    public ActionForward list(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {
		
        // TODO : 리스트 조회 기능 관련 로직
		
        return mapping.findForward("success_list");
    }

}

DefaultDispatchActionSupport를 상속한 Action 클래스는 DispatchAction이므로, AbstractActionSupport를 상속한 Action 클래스 작성과 다르게 한 Action 내에 여러 메소드 정의가 가능하다.

37.5.DefaultForwardAction

anyframe.web.struts.common.action.DefaultForwardAction은 Struts에서 기본으로 제공하는 ForwardAction과 동일한 기능을 수행한다. 단, 차이점은 DefaultForwardAction은 AbstractActionSupport를 상속받았기 때문에 공통 Exception 처리, Synchronized Toke 처리 등이 가능하다는 장점이 있다. 아래는 DefaultForwardAction을 이용해 Action 매핑을 정의한 struts-config-dispatch.xml 파일의 일부이다. 요청 URL이 dispatchActionView.do일 경우 DefaultForwardAction을 통해 /extensions/dispatch.jsp 페이지로 이동할 것이다.

<action path="/dispatchActionView"
    type="anyframe.web.struts.action.DefaultForwardAction"
    parameter="/extensions/dispatch.jsp" />

37.6.AnyframeMiPAction

프리젠테이션 레이어 구성시 X-Internet 제품인 MiPlatform을 이용하는 경우 MiPlatform에서 다루는 고유한 형태의 데이터를 쉽게 처리할 수 있도록 하기 위해 anyframe.web.struts.action.ria.mip.AnyframeMiPAction 클래스를 제공한다. 개발자는 AnyframeMiPAction을 상속받아 개별 Action을 개발하되, process 메소드를 구현해주도록 한다.

process 메소드는 MiPlatform과 관련하여 다음과 같은 입력 인자를 가진다.

TypeParamter NameDescription
VariableListinVlClient에서 GET 방식으로 전송한 parameter들 포함
VariableListoutVlClient로 전송하는 VariableList
DatasetListinDlClient에서 POST 방식으로 전송한 Dataset XML 포함
DatasetListoutDlClient로 전송하는 Dataset XML 설정

37.6.1.Sample Action

다음은 AnyframeMiPAction을 상속받아 구현한 클래스의 일부로, process 메소드 내부에서 비즈니스 서비스를 실행하고, 그 결과값을 반환하고 있다.

public void process(ActionMapping mapping, PlatformRequest request,
        VariableList inVl, DatasetList inDl, VariableList outVl,
        DatasetList outDl) throws Exception {
    this.inVl = inVl;
    this.inDl = inDl;
    this.outVl = outVl;
    this.outDl = outDl;

    MiPUserService userService = (MiPUserService)getService("userService");
    Dataset ds = userService.getUserList(inVl);
    outDl.addDataset("ds_access",ds);

    // 중략 ...
}

38.View

Anyframe 에서는 개발자들이 Struts 기반에서 보다 간편하게 UI를 개발할 수 있도록 Custom Tag Library를 제공한다. 이런 Custom Tag Library에는 Struts Html Tag를 확장한 Anyframe Html Tag와 페이지 네비게이션 부분을 별도 코드 없이 Tag를 이용하여 개발할 수 있도록 지원하는 Page Navigator Tag가 있다.

38.1.Tag Library

Anyframe 에서는 Struts Html Tag를 확장한 Anyframe Html Tag를 제공한다. Anyframe Html Tag Library는 errors, messages 두가지 Tag를 포함하고 있으나, errors Tag는 Struts 1.2 이후 deprecated Tag이므로 본 매뉴얼에서는 다루지 않을 것이다.

38.1.1.Page Navigator Tag

Anyframe 에서는 UI 구성시 Page Navigation 처리에 대한 편의성을 제공하기 위하여, Page Navigator Tag를 제공한다. 해당 UI 관련 JSP에서 이 Tag를 사용하기 위해서는 JSP의 상단에 다음과 같이 anyframe-tags.tld 파일을 taglib으로 지정해 주도록 한다.

<%@ taglib uri='/WEB-INF/anyframe-tags.tld' prefix='anyframe' %>

위에서와 같이 Page Navigator Tag의 prefix를 'anyframe'으로 정의하였을 경우 아래와 같이 해당 Tag를 사용할 수 있다.

<anyframe:pagenavigator linkUrl="javascript:fncGetUserList(2);" 
       pages="<%=resultPage%>" formName="listForm"
    firstImg="sample/images/ct_btn_pre.jpg" 
    prevImg="sample/images/ct_btn_pre01.jpg" 
    lastImg="sample/images/ct_btn_next.jpg" 
    nextImg="sample/images/ct_btn_next01.jpg" />

이 때 pages라는 속성의 값은 anyframe.common.Page 타입의 객체로 할당해 주어야 함에 유의하도록 한다.

38.1.2.Messages Tag

Messages Tag는 ActionMessage 객체 내의 대체 문자열에도 Message Resource Bundle을 이용하여 국제화를 지원할 수 있도록 Struts Html의 Messages Tag를 확장하였다. 다음은 Anyframe Messages Tag의 구현체인 anyframe.web.struts.util.DefaultMessagesTag의 일부이다.

ActionMessage report = (ActionMessage) this.iterator.next();

Object[] obj = report.getValues();
if (obj != null) {
    for (int i = 0; i < obj.length; i++) {
    
        String argKey = obj[i].toString();
        String argValue = TagUtils.getInstance().message(pageContext,
                bundle, locale, argKey, null);
        obj[i] = argValue == null ? argKey : argValue;
        
    }
}
String msg = TagUtils.getInstance().message(pageContext, bundle,
        locale, report.getKey(), obj);

위의 소스코드에서와 같이 대체 문자열에 대하여 Message Resource Bundle로부터 해당 key에 대한 값을 찾아 ActionMessage에 설정하게 된다. 이때 해당 Message Resource Bundle에 key가 정의되어 있지 않은 경우 해당 key를 그대로 대체 문자열로써 설정하게 구성되어 있다. 또한 ActionMessage에 suffix를 추가할 수 있다. 이 경우 UI에 해당 메시지 표현시 suffix가 추가되어 보여진다.

if (suffix != null && suffix.length() > 0) {
       String suffixMessage = TagUtils.getInstance().message(
       pageContext, bundle, locale, suffix);

       if (suffixMessage != null) {
           TagUtils.getInstance().write(pageContext, suffixMessage);
       }
}

38.1.2.1.Error Page 구성

다음은 Anyframe Messages Tag를 사용하여 해당 UI에 에러 메시지를 출력하는 commonError.jsp 의 일부분이다.

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>
<%@ taglib uri="/WEB-INF/anyframe-tags.tld" prefix="anyframe" %>

<table width="95%" align="left">
    <tr>
        <td class="common_error">
            <anyframe:message id="msg" 
                bundle='<%=request.getParameter("bundle")%>' 
                header="errors.header" suffix="errors.suffix">      
               <bean:write name="msg"/>
            </anyframe:message>
        </td>
    </tr>
</table>

39.Double Submit Prevention

Anyframe의 Struts Extensions에서는 Synchronized Token이라는 진보된 방법을 통해 선언적으로 중복 Form submit으로 인한 오동작을 방지할 수 있는 기능을 제공한다.

39.1.Double Submit의 개념

중복 Form Submit은 다음과 같은 경우에 발생할 수 있다.

  • Browser의 Refresh Button을 사용한 경우

  • Browser의 Back Button을 사용하여 이전 Page로 이동 후 다시 Form Submit 하는 경우

  • Browser History을 사용하여 이전 Page로 이동 후 다시 Form Submit 하는 경우

  • 서버에 영향을 주도록 고의적으로 악의적인 Form Submit을 하는 경우

  • submit Button을 한번 이상 Click 하는 경우

Browser Refresh Button을 사용한 경우에 중복 submit이 발생하는 이유는 Form Submit 이후에 Browse 주소창에 보이는 URL에 있다. 예를 들어, "<form name='test' action='/submitForm.do'>" 을 통해 Form Submit이 발생하였다라고 가정하자. 이 Form이 전송된 이후에 주소창에 '/submitForm.do'이 남아있고, 이 상황에서 Refresh Button을 누르면 동일한 URL이 재전송 되는 것이다. 이러한 상황을 막는 가장 손쉬운 방법은 Form Submit 후에 HTTP Redirect 기능을 사용하는 것이다. 만일 test Form 전송 후에 보여 주는 Page가 success.jsp 라고 하면, HTTP Redirect를 사용할 경우 Browse 주소창에 success.jsp가 보일 것이다. 이 경우에는 Refresh Button을 눌러도 success.jsp가 다시 로드된다. <forward name="success" path="/Success.jsp" redirect="true" /> 이렇게 함으로써 우연히 Browser Refresh Button을 눌렀을 때의 동작 오류를 방지할 수 있다. 하지만 Browser Back Button 등을 사용한 Form Resubmit을 근원적으로 막지는 못하게 된다. 따라서, Anyframe 에서는 DefaultActionMapping과 AbstractActionSupport를 제공함으로써, 선언적인 기법으로 Synchronzed Token을 처리하여 중복 Form Submit을 방지할 수 있게 한다. 다음은 Synchronized Token을 이용한 중복 Form Submit 방지 개념도이다.

39.2.일반적인 Token 처리

일반적으로 Action 클래스에서는 다음과 같은 로직을 통해 중복 Form Submit 여부를 체크할 수 있다.

  • Action Class

    boolean valid = isTokenValid(request, true);
    if (valid) {
        // TODO: submit 할 때 수행할 로직을 넣을 것
        System.out.println("status: performed");
    } else {
        // TODO: init / reload 할 때 수행할 로직을 넣을 것
        System.out.println("status: initialized or reloaded");
    }
    saveToken(request);

  • JSP

    <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" 
    value="<%= session.getAttribute(org.apache.struts.Globals.TRANSACTION_TOKEN_KEY) %>">

    UI(JSP, HTML)에서는 "org.apache.struts.taglib.html.TOKEN"을 Key로 하는 Hidden Field를 통해 할당된 Token 값을 서버로 전송하고, 해당 Action 클래스에서는 isTokenValid 메소드 호출을 통해 이 Token 값과 Session에 저장된 Token 값을 비교함으로써, Token의 유효성을 검사한다.

39.3.선언적인 Token 처리

중복 Form Submit 방지 처리가 필요한 모든 Action과 JSP에 Token 처리를 위한 로직이 중복 구현되지 않도록 하기 위해, Anyframe 에서는 AbstractActionSupport 클래스와 DefaultActionMapping 클래스를 이용하여 선언적으로 Sychronized Token을 처리할 수 있는 기능을 제공한다. 선언적인 Synchronized Token 처리를 위해서는 Struts 속성 정의 파일 내에 Action 매핑시 다음과 같은 속성을 추가 정의해 주어야 한다.

  • saveToken : 해당 Action 수행 후, Client에서 전달한 Token을 Session에 저장할지에 대한 설정

  • validateToken : 해당 Action 수행 전, Token의 유효성을 체크할지에 대한 설정

  • resetToken : Token의 유효성을 체크한 후에 Session에 저장된 Token을 reset할지에 대한 설정

아래는 struts-config-token.xml 파일의 일부로, 선언적인 기법으로 중복 Form Submit을 방지하는 예제이다.

<action-mappings type="anyframe.web.struts.action.DefaultActionMapping">
    <action path="/synchronizedTokenView"
            type="anyframe.web.struts.action.DefaultForwardAction"
            parameter="/extensions/token.jsp">
            <set-property property="saveToken" value="true" />
    </action>
    <action path="/doubleSubmit"
            type="anyframe.sample.struts.web.action.SampleTokenAction"
            name="submitForm" scope="request">
        <set-property property="validateToken" value="true" />
        <set-property property="resetToken" value="true" />
        <forward name="success" path="/extensions/tokenSuccess.jsp" />
    </action>
</action-mappings>

각 Action 매핑시 정의한 위의 세가지 속성이 AbstractActionSupport 클래스를 통해 어떻게 처리되는지에 대해서는 다음 예를 통해 상세히 살펴보자.

39.3.1.Samples

  1. 1.중복 Form Submit 방지가 필요한 UI의 경우, 해당 UI를 로드시키기 위한 Action 실행을 통해 Token을 Session에 저장해야 한다. 따라서 해당 Action 매핑 정의시 saveToken 속성값을 true로 정의해 주도록 한다.

    <set-property property="saveToken" value="true"/>

  2. 해당 Action 클래스의 process 메소드 수행후,AbstractActionSupport의 postProcess 메소드에서는 Action 매핑시 정의한 saveToken이 true일 때 상위 클래스에서 제공하는 saveToken 메소드를 호출함으로써, 이 Token을 Session에 저장한다.

    public ActionForward postProcess(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response,
            ActionForward forward) throws Exception {
        boolean saveToken = false;
        if (mapping instanceof DefaultActionMapping) {
            DefaultActionMapping t_mapping = (DefaultActionMapping) mapping;
            saveToken = t_mapping.isSaveToken();
        }
        if (saveToken) {
            saveToken(request);
        }
        return forward;
    }

  3. 해당 UI에서 Form Submit시 이를 처리하기 위한 Action 클래스의 매핑 정보에 validateToken, resetToken 속성값을 true로 정의해 주도록 한다.

    <set-property property="validateToken" value="true"/>
    <set-property property="resetToken" value="true"/>
    

  4. 해당 Action 클래스의 process 메소드 수행 전에,AbstractActionSupport의 preProcess 메소드에서는 Action 매핑시 정의한 validateToken, resetToken 값에 따라 Session에 있는 Token이 유효한지 체크하고, 유효하다면 Session에 있는 Token을 지우게 된다. 이렇게 함으로써 중복 Form Submit만 발생하는 경우 Session에 있는 Token은 이미 지워졌으므로, Token 유효성 체크시 오류가 발생하게 되는 것이다.

    public void preProcess(ActionMapping mapping, ActionForm form,
             HttpServletRequest request, HttpServletResponse response)
             throws Exception {
    
        boolean validateToken = false;
        boolean resetToken = false;
    
        if (mapping instanceof DefaultActionMapping) { 
            DefaultActionMapping t_mapping = (DefaultActionMapping) mapping;
            validateToken = t_mapping.isValidateToken();
            resetToken = t_mapping.isResetToken();
        }
        
        if (validateToken) {
            if (!isTokenValid(request, resetToken)) {
                throw new InvalidTokenException("common.msg.invalidtoken.error");
           }
        }
    }

39.3.2.참고 사항

  • <html:form> 사용

    <html:form>를 사용하여 Form을 생성하는 경우, 별도 정의없이 Token 할당을 위한 Hidden 필드가 추가된다. 만일 <html:form>를 사용하지 않는 경우에는 다음과 같이 Hidden Field를 추가 정의해 주어야 한다.

    <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" 
    value="<%= session.getAttribute(org.apache.struts.Globals.TRANSACTION_TOKEN_KEY) %>">

  • DefaultForwardAction 사용

    별도 Action 수행없이 단순 페이지 이동만이 필요한 경우 Struts에서 기본으로 제공하는 ForwardAction을 사용하게 된다. 그러나 이런 경우에도 입력 화면으로 이동하기 전에 Sychronized Token 처리를 위한 설정이 필요하다면, Anyframe 에서 제공하는 DefaultForwardAction을 사용토록 한다.

    <action path="/synchronizedTokenView"
        type="anyframe.web.struts.action.DefaultForwardAction"
        parameter="/extensions/token.jsp">
        <set-property property="saveToken" value="true" />
    </action>
    

40.Exception Handling

Anyframe에서 제공하는 BaseException 유형의 Exception이 throw되었을 때, 이를 처리하는 ExceptionHandler에 대해 알아보기로 하자.

  • DefaultExceptionHandler

    DefaultExceptionHandler는 Struts의 ExceptionHandler를 확장한 클래스로써, Anyframe에서 제공하는 BaseException이 catch 되었을때, BaseException 내에 정의된 메시지 key와 대체 문자열을 ActionMessage 객체에 저장하여 Forward하는 로직으로 구성되어 있다. 이 외, Exception이 catch 되었을 때는 Struts의 기본 ExceptionHandler에서와 같이 <exception>에 정의된 key 값을 이용하게 된다.

  • DefaultBaseExceptionHandler

    Anyframe에서 제공하는 BaseException을 상속받아 구현한 비즈니스 Exception은 생성 시점에 전달된 메시지 Key를 이용하여, Message Resource Bundle 내에 정의된 유형별 메시지(기본,해결책,원인)가 해당 Exception 객체에 담기게 된다. (본매뉴얼의 Tech. Service >> ExceptionHandling 참조) DefaultBaseExceptionHandler는 catch한 BaseException으로부터 유형별 메시지를 모두 처리할 수 있게 구성되어 있으므로 DefaultBaseExceptionHandler를 사용하는 것이 좋다. Tag Library를 통해 메시지 Key로써 에러 메시지를 출력하는 형태가 아닌 유형별 메시지 자체를 추출하고 있다. 또한, 비즈니스 Exception 외에 프리젠테이션 레이어에서 발생한 Exception에 대해서도 에러 메시지 처리가 가능토록 기본 Resource Bundle명인 "anyframe.web.struts.common.CommonResource"를 참조한다. 일반적으로, ExceptionHandler는 Anyframe에서 제공하는 DefaultBaseExceptionHandler를 상속받아 구현하면 된다.

다음에서는 선언적인 Exception Handling 기법과 DefaultBaseExceptionHandler 확장 방법에 대해 알아보기로 한다.

40.1.선언적인 Exception Handling

Anyframe 에서는 Action 클래스에서 직접 try-catch 문으로 Exception을 처리하지 않고, 속성 정의를 통해 선언적으로 exception을 처리할 수 있다.

40.1.1.Samples

다음은 struts-config-exception.xml 의 일부로 선언적인 exception 처리의 예이다.

<action path="/exceptionHandling"
    type="anyframe.sample.struts.web.action.ExceptionHandlingAction"
    scope="request">
    <exception type="anyframe.common.exception.BaseException"
        key="common.msg.action.error"
        path="/extensions/error.jsp" 
        handler="anyframe.web.struts.util.DefaultBaseExceptionHandler"/>
</action>

위 설정을 통해 ExceptionHandlingAction 수행시 Exception이 발생한 경우, 발생한 Exception이 BaseException 유형이면 DefaultBaseExceptionHandler를 통해 해당 Exception이 처리된다. 아래와 같이 <global-exceptions> 내에 Exception에 대한 처리를 공통 정의할 수도 있다. 만일, <action>과 <global-exceptions> 내에 동일한 Exception이 정의되어 있는 경우 <action>에 정의된 Exception 처리가 우선 적용된다.

<global-exceptions>
    <exception path="/extensions/error.jsp"
        key="common.msg.authentication.error"
        type="anyframe.web.struts.util.AuthenticationException"
        handler="anyframe.sample.struts.web.common.SampleExceptionHandler" />
    <exception path="/extensions/error.jsp"
        key="common.msg.authorization.error"
        type="anyframe.web.struts.util.AuthorizationException"
        handler="anyframe.sample.struts.web.common.SampleExceptionHandler" />
    <exception path="/extensions/error.jsp" 
        key="common.msg.base.error"
        type="anyframe.common.exception.BaseException"
        handler="anyframe.sample.struts.web.common.SampleExceptionHandler" />
</global-exceptions>

40.2.DefaultBaseExceptionHandler 확장

다음은 DefaultBaseExceptionHandler를 확장하여 해당 프로젝트의 Exception 처리 방식을 재정의한 SampleExceptionHandler.java 이다.

public class SampleExceptionHandler extends DefaultBaseExceptionHandler {

    public SampleExceptionHandler() {
        this.defaultBundle 
            = "anyframe.sample.struts.web.common.SampleResources";
	}

    public ActionForward execute(Exception exception, ExceptionConfig config,
            ActionMapping mapping, ActionForm form, HttpServletRequest request,
            HttpServletResponse response) throws ServletException {

        ActionForward forward = mapping.getInputForward();
		
        if (exception instanceof AuthenticationException) {
            String loginPageURI = "/loginView.do";
            forward.setPath(loginPageURI);
            request.setAttribute("authenticateFail", "true");
        } else if (exception instanceof AuthorizationException) {
            String homePageURI = "/authrizationView.do";
            forward.setPath(homePageURI);
            request.setAttribute("authFail", "true");
        } else {
            String forwardPath = forward.getPath();
            if (forwardPath == null || forwardPath.equals("")) {
                forwardPath = "/loginView.do";
                request.setAttribute("authFail", "true");
            }
            String url = forwardPath + "?";
			
            Enumeration enumrequest = request.getParameterNames();
            while (enumrequest.hasMoreElements()) {
                String parameterName = (String) enumrequest.nextElement();
                String parameterValue = request.getParameter(parameterName);
                url += parameterName + "=" + parameterValue + "&";
            }
            forward.setPath(url);
        }
        request.getSession().setAttribute("afterErrorPage", forward);
        return super.execute(exception, config, mapping, form, request,
            response);
    }
}

에러 메시지 처리를 위한 기본 Message Resource Bundle을 anyframe.sample.struts.web.common.SampleResources로 재정의하였고 이전 요청 정보를 get 방식으로 url에 추가하여 afterErrorPage로 포워딩하는 로직이 추가되어 있다. 또한, Exception 유형에 따른 처리는 super.execute 메소드를 그대로 사용하고 있음을 알 수 있다. 다음은 해당 SampleExceptionHandler를 통해 전달된 에러 메시지를 처리하는 error.jsp 의 일부이다.

<%
    ... 중략 ...
	String[] messages 
		= (String[])request.getAttribute(Globals.MESSAGE_KEY);
  	
	String userMessage = messages[0];
  	String solution = "";
  	String reason = "";
  	
  	if(messages.length==2) {
  		solution = messages[1];
  	}
  	
  	if(messages.length==3) {
  		solution = messages[1];
  		reason = messages[2];
  	} 
%>
	... 중략 ...
	<%= userMessage %><p/>
	<% if(!solution.equals("")) { %>
		<strong>* SOLUTION</strong><br/>
		<%= solution %>
	<% } %>
	<% if(!reason.equals("")) { %>
		<strong>* REASON</strong><br/>
		<%= reason %>
	<% } %>
	... 중략 ...
<td background="<html:rewrite page='/sample/images/ct_btnbg02.jpg'/>" 
    class="ct_btn01" style="padding-top:3px;">
    <a href="javascript:fncGoAfterErrorPage();">확인</a>
</td>
	... 중략 ...

41.Authentication and Authorization

요청을 보낸 클라이언트가 등록된 사용자인지 체크하여 로그인하게 해주는 Authentication과 사용자와 어플리케이션 내의 자원 간의 관계 정보를 기반으로 접근 권한을 관리하는 Authorization은 어플리케이션 개발 시 항상 고려되어야 하는 부분 중의 하나이며 두 부분이 밀접히 관련되어 있다. 인증과 권한 기능을 어플리케이션에 추가하려면 몇 가지 고려해야 할 것이 있다.

  • 사용자 정보는 어떤 방법(DB, LDAP, FILE, NT … )을 이용해 관리할 것인지 ...

  • 인증된 사용자 정보를 어떻게 (Session, Cookie… ) 저장할 것인지 ...

  • 사용자에게 권한을 부여하는 방법과 접근을 통제할 자원은 어떤 것인지 ...

JAAS 기반의 인증 방식을 적용하여 Login Context를 추상화함으로써 Login Module 개발 시 어플리케이션 코드의 수정 없이 타 시스템을 통한 인증을 수행할 수 있도록 처리하는 것이 가장 바람직하며, 프로젝트별 인증 처리 요건(ex. 외부 인증 솔루션 적용)에 맞게 작성해야 한다. 인증과 권한 관리 기능은 모든 어플리케이션에서 그대로 재사용할 수 있는 것은 아니다. 각 어플리케이션마다 정책이 다를 수 있으므로 커스터마이징이 필요하기 때문이다. 여기에서는 Anyframe 의 Struts 기반 개발시 인증과 권한 관리 방법에 대해 살펴보기로 한다.

41.1.Authentication

여기서는 일반적으로 많이 쓰이는 인증 방법인 DB에 저장된 사용자 정보를 기반으로, 인증을 수행하는 예에 대해 알아본다.

  1. Anyframe 기반에서 인증 수행을 위한 서비스 개발. 이때, 해당 서비스에서는 인증된 사용자 정보를 담은 javax.security.auth.Subject 객체 전달

  2. 로그인 수행을 위한 Action 코드에서 User ID, Password를 기반으로 해당 서비스를 호출하여 유효성 검증

  3. 인증된 사용자 정보를 담고 있는 Subject 객체 내에 사용자가 속한 그룹(Role)의 정보를 TypedPrincipal 형태로 저장

  4. Subject 객체를 'subject'라는 key값으로 Session에 저장

  5. 이 후 Session에 저장된 Subject 객체를 이용하여 권한 관리 수행

41.1.1.Samples

다음은 DB 기반의 사용자 인증 처리를 수행하는 서비스 DBAuthenticationServiceImpl.java 의 일부분이다.

public class DBAuthenticationServiceImpl implements AuthenticationService {

    // 중략 ...
     
    public Subject authenticate(anyframe.sample.struts.services.UserVO userVO)
            throws Exception {

        Subject subject = null;
        ResultSet rsu = null;
        PreparedStatement pstmt = null;
        Connection conn = null;

        String userId = userVO.getUserId();
        String password = userVO.getPassword();

        try {
            conn = dataSource.getConnection();

            pstmt = conn.prepareStatement(sqlQuery);

            pstmt.setString(1, userId);
            pstmt.setString(2, password);

            // 입력된 사용자 정보를 기반으로 등록된 사용자 정보 검색
            rsu = pstmt.executeQuery();

            if (rsu.next()) {

                userId = rsu.getString(1);
                String userName = rsu.getString(2);
                rsu.getString(3);
                String grade = rsu.getString(5);

                Set principals = new HashSet();
                Set credentials = new HashSet();

                principals
                    .add(new TypedPrincipal(userName, TypedPrincipal.USER));

                StringTokenizer tokens = new StringTokenizer(grade, ",");
                while (tokens.hasMoreTokens()) {
                    principals.add(new TypedPrincipal(tokens.nextToken(),
                            TypedPrincipal.GROUP));
                }

                // 사용자 정보를 Subject 객체에 저장
                subject = new Subject(false, principals, credentials,
                        credentials);
            } else {
                throw new FailedLoginException();
            }
        }catch (Exception e) {
            // 중략 ...
        } finally {
            // 중략 ...
        }
        return subject;
    }
}

다음은 앞서 구현한 DBAuthenticationServiceImpl 클래스의 속성 정의 파일인 context-authentication.xml 의 일부이다. USER_ID와 PASSWORD 정보를 이용하여 사용자 정보와 그룹(Role) 정보를 조회하는 쿼리문을 속성값으로 정의하고 있음을 알 수 있다. 해당 서비스는 이 쿼리문을 이용하게 될 것이다.

<bean id="authenticationService"
	class="anyframe.sample.struts.services.impl.DBAuthenticationServiceImpl">
	<property name="dataSource" ref="dataSource" />
	<property name="sqlQuery"
		value="SELECT u.USER_ID,u.USER_NAME,u.PASSWORD,u.ENABLED,a.AUTHORITY 
		FROM USERS u, AUTHORITIES a 
		WHERE u.USER_ID=? and u.PASSWORD=? and a.USER_ID = u.USER_ID" />
</bean>

다음은 LoginAction.java 로, 앞서 정의한 서비스를 이용하여 사용자의 유효성을 체크하고, 유효한 사용자 정보를 Session에 저장하는 역할을 수행하고 있다.

public ActionForward process(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) 
        throws Exception {
	
    AuthenticationService authenticationService = 
            (AuthenticationService) getService("authenticationService");

    UserForm userForm = (UserForm) form;
    UserVO userVO = new UserVO();
    BeanUtils.copyProperties(userVO, userForm);

    Subject subject = authenticationService.authenticate(userVO);

    HttpSession session = request.getSession();

    session.setAttribute("subject", subject);
		
    return (mapping.findForward("success"));
}

41.2.Authorization

Action 단위로 접근 권한 제어가 가능하다.

41.2.1.접근 권한 제어 프로세스

다음 그림에서 보여지는 사용자 정보를 기반으로 어떻게 특정 URL에 대한 접근 제어가 이루어지는지 살펴보도록 하자.

  1. Struts 속성 정의 파일 내에 Action 매핑시 roles 속성값 부여

  2. Anyframe 에서 확장한 DefaultRequestProcessor의 processRoles 메소드에서 Session에 저장되어 있는 Subject 객체의 값과 roles 정보 비교 후 해당 Action 수행 여부 결정

41.2.2.Samples

다음은 특정 URL에 접근 가능한 사용자 그룹(Role)을 지정하고 있는 struts-config-authorization.xml 의 Action 매핑 정보이다.

<action path="/authorization"
    type="anyframe.web.struts.action.DefaultForwardAction"
    roles="admin"
    scope="request"
    parameter="/extensions/accessSuccess.jsp">
</action>

위의 예에서 tester라는 USER_ID를 가진 사용자는 admin이란 GROUP에 속해 있으므로 authorization.do라는 요청을 수행할 수 있게 된다. roles 속성값이 부여되지 않은 Action의 경우에는 모든 사용자가 접근할 수 있음을 의미한다.

42.Spring Integration

Anyframe의 Struts 기반에서 웹 어플리케이션을 개발할 때 일반적으로 MVC의 Model 영역에 해당하는 비즈니스 객체는 Spring Framework 기반의 Bean 형태로 개발될 것이다. 따라서, 프리젠테이션 로직을 수행하는 Action 클래스에서 비즈니스 로직을 수행하는 Spring Framework 기반의 서비스에 접근하기 위한 방법에 대해서 살펴보도록 하자.

42.1.Configuration

Spring Framework과 연계를 위해 다음과 같은 속성 정의가 필요하다.

  • web.xml에 org.springframework.web.context.ContextLoaderListener를 listener로 등록

  • web.xml에 context-param 요소로 contextConfigLocation 등록

42.1.1.ContextLoaderListener, ContextConfigLocation 정의

Servlet 2.3 이상에서는 웹 컨텍스트(하나의 웹 어플리케이션) 라이프 사이클 관련 이벤트인 ServletContextEvent와 Session의 라이프 사이클 관련 이벤트인 HttpSessionEvent가 추가되었다. 따라서 web.xml에 이러한 웹 어플리케이션의 이벤트에 응답하는 Context Event Listner를 등록해줌으로써 해당 Listener 구현 클래스에서 웹 컨텍스트 초기나 종료 시점에 무언가 유용한 작업 (ex. 어플리케이션의 초기 속성 로드, 서비스 컨테이너 기동 등..)을 수행할 수 있도록 해야 한다. org.springframework.web.context.ContextLoaderListener 클래스는 ServletContextListener 인터페이스를 구현하고 있으며, 어플리케이션이 Servlet 컨테이너에 의해 처음으로 로드되는 시점에 발생하는 startup 이벤트와 어플리케이션이 종료되는 시점에 발생하는 shutdown 이벤트를 처리할 수 있도록 다음의 두 메소드를 포함한다.

  • contextInitialized : root WebApplicationContext를 초기화하고 contextConfigLocation에 정의된 Bean 속성 정의 XML 파일을 기반으로 관련된 서비스 인스턴스를 생성한다.

  • contextDestroyed : 관련 자원을 release하고 root WebApplicationContext를 close 한다.

다음은 Spring Framework 연계를 위한 web.xml (ContextLoaderListener, contextConfigLocation) 의 일부이다.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /config/spring/context-*.xml
    </param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

42.2.Action

다음은 DefaultDispatchActionSupport를 상속받아 상품조회 기능에 대한 프리젠테이션 로직을 처리하는 ProductAction클래스의 일부이다. Super Class(AbstractActionSupport)에서 제공하는 getService 메소드를 호출하여 접근해야 할 비즈니스 서비스를 얻어내고 있음을 알 수 있다.

public class ProductAction extends DefaultDispatchActionSupport {
    // 중략 ...
    public ActionForward list(ActionMapping mapping, ActionForm form,
        HttpServletRequest request, HttpServletResponse response) throws Exception {
        ProductService productService = 
            (ProductService) getService("productService");

        ProductSearchVO searchVO = new ProductSearchVO();

        ProductForm productForm = (ProductForm) form;
        BeanUtils.copyProperties(searchVO, productForm);
        
        // 중략 ...

        Page resultPage = productService.getPagingList(searchVO);

        request.setAttribute("search", searchVO);
        request.setAttribute("productList", resultPage.getList());
        request.setAttribute("size", resultPage.getTotalCount());
        request.setAttribute("pagesize", resultPage.getPagesize());
        request.setAttribute("pageunit", resultPage.getPageunit());

        return mapping.findForward("success_list");
    }
}

Spring Framework 기반의 서비스 개발시 Bean 속성 정의에 관련된 자세한 사항은 Spring IOC를 참고한다.

42.3.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.struts.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.struts를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.struts를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.struts를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-task-2.0.10.jar 파일이 있어야 한다.)

    표 42.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.struts.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

IX.Tech.Service

Anyframe은 자체 개발 또는 오픈 소스 활용 및 확장을 통해 어플리케이션 개발시 용이하게 재사용할 수 있는 Cache, DB 연결, 쿼리문 처리, 트랜잭션 관리, 로깅 등과 같은 다양한 Technical Service들을 제공한다. 이러한 Technical Service들은 Lightweight 컨테이너에서 동작 가능하도록 설계/개발되었으며, 인터페이스와 구현 클래스로 분리되어 구현되어 있으므로, 인터페이스 규약에 맞게 구현 클래스를 추가하거나 제공된 구현 클래스를 확장함으로써 언제든지 해당 어플리케이션의 용도에 맞게 변경이 용이하다. 여기에서는 Technical Service가 제공하는 기능과 활용 방법에 대해 살펴보도록 하자.

43.Common Configuration

Anyframe를 사용하여 개발할 때 필수적으로 필요한 설정 정보들이 있다. Anyframe 에서 제공하는 테크니컬 서비스들이 사용하는 메시지들과 Anyframe 에서 확장한 Spring 속성 정의 파일에 사용되는 태그에 대한 정의가 공통적으로 필요하다.

43.1.Anyframe MessageSource

Anyframe의 테크니컬 서비스들 중 Properties, Id Generation, Query Service에서 자체적으로 사용하는 메시지들을 공통 MessageSource에 설정해줘야 한다. 이 테크니컬 서비스들을 사용하여 개발할 때 유용한 정보 메시지나 분명한 에러 메시지를 제공함으로써 서비스 활용도를 높여준다.

보다 자세한 MessageSource 내용에 대해서는 본 매뉴얼의 Bean과 Container의 확장 Spring >> IoC >>Bean과 Container의 확장 >> MessageSource를 활용한 국제화(I18N) 지원 을 참고하도록 한다.

43.1.1.Samples

다음은 Anyframe 사용 시 작성하는 MessageSource 속성 설정에 대한 예제이다.

  • Configuration

    다음은 Anyframe 의 테크니컬 서비스들 중 Properties, Id Generation, Query 서비스에서 사용하는 메시지 Properties 파일(properties.properties, idgeneration.properties, query.properties)을 정의해놓은 것이다. 실제 각 Properties 파일은 각 서비스 라이브러리(JAR) 파일내에 존재한다.

    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
          <list>
            <value>anyframe/core/properties/messages/properties</value>
            <value>anyframe/core/idgen/messages/idgeneration</value>
            <value>anyframe/core/query/messages/query</value>
            <!-- 프로젝트에서 message properties 파일 추가 -->
          </list>
        </property>
    </bean>

43.2.Configuration Tag 확장

Anyframe의 Query Service 또는 Properties Service의 속성 정의 XML을 보면, <config:configuration>와 같은 태그를 볼 수 있다. 이는 Anyframe에서 확장한 태그로 anyframe-core-service-2.0.xsd의 구조 정의를 따르고 있다. <config:configuration> 태그를 사용하기 위해서는 ConfigurableCallback 빈을 설정해야 한다.

ConfigurableCallback 빈은 Anyframe Common 라이브러리(anyframe.common-x.x.x.jar)내의 spring.handlers의 정의에 따라, anyframe.common.config.ServiceNamespaceHandler에 의해 처리된다. 그리고 ServiceNamespaceHandler 내의 inner class인 PropertyModifyingBeanDefinitionDecorator에서 ReplaceOverride 인스턴스를 생성하는데 이때, setConfiguration이라는 메소드명과 configurableCallBack 이라는 bean name을 인자로 전달하고 있다.

Spring Container에서 Spring 속성 정의 파일들(context-xxx.xml)을 읽어서 정의된 Bean들의 인스턴스를 생성할 때, ConfigurableCallBack을 통해 <config:configuration>내에 정의된 내용은 org.apache.avalon.framework.configuration.Configuration 객체의 형태로 해당 구현 클래스의 configure() 메소드에 전달된다. 해당 서비스의 구현 클래스는 Configuration 객체를 이용하여 속성 정보를 읽어 처리할 수 있다.

즉, 이와 같이 <config:configuration> 태그 정의 부분이 있는 경우, 태그 내부에 작성된 정보들을 Configuration 객체 형태로 해당 구현 클래스의 configure() 메소드에 전달해주기 위해 ConfigurableCallback 정의가 필요하다.

43.2.1.Samples

다음은 Configuration 확장 Tag를 사용하기 위해 설정한 예이다.

  • Configuration

    다음은 Anyframe 에서 확장한 Configuration 확장 Tag를 사용하기 위해서는 반드시 아래와 같이 공통 Spring 속성 정의 파일 내에 configurableCallBack Bean을 설정해야 한다.

    이때 <config:configuration> 태그를 직접 사용하는 Spring 속성 정의 파일에서 <beans> 태그 내에 XML Namespace로 config를 작성하고, SchemaLocation 정보에 anyframe-core-service-2.0.xsd를 추가해야 함에 유의하도록 한다.

    <bean id="configurableCallBack" scope="prototype" 
            class="anyframe.common.config.ConfigurableCallback"/>

    다음은 <config:configuration> 태그를 사용하는 Anyframe 의 Properties 서비스 속성 정의 파일의 일부이다. 보다 자세한 Properties 서비스 내용에 대해서는 본 매뉴얼의 Tech. Service >> Properties Service 를 참고하도록 한다.

    <beans xmlns="http://www.springframework.org/schema/beans"
        ...중략
        xmlns:config="http://www.anyframejava.org/schema/service"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 	                
        ...중략
        http://www.anyframejava.org/schema/service 
        http://www.anyframejava.org/schema/service/anyframe-core-service-2.0.xsd">
    
    <bean name="propertiesService" 
        class="anyframe.core.properties.impl.PropertiesServiceImpl">
        <config:configuration>
          <properties>
               <!-- PAGE -->
               <element key="PAGE_SIZE" value="3"/>
               <element key="PAGE_UNIT" value="10"/>
               ...중략
          </properties>				
        </config:configuration>					
    </bean>
    

    다음은 <config:configuration> 태그를 사용하는 Anyframe 의 Query 서비스 속성 정의 파일의 일부이다. 보다 자세한 Query 서비스 내용에 대해서는 본 매뉴얼의 Tech. Service >> Query Service를 참고하도록 한다.

    <beans xmlns="http://www.springframework.org/schema/beans"
        ...중략
        xmlns:config="http://www.anyframejava.org/schema/service"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 	                
        ...중략
        http://www.anyframejava.org/schema/service 
        http://www.anyframejava.org/schema/service/anyframe-core-service-2.0.xsd">
    
    <bean id="sqlLoader" 
        class="anyframe.core.query.impl.config.loader.SQLLoader">
        <config:configuration>
          <filename>
             classpath:query/mapping-query-*.xml
          </filename>
          ...중략
        </config:configuration>
    </bean>
    

44.Generic Service

Generic Service는 Java 5부터 지원하는 Generics 개념을 기반으로 개발되었으며, 도메인 클래스 기반의 Service 인터페이스/구현 클래스, DAO 인터페이스/구현클래스(Hibernate/JPA, Query Service 지원) 등 기본 CRUD 메소드 기능이 모두 구현된 클래스를 직접 이용하거나 상속받아서 사용할 수 있는 기능을 제공하고 있다.

Generic Service는 AppFuse (http://appfuse.org/display/APF/Home)의 개념과 소스 코드(템플릿 포함)를 참고하여 Anyframe에 맞게 수정되었다. 아래의 Generic Service 구성 클래스들을 활용한 샘플 코드들은 직접 작성하여 사용할 수도 있고, Anyframe Gen 툴을 통해 자동 생성되는 코드를 사용할 수도 있다. Anyframe Gen에 대한 자세한 설명은 Anyframe Gen 매뉴얼을 참고하도록 한다.

Generic Service를 이용하여 개발 시 다음과 같은 특징을 갖는다.

  • 도메인 모델 객체를 중심으로 기본 CRUD 코드들을 쉽게 작성할 수 있다.

  • 개발자가 Business Layer, Data Access Layer 코드를 작성하지 않고 Generic Service에서 제공하는 Service 클래스와 Dao 클래스들을 그대로 재사용하여 기본 CRUD 기능을 구현할 수 있다.

  • 기본 CRUD 외의 부가 기능이 필요한 경우 Generic Service에서 제공하는 클래스를 상속받아서 부가 기능에 대해서만 추가 기능을 구현할 수 있다.

  • DAO Framework으로 Hibernate/JPA, Query Service를 지원한다.

다음은 Generic Service 클래스들의 관계에 대한 구조도이다.

다음은 Generic Service 사용 방법이다.

  • Domain Model 클래스 생성

  • Service 클래스 생성

  • DAO 클래스 생성

  • Test Code 생성

44.1.Domain Model 클래스 생성

Domain Model 클래스는 BaseObject를 상속받은 클래스로 작성한다. Anyframe Gen 툴을 사용하여 DB 테이블 기반의 도메인 클래스를 생성하는 경우, @Entity, @Table, @Id(혹은 @EmbeddedId) 등의 Annotation이 모두 설정된 도메인 클래스가 자동 생성된다. 이렇게 생성된 도메인 클래스는 Hibernate/JPA, QueryService DAO에서 모두 사용된다.

44.1.1.BaseObject

도메인 클래스 작성 시 BaseObject를 상속받아서 구현한다. BaseObject는 다음 세가지 메소드로 구현되어 있으며, BaseObject 를 상속받아 사용할 경우 세가지 메소드에 대해 오버라이드 해야한다.

package anyframe.core.basis.model;
public abstract class BaseObject implements Serializable {
  // key=value 형태의 String을 return한다.
  public abstract String toString();
  // 객체 비교 시 사용된다.
  public abstract boolean equals(Object o);
  // equals() 메소드를 오버라이드 할 경우에는 hashcode()를 반드시 오버라이드 할것.
  public abstract int hashCode();
}

44.1.2.Samples - Query Service 사용 시

다음은 BaseObject 를 이용한 Query Service DAO Framework 기반의 Product Domain 샘플코드 Product.java 의 일부분이다. BaseObject에 선언되어 있는 메소드를 오버라이드 하였다. 이때 @Entity이나 @Table과 같은 Annotation 설정 없이 @Id(복합키의 경우, @EmbeddedId) Annotation 설정만 있으면 Query Service를 이용하여 Generic Service의 CRUD 기능을 사용할 수 있다.

public class Product extends BaseObject implements Serializable {
  private String prodNo;
  중략...
	
  @Id
  public String getProdNo() {
    return this.prodNo;
  }
	
  public boolean equals(Object o) {
    Product pojo = (Product) o;
    if ((prodName != null) ? (!prodName.equals(pojo.prodName))
      : (pojo.prodName != null)) {
        return false;
    }
    중략...
    return true;
  }
	
  public int hashCode() {
    int result = 0;
    result = (31 * result) + ((prodName != null) ?
    prodName.hashCode() : 0);
    중략...
    return result;
  }
	
  public String toString() {
    StringBuffer sb = new StringBuffer(getClass().getSimpleName());
    sb.append(" [");
    sb.append("prodNo").append("='").append(getProdNo()).append("', ");
    중략...
    return sb.toString();
  }
}

44.1.3.Samples - Hibernate/JPA 사용 시

다음은 BaseObject 를 이용한 Hibernate/JPA DAO Framework 기반의 Product Domain 샘플코드 Product.java의 일부분이다. BaseObject에 선언되어 있는 메소드를 오버라이드 하였다. 이때 @Entity, @Table, @Column과 같은 Annotation 설정이 있으면 Hibernate/JPA를 이용하여 Generic Service의 CRUD 기능을 사용할 수 있다.

@Entity
@Table(name = "PRODUCT", schema = "PUBLIC")
public class Product extends BaseObject implements Serializable {
  private String prodNo;
  private Category category;
  private String prodName;
  중략...
  
  @Id
  @Column(name = "PROD_NO", unique = true, nullable = false, length = 16)
  public String getProdNo() {
    return this.prodNo;
  }
  
  public boolean equals(Object o) {
    Product pojo = (Product) o;
    if ((prodName != null) ? (!prodName.equals(pojo.prodName))
      : (pojo.prodName != null)) {
        return false;
    }
    중략...    
    return true;
  }
  
  public int hashCode() {
    int result = 0;
    result = (31 * result) + ((prodName != null) ? prodName.hashCode() : 0);
    중략...
  }
    
  public String toString() {
    StringBuffer sb = new StringBuffer(getClass().getSimpleName());
    sb.append(" [");
    sb.append("prodNo").append("='").append(getProdNo()).append("', ");
    sb.append("prodName").append("='").append(getProdName()).append("', ");
    중략...
    sb.append("]");	
    return sb.toString();
  }
}

44.2.Service 클래스 생성

Service 클래스의 경우 Generic Service에서 제공하는 기본 CRUD 메소드 이외의 기능을 제공하는 경우나 기본 CRUD 메소드를 확장하여 사용해야 하는 경우 Service 구현 클래스를 생성하여 사용하도록 하고 기본 CRUD 메소드를 그대로 사용하는 경우 Service 구현 클래스를 생성하지 않는다.

44.2.1.GenericService

서비스 인터페이스는 GenericService 를 사용한다. GenericService에는 신규 생성, 수정, 단건 조회, 목록 조회, 삭제, 데이터 존재 여부 확인에 관한 메소드가 선언되어 있다.

package anyframe.core.generic.service;

public interface GenericService<T, PK extends Serializable> {

  public void setGenericDao(GenericDao<T, PK> genericDao);
	
  T get(PK id) throws Exception;

  boolean exists(PK id) throws Exception;

  void create(T object) throws Exception;

  void update(T object) throws Exception;

  void remove(PK id) throws Exception;

  public Page getPagingList(SearchVO searchVO) throws Exception;
}

44.2.2.GenericServiceImpl

서비스 구현 클래스는 GenericServiceImpl을 사용하며 다음과 같은 구조로 되어 있다.

package anyframe.core.generic.service.impl;
public class GenericServiceImpl<T, PK extends Serializable> implements
    GenericService<T, PK> {

  protected GenericDao<T, PK> dao;
	
  public GenericServiceImpl() { }
	
  public GenericServiceImpl(GenericDao<T, PK> genericDao) {
    this.dao = genericDao;
  }

  public void setGenericDao(GenericDao<T, PK> genericDao) {
    this.dao = genericDao;
  }

  public T get(PK id) throws Exception {
    return dao.get(id);
  }

  public void create(T object) throws Exception {
    dao.create(object);
  }

  public Page getPagingList(SearchVO searchVO) throws Exception {
    return dao.getPagingList(searchVO);
  }							
  중략...
}

44.2.3.Samples

다음은 GenericService, GenericServiceImpl을 상속받은 ProductService.java , ProductServiceImpl.java 파일이며, 필요한 경우 오버라이드하여 사용한다.

서비스 인터페이스 클래스 예제이다.

public interface ProductService extends GenericService<Product, String> {

  // GenericService에 선언된 메소드 중 생성과 목록 조회 메소드만 오버라이드 한 경우이다
  // 나머지 단건 조회, 데이터 존재 여부 확인, 저장, 삭제 기능은 
  // GenericService에 정의된 그대로 사용한다
  void create(Product product) throws Exception;
  Page getPagingList(ProductSearchVO searchVO) throws Exception;

  // Category별 Product 건수 구하는 메소드는 GenericService에 없는 메소드로 추가한다
  int countProductListByCategory(String categoryNo) throws Exception;
}

서비스 구현 클래스 예제이다.

@Service("productService")
public class ProductServiceImpl extends GenericServiceImpl<Product , String>
  implements ProductService {
  @Resource
  IIdGenerationService idGenerationService;
  @Resource
  ProductDao productDao;

  @PostConstruct
  public void initialize() {
    super.setGenericDao(productDao);
  }

  // GenericService에 선언된 메소드 중 생성과 목록 조회 메소드만 오버라이드 한 경우이다
  // 나머지 단건 조회, 데이터 존재 여부 확인, 저장, 삭제 기능은 
  // GenericService에 정의된 그대로 사용한다
  public void create(Product product) throws Exception {
    product.setProdNo(idGenerationService.getNextStringId());
    productDao.create(product);
  }

  public Page getPagingList(ProductSearchVO searchVO) throws Exception {
    return this.productDao.getPagingList(searchVO);
  }

  // Category별 Product 건수 구하는 메소드는 GenericService에 없는 메소드로 추가한다
  public int countProductListByCategory(String categoryNo) throws Exception {
    int count = productDao.countProductListByCategory(categoryNo);
    return count;
  }
}

44.3.DAO 클래스 생성

44.3.1.GenericDao

DAO 인터페이스로 GenericDao<T, PK extends Serializable> Class를 이용한다. 여기서 T는 도메인 객체 타입으로 도메인 모델 클래스를 의미하고, PK는 도메인 객체의 Primary Key 타입을 의미한다. 다음은 GenericDao 클래스에 정의되어 있는 단건조회, 데이터 존재여부 확인, 저장, 삭제, 목록 조회와 관련한 메소드이다.

public interface GenericDao<T, PK extends Serializable> {

  public void setPersistentClass(final Class<T> persistentClass);

  T get(PK id) throws Exception;

  boolean exists(PK id) throws Exception;
 
  void create(T object) throws Exception;

  void update(T object) throws Exception;

  void remove(PK id) throws Exception;

  public Page getPagingList(SearchVO searchVO) throws Exception;
}
				

44.3.2.GenericDaoQuery

GenericDao 인터페이스 클래스를 구현하는 구현 클래스 중 하나로 QueryService를 DAO Framework으로 결정한 경우에 사용한다. DAO 구현 클래스에서 Query Service를 이용하기 위해 GenericDaoQuery 클래스와 QueryDaoUtils 클래스를 사용한다.

다음은 GenericDaoQuery 클래스의 모습이다.

public class GenericDaoQuery<T, PK extends Serializable> extends AbstractDAO
    implements GenericDao<T, PK> {
  private Class<T> persistentClass;

  public GenericDaoQuery() {}

  public GenericDaoQuery(final Class<T> persistentClass) {
    this.persistentClass = persistentClass;
  }

  public void setPersistentClass(final Class<T> persistentClass) {
    this.persistentClass = persistentClass;
  }

  public T get(PK id) throws Exception {
    T object = (T) findByPk(ClassUtils.getShortName(this.persistentClass),getObject(id));
    if (object == null) throw new BaseException(
      "'" + this.persistentClass+ "' object with id '" + id + "' not found");
    return object;
  }

  public void create(T object) throws Exception {
    String className = ClassUtils.getShortName(object.getClass());
    create(className, object);
  }

  public void update(T object) throws Exception {
    String className = ClassUtils.getShortName(object.getClass());
    update(className, object);
  }

  public Page getPagingList(SearchVO searchVO) throws Exception {
    중략...
    return this.findListWithPaging(ClassUtils.getShortName(getPersistentClass()),
                                  args, pageIndex, pageSize, pageUnit);
  }
}

QueryDaoUtils는 Primary Key 정보를 알아내는 Utility 클래스로 아래 코드를 참조한다.

public final class QueryDaoUtils {
  private static Map<String, XProperty>
  primaryKeyPropertyMap = new HashMap<String, XProperty>();
  private QueryDaoUtils() {}
  
  protected static XProperty getPrimaryKeyProperty(Object o) {
    String clazzName = o.getClass().getCanonicalName();

    if(primaryKeyPropertyMap.get(clazzName) != null) {
      return primaryKeyPropertyMap.get(clazzName);
    }
    try {
      EJB3ReflectionManager reflectionManager = new EJB3ReflectionManager();
      Class<?> loadedClass = ReflectHelper.classForName(clazzName);
      XClass persistentXClass = reflectionManager.toXClass(loadedClass);
      Lists<XProperty> properties = persistentXClass.getDeclaredProperties("property");

      for (XProperty property :properties) {
       if(property.isAnnotationPresent(Id.class) 
         ||property.isAnnotationPresent(EmbeddedId.class)) {
        primaryKeyPropertyMap.put(clazzName, property);
        return property;
    중략...}

  protected static Object getPrimaryKeyValue(Object o) {
    // Use reflection to find the first property
    String fieldName = getPrimaryKeyFieldName(o);
    String getterMethod = "get"+ Character.toUpperCase(fieldName.charAt(0))
                               + fieldName.substring(1);
    중략...}
}

44.3.3.GenericDaoHibernate

GenericDao 인터페이스 클래스를 구현하는 구현 클래스 중 하나로 Hibernate/JPA를 DAO Framework으로 결정한 경우에 사용한다.

DAO 구현 클래스를 생성할 때 Hibernate/JPA를 활용하기 위한 GenericDaoHibernate 클래스 모습이다. DAO 인터페이스에 선언된 메소드가 구현되어 있다.

public class GenericDaoHibernate<T, PK extends Serializable> 
    implements GenericDao<T, PK> {

  private Class<T> persistentClass;
  private HibernateTemplate hibernateTemplate;
  private SessionFactory sessionFactory;
  private IDynamicHibernateService dynamicHibernateService;

  public GenericDaoHibernate() {}

  public GenericDaoHibernate(final Class<T> persistentClass) {
    this.persistentClass = persistentClass;
  }

  public Class<T> getPersistentClass() {
    return persistentClass;
  }
  
  public void setPersistentClass(final Class<T> persistentClass) {
    this.persistentClass = persistentClass;
  }
 
  public T get(PK id) throws Exception {
    T entity = (T)
    hibernateTemplate.get(this.persistentClass, id);

    if (entity == null) throw new BaseException("'" + this.persistentClass
             + "' object with id '"+ id + "' not found");
    return entity;
  }

  public void create(T object) throws Exception {
    hibernateTemplate.save(object);
  }

  public void update(T object) throws Exception {
    hibernateTemplate.update(object);
  }

  public Page getPagingList(SearchVO searchVO) throws Exception {
    중략...
    Page resultPage 
      = new Page(resultList, pageIndex, totalSize.intValue(), pageUnit, pageSize);
    return resultPage;
  }
}

44.3.4.Samples

앞서 소개된 클래스들을 기반으로 하여 개발한 ProductDao.javaProductDaoImpl.java 코드의 일부이다. 여기서는 DAO Framework으로 Query Service를 사용하므로 GenericDaoQuery 클래스를 상속받은 DAO 클래스를 생성한다. Hibernate 또는 Query Service 를 사용하기 위한 Configuration 은 각각의 Tech.Service 매뉴얼을 참조한다.

  • UserDao로 GenericDao를 상속받았으며, getPagingList 메소드를 오버라이드하고, countProductListByCategory 메소드를 추가하였다.

    public interface ProductDao extends GenericDao<Product, String> {
      Page getPagingList(ProductSearchVO searchVO) throws Exception;
      int countProductListByCategory(String categoryNo) throws Exception;
    }
    						

  • Query Service를 이용한 DAO 구현 클래스의 일부분이다. 오버라이드 하지 않은 메소드(단건조회, 데이터 존재여부 확인, 저장, 삭제)의 기능은 GenericDaoQuery에 구현된 형태 그대로 사용한다.

    @Repository("productDao")
    public class ProductDaoImpl extends GenericDaoQuery<Product,String> 
      implements ProductDao {
    
      @Resource
      IPropertiesService propertiesService;
      @Resource
      IQueryService queryService;
    
      public ProductDaoImpl() {
        super(Product.class);
      }
    
      @PostConstruct
      public void initialize() {
        super.setQueryService(queryService);
        super.setPropertiesService(propertiesService);
      }
    
      public Page getPagingList(ProductSearchVO searchVO) throws Exception {
      중략...}
    
      public int countProductListByCategory(String categoryNo) throws Exception {
      중략...}
    }

44.4.Test Code 생성

44.4.1.Unit Test Case

Unit Test Case 작성을 위해 jMock을 사용한 BaseServiceMockTestCase 클래스를 이용한다.

@RunWith(JMock.class)
public abstract class BaseServiceMockTestCase {
  protected Mockery context = new JUnit4Mockery();

  public BaseServiceMockTestCase() {
    String className = this.getClass().getName();
    중략...}

  protected Object populate(Object obj) throws Exception {
    Map map = ConvertUtil.convertBundleToMap(rb);
    BeanUtils.copyProperties(obj, map);
    return obj;
  }
}

44.4.2.Integration Test Case

Integration Test Case는 Generic Service에서는 제공하지 않으며, Spring Framework의 테스트 케이스 클래스를 이용한다.

44.4.3.Samples

다음은 BaseServiceMockTestCase 클래스를 이용하여 작성된 Unit Test Case 클래스인 ProductServiceImplTest 파일이다.

public class ProductServiceImplTest extends BaseServiceMockTestCase {
  private ProductServiceImpl service = null;
  private ProductDao dao = null;

  @Before
  public void setUp() {
    dao = context.mock(ProductDao.class);
    service = new ProductServiceImpl(dao);
  }

  @After
  public void tearDown() {
    service = null;
  }

  @Test
  public void testGetProduct() throws Exception { 
    final String no = "proNo123";
    final Product product = new Product();
    product.setProdNo(no);

    context.checking(new Expectations() {
      { one(dao).get(with(equal(no)));will(returnValue(product));}});

    Product result = service.get(no);
    assertSame(product, result);
  
  중략...
}

다음은 Spring의 SpringJUnit4ClassRunner 클래스를 이용하여 작성된 Integration Test Case 클래스인 ProductServiceTest 파일이다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:/spring/context-*.xml" })
public class ProductServiceTest {

  @Resource(name = "foundationProductService")
  private ProductService productService;

  @Test
  public void testUpdateProduct() throws Exception {
    Product product = productService.get("PRODUCT-00001");

    String name = "samsungIndia" + System.currentTimeMillis();
    product.setProdName(name);
    productService.update(product);

    product = productService.get("PRODUCT-00001");

    Assert.assertNotNull("Fetching of product", product);
    Assert.assertEquals("matching product name", name, product.getProdName());
  }
  중략...
}

44.5.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.generic.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=anyframe.example.generic.Main이라는 명령어를 실행시켜 결과를 확인한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더의 anyframe/example/generic 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 44.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.generic.zipDownload

  • 참고자료

45.DynamicModule Service

DynamicModule Service는 공통/서비스/웹 등의 타입 별 프로젝트로 구분하여 프로젝트들을 구성하는 경우, 몇가지 설정을 추가함으로써 Dynamic Reloading 기능을 제공하는 서비스이다. 리로딩이 되는 단위는 각각의 프로젝트 단위이며 런타임 환경이 아닌 개발 환경에서 사용하도록 한다. 여기서 Dynamic Reloading 기능이란, WAS 재기동 없이 클래스나 JAR 파일 단위로 동적 리로딩되는 기능을 말한다. 개발 프로젝트 현장에서 동적 리로딩에 대한 요구사항이 끊임없이 나오고, WAS 재기동에 걸리는 오랜 시간으로 인해 개발자들의 개발 생산성이 저하되는 것을 막고자 DynamicModule Service가 개발되었다.

DynamicModule Service는 Impala (dynamic module framework: http://code.google.com/p/impala/) 를 확장하여 Dynamic Reloading 기능을 손쉽게 제공할 수 있도록 구현된 서비스로 개발자가 서비스 API를 호출하여 직접 사용하는 형태가 아닌, 설정 정보들을 통해서 서비스가 구동되는 형태로 사용된다. 여기서는 어떻게 설정 정보를 등록하여 Dynamic Reloading 기능을 사용할 수 있는지 살펴본다.

DynamicModule Service는 다음과 같은 특징을 갖는다.

  • Spring Framework 기반의 웹 어플리케이션을 대상으로 한다.

  • 간편함과 생산성에 초점을 두고, 손쉽게 Dynamic Reloading 기능을 제공한다.

  • Dynamic Reloading 기능의 단위를 Module이라 부르는데 개발 프로젝트 단위를 하나의 Module로 구분한다.

  • Dynamic Module Framework 오픈 소스인 Impala를 확장하여 구현되었다.

  • 기존 개발 방식 그대로 사용 가능하나 추가 설정 파일 작성이 필요하다. 이를 위해 소스 코드 자동 생성 툴인 Anyframe Gen 을 이용하도록 한다.

Impala가 자체적으로 가지고 있던 제약 사항들을 제거하여 기존 프로젝트 개발 방식을 그대로 적용할 수 있도록 확장하였다. 다음 표는 각 제약 사항에 대해서 어떻게 처리하고 있는지 설명하고 있다.

표 45.1. Impala 확장 부분

제약 사항해결 방안
서비스 Interface/VO 클래스 등이 반드시 부모 프로젝트 내에 존재해야 함각 서비스 프로젝트 내에 서비스 Interface/VO 클래스 등이 존재할 수 있도록 함
동적 리로딩을 위해 변경 여부를 확인하는 모니터링 주기가 10초로 고정됨모니터링 주기를 XML 설정으로 변경 가능하게 함
동적 리로딩 단위가 클래스와 JAR 파일 중 한가지 형태로만 가능클래스와 JAR 파일 모두 리로딩 단위로 함께 인식하되, 클래스 폴더에 우선순위가 부여됨
여러 프로젝트 간 사용되는 Spring 서비스에 대해서 부모 프로젝트에 반드시 ProxyBean으로 수동으로 등록해야 함 각 서비스 프로젝트 내에 외부로 공개되는 서비스에 대한 정보 등록 시, 부모 모듈의 ProxyBean 자동 생성
동적 리로딩을 위해 수동으로 추가 작성해야 하는 파일이 존재함프로젝트 기본 구조 및 소스 코드 자동 생성 기능 제공(Anyframe Gen 툴 이용)
빌드 프로세스 표준 미정립Anyframe 기반 개발 시 기본 어플리케이션 구조 및 빌드 프로세스 제공

45.1.Project Structure

DynamicModule Service를 사용하기 위해서는 공통(common)/서비스(service)/웹(web) 타입 별로 복수 개의 프로젝트로 구성하게 되는데 이때 구성되는 프로젝트 구조를 살펴보면 다음과 같다. DynamicModule Service를 사용하지 않는 프로젝트를 개발하는 경우와 비교하여 부가적으로 더 필요한 파일 혹은 정보들에 대해서 아래 트리 구조에서 확인할 수 있다.

  • 공통(common) 타입 프로젝트

    common
      |
      +-- src/main/resources
           |
           +-- module.properties
           +-- spring
                 |
                 +-- context-dynamic.xml
                 +-- ...
  • 서비스(service) 타입 프로젝트

    service
      |
      +-- src/main/resources
           |
           +-- module.properties
  • 웹(web) 타입 프로젝트

    web
      |
      +-- src/main/resources
      |    |
      |    +-- module.properties   
      |    +-- moduledefinitions.xml 
      |    +-- anyframe.properties         
      |    +-- META-INF
      |           |
      |           +-- impala-anyframe-bootstrap.xml
      |           +-- impala-anyframe-web-bootstrap.xml
      |           +-- impala-anyframe-web-classjar.xml
      |           +-- impala-anyframe-web-jar-module-bootstrap.xml
      |           +-- impala-anyframe-web-listener-bootstrap.xml
      +-- src/main/webapp/WEB-INF
           |
           +-- web.xml

45.2.Configuration

다음은 DynamicModule Service를 사용하기 위해 필요한 설정 파일들이다. 부가 설정이 필요한 파일 목록을 확인하고 각 파일 별로 설정하는 방법에 대해서 설명하도록 한다.

  • module.properties

  • web.xml

  • moduledefinitions.xml

  • anyframe.properties

  • impala configuration xml files

  • context-dynamic.xml

45.2.1.module.properties

Dynamic Reloading 단위인 프로젝트 별로 반드시 하나의 module.properties 파일을 갖고 있어야 한다. DynamicModule 서비스의 경우 공통/서비스/웹 타입 등으로 구분된 타입 별 프로젝트들로 구성하여 사용하게 되는데 이때 설정하게 되는 module.properties 내용이 달라진다.

45.2.1.1.공통(common) 프로젝트

다른 서비스나 웹 타입 프로젝트에 대한 부모 프로젝트로 공통적으로 사용되는 Exception, Aspect 클래스들과 기술 공통 서비스에 대한 설정 파일들을 관리한다.

다음은 공통 타입 프로젝트의 module.properties 파일의 예이다.

root-project-names=common
context-locations=spring/context-all-common.xml ,spring/context-dynamic.xml
export-packages=com.sds.emp.common.aspect,com.sds.emp.users.service,com.sds.emp.domain 
중략...

표 45.2. common type project - module.properties

propertydescription
root-project-names프로젝트 명
context-locationsspring configuration xml 파일 목록
export-packages상위 클래스로더에서 해당 패키지 하위의 클래스들을 사용할 수 있도록 패키지 목록 설정

45.2.1.2.서비스(service) 프로젝트

Business Layer 코드를 담당하는 프로젝트로 실제 비즈니스 서비스, DAO, SQL 등의 코드를 관리한다.

다음은 서비스 타입 프로젝트의 module.properties 파일의 예이다.

parent=common
beanid-interfaces=usersService\:com.sds.emp.users.service.UsersService
context-locations=spring/context-all-service.xml
export-packages=com.sds.emp.domain,com.sds.emp.users.service
중략...

표 45.3. service type project - module.properties

propertydescription
parent부모 프로젝트 명(즉, 공통(common) 타입 프로젝트 명)
beanid-interfaces다른 프로젝트에서도 호출하여 사용하게 되는 서비스의 경우 해당 서비스의 spring bean id와 인터페이스 클래스 명을 설정
context-locationsspring configuration xml 파일 목록
export-packages상위 클래스로더에서 해당 패키지 하위의 클래스들을 사용할 수 있도록 패키지 목록 설정(도메인 클래스와 서비스 인터페이스 클래스들의 패키지 정보 설정)

45.2.1.3.웹(web) 프로젝트

Presentation Layer 코드를 담당하는 프로젝트로 Controller, JSP 페이지 등의 코드를 관리한다.

다음은 웹 타입 프로젝트의 module.properties 파일의 예이다.

parent=common
context-locations=spring/context-all-web.xml
type=servlet
export-packages=

표 45.4. web type project - module.properties

propertydescription
parent부모 프로젝트 명(즉, 공통(common) 타입 프로젝트 명)
context-locationsspring configuration xml 파일 목록
type웹 프로젝트의 경우 servlet으로 설정
export-packages상위 클래스로더에서 해당 패키지 하위의 클래스들을 사용할 수 있도록 패키지 목록 설정

45.2.2.web.xml

웹 프로젝트를 생성하여 웹 어플리케이션을 개발하는 경우 web.xml 파일을 작성하게 되는데 Dynamic Reloading 기능을 위해서 필요한 설정은 다음과 같다. Spring의 org.springframework.web.context.ContextLoaderListener, org.springframework.web.servlet.DispatcherServlet 클래스가 아닌 ExtExternalModuleContextLoader, ExtImpalaContextLoaderListener, ExternalModuleServlet 클래스를 사용해야 함에 유의하도록 한다.

중략...
<context-param>
  <param-name>contextLoaderClassName</param-name>
  <param-value>anyframe.core.dynamicmodule.web.loader.ExtExternalModuleContextLoader</param-value>
</context-param>
<context-param>
  <param-name>bootstrapLocationsResource</param-name>
  <param-value>classpath:anyframe.properties</param-value>
</context-param>

<listener>
  <listener-class>anyframe.core.dynamicmodule.web.loader.ExtImpalaContextLoaderListener</listener-class>
</listener>

<servlet>
  <servlet-name>web</servlet-name>
  <servlet-class>org.impalaframework.web.servlet.ExternalModuleServlet</servlet-class>      
  <load-on-startup>1</load-on-startup>
</servlet>
중략...

45.2.3.moduledefinitions.xml

웹 프로젝트를 생성하여 웹 어플리케이션을 개발하는 경우 부가적으로 moduledefinitions.xml 파일이 필요하다. 이 파일 내에 Dynamic Reloading 대상이 되는 프로젝트 목록을 작성한다.

<?xml version="1.0" encoding="UTF-8"?>
<parent>
  <names>common service web</names>
</parent>

45.2.4.anyframe.properties

위에서 설명한 web.xml 설정 파일 내 bootstrapLocationsResource 컨텍스트 파라미터 값으로 anyframe.properties 파일을 설정하는데 이 파일을 통해 정의되는 속성 내용은 다음과 같다.

# 1. This entry is suitable for auto-reloading
bootstrapLocations=anyframe-bootstrap,\
				   anyframe-web-bootstrap,\
				   jmx-bootstrap,\
				   anyframe-web-listener-bootstrap,\
				   anyframe-web-classjar
app.home=/Users/soo/Devel/anyframe/Gen-1.0.0/applications/emarketplace
application.version=1.0.0
was.deploy=false

표 45.5. anyframe.properties

propertydescription
bootstrapLocations사용되는 impala configuration xml 파일 목록 정의
app.home어플리케이션 폴더 경로
application.versionDynamicModule Service를 이용하여 웹 어플리케이션을 구동시킬 때 로딩되는 프로젝트 JAR 파일 명 검색 시 사용될 디폴트 버전을 설정하는데 System Property로 application.version을 설정하여 변경할 수도 있음
was.deploy이클립스 프로젝트 내에서 WTP 웹 프로젝트 형태로 개발하는 경우 false로 설정하고 실제 WAS에 배포하여 웹 어플리케이션을 구동시키는 경우 true로 설정하는데 이 값을 기준으로 디폴트 workspace.root 정보를 알아낼 수 있음. System Property로 workspace.root를 설정하여 변경할 수도 있음

45.2.5.impala configuration xml files

웹 프로젝트를 생성하여 웹 어플리케이션을 개발하는 경우 부가적으로 impala 설정 파일들이 필요하다. 제공되는 이 파일들을 그대로 사용하거나 필요한 경우 설정 값을 변경하여 사용할 수 있다.

anyframe.dynamicmodule-x.x.x.jar 파일과 impala 라이브러리 파일내에 이미 impala 설정 파일들이 함께 패키징되어 배포되고 있으므로 개발자가 추가로 파일을 프로젝트 내에 가져올 필요는 없다. 설정 값을 변경하는 경우에 한하여 웹 프로젝트로 XML 파일들을 가지고 와서 수정하도록 한다. 예를 들어 JAR 파일 형태로 프로젝트를 패키징하여 Dynamic Reloading 기능을 수행하지 않고 프로젝트의 클래스 타켓 폴더를 지정하여 Dynamic Reloading 기능을 사용한다면, 아래와 같이 moduleClassDirectory bean의 defaultValue 값을 target/classes나 dist/classes가 아닌 다른 폴더로 변경할 수 있다.

<bean id="moduleClassDirectory"
    class="org.impalaframework.bean.SystemPropertiesFactoryBean">
  <property name="properties" ref="locationProperties" />
  <property name="propertyName" value="impala.module.class.dir" />
  <property name="defaultValue" value="target/classes, dist/classes" />
</bean>
중략...

45.2.6.context-dynamic.xml

공통/서비스/웹 타입 별 프로젝트를 구성한 경우 공통 타입 프로젝트가 가장 최상위 부모 프로젝트가 된다. 만약 서비스나 웹 타입 프로젝트에서 Anyframe에서 제공하는 Technical Service를 Spring Bean으로 등록하고 사용하고 있으며 이를 다른 서비스나 웹 타입 프로젝트에서도 함께 사용하고 싶다면, 공통 타입 프로젝트 context-dynamic.xml 파일에 아래와 같이 ContributionProxyFactoryBean을 이용하여 추가 정의하도록 한다.

아래 예시는 sessionFactory, DynamicHibernateService, QueryService를 모든 프로젝트에서 사용하기 위해서 정의한 것이다.

<bean id="sessionFactory"
    class="org.impalaframework.spring.module.ContributionProxyFactoryBean">
      <property name="proxyInterfaces" value="org.hibernate.SessionFactory" />
</bean>	 		

<bean id="dynamicHibernateService"
    class="org.impalaframework.spring.module.ContributionProxyFactoryBean">
      <property name="proxyInterfaces"
        value="anyframe.core.hibernate.IDynamicHibernateService" />
</bean>		

<bean id="queryService"
    class="org.impalaframework.spring.module.ContributionProxyFactoryBean">
      <property name="proxyInterfaces" value="anyframe.core.query.IQueryService" />
</bean>

신규 비즈니스 서비스 사용

개발자가 신규로 개발한 비즈니스 서비스의 경우에도 해당 서비스 프로젝트 외 다른 프로젝트에서도 함께 사용하려면 마찬가지로 ContributionProxyFactoryBean 정의가 필요하지만 DynamicModule Service에 의해 자동 생성되므로 추가 정의할 필요가 없다. 단, 자동 생성되기 위해서는 위에서 설명한 것과 같이 서비스 타입 프로젝트에 있는 module.properties 파일의 beanid-interfaces 속성 값을 작성해줘야만 한다.

45.3.Build

위에서 설명한 대로 타입(common/service/web) 별 프로젝트를 생성한 후, 실제 웹 어플리케이션을 구동시키는 방법은 크게 2가지로 구분하여 살펴볼 수 있다. 첫번째로 Eclipse 내 WTP Dynamic 웹 프로젝트 형태로 웹 어플리케이션을 구동시키는 방법, 그리고 두번째로 웹 어플리케이션 배포 파일을 구성하여 외부 WAS에 실제로 배포하여 구동시키는 방법이 있다. 두번째 방법에서는 class 파일 형태의 배포 방식과 jar 파일 형태의 배포 방식으로 구분하여 구동시킬 수 있다.

45.3.1.Eclipse 내에서 웹 어플리케이션 구동

Eclipse 내 WTP Dynamic 웹 프로젝트 형태로 Tomcat 등의 서버와 연동하여 웹 어플리케이션을 구동시킬 수 있다. 공통(common) 타입과 서비스(service) 타입 프로젝트들의 빌드 결과를 웹(web) 타입 프로젝트에 수동으로 배포할 필요 없이, 각 해당 프로젝트의 소스 폴더에 대한 output 폴더가 해당 프로젝트 내 target/classes 폴더로 지정되어 있으면 Eclipse 자동 빌드 기능에 의해 빌드가 이루어지고, 웹(web) 타입 프로젝트를 Tomcat 등의 서버와 연동하여 구동시키면 된다.

45.3.1.1.anyframe.properties

웹(web) 타입 프로젝트 내 src/main/resources 폴더 하위 anyframe.properties 파일에서 was.deploy 값이 false로 설정되어 있어야만 한다. 그리고 app.home 값을 dynamic reloading 대상이 되는 프로젝트들의 상위 폴더로 지정한다.

중략...
app.home=/anyframe.manual/example/anyframe.example.dynamicmodule
was.deploy=false

45.3.1.2.공통(common) 타입 프로젝트 빌드

공통(common) 타입 프로젝트의 경우 프로젝트 내부 코드에 대한 빌드를 포함하여 서비스(service) 혹은 웹(web) 타입 프로젝트 module.properties의 export-packages 값에 해당하는 패키지 하위 클래스들을 함께 빌드해야 한다. 그러나 target/classes 폴더에 서비스(service) 혹은 웹(web) 타입 프로젝트의 빌드된 클래스들을 배포하게 되면 Eclipse의 clean 기능 사용 시 모두 제거되므로, dist/classes 폴더에 배포하도록 한다.

Anyframe Gen 툴을 사용하여 타입 별 프로젝트들을 생성했다면 빌드 파일까지 함께 자동으로 생성해준다. 그러나 직접 프로젝트를 생성했다면 빌드 파일을 직접 작성해줘야 한다. 아래 Resources 항목에 첨부파일로 제공하는 예제 코드 내에서는 서비스(service) 타입 프로젝트의 경우, build.xml 예시를 제공하여 export-packages 값에 해당하는 패키지 하위 클래스들을 공통(common) 타입 프로젝트의 dist/classes 폴더로 배포해주고 있다.

45.3.2.WAS에 실제 배포하여 웹 어플리케이션 구동

타입(common/service/web) 별 프로젝트 빌드를 수행하여 배포 파일을 만드는데 이때 프로젝트 별로 class 형태의 파일로 만들거나 jar 파일 형태로 만들 수 있다. 어떤 형태이든 실제 WAS에 배포되는 위치는 웹 어플리케이션의 WEB-INF/modules 폴더 하위이다. 이 폴더가 DynamicModule Service가 인식하는 dynamic reloading 대상 프로젝트들이 존재하는 위치이다.

jar 파일 혹은 class 파일 형태로 배포할 수 있으며 하나의 어플리케이션을 구성하는 프로젝트 간에 혼합하여 사용할 수도 있다. 만약 하나의 프로젝트가 동시에 jar 파일과 class 파일 형태로 배포되었다면 class 파일 형태의 배포 파일이 우선 순위가 높다.

45.3.2.1.anyframe.properties

웹(web) 타입 프로젝트 내 src/main/resources 폴더 하위에 있는 anyframe.properties에서 was.deploy 값을 true로 설정하도록 한다. 이 경우 app.home 값은 사용되는 값이 아니므로 수정하지 않아도 된다.

중략...
was.deploy=true

45.3.2.2.jar 파일 배포

WEB-INF/modules 폴더 하위에 각 타입(common/service/web) 별 프로젝트 빌드 패키징 파일이 다음과 같이 위치한다. 아래는 jar 파일 배포에 대한 예시로 배포된 형태와 각 배포 파일 내용에 대해 설명하고 있다.

WEB-INF
  |
  +-- modules
       |
       +-- common-1.0.0.jar
       +-- service-1.0.0.jar
       +-- web-1.0.0.jar
  • common-1.0.0.jar: 공통(common) 타입 프로젝트에서 module.properties 파일을 포함한 공통 클래스, Spring configuration xml 그리고 서비스(service) 혹은 웹(web) 타입 프로젝트 module.properties의 export-packages 값에 해당하는 패키지 하위 클래스들을 함께 패키징한 jar 파일

  • service-1.0.0.jar: 서비스(service) 타입 프로젝트에서 module.properties 파일을 포함한 서비스 클래스, Spring configuration xml 그리고 Tech.Service 별로 필요한 설정 파일들을 함께 패키징한 jar 파일

  • web-1.0.0.jar: 웹(web) 타입 프로젝트에서 anyframe.properties와 moduledefinitions.xml 파일 그리고 impala configuration xml 파일을 제외한 module.properties와 Controller 클래스, Spring MVC configuration xml 파일들을 함께 패키징한 jar 파일

JSP Scriptlet을 사용하는 경우

JSP에서 도메인 클래스나 서비스 클래스에 접근해야 할 필요가 있는 경우가 있다. 이때 태그 라이브러리를 통해 접근하여 사용하는 경우가 아닌, JSP Scriptlet(<%%>)을 이용하여 클래스를 사용하는 경우에는 반드시 공통(common) 타입 프로젝트의 빌드 결과 파일(ex. common-1.0.0.jar)을 WEB-INF/lib 폴더에 배포하여 사용하도록 한다.

45.3.2.3.class 파일 배포

타입(common/service/web) 별 프로젝트 빌드하여 class 파일 형태로 배포할 수 있다. WEB-INF/modules/[프로젝트 명]/target/classes 폴더 하위에 각 타입(common/service/web) 별 프로젝트 빌드 결과 클래스 파일이 다음과 같이 위치한다. 아래는 class 파일 배포에 대한 예시로 배포된 형태와 각 배포 파일 내용에 대해 설명하고 있다.

WEB-INF
  |
  +-- modules
       |
       +-- common
       |    +-- target
       |         +-- classes
       +-- service
       |    +-- target
       |         +-- classes                 
       +-- web
            +-- target
                 +-- classes
  • common/target/classes: 공통(common) 타입 프로젝트에서 module.properties 파일을 포함한 공통 클래스, Spring configuration xml 그리고 서비스(service) 혹은 웹(web) 타입 프로젝트 module.properties의 export-packages 값에 해당하는 패키지 하위 클래스들을 함께 빌드한 class 파일

  • service/target/classes: 서비스(service) 타입 프로젝트에서 module.properties 파일을 포함한 서비스 클래스, Spring configuration xml 그리고 Tech.Service 별로 필요한 설정 파일들을 함께 빌드한 class 파일

  • web/target/classes: 웹(web) 타입 프로젝트에서 anyframe.properties와 moduledefinitions.xml 파일 그리고 impala configuration xml 파일을 제외한 module.properties와 Controller 클래스, Spring MVC configuration xml 파일들을 함께 빌드한 class 파일

JSP Scriptlet을 사용하는 경우

JSP에서 도메인 클래스나 서비스 클래스에 접근해야 할 필요가 있는 경우가 있다. 이때 태그 라이브러리를 통해 접근하여 사용하는 경우가 아닌, JSP Scriptlet(<%%>)을 이용하여 클래스를 사용하는 경우에는 반드시 공통(common) 타입 프로젝트의 빌드 결과 파일(ex. common 프로젝트 하위의 target/classes 폴더 내 class 파일들)을 WEB-INF/classes 폴더에 배포하여 사용하도록 한다.

45.3.2.4.Default Value Setting (Workspace Root, Version)

workspace root와 application version에 대한 default value는 웹(web) 타입 프로젝트 내 src/main/resources 폴더 하위에 있는 anyframe.properties에서 app.home과 application.version 값에 설정되어 있는데, Eclipse 내에서 웹 어플리케이션을 구동시키는 경우에만(was.deploy 값 false 설정) app.home 값이 workspace root가 되고 WAS에 실제 배포하여 웹 어플리케이션을 구동시키는 경우에는(was.deploy 값 true 설정) 자동으로 웹 어플리케이션 루트/WEB-INF/modules 폴더가 workspace root가 된다.

anyframe.properties 값을 변경시키거나 아래와 같이 웹 어플리케이션 구동 시 -D option을 이용하여 변경시킬 수 있다. -D option과 anyframe.properties 파일에 선언된 값이 다른 경우, -D option을 통해 설정된 값이 우선순위가 높다. workspace root 하위로 dynamic reloading 대상 프로젝트들을 찾으며, jar 파일로 배포 시에는 application version 값을 프로젝트 명과 붙여서 해당 jar 파일을 찾는다.

-Dworkspace.root=/anyframe.manual/example/anyframe.example.dynamicmodule -Dapplication.version=1.0.0

45.4.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.dynamicmodule.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다. 공통(common)/서비스(service)/웹(web) 타입 별 프로젝트들로 하나의 어플리케이션을 구성하도록 예제를 작성하였다. Maven 기반으로 예제를 실행시키지 않고 Eclipse 기반으로만 실행시키도록 한다.

    만들어진 예제 프로젝트는 Anyframe Gen을 이용하여 생성된 프로젝트들을 변경하여 재구성한 것으로 multi project 구조에 dynamic reloading 기능을 사용하는 어플리케이션 예제이다.

    각 프로젝트 및 어플리케이션 빌드는 Anyframe Gen을 설치한 경우에 한해서 동작할 것이다. 여기에서는 Eclipse 내에서 Eclipse 빌드 기능을 통해 각 프로젝트를 빌드하고, 웹 어플리케이션을 구동시켜보도록 하자.

    • Eclipse 기반 실행 - WTP 활용

      압축을 해제하면 3개의 Eclipse 프로젝트로 구분되는데 3개의 프로젝트(common,service,web)를 import한 후, 반드시 web 프로젝트 내 src/main/resources 폴더 하위에 있는 anyframe.properties에서 app.home을 anyframe.example.dynamicmodule.zip 압축 해제 위치로 변경해주도록 한다.

      web 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/web를 입력하여 실행 결과를 확인한다.

      dynamic reloading 기능을 테스트해보기 위해서 Tomcat Server를 띄운 상태에서 service 프로젝트 내 src/main/java 폴더 하위에 있는 anyframe.example.dynamicmodule.users.service.impl 패키지 내 UsersServiceImpl 클래스의 getPagingList 메소드를 수정한 후 반영이 잘 되는지 확인해보도록 한다.

    표 45.6. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.dynamicmodule.zipDownload

  • 참고자료

46.MiPlatform Service

어플리케이션의 UI를 MiPlatform을 사용해 개발 할 경우, MiPlatform 고유의 데이터 형태를 DB에 반영하기에는 많은 어려움이 있다.

예를 들어 Dataset에 10개의 컬럼과 10개의 Insert할 Record가 있을 경우 개발자가 일반적인 JDBC코딩을 하기 위해서는 Dataset의 10개의 컬럼 값을 일일이 꺼내야 하고 10번의 루프를 돌면서 Insert문을 실행하는 로직을 작성해야 한다.

또 DB에서 조회를 하고자 할 경우에는 ResultSet의 메타 정보를 이용해 Dataset의 컬럼을 셋팅하고 루프를 돌면서 ResultSet의 값들을 Dataset에 추가하는 로직을 작성해야 한다.

Anyframe Ria MiPlatform은 MiPlatform의 고유 데이터 형태를 사용해 DB에 CRUD하기 위한 공통 비즈니스 서비스와 Controller를 제공한다.

Anyframe Ria MiPlatform의 장점은 아래와 같다.

  • Dataset, VariableList와 같은 MiPlatform 고유의 데이터 형태를 변환하지 않고 비즈니스 서비스 개발을 할 수 있다.

  • 추가적인 비즈니스 로직이 필요 없는 CRUD에 대해서는 비즈니스 서비스 개발 없이 Query Mapping File에 필요한 Query만 작성하면 된다.

  • 확장이 필요한 부분만 오버라이드 해서 사용할 수 있기 때문에 비즈니스 서비스 개발이 쉽다.

  • 기능이 중복되거나 불필요한 클래스를 생성하지 않기 때문에 전체 클래스 수가 줄어 들고 유지보수 또한 용이하다.

Anyframe Ria MiPlatform은 크게 Controller, Service, Dao로 구성되어 있다.

  • Controller – MiPController : AnyframeMiPController의 operate()를 확장한 클래스로 사용자 요청에 따라 비즈니스 서비스의 메소드를 호출하고 결과값을 화면으로 전달한다.

  • Service 인터페이스 – MiPService : DatasetList, VariableList를 이용해 DB에서 데이터를 조회, 추가, 삭제, 수정 등을 할 수 있는 API를 제공한다.

  • Service 구현 클래스 – MiPServiceImpl : MiPService의 구현 클래스로 Dataset과 실행 하고자 하는 Query Id를 짝지은 후 MiPDao의 메소드를 호출하고 Query실행 결과를 DatasetList에 추가한다.

  • Dao 인터페이스 - MiPDao : Dataset또는 VariableList와 Query Id를 파라미터로 입력 받아 Query를 실행 할 수 있는 API를 제공하고 있다.

  • Dao 구현 클래스 – MiPDaoQuery : MiPDao의 구현 클래스로 파라미터의 형태에 따라 적절한 MiPQueryService의 메소드를 호출해 쿼리를 실행한다.

46.1.Controller

Anyframe에서는 MiPlatform 기반의 UI를 통한 사용자 요청을 처리할 수 있도록 Spring MVC의 Controller를 구현한 AnyframeMiPController, AnyframeMiPDispatchController를 제공하고 있다. 이 두 클래스를 상속받아 Controller 클래스를 구현하려면 사용자의 요청 별로 Controller 클래스를 구현해야 하므로 개발해야 할 Controller 클래스 수가 많아지고 유지보수 또한 어려워지는 단점이 있다. 이러한 단점을 보완하기 위해서 Anyframe Ria MiPlatform에서는 MiPController를 제공한다.

46.1.1.MiPController

JSP 기반의 UI일 경우, 사용자 요청에 따라 Controller가 호출되고, Controller에서는 비즈니스 서비스 호출 결과 값을 결과 페이지에 전달하는 로직이 필요하다. 그러나 MiPlatform 기반의 UI에서는 화면과 서버간의 주고받는 데이터의 유형(DatasetList, VariableList)이 동일하고, 요청 화면과 결과 화면이 같으므로 공통화 처리가 가능해진다. 따라서, 비즈니스 서비스 호출 외에 별도 로직이 없을 때는 MiPController를 공통 Controller로 사용할 수 있다.

아래는 MiPController의 operate()의 일부로, 화면에서 전달받은 비즈니스 서비스의 Bean Id 를 이용해 WebApplicationContext에서 비즈니스 서비스 객체를 얻어온다. 실행할 비즈니스 서비스의 Bean Id와 메소드 이름은 dsService의 SERVICE(예: boardService.getPagingList)의 값에 의해 결정된다.

public class MiPController extends AnyframeMiPController {
	
    public void operate(PlatformRequest platformRequest, VariableList inVl,
        DatasetList inDl, VariableList outVl, DatasetList outDl)
        throws Exception {

        String ServiceName = inVl.getValueAsString("service");
		
        Object bean = getWebApplicationContext().getBean(ServiceName);

        Method method = getMethod(bean,inVl.getValueAsString("method"));
		
        try {
            method.invoke(bean, new Object[] { inVl, inDl, outVl, outDl });
        } catch (Exception e) {
            Throwable te = e.getCause();
            if (te instanceof BaseException)
                throw (BaseException) te;
            else
                throw new BaseException("Fail to process client request.", te);
        }
    }
..중략

만약 아래 그림의 설정처럼SERVICE의 값이 없을 경우에는 비즈니스 서비스의 Bean Id는 mipService이고 메소드 이름은 dsService의 SVC_ID값의 prefix로 결정된다.

prefix로는 get, getList, getPagingList, create, update, remove, saveAll이 올 수 있다. prefix가 getList일 경우에는 MiPService의 getList()가 실행된다.

46.2.Service

Anyframe Ria MiPlatform의 Service는 Interface인 MiPService와 구현 클래스인 MiPServiceImpl로 구성되어 있다.

46.2.1.MiPService

MiPService는 MiPlatform의 고유 데이터 형태인 VariableList와 Dataset을 이용하여 외부에 제공할 수 있는 일반적인 기능을 정의하고 있는 인터페이스 클래스이다. 아래는 MiPService 소스 코드의 일부로 모든 메소드의 입력 파라미터는 (VariableList inVl, DatasetList inDl, VariableList outVl, DatasetList outDl)이며, Return Type는 void이다.

public interface MiPService {
    ..중략
    //리스트 조회
    void getList(VariableList inVl, DatasetList inDl, 
            VariableList outVl, DatasetList outDl) throws Exception;
    //리스트 조회(페이징 처리)	
    void getPagingList(VariableList inVl, DatasetList inDl, 
            VariableList outVl, DatasetList outDl) throws Exception;
    //추가
    void create(VariableList inVl, DatasetList inDl, 
            VariableList outVl, DatasetList outDl) throws Exception;
    ..중략
}

46.2.2.MiPServiceImpl

MiPServiceImpl은 MiPService의 구현 클래스로써 dsService에 설정된 정보를 기반으로 MiPDao의 메소드를 호출한다.

위의 2번 Row와 같이 dsService를 설정 했다면 VariableList에 아래 그림처럼 값이 셋팅되어 서버 측에 전달된다.

아래는 MiPServiceImpl의 getPagingList()의 일부로써, 특정 페이지에 속한 데이터를 조회하는 기능을 제공한다.

getPagingList() 에서는 위의 그림에서와 같이 입력 파라미터로 전달된 DatasetList에 ID가 “querySet1”인 Dataset이 포함되어 있는 경우, VariableList로부터 “querySet1”이라는 KEY에 해당하는 값을 변수 queryId에 할당한다. 또한 해당 DatasetList로부터 ID가 “querySet1”인 Dataset을 추출하여 inDs라는 변수에 할당한다. 그 후 queryId와 inDs를 이용해 MiPDao의 메소드를 호출한다.

public void getPagingList(VariableList inVl, DatasetList inDl,
        VariableList outVl, DatasetList outDl) throws Exception {

    int querySetCount = getQuerySetCount(inVl, outVl);
    String queryId = null;
    Dataset inDs = null;
    Dataset outDs = null;
    for( int i = 1 ; i <= querySetCount ; i++) {
        queryId = inVl.getValueAsString("querySet"+i);
        inDs = inDl.get("querySet"+i);
        try{
            if(inDs != null) {
                outDs = mipDao.getPagingList(queryId, inDs);
            }
            outDl.addDataset("querySet"+i, outDs);
// 중략
    }
}

MiPServiceImpl의 다른 메소드들도 이와 같이 입력 파라미터로부터 추출한 Dataset과 Query ID를 이용하여 사용자의 요청을 처리한다.

아래 그림처럼 dsService SVC_ID의 prefix가 get, getList, getPagingList, create, update, remove일 경우에는 querySet에 한 개의 Query Id가 셋팅되어야 하고

saveAll이면 QUERY_LIST에 “querySet1=createBoard,updateBoard,removeBoard”와 같이 추가, 수정, 삭제를 위한 3개의 Query Id가 셋팅되어 있어야 한다. 조회(get, getList, getPagingList)의 경우에는 결과 Dataset의 Id는 조회 시 검색 조건으로 사용했던 Dataset의 ID(“querySet+번호”)이다.

46.3.Dao

Anyframe Ria MiPlatform의 Dao는 Interface인 MiPDao와 구현 클래스인 MiPDaoQuery로 구성되어 있다.

46.3.1.MiPDao

MiPDao 인터페이스는 MiPlatform의 두 가지 데이터 형태(VariableList, Dataset)을 이용해 Database에 CRUD를 할 수 있는 기능을 제공한다. 입력 파라미터에 따라 메소드가 분리되어 있기 때문에 사용하기 쉽고, Dataset과 VariableList로 할 수 있는 DB작업에 대한 기능들이 대부분의 기능을 포함하고 있다.

//Primary Key를 이용한 단건 조회
Dataset get(String queryId, String primaryKey) throws Exception;
//  VariableList를 이용한 단건 조회
Dataset get(String queryId, VariableList inVl) throws Exception;

..중략

int saveAll(Map queryMap, Dataset inDs) throws Exception;
//  IMiPActionCommand을 이용한 Dataset 저장(추가, 삭제, 저장)
int saveAll(Map queryMap, Dataset inDs, IMiPActionCommand actionCommand ) throws Exception;

검색 기능을 제공하는 get, getList, getPagingList 이외의 메소드들의 Return Type는 int이고 DB에 CUD된 Record의 개수이다.

46.3.2.MiPDaoQuery

MiPDaoQuery 는 MiPDao인터페이스의 구현 클래스로 MiPQueryService를 이용해 Query를 실행한다.

아래는 MiPDaoQuery중 Dataset의 Record를 DB Table에 저장(추가, 수정, 삭제)하는 saveAll()이다.

public int saveAll(Map queryMap, Dataset inDs,
    IMiPActionCommand actionCommand) throws Exception {
	
    if(actionCommand == null){
        return miPQueryService.update(queryMap, inDs);
    }else{
        return miPQueryService.update(queryMap, inDs, actionCommand);
    }
}

insert, update, delete를 위한 Map형태의 Query Id와 Dataset을 이용해 MiPQueryService의 update()를 호출하고 있음을 알 수 있다.

MiPDaoQuery는 Anyframe Ria MiPlatform에서 제공한 구현체를 사용할 것을 추천하며, 꼭 필요한 경우에 한해 확장해서 사용한다.

46.4.Extension of MiPServiceImpl

MiPService에서 제공하는 기능 외에 추가적인 기능이 필요할 경우에는 API를 추가로 정의하거나 해당 메소드를 오버라이드 할 수 있다.

아래는 Dataset의 Record를 DB에 Insert하기 전 ‘PROD_NO’ 컬럼에 유일한 아이디를 셋팅하기 위해 saveAll()을 오버라이드 해 기능을 확장한 예이다.

@Service("productService")
public class ProductServiceImpl extends MiPServiceImpl implements ProductService {
    @Resource
    IIdGenerationService idGenerationService;
	
    ..중략
    public void saveAll(VariableList inVl, DatasetList inDl,
        VariableList outVl, DatasetList outDl) throws Exception {

    Map sqlMap = new HashMap();
    sqlMap.put(MiPQueryServiceImpl.QUERY_INSERT, "createProduct");
    sqlMap.put(MiPQueryServiceImpl.QUERY_UPDATE, "updateProduct");
    sqlMap.put(MiPQueryServiceImpl.QUERY_DELETE, "removeProduct");

    mipDao.saveAll(sqlMap, inDl.get("dsSave"), new ProductActionCommand(
        idGenerationService));
    }
}

MiPDao의 saveAll()의 파라미터에 Anyframe에서 제공하는 IMiPActionCommand를 구현한 ProductActionCommand를 전달하고 있다. IMiPActionCommand를 활용하면 특정 쿼리문을 수행하기 전/후에 필요한 비즈니스 로직을 수행 할 수 있다.

46.4.1.[참고] IMiPActionCommand

IMiPActionCommand는 MiPQueryService의 save() 가 호출 됐을 때 Insert, Update, Delete Query를 실행 하기 전, 후 필요한 비즈니스 로직을 추가 할 수 있도록 하기 위해 제공되는 인터페이스이다. IMiPActionCommand 인터페이스를 구현한 별도의 클래스를 정의하고 해당 메소드에 비즈니스 로직을 추가하면 된다.

아래는 앞서 언급한 ProductActionCommand 클래스의 preInsert()의 일부로써, Dataset을 특정 Table에 Insert하기 전에 IdGenerationService를 이용해 Primary Key에 해당하는 PROD_NO 컬럼에 유일한 값을 셋팅하고 있음을 알 수 있다.

public class ProductActionCommand implements IMiPActionCommand {
    private IIdGenerationService idGenerationService;

    public ProductActionCommand(IIdGenerationService idGenerationService) {
        this.idGenerationService = idGenerationService;
    }

    public void preInsert(Dataset ds, int index) {
        try {
            String id = idGenerationService.getNextStringId();
            Variant variant = new Variant();
            variant.setObject(id);
            ds.setColumn(index, "PROD_NO", variant);
        catch (BaseException e) {
            throw new RuntimeException(e);
        }
    }
}

따라서, MiPQueryService의save()에서는 Dataset의 Status가 ‘insert’인 Record를 DB에 Insert하기 전 ProductActionCommand의 preInsert()를 호출해 추가 로직을 실행한다.

46.5.Testcase

다음은 MiPService의 기능을 테스트 하는 Main.java 중 Dataset을 이용한 조회 기능을 테스트 하는 코드의 일부이다. querySet1, querySet2라는 Id를 가진 두 개의 Dataset의 "SEARCH_CONDITION", "SEARCH_KEYWORD" 컬럼에 검색 조건과 검색 문자를 입력한 후 , MiPService의 getList()를 호출 해 Query Id가 findBoardList인 query를 실행해 정상적으로 동작하는지 확인하는 테스트케이스이다.

/**
 * Dataset에 검색 조건이 세팅 되어 있을 때 
 * Dataset을 이용해 목록 조회를 한다. 
 * 검색조건과 검색키워드를 두 개의 Dataset에 세팅한 후 쿼리 문이 
 * 정상적으로 동작해 기대했던 값과 조회 결과 값을  비교한다.
 */
public void testGetListUsingDataset() throws Exception {
		
    ..중략

    inVl.add("querySetCount", 2);
    inVl.add("querySet1", "findProductList");
    inVl.add("querySet2", "findProductList");

    Dataset dsSearch1 = new Dataset("querySet1");
    dsSearch1.addStringColumn("SEARCH_PROD_NAME");
    dsSearch1.addStringColumn("SEARCH_AS_YN");

    dsSearch1.appendRow();
    dsSearch1.setColumn(0, "SEARCH_PROD_NAME", "bordo");
    dsSearch1.setColumn(0, "SEARCH_AS_YN", "Y");
    inDl.addDataset("querySet1", dsSearch1);

    Dataset dsSearch2 = new Dataset("querySet2");
    dsSearch2.addStringColumn("SEARCH_PROD_NAME");
    dsSearch2.addStringColumn("SEARCH_AS_YN");

    dsSearch2.appendRow();
    dsSearch2.setColumn(0, "SEARCH_PROD_NAME", "inline");
    dsSearch2.setColumn(0, "SEARCH_AS_YN", "Y");
    inDl.addDataset("querySet2", dsSearch2);

    miPService.getList(inVl, inDl, outVl, outDl);

    // outDl.size() : 2
    System.out.println("outDl.size : " + outDl.size());

    Dataset ds1 = outDl.get("querySet1");
    Dataset ds2 = outDl.get("querySet2");

    // PROD_NO : PRODUCT-00003, PROD_DETAIL : good
    System.out.println("PROD_NO : " + ds1.getColumnAsString(0, "PROD_NO"));
    System.out.println("PROD_DETAIL : " + ds2.getColumnAsString(0, "PROD_DETAIL"));

46.6.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.miplatform.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.miplatform를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.miplaform를 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.miplatform를 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-tasks-2.0.10.jar 파일이 있어야 한다.)

    표 46.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.miplatform.zipDownload
    maven-ant-tasks-2.0.10.jarDownload

47.Cache Service

Cache Service는 opensymphony의 OSCache 를 기반으로 개발되었으며, 상태를 공유할 수 있는 객체를 cache하는 기능을 제공하는 서비스이다. 변경이 자주 일어나지 않지만 사용빈도가 높고 생성하는데 비용이 많이 드는 객체일 경우, Cache를 이용하면 다음과 같은 장점을 얻을 수 있다.

  • 자주 접근하는 데이터를 매번 데이터베이스로부터 fetch할 필요가 없으므로 오버헤드가 줄어든다.

  • 객체를 매번 생성하지 않기 때문에 메모리를 효율적으로 사용할 수 있다.

Cache 서비스에 대한 구현체는 1가지이며, 다음은 각 구현체별 사용법이다.

47.1.DefaultCacheService

DefaultCacheService는 OSCache의 속성을 그대로 따라, Caching된 객체의 상태를 보호하기 위해 ThreadSafe한 Cache를 제공한다. DefaultCacheService에서 OSCache가 제공하는 Cache를 사용하기 위해서는 다음과 같은 설정을 필요로 한다.

Property NameDescriptionRequiredDefault Value
cache사용할 Cache의 속성을 정의한 Cache의 id를 참조한다.YN/A

다음은 OSCache가 가지는 설정 정보에 대한 설명이다.

Property NameProperty NameRequiredDefault Value
cache.memory메모리 Cache를 사용할 것인지 정의한다. false로 설정되면 메모리로 캐싱될 수 없다.Ntrue
cache.capacityCache에 저장할 수 있는 object의 최대 갯수를 지정한다. 음수로 설정되면 이 기능을 사용하지 않는다. 캐싱 가능한 object의 갯수를 제한하지 않는다.N-1
cache.algorithmcaching algorithm의 classname을 지정한다. 이 클래스는 com.opensymphony.oscache.base. algorithm.AbstractConcurrentReadCache를 extend 해야한다. cache capacity가 양수로 설정되면 default algorithm으로 LRUCache가 사용되고, 음수로 설정되면 com.opensymphony.oscache.base. algorithm.UnlimitedCache가 사용된다.NN/A
cache.unlimited. diskPersistence cache의 size를 제한할 것인지 또는 in-memory cache와 동일한 사이즈로 제한할 것인지를 나타낸다. 이 값이 true 로 설정되면 persistent cache는 제한없이 사용될 수 있다.Nfalse
cache.blocking새로운 content를 캐싱하거나 이미 캐싱된 content를 검색할 때 block waiting 해야 하는지를 정의한다.Nfalse
cache.persistence. classPersistence cache를 사용하고자 할 때 Persistence cache를 구현한 classname을 정의한다. 이 클래스는 PersistenceListener를 extend 해야한다.NN/A
cache.persistence. overflow.only메모리 Cache가 overflow mode일때 Persistence Cache를 사용할지 지정한다.Nfalse
cache.event. listenersCache에 적용한 event handler를 지정한다. event handler가 여러개 일 경우 각각의 classname을 콤마로 구분하여 정의한다.NN/A
cache.cluster. propertiesJavaGroupsBroadcastingListener를 사용할때 이 property를 정의한다. JavaGroups channel properties를 사용한다. JavaGroups의 실행을 제어할 수 있다.N 
cache.cluster. multicast.ipJavaGroupsBroadcastingListener를 사용할 때 이 property를 정의한다. broadcasting을 사용하기 위해 JavaGroups는 multicast IP를 사용해야 한다.N231.12. 21.132
cache.cluster.jms. node.nameJMS10BroadcastingListener 또는 JMSBroadcastingListener를 사용할 때 이 property를 정의한다. JMS connection factory를 사용한다.YN/A
cache.cluster.jms. topic .name JMS10BroadcastingListener 또는 JMSBroadcastingListener를 사용할때 이 property를 정의한다. 이것은 JMS topic name 이다.YN/A
cache.cluster.jms. topic .factory JMS10BroadcastingListener 또는 JMSBroadcastingListener를 사용할때 이 property를 정의한다. 이 노드의 이름은 cluster에 존재하고, 각각의 node마다 unique한 값을 갖는다.YN/A
cache.pathDiskPersistenceListener를 사용할 때 이 property를 정의한다. 데이터를 캐싱하기 위한 path를 지정한다.YN/A
cache.persistence. disk.hash. algorithmdisk의 filname으로 간단한 cache key를 생성하기 위한 hash algorithm이다.NMD5

47.1.1.Samples

다음은 DefaultCacheService의 속성 설정 및 테스트 코드에 대한 예제이다.

  • Configuration

    다음은 DefaultCacheService와 DefaultCacheService에서 사용할 Cache의 속성을 정의한 context-cache.xml 의 일부이다. 아래 속성 정의 파일 내용에 따르면, cacheAdministrator Bean에 테스트에서 사용할 Cache의 속성이 정의되어 있다. 또한 cacheService Bean에서 cacheAdministrator의 getCache 메소드를 통해 도출한 캐시를 참조하고 있다.

    <bean id="cacheService" class="anyframe.core.cache.impl.DefaultCacheService">
    	<property name="cache" ref="cache" />
    </bean>
    
    <bean id="cache" factory-bean="cacheAdministrator" factory-method="getCache" />
    
    <bean id="cacheAdministrator"
    	class="com.opensymphony.oscache.general.GeneralCacheAdministrator"
    	destroy-method="destroy">
    	<constructor-arg index="0">
    		<props>
    			<prop key="cache.capacity">10</prop>
    		</props>
    	</constructor-arg>
    </bean>

  • TestCase

    다음은 앞서 정의한 속성 설정을 기반으로 하여 Cache 내에 특정 데이터를 저장하고 추출하는 Main.java 코드의 일부이다.

    public void manageCategory() throws Exception {
    	// 1. lookup categoryService
    	CategoryService service = (CategoryService) context
    			.getBean("categoryService");
    
    	// 2. get category list
    	Map categories = service.getList();
    	System.out.println("before inserting, the size of category is "
    			+ categories.size());
    
    	// 3. create a new category
    	Category category = new Category();
    	category.setCategoryNo("CATEGORY-****1");
    	category.setCategoryName("example");
    	service.create(category);
    
    	// 4. get category list
    	categories = service.getList();
    	System.out.println("after inserting, the size of category is "
    			+ categories.size());
    
    	// 5. remove a category
    	service.remove("CATEGORY-****1");
    
    	// 6. get category list
    	categories = service.getList();
    	System.out.println("after removing ,the size of category is "
    			+ categories.size());
    }

47.2.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.cache.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=anyframe.example.cache.Main이라는 명령어를 실행시켜 결과를 확인한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더의 anyframe/example/cache 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 47.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.cache.zipDownload

  • 참고자료

48.DataSource Service

주어진 Database에 연결하기 위한 Connection(javax.sql.Connection) 객체를 생성하는 서비스이다. Anyframe 에서는 Connection Provider별로 Connection 객체를 얻어내기 위한 로직을 구현하고 있는 다음의 DataSource 구현체들을 그대로 사용하고자 한다.

48.1.JDBCDataSource Configuration

Description copied from class: DriverManagerDataSource

JDBC driver를 이용하여 Database Connection을 생성한다. 모든 getConnection() call에 대해 새로운 connection을 리턴한다. 실제 운영 환경에서는 JDBCDataSource의 사용은 추천하지 않으며, DBCPDataSource 나, C3P0DataSource 가 사용된다.

Property NameDescriptionRequiredDefault Value
urlDataBase에 access하기 위한 JDBC URLYN/A
driverClassNameJDBC driver class name을 설정한다.YN/A
usernameDataBase에 access하기 위해 사용된다.NN/A
passwordDataBase에 access하기 위해 사용된다.NN/A

48.1.1.Samples

다음은 JDBCDataSource의 속성 설정에 대한 예제이다.

  • Configuration

    다음은 JDBCDataSource의 속성을 정의한 context-datasource.xml 의 일부이다. 아래 속성 정의 파일에서는 HSQL DB를 기반으로 한 JDBCDataSource Bean을 정의하고 있다.

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
        <property name="url" value="jdbc:hsqldb:file:/./db/sampledb" />
        <property name="username" value="sa" />
    </bean>

48.2.DBCPDataSource Configuration

JDBC driver를 이용하여 Database Connection을 생성하는 또다른 구현체이다.Commons DBCP 라 불리는 Jakarta의 Database Connection Pool이다. Configuration parameter 전체 DBCP documentation 을 통해 확인 가능하다.

Property NameDescriptionRequiredDefault Value
driverClassNamejdbc driver의 class name을 설정한다.YN/A
urlDataBase url을 설정한다.YN/A
usernameDataBase에 접근시 사용할 username을 설정한다.NN/A
passwordDataBase에 접근시 사용할 password를 설정한다.NN/A
maxActive동시에 할당할 수 있는 active connection의 최대 갯수를 설정한다.N8
maxIdlepool에 남겨놓을 수 있는 idle connection의 최대 갯수를 설정한다.N8
maxWait모든 Connection이 사용중일 경우 최대 대기 시간을 설정한다.Nindefinitely
defaultAutoCommit이 datasource로부터 리턴된 connection에 대한 auto-commit 여부를 설정한다.Ntrue
defaultReadOnlyConnection Pool에 의해 생성된 Connection에 read-only 속성을 부여한다.Ndriver default
defaultTransactionIsolation리턴된 connection에 대한 transaction isolation 속성을 부여한다.Ndriver default
defaultCatalogConnection의 catalog를 설정한다.NN/A
minIdleConnection pool의 최소한 idle connection 갯수를 설정한다.N0
initialSizeConnection pool에 생성될 초기 connection size를 설정한다.N0
testOnBorrowConnection pool에서 객체를 가지고 오기 전에 그 객체의 유효성을 확인할 것인지 결정한다. true값은 아무 영향을 미치지 않지만 validationQuery property는 non-null string으로 설정되어야 한다.Ntrue
testOnReturn객체를 return하기 전에 객체의 유효성을 확인할 것인지 결정한다. true값은 아무 영향을 미치지 않지만 validationQuery property는 non-null string으로 설정되어야 한다.Nfalse
testWhileIdleidle object evictor가 connection의 유효성을 확인할 것인지를 설정한다. true값은 아무 영향을 미치지 않지만 validationQuery property는 non-null string으로 설정되어야 한다.Nfalse
validationQueryvalidationQuery를 설정한다.NN/A
loginTimeoutDatabase에 연결하기 위한 login timeout(in seconds)을 설정한다. createDataSource()를 호출 해서 connection pool을 초기화한다.NN/A

48.2.1.Samples

다음은 DBCPDataSource의 속성 설정에 대한 예제이다.

  • Configuration

    다음은 DBCPDataSource의 속성을 정의한 context-datasource.xml 의 일부이다. 아래 속성 정의 파일에서는 HSQL DB를 기반으로 한 DBCPDataSource Bean을 정의하고 있다.

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
            	destroy-method="close">
            <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
            <property name="url" value="jdbc:hsqldb:file:/./db/sampledb"/>
            <property name="username" value="sa"/>
            <property name="maxActive" value="100"/>
            <property name="maxIdle" value="30"/>
            <property name="maxWait" value="1000"/>
            <property name="defaultAutoCommit" value="true"/>
            <property name="removeAbandoned" value="true"/>
            <property name="removeAbandonedTimeout" value="60"/>
            <property name="logAbandoned" value="true"/>		
    </bean>

  • Test case

    예제 코드는 Test Case 에 포함되어 있다.

48.3.C3P0DataSource Configuration

JDBC driver 를 이용하여 Database Connection을 생성하는 또다른 구현체이다. C3P0 Library 에 관한 자세한 사항은 C3P0 Configuration 에서 확인할 수 있다.

48.3.1.Samples

다음은 C3P0DataSource의 속성 설정에 대한 예제이다.

  • Configuration

    다음은 C3P0DataSource의 속성을 정의한 context-datasource.xml 의 일부이다. 아래 속성 정의 파일에서는 HSQL DB를 기반으로 하는 C3P0DataSource Bean을 정의하고 있다.

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" 
              destroy-method="close">
        <property name="driverClass" value="org.hsqldb.jdbcDriver"/>
        <property name="jdbcUrl" value="jdbc:hsqldb:file:/./db/sampledb"/>
        <property name="user" value="sa"/>
        <property name="minPoolSize" value="5"/>
        <property name="acquireIncrement" value="5"/>
        <property name="maxPoolSize" value="15"/>
    </bean>

48.4.JNDIDataSource Configuration

JNDIDataSource는 JNDI Lookup을 이용하여 Database Connection을 생성한다. JNDIDataSource는 대부분 Enterprise application server에서 제공되는 JNDI tree로 부터 DataSource를 가져온다.

Description copied from class: JndiObjectFactoryBean

JNDIDataSource는 일반적으로 application context의 singleton factory(e.g. JNDI-bound DataSource)를 등록하여 사용할 수 있고, 필요한 application service를 빈으로 참조할 수 있다.

기본적으로 startup시 캐싱된 JNDI 객체를 검색한다. 이것은 "lookupOnStartup"과 "cache" property를 통해 customized 할 수 있으며, JndiObjectTargetSource를 사용할 수 있다. 실제 JNDI object type이 미리 정의되어 있지 않은 경우 proxyInterface의 정의가 필요하다.

Property NameDescriptionRequiredDefault Value
jndiTemplateJNDI 검색을 위해 JNDI 템플릿을 설정한다. 또한 "jndiEnvironment"로 JNDI 환경 설정을 할 수 있다.NN/A
jndiEnvironmentJNDI를 검색하기 위해 JNDI 환경을 설정한다. 환경 설정에 제공된 JndiTemplate을 생성한다.NN/A
resourceRefJ2EE 컨테이너에서 검색할 수 있는지 설정한다. 만약 prefix가 "java:comp/env/"이면 JNDI 이름이 포함되어 있지 않으므로 추가해 주어야 한다. 디폴트 값은 "false"이다. 주의 : 만약 "java:" 와 같이 주어진 scheme이 아니라면 적용할 수 없다.Nfalse
expectedTypeJNDI 객체의 타입을 지정한다.NN/A
jndiName검색을 위해 JNDI 이름을 설정한다. 만약 resourceRef가 true로 설정되어 있고, "java:comp/env/"로 시작되지 않으면 이 prefix를 추가한다.YN/A
proxyInterface검색을 위해 JNDI 이름을 설정한다. 만약 resourceRef가 true로 설정되어 있고, "java:comp/env/"로 시작되지 않으면 이 prefix를 추가한다.NN/A
lookupOnStartupstarup시에 JNDI object를 검색할 지 여부를 설정한다. lazy lookup시에는 proxy interface 정의가 필요하다.Ntrue
cacheJNDI 객체를 캐싱할 것인지 설정한다.Ntrue
defaultObjectJNDI lookup에 실패하였을 경우 전달할 default object를 지정한다. 이것은 임의의 bean reference나 literal value가 될 수 있다. 주의 : 이것은 startup시 lookup에서만 지원된다.Nnone

48.4.1.Samples

다음은 JNDIDataSource의 속성 설정에 대한 예제이다. "jnditemplate" Bean에 JNDI Server에 대한 속성을 정의하고, "dataSource" Bean에서 "jnditemplate" Bean을 참조하여 Connection 객체를 얻어낼 수 있도록 하고 있다.

  • Configuration

    <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="AnyframeDS"/>
        <property <emphasis role="bold">name="jndiTemplate" ref="jnditemplate"/>
    </bean>
    
    <bean id="jnditemplate" class="org.springframework.jndi.JndiTemplate">
        <property name="environment">
           <props>
              <prop key="java.naming.factory.initial">
                   weblogic.jndi.WLInitialContextFactory
              </prop>
              <prop key="java.naming.provider.url">
                   t3://server.ip:7001
              </prop>			
           </props>
        </property>
    </bean>

48.4.2.jee schema 를 통한 JNDIDataSource 사용

Spring 2.0 이후 버전에서는 jee Namespace 태그를 통해 JNDI 객체를 lookup 할 수 있는 간소한 설정을 지원한다. 아래에서는 jee:jndi-lookup 를 사용한 JNDIDataSource 설정이다.

  • Configuration

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:jee="http://www.springframework.org/schema/jee"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd">
    
        <jee:jndi-lookup id="dataSource" jndi-name="AnyframeDS" resource-ref="true">
            <jee:environment>
              java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
              java.naming.provider.url=t3://server.ip:7001
            </jee:environment>
        </jee:jndi-lookup>
    
    </beans>

JndiObjectFactoryBean 와 JndiTemplate 을 통한 설정에 비해 jee 태그를 사용하면 설정이 매우 간소하므로 이 방법을 사용할 것을 권고한다. jee schema 에 대한 상세 내용은 이곳을 참고하도록 한다.

48.5.Test Case

다음은 앞서 정의한 속성 설정 파일들을 기반으로 하여 DataSource로부터 connection을 가져오는 Main.java 코드의 일부이다.

public void getConnection() throws Exception {
	// 1. lookup dataSource
	DataSource datasource = (DataSource) context.getBean("dataSource");
	// 2. try to get a connection from dbcp connection pool
	Connection conn = datasource.getConnection();
	System.out.println("Connection is " + conn + "");
}

48.6.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.datasource.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=anyframe.example.datasource.Main이라는 명령어를 실행시켜 결과를 확인한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더의 anyframe/example/datasource 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 48.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.datasource.zipDownload

  • 참고자료

49.Hibernate Service

Hibernate는 객체 모델링(Object Oriented Modeling)과 관계형 데이터 모델링(Relational Data Modeling) 사이의 불일치를 해결해 주는 ORM 도구로, EJB의 Entity Bean과 같이 특정 플랫폼에 의존적인 제약을 정의하고 있지 않기 때문에 POJO 기반 개발이 가능하다. 또한 Java에서 지원하는 다양한 Collection 유형을 지원함으로써 객체 모델링을 관계형 모델링으로 매핑하는데 따르는 제약을 최소화하고 있다.

Hibernate의 특징을 살펴보면, 다음과 같다.

  • Hibernate 기반 개발시 특정 DBMS에 영향을 받지 않으므로 DBMS가 변경되더라도 데이터 액세스 처리 코드에 대한 변경없이 설정 정보의 변경만으로도 올바르게 동작 가능하다.

  • SQL을 작성하고 SQL 실행 결과로부터 전달하고자 하는 객체로 변경하는 코드를 작성하는 시간이 줄어들기 때문에 개발자는 비즈니스 로직에 집중할 수 있게 되고, 개발 시간이 단축될 수 있다.

  • JDBC Api를 사용한 코드의 양이 줄어들고, 매핑 파일을 별도로 관리하게 되면서 DB 정보 변경으로 인해 영향을 받는 부분 또한 감소한다.

  • 다음과 같은 접근 방법을 취함으로써, DBMS에 대한 접근 횟수를 줄여나가 궁극적으로 어플리케이션의 성능 향상을 도모한다.

    • 기본적으로 필요 시점에만 DBMS에 접근하는 Lazy Loading 전략 채택

    • Session 종료 시에 변경 사항에 대해 일괄 batch 처리

    • 1st Level Cache, 2nd Level Cache를 활용하여 DBMS에 대한 재접근없이 Caching된 객체 사용

  • 대부분의 개발자가 어플리케이션의 데이터 액세스 로직을 개발하기 위해 DTO(Data Transfer Object), DAO 패턴을 사용하는데 익숙하기 때문에 데이터와 로직을 가진 객체를 설계하는데 익숙하지 못하다는 단점을 가지고있다.

본 페이지에서는 Hibernate 3.3.1.ga 버전을 이용하여 Hibernate 기본 개념에 대해서 살펴볼 것이다. 먼저 어플리케이션 실행 여부와 상관없이 물리적으로 존재하는 데이터들을 정의하고 있는 Persistent Class와 Persistent Class의 Lifecycle에 대해 알아보고 이러한 객체들의 영속성을 관리하는 Hibernate Session에 대해 정리해 보고자 한다.

Conceptual Architecture

Hibernate 기본 구성은 다음 그림과 같다.

위 그림에서와 같이 Hibernate이 DBMS 기반의 어플리케이션 수행을 하기 위해 필요한 주요 구성 요소는 Persistent Objects, Hibernate Properties, XML Mapping 이며, 각각은 다음과 같은 역할을 수행한다.

  • Persistent Objects : Persistent Object는 어플리케이션 실행 여부와 상관없이 물리적으로 존재하는 데이터들을 다룬다. 일반적으로 DBMS 데이터를 이용하는 어플리케이션을 개발할 경우 어플리케이션의 비즈니스 레이어에서 특정 DBMS에 맞는 SQL을 통해 어플리케이션의 데이터를 처리하게 된다. 그러나 Hibernate 기반의 어플리케이션에서는 Persistent Object를 중심으로 하여 어플리케이션의 데이터와 DBMS 연동이 가능해진다.

  • Hibernate Properties : Hibernate 실행에 관련된 속성 정보를 포함하고 있는 파일로, hibernate.cfg.xml 또는 hibernate.properties 형태로 정의할 수 있다. 주로 DBMS, Logging, Cache, Mapping File 정보 등을 다루고 있다.

  • XML Mapping : Persistent Object과 특정 테이블 사이의 다양한 매핑 정보를 명시하기 위한 XML 파일이다. Hibernate는 Hibernate Mapping XML을 기반으로 하여 실행할 SQL을 생성하고 있다.

Persistent Classes

Persistent Class는 Hibernate를 이용하여 DB의 특정 테이블과 매핑되는 객체로 Hibernate를 제대로 사용하기 위해서는 Persistence Class작성이 중요하다. Java Class를 Hibernate의 Persistent Class로 사용하기 위한 기본 요건은 다음과 같다.

  • [필수] Default Constructor 구현 : Hibernate에서는 Constructor.newInstance()를 이용하여 해당 클래스의 인스턴스를 생성하므로 Persistence Object로 사용하기 위해서는 해당 클래스 내에 입력 인자를 갖지 않은 Default Constructor를 생성해야 함에 유의하도록 한다.

    public class Category implements java.io.Serializable {
    <!-- 중략  -->
        public Category() {
        }
    }
  • [권장] 테이블의 Primary Key 칼럼과 매핑 되는 identifier field 정의 : 일반적인 경우 Hibernate Persistent Class에 DB 테이블의 primary key와 매핑되는 identifier field가 반드시 존재해야 할 필요는 없지만 몇 가지 경우에 반드시 필요하다. (예 : Session.saveOrUpdate or Session.merge 메소드 이용 시) 하지만 일반적인 Domain Object에서 identifier를 갖는 것이 일반적이므로 Persistent Class에 identifier field 정의하는 것을 추천한다.

  • [권장] final 클래스로 정의하지 않음 : Hibernate의 lazy loading은 proxy를 사용해야 하는데 final로 Persistent class를 선언할 경우 proxy를 사용 할 수 없다.

  • [권장] 속성 정보 접근을 위한 getter, setter 정의 : Hibernate는 getter/setter로 구성된 method가 존재할 경우 매핑 처리를 할 수 있다.

    public class Category implements java.io.Serializable {
    
        private String categoryId;
        private String categoryName;
    	
    	...중략
        public String getCategoryId() {
            return this.categoryId;
        }
    
        public void setCategoryId(String categoryId) {
            this.categoryId = categoryId;
        }
    
        public String getCategoryName() {
            return this.categoryName;
        }
    
        public void setCategoryName(String categoryName) {
            this.categoryName = categoryName;
        }
       ...중략
    }

    만약 Mapping File에 DB컬럼에 대한 매핑 정보를 아래와 같이 설정 한다면 gettter/setter메소드가 필요없다.

    <property name="name" column="NAME" access="field"/>

  • [선택] equals(), hashCode() 메소드 구현 : 다음에서 동일한 테이블의 동일한 행의 데이터를 읽어온 category1과 category2는 다른 Session을 통해 얻어졌으므로 동일한 객체가 아닌 것으로 처리된다. 이처럼 다른 Session을 통해 얻어 온 객체의 동일함을 비교할 필요가 있을 경우에는 equals() 메소드를 적절히 구현해 주도록 한다.

    Session session1 = SessionFactory.openSession();
    Category category1 = (Category)session1.get(Category.class, "test");
    session1.close();
    
    Session session2 = SessionFactory.openSession();
    Category category2 = (Category)session2.get(Category.class, "test");
    session2.close();

    public boolean equals(Object other) {
        if ( !(other instanceof Category) ) return false;
        Category castOther = (Category) other;
        return new EqualsBuilder().append(this.getCategoryId(), 
                            castOther.getCategoryId()).isEquals();
    }
    public int hashCode() {
        return new HashCodeBuilder().append(getCategoryId()).toHashCode();
    }

  • [선택] Serializable 인터페이스 구현 : Hibernate에서 persistent class들이 java.io.Serializable 인터페이스를 반드시 implement 해야 하는 것은 아니지만, persistent object가 HttpSession에 저장되거나 RMI를 이용해서 전달할 때는 반드시 필요한다.

    public class Category implements java.io.Serializable {
        …중략
    }

Dynamic Model

Hibernate는 Dynamic Model(Map)을 지원하기 때문에 Persistent entity가 JavaBean이나 POJO일 필요는 없다. Dynamic Model을 이용할 때는 Persistent Class를 작성하지 않고 Hibernate Mapping파일에 정의만 하면 된다.

다음은 Map을 이용해 Hiberante의 Session에 접근하는 소스의 일부이다.

Session sessions = SessionFactory.openSession();
        
    Map categoryMap = new HashMap();
    categoryMap.put("categoryId", "CTGR-0001");
    categoryMap.put("categoryName", "Romantic");
    categoryMap.put("categoryDesc, "Romantic genre");
		
	sessions.save("Category", categoryMap);
	…중략

Hibernate Session

Session은 Hibernate과 DB Connection 사이의 연결 고리 역할을 수행하는 객체로써, Session 생성시에 단일 DB Connection을 열고 Session이 종료될 때까지 Connection을 유지하게 된다. Hibernate에 의해 로드된 모든 객체(Persistent 객체)는 Session과 연관되어 있어, Session에 의해 객체의 변경 사항이 자동 반영되거나 Lazy Loading 처리될 수 있다.

Session은 Hibernate과 DB Connection 사이의 연결 고리 역할을 수행하는 객체로써, Session 생성시에 단일 DB Connection을 열고 Session이 종료될 때까지 Connection을 유지하게 된다. Hibernate에 의해 로드된 모든 객체(Persistent 객체)는 Session과 연관되어 있어, Session에 의해 객체의 변경 사항이 자동 반영되거나 Lazy Loading 처리될 수 있다.

새로운 Session 생성은 SessionFactory를 통해 이루어질 수 있으며 다음은 Hibernate에서hibernate.cfg.xml 파일을 기반으로 SessionFactory를 초기화시키는 예제 코드이다.

SessionFactory initialSessionFactory 
    = new Configuration().configure(hibernateconfig/hibernate.cfg.xml")
        .buildSessionFactory();

SessionFactory를 통해 신규 Session을 생성하는 방법은 openSession()과 getCurrentSession() 두 가지로 구분해 볼 수 있다.

  • openSession

    SessionFactory의 openSession() 메소드를 호출할 때마다 새로운 Session이 생성된다.

    Session session1 = SessionFactory.openSession();
    Session session2 = SessionFactory.openSession();        

    위 소스에서 생성된 session1과 session2는 서로 다른 session이다.

  • getCurrentSession

    SessionFactory에서 Session을 생성하는 또 다른 방법으로, getCurrentSession() 메소드를 이용하는 방법이 있다. getCurrentSession()은 Proxy를 리턴하고 생성된 Session이 있을 경우에는 생성된 Session을, 없을 경우에는 신규 Session을 리턴한다.

    Session session1 = initialSessionFactory.getCurrentSession();
    Session session2 = initialSessionFactory.getCurrentSession();
    session1.close();
    Session session3 = initialSessionFactory.getCurrentSession();
    

    getCurrentSession()을 호출하는 경우로 session1과 session2는 동일한 Session의 Proxy 객체이나 session3은 다른 Proxy 객체임을 확인 할 수 있다.

openSession(), getCurrentSession() 메소드를 호출해서 얻어진 Session에는 차이가 있다. openSession()으로 생성된 Session은 관련된 트랜잭션이 종료되더라도 종료되지 않는 반면, getCurrentSession()으로 얻어진 Session은 트랜잭션 종료 시 해당 Session도 함께 종료된다. 따라서 openSession()으로 얻어진 Session에 대해서는 session.close()를 호출해서 직접 Session을 종료해 주어야 하며, getCurrentSession()으로 얻어진 Session에 대해서는 따로 Session.close() 메소드를 호출하지 않아야 한다.

Persistent Object States

  • Lifecycle

    Hibernate에서 Persistent Object는 Transient, Persistent, Detached의 3가지 상태를 가질 수 있으며, 상태 변이를 발생시킨다. 각 상태에 관한 내용은 다음과 같다.

    • Transient : 객체는 생성되었으나 아직 Hibernate에 의해 관리되지 않는 경우

      ...중략
      Category category = new Category();
      
      category.setCategoryId("CTGR-0001");
      category.setCategoryName("Romantic");
      category.setCategoryDesc("Romantic genre");
      ...중략

      위의 소스에서 처럼 persistent class의 인스턴스는 생성되었지만 Hibernate에 의해서 아직 관리되지 않은 상태를 Transient 상태라고 한다.

    • Persistent : Hibernate에 의해 관리되는 객체로써 Hibernate에서 제공하는 기능(테이블의 특정 행과 매핑되며 변경 값 자동 반영, Lazy Loading 등)이 적용되는 경우

      Category category = new Category(); 
      category.setCategoryId("CTGR-0001");
      category.setCategoryName("Romantic");
      category.setCategoryDesc("Romantic genre"); // -- transient 상태
      session.save(category); // -- Persistent 상태 

      Transient상태의 persistent class를 Hibernate의 Session의 save(),persist()와 같은 method를 이용하면 Hibernate에서 제공하는 기능을 사용할 수 있는 상태인 Persistent 상태가 된다.

      아래는 Persistent 상태인 Object의 값을 변경 했을 때 자동으로 DB에 변경된 내용이 반영 되는 것을 테스트 하기 위한 HibernatePersistentObjcetStates.java 의 일부분이다.

      public void persistentState() throws Exception {
          newSession();
             
          Category category = new Category();
          category.setCategoryId("CTGR-0001");
          category.setCategoryName("Romantic");
          category.setCategoryDesc("Romantic genre");
             
          session.save(category);
           
          category.setCategoryName("Comedy");
          category.setCategoryDesc("Comedy consists...");
             
          closeSession();
      
      }
      다음은 위 테스트 코드를 실행 시켰을 때 query log의 일부분이다.
      1:  insert into PUBLIC.CATEGORY (CATEGORY_NAME, CATEGORY_DESC, CATEGORY_ID) 
                  values ('Romantic','Romantic genre', 'CTGR-0001') 
      1:  update PUBLIC.CATEGORY set CATEGORY_NAME='Comedy', CATEGORY_DESC='Comedy consists' 
                  where CATEGORY_ID='CTGR-0001'

      위 로그에서 알 수 있듯이 category의 값을 변경한 후 save나 update명령을 실행하지 않았음에도 불구하고 transaction이 종료 됐을 때 update query가 실행된다. 이처럼 persistent 상태가 되면 DB 테이블의 특정 행과 매핑되어 값이 자동으로 반영된다.

    • Detached : Hibernate에 의해 관리되는 객체이나 Persistent 상태와는 달리 Hibernate에서 제공하는 기능이 지원되지 않는 상태로 객체의 속성값이 바뀌더라도 DB에 변경된 값이 자동 반영되지 않는 경우

      아래는 Detached 상태가 된 persistent object의 값을 변경시키고 Session을 닫았을 때 DB에 반영이 안되는 경우를 테스트 하는 HibernatePersistentObjcetStates.java 의 일부분이다.

      public void detachedState() throws Exception {
          newSession();
      	
          Category category = new Category();
          category.setCategoryId("CTGR-0001");
          category.setCategoryName("Romantic");
          category.setCategoryDesc("Romantic genre");
      	
          session.save(category);
      	
          closeSession();
      	
          newSession();
      	
          category.setCategoryName("Comedy");
          category.setCategoryDesc("Comedy consists");
          closeSession();
      ...중략

      위 테스트 케이스에서 보면 closeSession()을 해서 persistent object의 state를 detached 상태로 만든 후에 값을 변경하고 다음 Session을 close시키면 persistent state때와는 달리 update가 되지 않는다. 실행되는 query 로그는 다음과 같다.

      insert into 
           PUBLIC.CATEGORY (CATEGORY_NAME, CATEGORY_DESC, CATEGORY_ID) values ('Romantic', 
           'Romantic genre', 'CTGR-0001'

      위 query 로그에서 보듯이 persistent state상태일 때와는 달리 persistent object가 변경 되었음에도 update query가 실행 되지 않는다.

      참고 : 한 Session 내에서 Initialize되지 않은 객체는 해당 Session 종료로 인해 Detached 상태로 변경되었을 때에는 객체 정보를 읽을 수 없다. 아래는 Session이 종료 되서 Detached상태로 된 객체에서 initialize되지 않은 연관 객체의 정보를 읽을 때 LazyInitializationException발생하는 것을 확인 하는 테스트 코드 HibernateLazyInitializationException.java 의 일부분이다.

      public void findMovie() throws Exception {
      ...중략
          Movie movie = (Movie) session.get(Movie.class, "MV-00001");
      ...중략
          session.close();
      			
          try {
              movie.getCategories().iterator();
              fail("expected LazyInitializationException");
          } catch (Exception e) {
      ...중략
      }

      Movie와 Category는 1:n 관계를 갖고 있다. MOVIE 테이블을 대상으로 단건 조회 작업을 수행한 후, Session을 종료하여 Movie Object를 Detached 상태로 만든다. 그리고 initialize되지 않은 Category 목록 정보를 얻으려 할 때 LazyInitializationException이 발생한다.

49.1.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.hibernate.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=anyframe.example.hibernate.Main이라는 명령어를 실행시켜 결과를 확인한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더의 anyframe/example/hibernate 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 49.1. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.hibernate.zipDownload

49.2.Mapping File

Mapping XML File은 모델 객체와 데이터베이스의 테이블간의 매핑 정보를 담고 있는 설정 파일이다. Mapping File를 작성할 때는 일정한 규약이 있으며 http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd에 맞게 작성을 해야 한다. 다음은 Mapping File 작성 방법, Java Data Type와 DB의 Data Type과의 매핑, 그리고 Hibernate Generator에 대한 내용이다.

49.2.1.Mapping File의 작성

49.2.1.1.Mapping File 구성

Mapping File의 전체적인 구성은 아래와 같다. Movie.hbm.xml 파일의 일부이다.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="anyframe.sample.model.bidirection.Country" 
        table="COUNTRY" lazy="true" schema="PUBLIC">
        <id name="countryCode" type="string">
            <column name="COUNTRY_CODE" length="12" />
            <generator class="assigned" />
        </id>
        <property name="countryId" type="string">
            <column name="COUNTRY_ID" length="2" not-null="true" />
        </property>
        <property name="countryName" type="string">
            <column name="COUNTRY_NAME" length="50" not-null="true" />
        </property>
        ...중략
    </class>
</hibernate-mapping>        

Hibernate 매핑 파일은 크게 다섯 부분으로 나눠져 있다.

  1. Hibernate mapping DTD정의

    Hibernate mapping DTD를 정의하는 부분으로 XML 파일의 validation 체크를 위해서 필요하다.

    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">        

  2. <hibernate-mapping>태그

    <hibernate-mapping> 태그 안에는 위에서 보는 것 처럼 여러개의 하위 태그를 정의한다.

    속성설명필수/선택기본값
    schemaDB schema의 이름선택N/A
    catalogDB catalog의 이름선택N/A
    package매핑 클래스가 있는 패키지 이름선택N/A
    default-lazyClass, Class내 정의된 Collection에 대한 기본 lazy 로딩 속성선택true
  3. 클래스와 DB 테이블 매핑

    <hibernate-mapping> 하위에 한개 이상의 <class>를 정의할 수 있다.

    <hibernate-mapping schema=".." package=“…”>
        <class name="Foo" table=“TBL_FOO">
            …
        </class>
    </hibernate-mapping>

    <class>태그의 property는 다음과 같다.

    속성설명필수/선택기본값
    name매핑 클래스의 이름(hibernate-mapping 태그에서 package를 정의하지 않았다면 클래스의 패키지명도 함께 정의한다.)필수N/A
    tableDB 테이블 명필수N/A
    lazytrue일 경우 객체가 필요한 순간에 로딩한다.선택<hibernate-mapping> 내의 default-lazy 속성값
    schemaDB schema의 이름(상위 태그에서 명시를 안했을 경우 정의 할 수 있다.)선택<hibernate-mapping> 내의 schema 속성값
  4. 식별자 필드 매핑

    <id>태그는 DB의 특정 Table의 Primary Key와 매핑될 attribute를 명시한다. <id>태그는 <generator>태그와 함께 사용된다. <generator>에 대한 상세 내용은 Hibernate Generator 를 참고한다.

    <class name="Foo" table="TBL_FOO">
        <id name="id" column="ID" type="int">
            <generator class="assigned"/>
        </id>
        <property name="name" column="NAME" type="string"/>
    </class>

    Primary Key가 여러 개인 경우, 아래와 같이 <composite-id> 태그를 사용하여 정의한다.

    <class name="Foo" table="TBL_FOO">
        <composite-id>
            <key-property name="username“
                    column="USERNAME" />   
           <key-property name="organizationId" 
                    column="ORGANIZATION_ID" />  
        </composite-id>
    						
        …중략
    </class>

    <id>의 property는 다음과 같다.

    속성설명필수/선택기본값
    nameDB의 primary key칼럼과 매핑 될 attribute 이름선택N/A
    columnDB 테이블의 key칼럼 이름선택name 속성의 값
    typeattribute 타입(Java 객체의 type가 아니라 Hibernate의 매핑타입)선택N/A

    * name 속성값이 정의되어 있지 않은 경우 해당 클래스는 식별자를 가지지 않은 것으로 간주된다. 또한 type 속성값이 정의되어 있지 않은 경우 해당 클래스의 식별자 필드의 타입을 찾아, Hibernate의 타입으로 매핑된다.

  5. 일반 필드 매핑

    <property>태그는 DB의 일반 컬럼과 매핑 클래스의 attribute를 명시한다. <property>의 태그를 사용하여 매핑 정보를 설정하는 방법은 다음과 같다. <property>는 하위에 <column>, <formula>, <meta>를 가질 수 있으나, 여기에서는 일반적으로 사용되는 <column>태그를 사용해서 작성하는 방법에 대해서만 다루기로 한다.

    <property name="countryId" type="string">
        <column name="COUNTRY_ID" length="2" not-null="true" />
    </property>

    <property>태그

    속성설명필수/선택기본값
    name매핑 될 attribute 이름필수N/A
    typeattribute 타입(Java 객체의 type가 아니라 Hibernate의 매핑타입)선택N/A

    * type 속성값이 정의되어 있지 않은 경우 해당 클래스의 필드 타입을 찾아, Hibernate의 타입으로 매핑된다.

    <column>태그

    속성설명필수/선택기본값
    nameDB 테이블의 컬럼 이름필수N/A
    length컴럼의 값의 길이선택255
    not-null컬럼값이 필수인지 아닌지 설정 (true or false)N/Atrue

49.2.2.Data Type의 매핑

49.2.2.1.Data Type의 매핑

Mapping File에 대한 기본 설정 방법은 위에서 언급한 것처럼 다음과 같다.

<property name="countryId" type="string">
    <column name="COUNTRY_ID" length="2" not-null="true" />
</property>

위의 설정에서 보면 type은 countryId라는 자바 attribute와 COUNTRY_ID라는 DB 컬럼의 매핑을 위해 설정한다. type에 설정된 매핑 타입을 이용해 자바 attribute와 DB 컬럼 사이의 값을 알맞게 변환한다. type에 설정되는 매핑 타입은 Hibernate에서 기본적으로 제공하는 것 외에 개발자가 커스터마이즈할 수 있다.

  • Java Primitive Mapping Type

    다음은 Hibernate에서 제공하는 Java primitive mapping type이다. (참고 : Oracle Data Type는 Oracle 10g에서 테이블 생성 시 설정할 Column Type 이다.)

    Mapping TypeJava TypeStandard SQL built-in typeOracle Column Type
    integerint or java.lang.IntegerINTEGERNUMBER(10,0)
    longlong or java.lang.LongBIGINTNUMBER(19,0)
    shortshort or java.lang.ShortSMALLINTNUMBER(5,0)
    floatfloat or java.lang.FloatFLOATFLOAT
    doubledouble or java.lang.DoubleDOUBLEDOUBLE PRECISION
    big_decimaljava.math.BigDecimalNUMERICNUMBER(19,2)
    characterchar or java.lang.CharacterCHAR(1)CHAR(1 CHAR)
    stringjava.lang.StringVARCHARVARCHAR2(255 CHAR)
    bytebyte or java.lang.ByteTINYINTNUMBER(3,0)
    booleanboolean or java.lang.BooleanBITNUMBER(1,0)
    yes_noboolean or java.lang.BooleanCHAR(1) ( ture : false = Y : N )CHAR(1 CHAR)
    true_falseboolean or java.lang.BooleanCHAR(1) ( ture : false = T : F )CHAR(1 CHAR)

    위 표를 참고하여 자바 프로퍼티와 DB 컬럼 type에 맞게 설정하면 된다. 다음은 Java primitive type을 테스트 하기 위해 작성한 JavaDataType.java 파일의 일부이다.

    public class JavaDataType {
        private int id;
        private int intType;
        private long longType;
        private short shortType;
        private float floatType;
        private double doubleType;
        private java.math.BigDecimal bigDecimalType;
        private String stringType;
        private char charType;
        private byte byteType;
        private boolean booleanType;
        private boolean yesNoType;
        private boolean trueFalseType;
    	...중략
    아래는 JavaDataType.java에 정의 된 attribute 타입과 DB Coulmn 타입 매핑 설정을 위해 작성한 JavaDataType.hbm.xml 파일의 일부이다.
    <property name="intType" column="INT_TYPE" type="int"/>
    <property name="longType" column="LONG_TYPE" type="long"/>
    <property name="shortType" column="SHORT_TYPE" type="short"/>
    <property name="floatType" column="FLOAT_TYPE" type="float"/>
    <property name="doubleType" column="DOUBLE_TYPE" type="double"/>
    <property name="bigDecimalType" column="BIGDECIMAL_TYPE" type="big_decimal"/>
    <property name="charType" column="CHAR_TYPE" type="character"/>
    <property name="stringType" column="STRING_TYPE" type="string"/>
    <property name="byteType" column="BYTE_TYPE" type="byte"/>
    <property name="booleanType" column="BOOLEAN_TYPE" type="boolean"/>
    <property name="yesNoType" column="YES_NO_TYPE" type="yes_no"/>
    <property name="trueFalseType" column="TRUE_FALSE_TYPE" type="true_false"/>

    Java primitive type과 DB Column type에 대한 테스트 코드 보기

  • Date And Time Mapping Type

    아래는 Hibernate에서 제공하는 date와 time에 대한 mapping type이다. (참고 : Oracle Data Type는 Oracle 10g에서 테이블 생성 시 설정할 Column Type 이다.)

    Mapping TypeJava TypeStandard SQL built-in typeOracle Column Type
    datejava.util.Date or java.sql.DateDATEDATE
    timejava.util.Date or java.sql.TimeTIMEDATE
    timestampjava.util.Date or java.sql.TimeStampTIMESTAMPTIMESTAMP
    calendarjava.util.CalendarTIMESTAMPTIMESTAMP
    calendar_datejava.util.CalendarTIMESTAMPDATE

    mapping flie작성 시 위 표를 참고하여 자바 객체의 attribute 타입에 맞게 mapping file을 작성하면 된다. 다음은 time, data type을 테스트 하기 위해 작성한 TimeDateType.java 파일의 일부이다.

    public class TimeDateType {
        private java.sql.Date dateType;
        private java.sql.Time timeType;
        private java.sql.Timestamp timestampType;
        private java.util.Calendar calendarType;
        private java.util.Calendar calendarDateType;
        ...중략

    위의 attribute 타입에 맞게 mapping file를 설정하면 된다. 다음은 TimeDataType.java파일과 DB 테이블의 mapping정보를 설정한 TimeDateType.hbm.xml 파일의 일부이다.

    <property name="dateType" column="DATE_TYPE" type="date"/>
    <property name="timeType" column="TIME_TYPE" type="time"/>
    <property name="timestampType" column="TIMESTAMP_TYPE" type="timestamp"/>
    <property name="calendarType" column="CALENDAR_TYPE" type="calendar"/>
    <property name="calendarDateType" column="CALENDAR_DATE_TYPE" type="calendar_date"/>
    

    Java Date, Time type과 DB Column type에 대한 테스트 코드 보기

  • Binary And Large Object Mapping Type

    Mapping TypeJava TypeStandard SQL built-in typeOracle Column Type
    binarybyte[]VARBINARYBLOB(자동 생성 시 RAW)
    textjava.lang.StringCLOBCLOB
    clobjava.sql.ClobCLOBCLOB
    blobjava.sql.BlobBLOBBLOB
    serializablejava.io.SerializableVARBINARY-

    mapping flie작성 시 위 표를 참고 하여 자바 attribute 타입에 맞게 mapping file을 작성하면 된다. 다음은 binary, large object type을 테스트 하기 위해 작성한 BlobDataType.java , ClobDataType.java 의 일부분이다.

    public class BlobDataType {
        private String fileName;
        private java.math.BigDecimal fileSize;
        private byte[] fileContentByte;
        private Blob fileContentBlob;

    public class ClobDataType {
        private String title;
        private String contentString;
        private Clob contentClob;
    위의 attribute 타입에 맞게 mapping file을 설정하면 된다. 다음은 BlobData.java파일과 DB 테이블의 mapping정보를 설정한 BlobDataType.hbm.xml 과 ClobDataType.hbm.xml 파일의 일부이다.
    <property name="fileName" column="FILE_NAME" type="text"/>
    <property name="fileSize" column="FILE_SIZE" type="big_decimal"/>
    <property name="fileContentByte" column="FILE_CONTENT_BYTE" type="binary" />
    <property name="fileContentBlob" column="FILE_CONTENT_BLOB" type="blob"/>

    <property name="title" column="TITLE" type="text"/>
    <property name="contentString" column="CONTENT_STRING" type="text"/>
    <property name="contentClob" column="CONTENT_CLOB" type="clob"/>
    Binary, BLOB Type과 DB Column type 매핑에 대한 테스트 코드 보기

    CLOB Type과 DB Column type 매핑에 대한 테스트 코드 보기

49.2.3.Hibernate Generator

앞에서 설명한 식별자 필드 매핑에 이용되는 <id>태그안의 <generator>태그는 객체 저장 시 식별자 값의 생성 방식을 지정한다. 그렇기 때문에 Mapping XML 파일 작성 시 신규 데이터를 추가하기 위해 해당 데이터의 유일한 Id를 할당받기 위한 방법을 선택해야 한다. 생성 방법에는 Hibernate에서 제공하는 기본 Id Generator 이용하는 방법과 직접 생성하는 방법이 있다.

49.2.3.1.Hibernate 기본 Id Generator

Hibernate에서 제공하는 기본 Id Generator

  • identity : DB2, MySQL, MS SQL Server, Sybase, HypersonicSQL에서 제공하는 identity column을 지원하고 return되는 identifier type는 int, short, long이다.

  • native : DB에 의존하여 Hibernate가 자동으로 신규 Id를 할당한다.

  • hilo : hi/lo 알고리즘이 적용된 특정 테이블의 칼럼값을 이용하여 Id를 생성한다. return되는 identifier type는 int, short, long이다.

  • increment : Hibernate가 값을 1씩 증가시켜 Id를 생성한다.

  • guid : MS SQL과 MySQL에서 생성한 GUID 문자열을 Id로 전달한다.

  • sequence : Oracle, DB2, PostgreSQL, SAP DB, Mckoi에서 사용하는 Sequence를 사용하여 Id를 생성한다. 리턴되는 identifier type는 int, short, long이다.

  • uuid : UUID 알고리즘을 이용하여 128 bit Id를 생성한다. 생성된 문자열은 32 글자의 16진법으로 인코딩되어 표시된다.

  • seqhilo : hilo와 동일하나 주어진 DB의 Sequence로부터 hi 값을 가져온다.

  • identity

    identity는 MySQL, MS SQL Server와 같이 DBMS에서 제공하는 identifier를 제공한다. identity generator를 이용하여 identifier를 생성하기 위한 설정을 보여주는 CountryWithIdentity.hbm.xml 의 일부분이다.

    <class name="anyframe.sample.model.unidirection.generator.CountryWithIdentity"
            table="COUNTRY_IDENTITY" lazy="true" schema="PUBLIC">
        <id name="countryCode" column="COUNTRY_CODE" type="int">
            <generator class="identity" />
    	</id>
    ...중략

    아래는 identity generator를 이용하여 COUNTRY 테이블의 primary key인 COUNTRY_CODE를 자동 생성하고 테스트 하는 HibernateIdGenerator.java 의 일부분이다.

    public void addCountryWithIdentityGenerator() throws Exception {
        CountryWithIdentity country1 = new CountryWithIdentity();
        country1.setCountryId("KR");
        country1.setCountryName("Korea");
    
        Integer countryCode = (Integer) session.save(country1);
        ...중략
    }

    위 테스트 케이스를 실행해보면 COUNTRY_CODE에 자동으로 identifier가 생성되어 저장되는 것을 확인할 수 있다.

  • sequence

    Oracle과 같이 Sequence를 사용할 수 있는 DBMS에서 Sequence를 사용하여 Id를 생성한다. 다음은 sequence generator를 이용하여 identifier를 생성하기 위한 설정파일 CountryWithSequence.hbm.xml 의 일부분이다.

    <class name="anyframe.sample.model.unidirection.generator.CountryWithSequence"
            table="COUNTRY_SEQ" lazy="true" schema="PUBLIC">
        <id name="countryCode" type="int">
            <column name="COUNTRY_CODE" length="12" />
            <generator class="sequence">
                <param name="sequence">COUNTRY_ID_SEQ</param>
            </generator>
        </id>
    ...중략

    DBMS의 COUNTRY_ID_SEQ 이름의 Sequence값으로 identifier를 생성한다. 아래는 sequence generator를 이용해 DBMS의 특정 Sequence으로 primary key column에 데이터를 저장하고 테스트하는 HibernateIdGenerator.java 의 일부분이다.

    public void addCountryWithSequenceGenerator() throws Exception {
        CountryWithSequence country1 = new CountryWithSequence();
        country1.setCountryId("KR");
        country1.setCountryName("Korea");
    
        Integer countryCode = (Integer) session.save(country1);
    ...중략
    }

    위 테스트 케이스를 실행해 보면 DBMS에서 COUNTRY_ID_SEQ의 Sequence값이 COUNTRY_CODE에 입력되는 것을 확인 할 수 있다.

  • hilo

    hilo generator는 hi/lo알고리즘을 사용하여 identifier를 생성한다. 다음은 hilo를 이용해 identifier를 생성하도록 설정한 CountryWithHilo.hbm.xml 의 일부분이다.

    <class name="anyframe.sample.model.unidirection.generator.CountryWithHilo"
        table="COUNTRY_HILO" lazy="true" schema="PUBLIC">
        <id name="countryCode" column="COUNTRY_CODE" type="int">
            <generator class="hilo">
                <param name="table">ID_MANAGEMENT</param>
                <param name="column">NEXT_VALUE</param>
                <param name="max_lo">2</param>
            </generator>
        </id>
    ...중략

    위 Mapping File는 ID_MANAGEMENT 테이블의 NEXT_VALUE 컬럼에서 identifier를 얻고 다음에 유일한 아이디를 제공하기 위해 NEXT_VALUE 컬럼의 값에 1을 더한 값을 업데이트 한다. max_lo는 hilo generator 실행 시 생성 되는 신규 identifier의 개수이다. 다음은 위 Mapping File로 테스트케이스를 실행했을 때 identifier가 생성되는 query에 대한 로그이다.

    select NEXT_VALUE from ID_MANAGEMENT 
    update ID_MANAGEMENT set NEXT_VALUE = 1 where NEXT_VALUE = 0

    ID_MANAGEMENT 테이블에서 NEXT_VALUE을 얻어와서 identifier를 생성한 다음 update하는 query를 볼 수있다. 다음은 hilo generator를 테스트 하기 위한 HibernateIdGenerator.java 의 일부분이다.

    public void addCountryWithHiloGenerator() throws Exception {
        CountryWithSeqHilo country1 = new CountryWithSeqHilo();
        country1.setCountryId("KR");
        country1.setCountryName("Korea");
    
        Integer countryCode1 = (Integer) session.save(country1);
    ...중략
        CountryWithSeqHilo country2 = new CountryWithSeqHilo();
        country2.setCountryId("JP");
        country2.setCountryName("Japan");
    
        Integer countryCode2 = (Integer) session.save(country2);
    ...중략
        CountryWithSeqHilo country3 = new CountryWithSeqHilo();
        country3.setCountryId("US");
        country3.setCountryName("U.S.A");
    
        Integer countryCode3 = (Integer) session.save(country3);
    }

    위 테스트 코드를 디버그 모드로 실행해 보면 country2를 저장 할 때까지는 ID_MANAGEMENT 테이블에서 NEXT_VALUE 컬럼의 값을 select하는 로그는 한 번만 남을 것이다. 그리고 country3를 저장 할 때 다시 한번 ID_MANAGEMENT 테이블에서 NEXT_VALUE 컬럼의 값을 select하는 로그가 남는 것을 확인할 수 있다. 이는 Mapping File에 max_lo값을 2로 세팅 했기 때문에 처음 identifier 생성 시 2개를 생성하기 때문이다.

    #참고 : table, column을 Mapping File에 세팅하지 않을 경우 기본 table, column은 hibernate_unique_key, next_hi이다.

  • seqhilo

    hilo와 동일 하지만 DB의 특정 테이블의 컬럼이 아닌 DBMS의 Sequence로부터 hi값을 가져와 identifier를 생성한다. 아래는 seqhilo를 이용하여 identifier를 생성하기 위한 CountryWithSeqHilo.hbm.xml 의 일부분이다.

    <class name="anyframe.sample.model.unidirection.generator.CountryWithSeqHilo"
        table="COUNTRY_SEQHILO" lazy="true" schema="PUBLIC">
        <id name="countryCode" column="COUNTRY_CODE" type="int">
            <generator class="seqhilo">
                <param name="sequence">COUNTRY_ID_SEQ</param>
                <param name="max_lo">2</param>
            </generator>
        </id>
    ...중략

    위 Mapping File에서는 Primary Key인 COUNTRY_CODE의 identifier를 생성하기 위해서 DBMS의 COUNTRY_ID_SEQ란 이름의 sequence를 이용해 identifier를 생성한다. 아래는 seqhilo generator를 이용해 identifier를 생성할 때 DBMS에서 값을 얻기 위해 실행되는 query 로그이다.

    call next value for COUNTRY_ID_SEQ

    다음은 seqhilo generator에 대한 테스트 코드 HibernateIdGenerator.java 의 일부분이다.

    public void addCountryWithSeqHiloGenerator() throws Exception {
        CountryWithSeqHilo country1 = new CountryWithSeqHilo();
        country1.setCountryId("KR");
        country1.setCountryName("Korea");
    
        Integer countryCode1 = (Integer) session.save(country1);
    ...중략
        CountryWithSeqHilo country2 = new CountryWithSeqHilo();
        country2.setCountryId("JP");
        country2.setCountryName("Japan");
    
        Integer countryCode2 = (Integer) session.save(country2);
    ...중략
        CountryWithSeqHilo country3 = new CountryWithSeqHilo();
        country3.setCountryId("US");
        country3.setCountryName("U.S.A");
    
        Integer countryCode3 = (Integer) session.save(country3);
    ...중략
    }

    위의 테스트 케이스도 hilo와 마찬가지로 max_lo를 2로 설정 했기 때문에 country2를 save할 때까지 DBMS의 sequence를 이용해 identifier를 생성하는 로그가 한번만 남는다. 그리고 country3를 save할 때 identifier를 생성하기 위해 DBMS에서 sequence를 얻어 오는 로그가 남는다.

  • increment

    increment generator는 매핑되는 primary key값의 최고값을 얻어와서 Hibernate가 1을 증가시킨 다음에 identifier를 생성한다. 아래는 increment generator를 이용해 identifier를 생성하기 위해 설정한 CountryWithIncrement.hbm.xml 의 일부분이다.

    <class
        name="anyframe.sample.model.unidirection.generator.CountryWithIncrement"
        table="COUNTRY_INCREMENT" lazy="true" schema="PUBLIC">
        <id name="countryCode" type="int">
            <column name="COUNTRY_CODE" length="12" />
            <generator class="increment" />
        </id>
    ...중략	

    increment generator를 이용하여 키 생성이 필요할 때 실행되는 query는 아래와 같다.

    select max(COUNTRY_CODE) from COUNTRY_INCREMENT

    위의 query는 identifier가 필요할 때마다 생성 되는 것이 아니라 처음 실행된 이후 메모리에서 1씩 증가하는 것이기 때문에 분산환경에서 사용 할 경우 제대로 된 identifier를 생성하지 못 할 수도 있다. 다음은 increment generator를 이용해 identifier를 생성하는 테스트 코드 HibernateIdGenerator.java 의 일부분이다.

    public void addCountryWithIncrementGenerator() throws Exception {
        CountryWithIncrement country1 = new CountryWithIncrement();
        country1.setCountryId("KR");
        country1.setCountryName("Korea");
    
        Integer countryCode1 = (Integer) session.save(country1);
    ...중략                
        CountryWithIncrement country2 = new CountryWithIncrement();
        country2.setCountryId("JP");
        country2.setCountryName("Japan");
    
        Integer countryCode2 = (Integer) session.save(country2);
    ...중략                
        CountryWithIncrement country3 = new CountryWithIncrement();
        country3.setCountryId("US");
        country3.setCountryName("U.S.A");
    
        Integer countryCode3 = (Integer) session.save(country3);
    }

    위의 테스트 코드를 실행해 보면 처음 DB에서 최대 키 값을 얻어 온 이후 다시 얻어오는 query는 실행 되지 않는다.

  • uuid

    UUID알고리즘을 사용하여 16진법으로 32글자의 identifier를 생성한다. 아래는 UUID를 사용해 identifier를 생성하기 위해 설정한 CountryWithUUID.hbm.xml 의 일부분이다.

    <class name="anyframe.sample.model.unidirection.generator.CountryWithUUID"
        table="COUNTRY_UUID" lazy="true" schema="PUBLIC">
        <id name="countryCode" column="COUNTRY_CODE" type="string">
            <generator class="uuid">
                <param name="separator">#</param>
            </generator>
        </id>
    ...중략	

    다음은 UUID generator에 대한 테스트 코드 HibernateIdGenerator.java 의 일부분이다.

    public void addCountryWithUUIDGenerator() throws Exception {
        CountryWithUUID country1 = new CountryWithUUID();
        country1.setCountryId("KR");
        country1.setCountryName("대한민국");
    	
        String countryCode = (String) session.save(country1);
    }

    위의 테스트 코드를 실행 시켰을 때 query 로그는 다음과 같다.

    insert into PUBLIC.COUNTRY_UUID 
                (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) values ('KR', '대한민국', 
    'c687b6dc#1c894fc4#011c#894fc5ef#0001')
    Mapping File에 separator에 대한 값을 #으로 했기 때문에 생성되는 identifier값에 구분자로 '#'이 사용 된 것을 확인 할 수 있다.

49.2.3.2.직접생성

Hibernate에서 제공하는 기본 gernerator를 이용할 수도 있겠지만 직접 키 값을 생성해서 저장하는 경우도 있다. 'product-00001', 'product-00002'과 같이 identifier를 저장하고 싶을 때는 위에서 언급한 Hibernate 기본 generator를 사용 할 수 없다.

  • assigned

    <generator>의 class 속성 값을 assigned로 정의한 경우 객체에 저장된 값을 그대로 이용하게 된다. 사용자가 정의한 별도 Id Generator가 있는 경우 class 속성 값에 해당 클래스를 정의할 수 있다. 다음은 assigned하기 위해 Mappping File에 설정한 샘플 소스이다.

    <id name="categoryNo" type="string">
        <column name="CATEGORY_NO" length="16" />
        <generator class="assigned" />
    </id>

    generator를 assigned로 설정 했다면 객체를 저장하기 전에 categoryNo에 값을 세팅해야 한다. 다음은 Anyframe의 기술공통 서비스인 IdGenerationService를 이용해 identifier을 얻어서 객체에 세팅하고 저장하는 샘플 소스이다.

    category.setCategoryNo(idGenerationService.getNextStringId());
    ...중략
    session.save(category);
    

    assigned generator는 일반적으로 가장 많이 이용하는 형태로 Anyframe의 IdGenerationService과 함께 사용 시 유용하다.

49.3.Persistence Mapping

Hibernate을 이용하여 영속성을 가지는 Persistence 객체를 특정 테이블과 매핑하는 Object Relational Mapping 작업이 필요하다. 이 페이지에서는 하나의 객체를 하나의 테이블로 매핑하기 위해 필요한 사항들에 대해서는 언급하지 않으며, Association과 Inheritance 관계에 놓여 있는 객체 사이의 관계를 테이블로 매핑하는 방법에 대해 살펴보도록 할 것이다. (*참고. 하나의 객체를 하나의 테이블로 매핑하기 위해 필요한 사항들에 대해서는 Hibernate 하위 메뉴인 Mapping XML File 를 참고하도록 한다.)

49.3.1.Persistence Mapping - Association

본 페이지에서는 두 클래스 사이의 Association 유형별 매핑 방법에 대해 자세히 살펴보도록 하자. 특히, 객체 모델링에서 가장 많이 사용될 One to Many Mapping에서는 다양한 Collection 매핑 방법 및 Collection의 주요 속성인 inverse, cascade에 대해 샘플 코드를 중심으로 분석해 볼 것이다.

49.3.1.1.One to One Mapping

A:B = 1:1 관계에 놓여 있는 두 클래스 사이의 관계를 매핑하는 방법은 여러가지가 있는데 그 중의 하나가 동일한 Primary Key를 기반으로 클래스 사이의 참조 관계를 매핑하는 것이다.

왼쪽 그림은 클래스 다이어그램으로 Foo : Bar = 1:1 이며, 단방향 참조 관계를 표현하고 있다. 이것은 오른쪽 그림 상단의 ERD와 같이 각각 TBL_FOO와 TBL_BAR로 매핑될 수 있으며, 별도 추가 컬럼을 필요로 하지는 않으면서 동일한 Primary Key를 사용하고 있음을 알 수 있다. 이와 같은 매핑 관계를 Hibernate Mapping XML 파일에 정의하는 방법은 오른쪽 그림 하단의 내용과 같다. 그림 내용에 대해 설명하자면, Foo, Bar 각각의 클래스 및 속성 정보를 class 태그를 이용하여 정의하고, 두 클래스간 참조 관계에 대해서는 참조하는 측에서 one-to-one 태그를 이용하여 참조 관계에 놓인 클래스와 테이블을 명시하고 있다.

49.3.1.2.One to Many Mapping

A:B = 1:m 관계에 놓여 있는 두 클래스 사이의 관계를 매핑할 때 객체 B는 기본적으로 Collection의 형태이다. One to Many Mapping에서는 샘플을 기반으로 Hibernate Mapping XML 파일 정의 방법에 대해 살펴본 후, Hibernate에서 지원하는 다양한 Collection 유형 과 Collection의 중요한 속성 중의 하나인 inverse와 cascade 에 대해 알아보도록 한다.

왼쪽 그림은 클래스 다이어그램으로 Foo : Bar = 1:m이며, 단방향 참조 관계를 표현하고 있다. 이것은 오른쪽 그림 상단의 ERD와 같이 각각 TBL_FOO, TBL_BAR로 매핑될 수 있으며, TBL_BAR 테이블은 TBL_FOO 테이블에 대한 Foreign Key가 필요하다. 이와 같은 매핑 관계를 Hibernate Mapping XML 파일에 정의하는 방법은 오른쪽 그림 하단의 내용과 같다. 그림 내용에 대해 설명하자면, Foo, Bar 각각의 클래스 및 속성 정보를 class 태그를 이용하여 정의하고, A 측에서 B 클래스에 대해 set 태그를 이용하여 Collection 형태를 표현한다. 또한 set 태그 내에서는 one-to-many 태그를 이용하여 두 객체 사이의 관계를 명시하면서, key 태그를 통해 Foreign Key 컬럼을 명시하고 있다. 1:m 단방향 관계에 대한 Hibernate Mapping XML 정의 방법은 Country : Movie = 1:m 단방향 관계를 표현한 Country.hbm.xml 과 Movie.hbm.xml 를 참고하도록 한다. Hibernate Mapping XML 파일 내의 B 측에 다음과 같이 매핑 정보를 추가할 경우 양방향 참조도 가능해진다.

<class name="Bar" table="TBL_BAR">
    …
    <many-to-one name="TBL_FOO" class="Foo" column="FOO_ID"/>
</class>

1:m 양방향 관계에 대한 Hibernate Mapping XML 정의 방법은 Country : Movie = 1:m 양방향 관계를 표현한 Country.hbm.xml 과 Movie.hbm.xml 를 참고하도록 한다.

Collection Mapping

앞서 언급한 것처럼, 1:m 관계에 놓여 있는 두 클래스 사이의 관계를 매핑할 때 객체 B는 기본적으로 Collection의 형태이며, Hibernate에서 지원하는 Collection 타입은 set외에도 다음과 같이 여러가지가 있다.

  • set : java.util.Set 타입으로 <set>을 이용하여 정의한다. 객체의 저장 순서를 알 수 없으며, 동일 객체의 중복 저장을 허용하지 않는다. (HashSet 이용) 다음은 set 태그를 이용하여 Collection 객체를 정의한 Hibernate Mapping XML과 소스 코드의 예이다.

    1. Hibernate Mapping XML
    						
    <class name="anyframe.sample.model.unidirection.relation.collection.CountryWithSet" 
        table="COUNTRY_SET" lazy="true" schema="PUBLIC">
        ...중략
            <set name="movies" inverse="true" cascade="save-update">
            <key>
                <column name="COUNTRY_CODE" length="12" />
            </key>
            <one-to-many class="anyframe.sample.model.bidirection.Movie" />
        </set>
    </class>
    2. CountryWithSet.java
    
    public class CountryWithSet implements java.io.Serializable {
    
        private String countryCode;
        private String countryId;
        private String countryName;
        private Set movies = new HashSet(0);
    
        ...중략
    }

  • list : java.util.List 타입으로 <list>를 이용하여 정의한다. List 타입의 경우 저장된 객체의 순서를 알 수 있으며, 저장 순서를 테이블에 보관하기 위해서 별도 인덱스 컬럼 정의가 필요하다. 객체 저장 순서를 저장할 별도 컬럼은 <list> 하위에 <list-index>를 이용하면 된다. (ArrayList 이용) 다음은 list 태그를 이용하여 Collection 객체를 정의한 Hibernate Mapping XML과 소스 코드의 예이다.

    1. Hibernate Mapping XML
    						
    <class name="anyframe.sample.model.unidirection.relation.collection.CountryWithList" 
        table="COUNTRY_LIST" lazy="true" schema="PUBLIC">
        ...중략
            <list name="movies" cascade="save-update">
            <key>
                <column name="COUNTRY_CODE" length="12" />
            </key>
            <list-index column="MOVIE_IDX"/>
            <one-to-many class="anyframe.sample.model.unidirection.Movie" />
        </list>
    </class>
    2. CountryWithList.java
    
    public class CountryWithList implements java.io.Serializable {
    
        private String countryCode;
        private String countryId;
        private String countryName;
        private List movies = new ArrayList(0);
    
    	
        ...중략
    }

  • bag : java.util.Collection 타입인 경우 <bag> 또는 <idbag>을 이용하여 정의한다. 객체의 저장 순서를 알 수 없으나, 동일 객체의 중복 저장은 허용한다. 내부적으로 List를 사용하나 인덱스 값을 사용하지는 않는다. (ArrayList 이용) 또한 Bag은 Set과 비슷하나 모든 Collection를 로드하지 않고도 해당 Collection에 신규 객체를 추가할 수 있으므로 성능면에서 유리하다. 다음은 <bag>을 이용하여 Collection 객체를 정의한 Hibernate Mapping XML과 소스 코드의 예이다.

    1. Hibernate Mapping XML
    
    <class name="anyframe.sample.model.unidirection.relation.collection.CountryWithBag" 
        table="COUNTRY_BAG" lazy="true" schema="PUBLIC">
        ...중략
        <bag name="movies"
        inverse="true" cascade="save-update">
            <key>
                <column name="COUNTRY_CODE" length="12" />
            </key>
            <one-to-many class="anyframe.sample.model.unidirection.Movie" />
        </bag>
    </class>				
    
    2. CountryWithBag.java
    public class CountryWithBag implements java.io.Serializable {
    
        private String countryCode;
        private String countryId;
        private String countryName;
        private Collection movies = new ArrayList(0);
    	
        ...중략
    }

    다음은 <idbag>을 이용하여 Collection 객체를 정의한 Hibernate Mapping XML과 소스 코드의 예이다. idbag은 bag과는 달리 순서가 보장되며, One to Many 관계에서 다른 Collection 매핑 방법과는 다르게 composite-element 태그를 이용한 value type으로 정의해야 한다.

    1. Hibernate Mapping XML
    						
    <class name="anyframe.sample.model.unidirection.relation.collection.CountryWithIdBag" 
        table="COUNTRY_IDBAG" lazy="true" schema="PUBLIC">
        ...중략
         <idbag name="movies"
            table="MOVIE"> 
            <collection-id column="id" type="java.lang.String"> 
                <generator class="uuid"/> 
            </collection-id> 
            <key column="COUNTRY_CODE" />
            <composite-element class="anyframe.sample.model.unidirection.Movie"> 
                <property name="title" type="string">
                    <column name="TITLE" length="100" not-null="true" />
                </property>
                <property name="director" type="string">
                    <column name="DIRECTOR" length="10" not-null="true" />
                </property>
                <property name="releaseDate" type="date">
                    <column name="RELEASE_DATE" length="0" />
                </property>
            </composite-element>
        </idbag> 
    </class>
    2. CountryWithIdBag.java
    
    public class CountryWithIdBag implements java.io.Serializable {
    
        private String countryCode;
        private String countryId;
        private String countryName;
        private Collection movies = new ArrayList(0);
    	
    	...중략
    }

  • map : java.util.map 타입으로 <map>을 이용하여 (키,값)을 쌍으로 정의한다. (HashMap 이용) 다음은 <map>을 이용하여 Collection 객체를 정의한 Hibernate Mapping XML과 소스 코드의 예이다.

    1. Hibernate Mapping XML
    						
    <class name="anyframe.sample.model.unidirection.relation.collection.CountryWithMap" 
        table="COUNTRY_MAP" lazy="true" schema="PUBLIC">
        ...중략
        <map name="movies"
            cascade="save-update">
            <key>
                <column name="COUNTRY_CODE" length="12" />
            </key>
            <map-key column="MOVIE_MAP_KEY" type="string"/>
            <one-to-many class="anyframe.sample.model.unidirection.Movie" />
        </map>
    </class>
    2. CountryWithMap.java
    
    public class CountryWithMap implements java.io.Serializable {
    
        private String countryCode;
        private String countryId;
        private String countryName;
        private Map movies = new HashMap(0);
    	
        ...중략
    }

  • StoredSet, StoredMap : <set>, <map>을 그대로 이용하되, sort라는 attribute를 이용하여 정렬 방식을 정의한다. (TreeSet, TreeMap 이용)

HibernateCollectionMapping.java 코드를 통해 위에서 언급한 각종 유형별 Collection에 대한 사용 방법 및 차이점을 직접 확인할 수 있을 것이다.

Inverse, Cascade 속성

inverse와 cascade는 Collection 정의시 중요한 의미를 가지는 속성 중의 하나로, 다음과 같은 의미를 지닌다.

  • inverse : 객체간 관계의 책임을 어디에 둘지에 대한 옵션을 정의하기 위한 속성이다. 즉, 한 쪽은 owner의 역할을 맡기고, 다른 한 쪽에는 sub의 역할을 맡기기 위함이다.

  • cascade : 부모 객체에 대한 CUD를 자식 객체에도 전이할지에 대한 옵션을 정의하기 위한 속성이다.

이제부터 inverse, cascade 속성 정의에 따라 실행되는 쿼리문이 어떻게 달라지는지를 살펴봄으로써, inverse와 cascade 속성에 대해 자세히 알아보도록 하자.

  • 단방향 1:m 관계

    1. inverse="false", cascade="false"

      public void addCountryMovieWithoutInverseCascade() throws Exception {
          // 1. make init data
          newSession("anyframe/core/hibernate/inverse/unidirection/"
              + "hibernate-without-inversecascade.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between country and movie
          /* #1 */ country.getMovies().add(movie);
      
      	// 3. try to insert a country, movie
          /* #2 */ session.save(country);
          /* #3 */ session.save(movie);
      
          closeSession();
      
          ...중략
      }

      addCountryMovieWithoutInverseCascade() 메소드 실행 결과 #2,#3 번 코드에 의해 신규 Country 정보와 Movie 정보를 등록하기 위한 INSERT 문이 실행된다. 그리고, Country 측에 Movie Collection에 대한 inverse 속성값을 false로 설정하였으므로, #1번 코드에 의해 Country와 Movie 관계 정보 셋팅을 수행하기 위한 UPDATE 문이 한번 더 실행된다. 즉, 다음과 같이 3 개의 쿼리문이 수행된다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')
       	
      insert into PUBLIC.MOVIE
       (TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values ('My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')
       	
      update PUBLIC.MOVIE
       set COUNTRY_CODE='COUNTRY-0001' where MOVIE_ID='MV-00001'

    2. inverse="true", cascade="false"

      public void addCountryMovieWithoutCascade() throws Exception {
          // 1. make init data
          newSession("anyframe/core/hibernate/inverse/unidirection/"
      + "hibernate-without-cascade.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      	
          // 2. try to make a relation between country and movie
          country.getMovies().add(movie);	// no effect code!!
      
          // 3. try to insert a country, movie
          /* #1 */ session.save(country);
          /* #3 */ // movie.setCountryCode(country.getCountryCode());
          /* #2 */ session.save(movie);
      
          closeSession();
      
          ...중략
      }

      addCountryMovieWithoutCascade() 메소드 실행 결과 #1,#2 번 코드에 의해 신규 Country 정보와 Movie 정보를 등록하기 위한 INSERT 문이 실행된다. 또한 inverse="true"인 경우 Move 측에서 관련된 Country 정보와의 Relation 정보 셋팅을 해야 하나 Country -> Movie인 단방향 관계이므로 이것도 가능하지 않다. 따라서 Country와 Movie 사이의 Relation 정보 누락이 발생할 수 있다. 이러한 경우에서는 #3번 코드에서와 같이 Movie Mapping File 내에 COUNTRY_CODE 컬럼을 위한 별도 속성 정보를 정의하고, Movie 등록시에 countryCode를 직접 셋팅해 줌으로써 두 객체 사이의 관계를 유지시킬 수 있도록 해야 할 것이다. 다음은 addCountryMovieWithoutCascade() 메소드 실행 결과 수행되는 쿼리문이다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')
       	
      insert into PUBLIC.MOVIE
       (TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values ('My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')

    3. inverse="false", cascade="true"

      public void addCountryMovieWithoutInverse() throws Exception {
          // 1. make init data
          newSession("anyframe/core/hibernate/inverse/unidirection/"
              + "hibernate-without-inverse.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between country and movie
          /* #2 */ country.getMovies().add(movie);
      
      
          // 3. try to insert a country
          /* #1 */ session.save(country);
      
          closeSession();
      
          ...중략
      }

      addCountryMovieWithoutInverse() 메소드 실행 결과 #1번 코드와 cascade 속성 값에 의해 신규 Country 정보와 함께 Movie 정보가 함께 INSERT된다. 그리고, Country 측에 Movie Collection에 대한 inverse 속성값을 false로 설정하였으므로, #2번 코드에 의해 Country와 Movie 관계 정보 셋팅을 수행하기 위한 UPDATE 문이 한번 더 실행된다. 즉, 다음과 같이 3 개의 쿼리문이 수행된다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')
       	
      insert into PUBLIC.MOVIE
       (TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values ('My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')
       	
      update PUBLIC.MOVIE
       set COUNTRY_CODE='COUNTRY-0001' where MOVIE_ID='MV-00001'

    4. inverse="true", cascade="true"

      public void addCountryMovie() throws Exception {
          // 1. make init data
          newSession("anyframe/core/hibernate/inverse/unidirection/hibernate.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between country and movie
          country.getMovies().add(movie); // no effect code!!
      
          // 4. try to insert a country
          /* #2 */ // movie.setCountryCode(country.getCountryCode());
          /* #1 */session.save(country);
      
          closeSession();
      
          ...중략
      }

      addCountryMovie() 메소드 실행 결과 #1번 코드와 cascade 속성 값에 의해 신규 Country 정보와 함께 Movie 정보가 함께 INSERT된다. 또한 inverse="true"인 경우 Move 측에서 관련된 Country 정보와의 Relation 정보 셋팅을 해야 하나 Country -> Movie인 단방향 관계이므로 이것도 가능하지 않다. 따라서 Country와 Movie 사이의 Relation 정보 누락이 발생할 수 있다. 이러한 경우에서는 #2번 코드에서와 같이 Movie Mapping File 내에 COUNTRY_CODE 컬럼을 위한 별도 속성 정보를 정의하고, Movie 등록시에 countryCode를 직접 셋팅해 줌으로써 두 객체 사이의 관계를 유지시킬 수 있도록 해야 할 것이다. 다음은 addCountryMovie() 메소드 실행 결과 수행되는 쿼리문이다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001') 	
      insert into PUBLIC.MOVIE
       (TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values ('My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')

    표에서 언급한 코드들을 포함한 전체 테스트 코드는 HibernateUnidirectionInverseCascade.java 를 참고하도록 한다.

  • 양방향 1:m 관계

    1. inverse="false", cascade="false"

      public void addCountryMovieWithoutInverseCascade() throws Exception {
          // 1. make init data
          newSession("anyframe/core/hibernate/inverse/bidirection/
                  hibernate-without-inversecascade.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between country and movie
          /* #3 */ country.getMovies().add(movie);
      
      	// 3. try to insert a country, movie
          /* #1 */ session.save(country);
          /* #2 */ session.save(movie);
      
          closeSession();
      }

      addCountryMovieWithoutInverseCascade() 메소드 실행 결과 #1,#2 번 코드에 의해 신규 Country 정보와 Movie 정보를 등록하기 위한 INSERT 문이 실행된다. 그리고, Country 측에 Movie Collection에 대한 inverse 속성값을 false로 설정하였으므로, #3번 코드에 의해 MOVIE 테이블의 COUNTRY_CODE 정보를 null 에서 'COUNTRY-0001'로 셋팅하기 위한 UPDATE 쿼리가 추가적으로 실행된다. 즉, 다음과 같이 3 개의 쿼리문이 수행된다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')
      	
      insert into PUBLIC.MOVIE
       (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values (null
      , 'My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')
      
      update PUBLIC.MOVIE
       set COUNTRY_CODE='COUNTRY-0001' where MOVIE_ID='MV-00001'

    2. inverse="true", cascade="false"

      public void addCountryMovieWithoutCascade() throws Exception {
          // 1. make init data
          newSession(
              "anyframe/core/hibernate/inverse/bidirection/hibernate-without-cascade.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between movie and country
          /* #4 */ // country.getMovies().add(movie);
          /* #3 */ movie.setCountry(country);
      
          // 3. try to insert a country, movie
          /* #1 */ session.save(country);
          /* #2 */ session.save(movie);
      
          closeSession();
      }

      addCountryMovieWithoutCascade() 메소드 실행 결과 #1,#2 번 코드에 의해 신규 Country 정보와 Movie 정보를 등록하기 위한 INSERT 문이 실행된다. 그리고 inverse="true"이고 Country <-> Movie인 양방향 관계이므로, #3번 코드 실행을 통해 Movie INSERT 시점에 Country와 Movie의 Relation을 표현하는 CountryCode 정보가 셋팅된다. 또한 cascade="false"이므로 #4번 코드는 불필요하다.

      위의 코드에서는 단방향 관계에서와 달리 Movie INSERT 시점에 이미 Country와 Movie Relation 정보가 셋팅되므로, Relation 정보 누락이 발생하지 않는다. 또한 inverse="false"인 경우와 달리 별도 Relation 정보 셋팅을 위한 별도 UPDATE 문 실행을 필요로 하지 않아 성능면에서 유리하다.

      다음은 addCountryMovieWithoutCascade() 메소드 실행 결과 수행되는 쿼리문이다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')
      	
      insert into PUBLIC.MOVIE
       (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values ('COUNTRY-0001'
      , 'My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')

    3. inverse="false", cascade="true"

      public void addCountryMovieWithoutInverse() throws Exception {
          // 1. make init data
          newSession(
              "anyframe/core/hibernate/inverse/bidirection/hibernate-without-inverse.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between country and movie
          /* #2 */ country.getMovies().add(movie);
          /* #3 */ // movie.setCountry(country); // no effect code!! 
      
          // 3. try to insert a country
          /* #1 */ session.save(country);
      
          closeSession();
      }

      addCountryMovieWithoutInverse() 메소드 실행 결과 #1번 코드와 cascade 속성 값에 의해 신규 Country 정보와 함께 Movie 정보가 함께 INSERT된다. 그리고, Country 측에 Movie Collection에 대한 inverse 속성값을 false로 설정하였으므로, #2번 코드에 의해 Country와 Movie 관계 정보 셋팅을 수행하기 위한 UPDATE 문이 한번 더 실행된다. 여기서 Relation 관계 셋팅을 위해 #2번 코드가 영향을 미치므로 #3번 코드는 불필요하다. 즉, 다음과 같이 3 개의 쿼리문이 수행된다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')
      	
      insert into PUBLIC.MOVIE
       (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values (null
      , 'My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')
      update PUBLIC.MOVIE
       set COUNTRY_CODE='COUNTRY-0001' where MOVIE_ID='MV-00001'

    4. inverse="true", cascade="true"

      public void addCountryMovie() throws Exception {
          // 1. make init data
          newSession("anyframe/core/hibernate/inverse/bidirection/hibernate.cfg.xml");
          Country country = makeCountry();
          Movie movie = makeMovie();
      
          // 2. try to make a relation between country and movie
          /* #2 */ country.getMovies().add(movie);
      
          // 3. try to make a relation between movie and country
          /* #3 */ movie.setCountry(country);
      
          // 4. try to insert a country
          /* #1 */ session.save(country);
      
          closeSession();
      }					

      addCountryMovie() 메소드 실행 결과 #1,#2번 코드와 cascade 속성 값에 의해 신규 Country 정보와 함께 Movie 정보가 함께 INSERT된다. 그리고 inverse="true"이고 Country <-> Movie인 양방향 관계이므로, #3번 코드 실행을 통해 Movie INSERT 시점에 Country와 Movie의 Relation을 표현하는 CountryCode 정보가 셋팅된다.

      위의 코드에서는 단방향 관계에서와 달리 Movie INSERT 시점에 이미 Country와 Movie Relation 정보가 셋팅되므로, Relation 정보 누락이 발생하지 않는다. 또한 inverse="false"인 경우와 달리 별도 Relation 정보 셋팅을 위한 별도 UPDATE 문 실행을 필요로 하지 않아 성능면에서 유리하다.

      다음은 addCountryMovie() 메소드 실행 결과 수행되는 쿼리문이다.

      insert into PUBLIC.COUNTRY
       (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
      values ('KR', 'Korea', 'COUNTRY-0001')	
      insert into PUBLIC.MOVIE
       (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
      values ('COUNTRY-0001', 'My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001')

    표에서 언급한 코드들을 포함한 전체 테스트 코드는 HibernateBidirectionInverseCascade.java 를 참고하도록 한다.

49.3.1.3.Many to Many Mapping

두 클래스 사이의 관계가 m:n 일 경우 각각의 Foreign Key를 가진 Association 테이블을 정의함으로써 매핑한다.

왼쪽 그림은 클래스 다이어그램으로 User : Role = m:n이며, 단방향 참조 관계를 표현하고 있다. 이것은 오른쪽 그림 상단의 ERD와 같이 각각 TBL_USER, TBL_ROLE로 매핑될 수 있으며, 각 테이블의 Primary Key를 Foreign Key로 가지는 Association 테이블인 TBL_USER_ROLE이 필요하다. 이와 같은 매핑 관계를 Hibernate Mapping XML 파일에 정의하는 방법은 오른쪽 그림 하단의 내용과 같다. 그림 내용에 대해 설명하자면, User, Role 각각의 클래스 및 속성 정보를 class 태그를 이용하여 정의하고, User 측에서 Role 클래스에 대해 set 태그를 이용하여 Collection 형태를 표현하고 연관된 테이블로는 Association 테이블인 TBL_USER_ROLE로 정의한다. 또한 set 태그 내에서는 many-to-many 태그를 이용하여 두 객체 사이의 관계를 명시하면서, key 태그를 통해 Foreign Key 컬럼을 명시하고 있다. 단방향 m:n 관계에 대한 Hibernate Mapping XML 정의 방법은 Category : Movie = m:n 양방향 관계를 표현한 Category.hbm.xml 과 Movie.hbm.xml 를 참고하도록 한다. 양방향의 m-n 관계 설정을 위해서는 양쪽 모두 <class>에 <many-to-many>를 사용하되, 반드시 Relation의 책임을 지는 한 쪽에는 "inverse=true"를 지정하여 매핑 관리를 한쪽에서 처리할 수 있도록 하는 것이 좋다. 양방향 m:n 관계에 대한 Hibernate Mapping XML 정의 방법은 Category : Movie = m:n 양방향 관계를 표현한 Category.hbm.xml 과 Movie.hbm.xml 를 참고하도록 한다.

49.3.2.Persistence Mapping - Inheritance

본 페이지에서는 상속 관계에 참여하는 각 클래스에 대한 여러 매핑 방법에 대해 자세히 살펴보도록 하자.

49.3.2.1.Table per Class Hierarchy

상속 관계에 참여하는 모든 Parent, Child 클래스들을 모두 한 개의 테이블로 매핑하고, Child 클래스의 유형을 구분하기 위해 별도 Discriminator 칼럼을 추가로 정의하는 매핑 방법이다.

왼쪽 상단 그림은 클래스다이어그램으로 User 클래스를 상속받은 Guest, Member, Staff 클래스들이 존재한다. 그리고 왼쪽 하단 그림은 ERD로 User, Guest, Member, Staff 클래스를 TBL_USER라는 하나의 테이블로 매핑하고 있다. 이와 같은 매핑 관계를 Hibernate Mapping XML 파일에 정의하는 방법은 오른쪽 그림의 내용과 같다. 그림 내용에 대해 설명하자면, User 클래스 정보를 class 태그를 이용하여 정의하고, 하위에 discriminator 태그를 이용하여 Child 클래스의 유형을 구분하기 위해 별도로 필요한 Discriminator 칼럼이 추가 정의되어야 한다. 또한 하위에 subclass 태그를 이용하여 User 클래스를 상속하는 하위 클래스와 속성들에 대해 정의하는데 이 때 각 subclass에 대한 Discriminator 값 정의가 필요하다. Table per Class Hierarchy 매핑의 장,단점에 대해 살펴보면 다음과 같다.

[장점]

  • 별도 Join 처리가 필요하지 않아 쿼리문 작성이 용이

  • 상속 관계 제어를 위한 overhead가 최소화

[단점]

  • 특정 테이블을 기준으로 NOT NULL constraints를 정의할 수 없음

  • 하위 클래스가 추가될 테이블 구조 변경 불가피

  • 관리 대상이 되는 칼럼과 NULL 값을 가진 칼럼들의 개수 증가

  • 해당 Domain의 성격과 무관하게 하위 클래스의 타입을 구분하기 위한 별도 칼럼 정의 필요

별도 Discriminator 칼럼을 만들지 않고자 하는 경우에는 다음과 같이 formula라는 속성을 이용할 수 있다. 특정 칼럼의 값에 대한 연산을 통해 Discriminator 값을 정의하는 경우로 아래의 예에서는 DEPT_ID의 값이 NOT NULL인 경우 Discriminator 값은 'STAFF'이므로 해당되는 객체는 Staff가 될 것이다.

<discriminator
    formula="CASE WHEN DEPT_ID IS NOT NULL THEN 'STAFF' 
                    …"
    type="string"/>

49.3.2.2.Table per Subclass

상속 관계에 참여하는 모든 Parent, Child 클래스들을 각각의 테이블로 매핑시키되, 모든 하위 테이블들이 상위 클래스와 동일한 Primary Key를 공유하는 형태이다.

왼쪽 상단 그림은 클래스다이어그램으로 User 클래스를 상속받은 Guest, Member, Staff 클래스들이 존재한다. 그리고 왼쪽 하단 그림은 ERD로 상속 관계에 참여하는 모든 클래스를 TBL_USER, TBL_GUEST, TBL_MEMBER, TBL_STAFF라는 테이블로 매핑하고 있다. 이와 같은 매핑 관계를 Hibernate Mapping XML 파일에 정의하는 방법은 오른쪽 그림의 내용과 같다. 그림 내용에 대해 설명하자면, User 클래스 정보를 class 태그를 이용하여 정의하고, 하위에 joined-subclass 태그를 이용하여 User 클래스를 상속하는 하위 클래스와 속성들에 대해 정의하고 있다. Table per Subclass 매핑의 장,단점에 대해 살펴보면 다음과 같다.

[장점]

  • 객체 지향에 가장 근접한 Mapping이고 하위 클래스가 많은 속성 정보를 가진 경우 가장 자연스러운 Mapping 기법

  • 특정 테이블을 기준으로 NOT NULL Constraints 지정 가능

  • Hibernate을 사용할 경우, 하위 클래스 유형을 구분하기 위한 별도 Discriminator 칼럼 불필요

[단점]

  • 테이블 조회시 각 테이블 간의 Join이 필요하여 계층 관계가 복잡할수록 성능 이슈 유발 가능

  • Hibernate을 이용하지 않고 외부에서 직접 데이터가 추가될 경우 데이터의 정합성이 깨질 우려가 있음

※ 조회하고자 하는 특정 Child 클래스를 명시하지 않고 Parent 클래스를 통해 조회를 요청할 경우 (즉, Parent 클래스를 이용하여 Query를 수행시키는 경우), Hibernate은 실제 조회 대상 클래스를 알 수 없어 해당 클래스의 유형을 찾기 위해 상속 관계에 참여하는 모든 테이블들에 대해 Outer JOIN이 수행되므로 성능 저하를 유발할 수 있음에 유의해야 한다.

(* 특정 Child 클래스를 조회 대상으로 명시하였을 경우에는 Inner JOIN이 수행된다.)

49.3.2.3.Table per Concrete Class

상속 관계에 참여하는 모든 Concrete 클래스들을 각각 한 개의 테이블로 매핑하는 방법으로, 매핑되는 모든 테이블에 상위 클래스의 속성 정보가 반복 정의되어야 한다. Parent 클래스가 추상 클래스일 경우 별도 테이블 정의는 필요하지 않으며, abstract를 true로 정의해야 한다.

왼쪽 상단 그림은 클래스다이어그램으로 User 클래스를 상속받은 Guest, Member, Staff 클래스들이 존재한다. 그리고 왼쪽 하단 그림은 ERD로 TBL_GUEST, TBL_MEMBER, TBL_STAFF라는 테이블로 매핑하고 있다. 이와 같은 매핑 관계를 Hibernate Mapping XML 파일에 정의하는 방법은 오른쪽 그림의 내용과 같다. 그림 내용에 대해 설명하자면, User 클래스 정보를 class 태그를 이용하여 정의하고 User 클래스의 abstract 속성을 true로 정의하고 있다. 또한 하위에 union-subclass 태그를 이용하여 각 하위 클래스들과 속성들에 대해 정의하고 있다. Table per Concrete Class 매핑의 장,단점에 대해 살펴보면 다음과 같다.

[장점]

  • 특정 테이블을 기준으로 NOT NULL Constraints 지정 가능

  • Polymorphic Query 가 필요하지 않은 경우에 사용 시 유용함(* Polymorphic Query : 조회 대상이 되는 특정 클래스/인터페이스를 extends 또는 implements하는 모든 클래스에 대해 조회)

[단점]

  • 데이터 조회시 UNION을 이용해야 하나, UNION은 모든 DB에서 지원되는 것은 아니므로 유의

  • 상위 클래스가 가진 공통 정보가 각 테이블에 중복됨

  • Hibernate을 이용하지 않고 외부에서 직접 데이터가 추가될 경우 데이터의 정합성이 깨질 우려가 있음

49.4.Basic CRUD

Hibernate에서 제공하는 기본 API를 호출함으로써, Persistence 객체를 이용하여 특정 DB에 데이터를 입력,수정,삭제,조회하는 방법에 대해 알아보도록 한다.

49.4.1.단건 조회

get() 또는 load() 메소드를 호출하여 DB로부터 단건의 데이터를 조회할 수 있다. get() 또는 load() 메소드 호출시 대상이 되는 Persistence 클래스와 Primary Key 값에 해당하는 속성값을 입력 인자로 전달해야 한다.

  • get() : 호출 시점에 SELECT 쿼리 실행

  • load() : 객체의 값이 실제로 필요한 시점에 쿼리 실행

Persistence 클래스인 Country 에 대한 매핑 정보가 다음과 같이 정의되어 있다라고 가정해 보자.

<class name="anyframe.sample.model.bidirection.Country" table="COUNTRY" 
    lazy="true" schema="PUBLIC">
    <id name="countryCode" type="string">
        <column name="COUNTRY_CODE" length="12" />
        <generator class="assigned" />
    </id>
    ...
</class>				

Country의 식별자인 countryCode의 값을 이용하여 단건 Country 정보를 조회하고자 할 경우에는 HibernateBasicCRUD 의 countryInfo(...) 메소드에서와 같이 호출하면 된다.

private void countryInfo(String countryCode, Country country)
            throws Exception {
    Country result = (Country) session.get(Country.class, countryCode);
}

load() 메소드의 경우 SELECT 쿼리를 실행하지 않고, 전달된 식별자에 해당하는 객체의 Proxy를 리턴한 후, 해당 객체를 통해 테이블에 저장된 식별자 이외의 값 접근시 SELECT 문을 실행하여 결과값을 Proxy 객체에 저장한다. 다음과 같이 load() 메소드 수행 결과 전달된 객체의 클래스명을 출력해 보면, Proxy 객체가 전달되었음을 알 수 있을 것이다.

User user = session.load(User.class, "test");
    // expected to print : com.sds.emp…User$$EnhancerByCGLIB$$...

    System.out.println(user.getClass().getName());

49.4.2.단건 저장

save() 또는 persist() 메소드를 호출하여 DB에 단건의 데이터를 추가할 수 있다. save() 또는 persist() 메소드 호출시 대상이 되는 Persistence 객체를 입력 인자로 전달해야 한다.

  • save() : 단건의 데이터를 추가한 후, 해당 객체의 식별자를 return

  • persist() : 단건의 데이터 추가. return 값이 없음

신규 Country 정보를 추가하고자 할 경우에는 HibernateBasicCRUD 의 addCountry() 메소드에서와 같이 호출하면 된다.

private Country addCountry() throws Exception {
    // 1. insert a new country information
    Country country = new Country();
    String countryCode = "COUNTRY-0001";
    country.setCountryCode(countryCode);
    country.setCountryId("KR");
    country.setCountryName("Korea");
    session.save(country);

...중략
}

49.4.2.1.Tip. A:B=1:m인 경우 A에 대한 save()

A 객체에서 Collection B에 대한 cascade 속성을 true로 정의하고, Collection B를 포함한 해당 객체 A에 대해 save() 메소드를 호출하는 경우를 가정해 보자. 상위 객체인 A에 대해서는 예상하는 바와 동일하게 동작하나, Collection B에 대해서는 saveOrUpdate()와 동일하게 동작함을 알 수 있다.

이를 확인하기 위해서 Country:Movie = 1:m 관계에 대한 HibernateSaveOrUpdateParentChild 의 각 테스트 메소드 실행 결과를 중심으로 확인해보도록 하자.

1. DB에 추가되어 있지 않은 Country 정보에 대해 save() 메소드 호출하는 경우

Transaction Commit시 신규 생성한 Country 객체에 대해 INSERT문이 실행된다.

public void addCountryCallingSave() throws Exception {
    // 1. try to insert a country information without movies
    newSession();
    Country country1 = makeNewCountry();
    session.save(country1);

    closeSession();

    ...중략
}

* 콘솔 - 실행된 SQL문
insert
 into PUBLIC.COUNTRY (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
values ('KR', 'Korea', 'COUNTRY-0001')          

2. DB에 추가되어 있지 않은 Country 정보에 대해 saveOrUpdate() 메소드를 호출하는 경우

신규 생성한 Country 객체가 DB에 존재하지 않으므로 Transaction Commit시 해당 객체에 대해 INSERT문이 실행된다.

public void addCountryCallingSaveOrUpdate() throws Exception {
    // 1. try to insert a country information without movies
    newSession();
    Country country1 = makeNewCountry();
    session.saveOrUpdate(country1);

    closeSession();

    ...중략
}

* 콘솔 - 실행된 SQL문
insert
 into PUBLIC.COUNTRY (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
values ('KR', 'Korea', 'COUNTRY-0001')        

3. DB에 추가되어 있지 않은 Movie 정보를 포함한 Country에 대해 update() 메소드를 호출하였을 경우

첫번째 Transaction에서 신규 Country 정보를 추가하였고, 두번째 Transaction에서 앞서 등록한 Country 객체에 신규 Movie Collection 정보를 셋팅한 후 update() 메소드를 호출한 경우이다. 두번째 Transaction Commit시 Country 객체에 대해서는 변경 정보가 있다면 UPDATE문이 실행되고, 신규 Movie Collection 정보에 대해서는 INSERT문이 실행된다.

public void addMoviesCallingUpdate() throws Exception {
    // 1. try to insert a country information without movies
    newSession();
    Country country1 = makeNewCountry();
    session.save(country1);

    closeSession();

    // 2. try to insert a country information with movies.
    newSession();
    Country country2 = makeNewMovieSet(country1.getCountryCode());
    session.update(country2);

    closeSession();

    ...중략
}

* 콘솔 - 실행된 SQL문
// 첫번째  Transaction
insert
 into PUBLIC.COUNTRY (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
values ('KR', 'Korea', 'COUNTRY-0001') 
...
// 두번째 Transaction
insert
 into PUBLIC.MOVIE (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
values ('COUNTRY-0001', 'My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001') 
insert
 into PUBLIC.MOVIE (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
values ('COUNTRY-0001', 'My Little Bride', 'Hojun Kim', 2004-04-02, 'MV-00002') 
...        

4. DB에 추가되어 있지 않은 Country 정보를 추가한 후, Movie 정보에 대해 save() 메소드를 호출하였을 경우

첫번째 Transaction에서 신규 Country 정보를 추가하였고, 두번째 Transaction에서 앞서 등록한 Country 객체에 신규 Movie Collection 정보를 셋팅한 후 save() 메소드를 호출한 경우이다. 3번의 경우와 동일하게 동작한다.

public void addMoviesCallingSave() throws Exception {
    // 1. try to insert a country information without movies
    newSession();
    Country country1 = makeNewCountry();
    session.save(country1);

    closeSession();
    
    // 2. try to insert a country information with movies.
    newSession();
    Country country2 = makeNewMovieSet(country1.getCountryCode());
    session.save(country2);

     closeSession();

    ...중략
}

* 콘솔 - 실행된 SQL문
// 첫번째  Transaction
insert
 into PUBLIC.COUNTRY (COUNTRY_ID, COUNTRY_NAME, COUNTRY_CODE) 
values ('KR', 'Korea', 'COUNTRY-0001') 
...
// 두번째  Transaction
insert
 into PUBLIC.MOVIE (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
values ('COUNTRY-0001', 'My Sassy Girl', 'Jaeyong Gwak', 2001-07-27, 'MV-00001') 
insert
 into PUBLIC.MOVIE (COUNTRY_CODE, TITLE, DIRECTOR, RELEASE_DATE, MOVIE_ID) 
values ('COUNTRY-0001', 'My Little Bride', 'Hojun Kim', 2004-04-02, 'MV-00002') 
...        

49.4.3.단건 수정

update() 메소드를 호출하여 DB의 단건 데이터를 수정할 수 있다. update() 메소드 호출시 대상이 되는 Persistence 객체를 입력 인자로 전달해야 한다. 입력 인자로 전달된 객체에는 모든 값이 설정되어 있어야 함에 유의하도록 한다. 속성값이 설정되어 있지 않은 경우 해당 속성값이 null로 저장된다. 기 등록된 Country 정보를 수정하고자 할 경우에는 HibernateBasicCRUD 의 updateCountry() 메소드에서와 같이 호출하면 된다.

public void updateCountry() throws Exception {
    // 1. insert a new country information
    Country country = addCountry();

    // 2. update a country information
    country.setCountryName("Republic of Korea");
    session.update(country);

    ...중략...
}

특정 객체가 Persistent 상태이고, 동일한 Session 내에서 해당 객체의 속성 값에 변경이 발생한 경우 update() 메소드를 직접적으로 호출하지 않아도 트랜잭션 종료 시점에 Hibernate에 의해 변경 여부가 체크되어 변경 사항이 DB에 반영된다.

public void updateCountry() throws Exception {
    // start transaction

    Country country = addCountry();

    country.setCountryName("Republic of Korea");

    // commit. successful update!!!
}

49.4.4.단건 저장 또는 수정

기 등록된 객체에 대해 save() 메소드를 호출한 경우 또는 DB에 존재하지 않는 객체에 대해 update() 메소드를 호출한 경우 addCountryCallingUpdate() 메소드에서처럼 Exception이 발생한다.

public void addCountryCallingUpdate() throws Exception {
    // 1. start a new session and transaction
    newSession();

    // 2. try to insert a country information without movies
    Country country1 = makeNewCountry();
    session.update(country1);

    // 3. close session
    try {
        closeSession();
        fail("expected throw HibernateException");
    } catch (Exception e) {
    ...중략...
    } 

}

두 메소드(save(), update())의 특징을 포함한 saveOrUpdate() 메소드는 해당 객체가 존재하는 경우에는 update()와 같은 역할을 수행하고 존재하지 않을 경우에는 save()를 수행한다. saveOrUpdate() 메소드 호출시 대상이 되는 Persistence 객체를 입력 인자로 전달해야 한다. saveOrUpdate() 메소드는 HibernateSaveOrUpdateParentChild 의 addCountryCallingSaveOrUpdate() 메소드에서와 같이 호출하면 된다.

public void addCountryCallingSaveOrUpdate() throws Exception {
    // 1. try to insert a country information without movies
    newSession();
    Country country1 = makeNewCountry();
    session.saveOrUpdate(country1);

    closeSession();

    ...중략
}

49.4.5.단건 삭제

delete() 메소드를 호출하여 DB의 단건 데이터를 삭제할 수 있다. delete() 메소드 호출시 식별자 값을 포함하고 있는 Persistence 객체를 입력 인자로 전달해야 한다. 기 등록된 Country 정보를 삭제하고자 할 경우에는 HibernateBasicCRUD 의 deleteCountry() 메소드에서와 같이 호출하면 된다.

public void deleteCountry() throws Exception {
    // 1. insert a new country information
    Country country = addCountry();

    // 2. delete a country information
    session.delete(country);

    ...중략
}

49.4.6.복수건 저장

하나의 트랜잭션 내에서 동일한 Persistence 클래스에 대해 복수건의 데이터 저장 또는 수정이 발생할 경우에는 loop을 수행하면서 save(), update() 메소드를 호출해 주도록 한다. 단, 이 때 1st Level Cache, 2nd Level Cache에 Persistent 상태의 객체들이 Caching되면서 memory overflow가 발생할 수 있으므로 로직 구성에 주의가 필요하다.

  • 2nd Level Cache Mode : 해당 메소드 수행시에는 2LC를 적용하지 않도록 Cache Mode를 IGNORE로 설정.

  • Session Flush : memory size를 고려하여 적절한 수의 Persistence 객체에 대한 save()가 이루어진 후에 session.flush() 메소드를 호출하여 DB에 반영할 수 있도록 한다. 한번에 flush할 객체의 수는 hibernate configuration file (hibernate.cfg.xml) 내에 정의한 hibernate.jdbc.batch_size와 동일하게 맞추는 것이 좋다. hibernate.jdbc.batch_size 속성에 대해 알고자 하면, 여기 를 참조하도록 한다.

  • 1st Level Cache Clear : memory size를 고려하여 적절한 수의 Persistence 객체에 대한 save()가 이루어진 후에 1LC에 Caching된 Persistent 상태의 객체들을 지워주도록 한다.

다음은 하나의 트랜잭션 내에서 복수건의 데이터를 저장하는 multiSave()를 포함한 HibernateMultiDataSave 의 일부이다.

public void multiSave() throws Exception {
    session.setCacheMode(CacheMode.IGNORE);

    // insert country
    for (int i = 0; i < 90; i++) {

        Country country = new Country();
        String countryCode = "COUNTRY-000" + i;
        country.setCountryCode(countryCode);
        country.setCountryId("KR" + i);
        country.setCountryName("Korea" + i);

        session.save(country);


        if (i != 0 && i % 9 == 0) {
            session.flush();
            session.clear();
        }
    }
}

49.5.HQL(Hibernate Query Language)

Hibernate은 별도 Query Language를 제공함으로써 객체 지향 관점에서 객체의 속성 또는 Relation 정보를 기반으로 특정 객체에 대한 조회와 DB 유형에 독립적인 Query 정의를 가능하도록 한다. HQL의 구성요소 및 작성 방법은 아래와 같다.

49.5.1.구성 요소

49.5.1.1.[선택] SELECT 절

전달받고자 하는 조회 결과값을 구체적으로 명시하고자 할 경우 정의한다.

SELECT [object 또는  property], …        

여러 건의 데이터를 조회할 경우 조회 결과값을 List, Map 또는 사용자 정의 Type으로 정의 가능하다. (Default = Object[])

SELECT new List(prop1, prop2, …)        

Hibernate에서 제공하는 다양한 aggregate function(sum, avg, min, max, count, count(distinct), count(all)), arithmetic operator(+, -, …), concatenation 그리고 일반 SQL에서 사용 가능한 keyword(distinct, …)들도 정의 가능하다.

이외에도 Hibernate은 문자열, 숫자, 날짜 및 시간 처리를 위한 함수를 제공하며 자세한 사항은 아래와 같다.

  • 문자열 처리를 위한 함수

    함수명설명
    UPPER(str)대문자로 변환한다.
    LOWER(str)소문자로 변환한다.
    SUBSTRING(str, idx, length)문자열의 지정한 idx 위치에서 length만큼의 문자열을 얻어낸다
    CONCAT(str1, str2)두개의 문자열을 연결한다.
    LENGTH(str)문자열의 전체 길이를 구한다.
    LENGTH(str, s, idx)해당 문자열 str에서 정의된 문자열 s가 포함되어 있는 위치를 구한다. 검색 시작 위치는 idx이다.
    TRIM([type] str)문자열의 앞뒤 공백을 삭제한다. (Type이 BOTH일 경우 앞뒤공백 삭제, Type이 LEADING일 경우 앞 공백 삭제, Type이 TRAILING일 경우 뒤 공백 삭제)
  • 숫자 처리를 위한 함수

    함수명설명
    ABS(num)숫자의 절대값을 구한다.
    SQRT(num)숫자의 제곱근을 구한다.
    MOD(num1, num2)num2을 num2로 나눈 나머지값을 구한다.
    BIT_LENGTH(str)문자열의 비트 길이를 구한다.
  • 날짜 및 시간 처리를 위한 함수

    함수명설명
    CURRENT_DATE()현재 날짜를 구한다.
    CURRENT_TIME()현재 시간을 구한다.
    CURRENT_TIMESTAMP()현재 날짜 및 시간을 구한다.
    HOUR(date), MINUTE(date), SECOND(date)시,분,초 값을 구한다.
    YEAR(date), MONTH(date), DAY(date)년,월,일 값을 구한다.

49.5.1.2.[필수] FROM 절

조회 대상 객체를 정의하며, SELECT 절이 생략되었을 경우 FROM 절에 정의된 객체가 전달 대상이 된다.

FROM [object] ((as) alias), …        

49.5.1.3.[선택] WHERE 절

조회 결과 영역을 보다 상세히 구분하고자 할 경우 정의한다.

WHERE [condition], …        

Mapping XML 파일에 정의한 특정 객체의 식별자 값을 추출하기 위해 "id"를 사용할 수 있다. (Hibernate 3.2.2 이상부터 해당 클래스의 식별자 필드가 아닌 다른 필드명이 id일 경우 id라는 이름을 가진 필드의 값을 전달한다)

WHERE user.id = 'test'        

또한 Discriminator 값에 접근하기 위해서는 아래와 같이 "class"를 사용할 수 있다. 이 외에도 Hibernate에서 제공하는 다양한 expression을 활용하여 WHERE 절 정의가 가능하다.

WHERE user.class = 'MEMBER'        

HQL WHERE 절에서 사용 가능한 Operation은 다음과 같은 것들이 있다.

  • 수학연산자 : +, -, *, /

  • 비교연산자 : <>, <, >, <=, <=, !

  • 논리연산자 : and, or, not

  • Grouping : in, not in, between, is null, is not null, is empty, is not empty, member of, not member of

  • Case : case … when … then … else … end

  • 문자열 concatenation : … || …, concat (…,…)

  • 날짜처리 : current_date(), current_time(), current_timestamp(), second(...), minute(…), hour(…), day(…), month(…), year(…)

  • str() : 주어진 값을 문자열로 변환

SELECT 또는 WHERE 절에서 괄호 사이에 Sub Query 형태의 또다른 HQL을 정의할 수 있다.

49.5.1.4.[선택] ORDER BY 절

조회 결과의 정렬 방법을 정의한다.

ORDER BY [condition] (ASC 또는 DESC), …        

49.5.1.5.[선택] GROUP BY 절

조회 결과를 특정 기준으로 그룹핑하고자 할 경우 정의한다.

GROUP BY [condition], …        

※ Order By, Group By 절에 수식 정의는 불가하며, 일반 SQL처럼 Having 절을 추가하는 것은 가능하다.

49.5.2.기본적인 사용 방법

HQL을 이용한 기본적인 CRUD 방법과 Join 방법은 다음과 같다.

49.5.2.1.Case 1. Basic

HQL을 통해 하나의 테이블을 대상으로 조회 작업을 수행할 수 있다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("FROM Country country ");
hqlBuf.append("WHERE country.countryName like :condition ");
hqlBuf.append("ORDER BY country.countryName");
Query hqlQuery = session.createQuery(hqlBuf.toString());
hqlQuery.setParameter("condition", "%%");
List countryList = hqlQuery.list();        

위와 같이 정의된 HQL문을 통해 조회 조건에 맞는 Country 객체의 List가 리턴된다. WHERE절의 조회 조건은 객체명.Attribute명(country.countryName)으로 정의할 수 있으며 ':'을 사용하여 정의된 속성과 값을 전달하여 조회 조건을 완성할 수 있다. 조회 조건의 값은 org.hibernate.Query의 setParameter() 메소드를 통해 지정해 주고 있다.

49.5.2.2.Case 2. Join

HQL을 이용하여 테이블 간의 JOIN을 수행하고자 할 경우 Explicit Join, Implicit Join 으로 처리 가능하다. Hibernate에서는 (inner) join, left (outer) join, right (outer) join, full join을 지원하며, Explicit Join은 FROM 절에 join 키워드를 명시적으로 정의하여 사용하는 방법이다. Implicit Join은 join 키워드를 별도로 사용하지 않고 "." 을 이용하여 HQL 어느 절에서나 정의할 수 있으며, Inner Join으로 처리된다.

다음은 Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Inner Join을 수행한 조회 작업의 예이다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT movie ");
hqlBuf.append("FROM Movie movie join movie.categories category ");
hqlBuf.append("WHERE category.categoryName = ?"); 
Query query = session.createQuery(hqlBuf.toString());
query.setParameter(0, "Romantic");...        

위의 코드와 같이 'join'을 이용해 relation 관계에 놓여있는 MOVIE 테이블과 CATEGORY 테이블을 Inner Join할 수 있으며 기본적인 HQL 사용 때와 마찬가지로 검색 조건을 정의할 수 있다. 또한 Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Right Outer Join을 수행할 수 있다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT distinct category ");
hqlBuf.append("FROM Movie movie right join movie.categories category ");
hqlBuf.append("ORDER BY category.categoryName ASC ");
...        

Inner Join과 마찬가지로 'right join' 또는 'left join'을 사용할 수 있으며 위의 예는 right join을 사용하였다.

두 테이블 간의 Relation 관계가 정의되어 있지 않을시 ','를 통해 두 테이블을 Join 할 수 있으며 WEHER 절에 'movie.country.countryCode = country.countryCode'와 같이 join을 위한 조건문을 정의하여 사용한다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT distinct movie ");
hqlBuf.append("FROM Movie movie, Country country ");
hqlBuf.append("WHERE movie.country.countryCode = country.countryCode ");
hqlBuf.append("AND country.countryId = :condition1 ");
hqlBuf.append("AND movie.title like :condition2 ");

Query query = session.createQuery(hqlBuf.toString());
query.setParameter("condition1", "KR");
query.setParameter("condition2", "%%");
...        

위에서 설명된 코드를 포함하는 HQL을 이용한 기본적인 조회 방법에 대한 예제는 HibernateBasicHQL.java 에서 확인할 수 있다.

49.5.3.원하는 객체 형태로 전달

HQL을 통해 조회 작업을 수행한 후, 조회 작업의 결과를 원하는 객체 형태로 전달받을 수 있다. 이는 여러 테이블을 Join할 경우 한 테이블에 매핑되는 Persistence 클래스가 아닌 composite 클래스로 리턴받고자 할 때 사용할 수 있다.

49.5.3.1.Case 1. 특정 객체 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 HQL(Inner Join)을 이용한 조회 결과를 특정 객체(예에선 Movie 객체)형태로 전달받는다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT new Movie(movie.movieId as movieId, ");
hqlBuf.append("   movie.title as title, movie.director as director, ");
hqlBuf.append("   category.categoryName as categoryName, ");
hqlBuf.append("   movie.country.countryName as countryName) ");
hqlBuf.append("FROM Movie movie join movie.categories category ");
...         

위와 같이 정의할 경우 Movie라는 객체의 형태로 결과값이 리턴되는데 정의된 클래스에 해당 Constructor가 존재해야 함에 유의하도록 한다. 다음은 Movie.java 의 Constructor 정의 부분의 일부이다.

public Movie(String movieId, String title, String director,
                  String categoryName, String countryName) {
         this.movieId = movieId;
         this.title = title;
         this.director = director;
         this.categoryName = categoryName;
         this.countryName = countryName;
}        

또한 리턴된 결과값에서 각각의 attribute에 해당하는 값을 꺼낼 때에는 List에서 각 Movie 객체를 꺼낸 다음 getter 메소드를 사용하도록 한다.

List movieList = query.list();
Movie movie1 = (Movie) movieList.get(0);
movie1.getTitle());
Movie movie2 = (Movie) movieList.get(1);
movie2.getTitle());
...        

49.5.3.2.Case 2. Map 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 HQL(Inner Join)을 이용한 조회 결과를 Map 형태로 전달받는다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT new Map(movie.movieId as movieId, ");
hqlBuf.append("   movie.title as title, movie.director as director, ");
hqlBuf.append("   category.categoryName as categoryName, ");
hqlBuf.append("   movie.country.countryName as countryName) ");
hqlBuf.append("FROM Movie movie join movie.categories category ");
...         

위와 같이 정의할 경우 조회 결과는 Map의 List 형태가 된다. 이때 alias로 정의한 movieId, title, director, categoryName, countryName이 Map의 Key 값이 된다. 따라서 다음과 같이 Map으로 정의된 Key 값을 통해 결과값을 조회할 수 있다.

List movieList = query.list();
Map movie1 = (Map) movieList.get(0);
movie1.get("title");
movie1.get("director");
Map movie2 = (Map) movieList.get(1);
movie2.get("title");
movie2.get("director");        

49.5.3.3.Case 3. List 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 HQL(Inner Join)을 이용한 조회 결과를 List 형태로 전달받을 수 있다.

hqlBuf.append("SELECT new List(movie.movieId as movieId, ");
hqlBuf.append("   movie.title as title, movie.director as director, ");
hqlBuf.append("   category.categoryName as categoryName, ");
hqlBuf.append("   movie.country.countryName as countryName) ");
hqlBuf.append("FROM Movie movie join movie.categories category ");        

위와 같이 정의할 경우 조회 결과는 List의 List 형태가 된다. List에서 결과값을 꺼낼 때에는 정의된 순서에 따르면 된다.

List movieList = query.list();
List movie1 = (List) movieList.get(0);
movie1.get(1); //title
movie1.get(2); //director
List movie2 = (List) movieList.get(1);
movie2.get(1); //title
movie2.get(2); //director        

위에서 설명된 HQL을 이용하여 결과값을 특정 객체로 전달받는 전체 테스트 코드는 HibernateHQLWithDefinedResult.java 에서 확인할 수 있다.

49.5.4.XML에 HQL 정의하여 사용

HQL을 별도 Hibernate Mapping XML 파일 내에 정의하고 정의된 HQL문의 name을 입력하여 실행시킬 수 있다. 이는 HQL이 변경될 경우 소스 코드 변경없이 XML문에 정의된 HQL만 변경함으로써 소스 코드 재컴파일이 불필요하며 HQL문만을 따로 관리 할 수 있도록 한다.

Query hqlQuery = session.getNamedQuery("findCountryList");
hqlQuery.setParameter("condition", "%%");
List countryList = hqlQuery.list();       

위와 같이 org.hibernate.Session의 getNamedQuery() 메소드에 query name을 넘겨주면 Hibernate은 이 이름에 맞는 HQL문을 XML에서 찾아서 실행하게 된다. 다음은 HQL이 정의되어 있는 Country.hbm.xml 의 일부이다.

<query name="findCountryList">
    <![CDATA[	
    FROM Country country
    WHERE country.countryName like :condition 
    ORDER BY country.countryName
    ]]>
</query>

HQL의 작성 방법은 앞서 설명한 방법과 동일하며 위에서 설명한 테스트 코드는 HibernateNamedHQL.java 에서 확인할 수 있다.

49.5.5.Pagination

Pagination은 한 페이지에 보여줘야 할 조회 목록에 제한을 둠으로써 DB 또는 어플리케이션 메모리의 부하를 감소시키고자 하는데 목적이 있다. HQL 수행시 페이징 처리된 조회 결과를 얻기 위한 방법에 대해 알아보도록 한다. 특정 테이블을 대상으로(예에서는 MOVIE 테이블) HQL을 이용한 조회 작업을 수행한다. 이때, 조회를 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리가 가능해진다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("FROM Movie movie ");
Query hqlQuery = session.createQuery(hqlBuf.toString());
// 첫번째로 조회해야 할 항목의 번호
hqlQuery.setFirstResult(1);
// 조회 항목의 전체 개수
hqlQuery.setMaxResults(2);
List movieList = hqlQuery.list();        

위와 같이 정의할 경우 HQL에서는 Hibernate Configuration 파일(hibernate.cfg.xml)에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞는 SQL을 생성한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야 할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다. 다음은 hibernate.dialect를 HSQL DB로 정의하였을 때 페이징 처리가 되어 수행된 쿼리문이다.

select limit 1 2 movie0_.MOVIE_ID as MOVIE1_3_, movie0_.COUNTRY_CODE as COUNTRY2_3_, 
movie0_.TITLE as TITLE3_, movie0_.DIRECTOR as DIRECTOR3_, 
movie0_.RELEASE_DATE as RELEASE5_3_ from PUBLIC.MOVIE movie0_

위의 코드에서 정의한 것처럼 첫번째로 조회해야 할 항목의 번호를 1, 조회 항목의 전체 개수를 2로 정의하였으므로 Hibernate에서는 HSQL DB의 특성에 맞게 'limit 1 2'가 추가된 SQL을 실행하여 페이징 처리를 수행하였다. 또한 아래의 코드와 같이 ResultSet 내에서 앞,뒤로 이동할 수 있는 ScrollableResults를 얻어 코드 내에서 직접 페이징 처리를 수행하는 것도 가능하다. (단, 해당 JDBC 드라이버가 Scroll 가능한 ResultSet을 지원하는 경우에만 가능)

Query query = session.createQuery(“from Users as user”);
ScrollableResults userList = query.scroll();        

위에서 사용된 org.hibernate.Query 클래스는 데이터 조회를 위한 3가지 메소드를 제공한다.

  • list() : DB 테이블로부터 모든 데이터를 한번에 로딩한다.

  • iterate() : 식별자 값만을 로딩한 뒤, 데이터가 실제로 필요한 시점에 데이터를 로딩한다. 이는 캐쉬를 사용하기 위함으로 iterator() 메소드가 전달한 Iterator 객체의 next() 메소드는 캐쉬에 동일한 식별값을 갖는 객체가 존재하는지 체크하여 해당 객체가 존재하면 객체를, 존재하지 않으면 Proxy 객체를 리턴한다.

  • scroll() : Cursor를 이용하여 데이터를 로딩한다.

위와 같이 HQL을 이용한 Page 처리 방법에 대한 코드는 HibernateHQLPaging.java 파일을 참고한다.

49.5.6.HQL을 이용한 CUD

기본적으로 Hibernate을 이용한 CUD(Create, Update, Delete)를 할 때에는 Hibernate에서 제공하는 기본 API를 사용하게 된다. (Hibernate Basic CRUD 참고) 그러나 특이한 경우 HQL을 통해 기본 CUD를 수행해야 하는 경우가 발생할 수 있다. (ex> 특정 한 컬럼에 대한 Update) 이를 위해 HQL을 이용한 기본적인 CUD 방법에 대해 알아보도록 하자.

49.5.6.1.등록 (Insert)

다음은 HQL을 사용한 Insert문의 예이다.

StringBuffer hql = new StringBuffer();
hql.append("INSERT INTO Country (countryCode,countryId,countryName) ");
hql.append("SELECT CONCAT(countryCode,'UPD'), CONCAT(countryId,'UPD'), countryName ");
hql.append("FROM Country country ");
hql.append("WHERE countryCode = :countryCode");
Query query = session.createQuery(hql.toString());
query.setParameter("countryCode", "COUNTRY-0001");

query.executeUpdate();
closeSession();

위와 같이 작성할 경우 HQL을 이용하여 신규 Country 정보를 등록할 수 있다. 단, Hibernate에서는 INSERT INTO ... VALUES 형태의 INSERT문은 지원되지 않으며, INSERT INTO ... SELECT 형태의 INSERT문만 지원됨에 유의하도록 한다.

49.5.6.2.수정 (Update)

다음은 HQL을 사용한 Update문의 예이다.

newSession();
StringBuffer hql = new StringBuffer();
hql.append("UPDATE Country country ");
hql.append("SET country.countryName = :countryName ");
hql.append("WHERE country.countryCode = :countryCode and country.countryId = :countryId ");

Query query = session.createQuery(hql.toString());
query.setParameter("countryName", "Republic of Korea");
query.setParameter("countryCode", "COUNTRY-0001");
query.setParameter("countryId", "KR");

query.executeUpdate();
closeSession();        

위의 예는 HQL을 사용하여 Country 정보를 수정한 것이며 Query의 setParameter() 메소드를 통해 인자값을 셋팅하고 있다.

49.5.6.3.삭제 (Delete)

다음은 HQL을 사용한 Delete문의 예이다.

newSession();
StringBuffer hql = new StringBuffer();
hql.append("DELETE Country country ");
hql.append("WHERE country.countryCode = :countryCode ");

Query query = session.createQuery(hql.toString());
query.setParameter("countryCode", "COUNTRY-0001");

query.executeUpdate();
closeSession();        

또한 위에서 언급된 HQL을 이용한 CUD를 위한 코드는 HibernateCUDHQL.java 에서 확인할 수 있다.

49.6.Criteria Queries

Hibernate에서는 HQL에 익숙하지 못하거나 HQL 작성시 발생할 수 있는 오타로 인한 오류를 최소화 하기 위해 org.hibernate.Criteria API를 사용할 수 있도록 한다. Criteria API 호출을 통해 특정 객체에 대한 조회가 가능하고 org.hibernate.criterion.Restrictions API 호출을 통해 WHERE문에 해당하는 기본 조회 조건을 정의할 수 있다.

49.6.1.기본적인 사용 방법

Hibernate Criteria를 이용하여 특정 객체 정보에 대해 조회할 수 있다.

49.6.1.1.Case 1. Basic

다음은 하나의 테이블을 대상으로 Criteria를 이용하여 조회를 수행하는 예이다.

Criteria criteria = session.createCriteria(Country.class);
criteria.add(Restrictions.like("countryName", "", MatchMode.ANYWHERE));
criteria.addOrder(Order.asc("countryName"));
List countryList = criteria.list();

대상이 되는 테이블과 매핑되는 클래스로 Criteria를 생성하고 Restriction API를 호출해 WHERE조건에 해당하는 조건절을 정의할 수 있다. 위와 같이 정의 한 경우 WHERE Country.countryName like '%%'와 같은 조건절이 생성된다. 또한 addOrder()를 통해 order by절을 정의할 수 있다. 이와 같이 Criteria API를 이용할 경우 메소드를 통해 검색 조건을 정의하기 때문에 오타로 인한 오류를 최소화할 수 있게 된다. 조회 조건을 정의하기 위한 org.hibernate.criterion.Restrictions 는 eq, gt, ge, isNull 등을 비롯하여 다양한 API를 제공하고 있다. 보다 자세한 내용을 알기 위해서는 여기 를 참고하도록 한다.

49.6.1.2.Case 2. Join

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Hibernate Criteria(Inner Join)를 이용한 조회 작업을 수행할 수 있다.

Criteria movieCriteria = session.createCriteria(Movie.class);
Criteria categoryCriteria = movieCriteria.createCriteria("categories");
categoryCriteria.add(Restrictions.eq("categoryName", "Romantic"));
List movieList = movieCriteria.list();        

위 코드에서는 Movie 클래스와 Relation 관계에 놓인 Category를 Join하기 위해 각 Movie 객체에 해당하는 Criteria에 Category 객체에 해당하는 Criteria를 생성하고 있다. 여기서는 Restrictions API를 사용하여 categoryName = 'Romantic'인 결과값을 찾게될 것이다.

또한, Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Hibernate Criteria(Left Outer Join)을 이용한 조회 작업을 수행할 수 있다.

Criteria categoryCriteria = session.createCriteria(Category.class);
Criteria movieCriteria = categoryCriteria.createCriteria("movies",
                            CriteriaSpecification.LEFT_JOIN);
categoryCriteria.addOrder(Order.asc("categoryName"));
categoryCriteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

List categoryList = categoryCriteria.list();

Relation 관계에 있는 테이블의 Criteria를 생성할 때 CriteriaSpecification을 통해 LEFT_JOIN, RIGHT_JOIN 등을 명시할수 있다. 또한 Criteria.DISTINCT_ROOT_ENTITY를 사용하면 List에 중복 포함된 루트 개체를 제거할 수 있다. 위에서 설명된 코드들은 HibernateBasicCriteria.java 에서 확인할 수 있다.

49.6.2.원하는 객체 형태로 전달

Criteria의 setResultTransformer 메소드를 사용하여 Criteria를 이용한 조회 결과를 별도 정의한 객체 형태로 전달받을 수 있다.

49.6.2.1.Case 1. 특정 객체 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Criteria를 이용한 조회 결과를 특정 객체인 Movie 객체 형태로 전달받을 수 있다.

Criteria movieCriteria = session.createCriteria(Movie.class);
ProjectionList projectionList = Projections.projectionList();
projectionList.add(Projections.id().as("movieId"));
projectionList.add(Projections.property("title").as("title"));
projectionList.add(Projections.property("director").as("director"));
movieCriteria.setProjection(projectionList);
movieCriteria.setResultTransformer(new AliasToBeanResultTransformer(Movie.class));

Criteria categoryCriteria = movieCriteria.createCriteria("categories", "category");
Criteria countryCriteria = movieCriteria.createCriteria("country", "country");
categoryCriteria.add(Restrictions.eq("categoryName", "Romantic"));
countryCriteria.add(Restrictions.like("countryName", "", MatchMode.ANYWHERE));

List movieList = movieCriteria.list();

ProjectionList에 SELECT 절을 구성할 조회 대상 attribute들을 추가시키고 as() 메소드를 이용하여 각각의 attribute에 대한 alias를 정의할 수 있다. AliasToBeanResultTransformer 클래스를 사용하여 조회 결과의 형태를 Movie 클래스로 지정해준다. 따라서 위에서 정의한 Criteria 수행 결과는 Movie 객체의 List 형태가 될 것이다.

Movie movie1 = (Movie) movieList.get(0);
movie1.getTitle();
movie1.getDirector();

49.6.2.2.Case 2. Map 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Criteria를 이용한 조회 결과를 Map 형태로 전달받을 수 있다.

Criteria movieCriteria = session.createCriteria(Movie.class);
ProjectionList projectionList = Projections.projectionList();
projectionList.add(Projections.id().as("movieId"));
projectionList.add(Projections.property("title").as("title"));
projectionList.add(Projections.property("director").as("director"));
movieCriteria.setProjection(projectionList);
movieCriteria.setResultTransformer(Criteria.ALIAS_TO_ENTITY_MAP);

Criteria categoryCriteria = movieCriteria.createCriteria("categories","category");
Criteria countryCriteria = movieCriteria.createCriteria("country","country");
categoryCriteria.add(Restrictions.eq("categoryName", "Romantic"));
countryCriteria.add(Restrictions.like("countryName", "",MatchMode.ANYWHERE));

List movieList = movieCriteria.list();

위에서 생성한 Criteria의 resultTransformer를 ALIAS_TO_ENTITY_MAP으로 지정하여 Map 형태의 결과값으로 전달 받을 수 있다. 이 때 조회 결과는 Map의 List 형태이며, alias로 정의한 movieId, title, director 등이 Map의 Key 값이 된다. 따라서 다음과 같이 Map의 Key 값을 통해 다음과 같이 결과값을 알아낼 수 있다.

Map movie1 = (Map) movieList.get(0);
movie1.get("title");
movie1.get("director");        

위에서 언급된 코드는 HibernateCriteriaWithDefinedResult.java 에서 확인할 수 있다.

49.6.3.Pagination

Criteria를 이용하여 객체 조회시 페이징 처리된 결과를 얻기 위한 방법에 대해 알아본다. HQL을 사용한 Pagination과 마찬가지로 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리를 할 수 있다. 사용 예는 다음과 같다.

Criteria criteria = session.createCriteria(Movie.class);
criteria.setFirstResult(1);
criteria.setMaxResults(2);
List movieList = criteria.list();

위와 같이 정의할 경우 Hibernate Configuration 파일(hibernate.cfg.xml)에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞는 SQL을 생성한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다. 다음은 hibernate.dialect를 HSQL DB로 정의하였을 때 페이징 처리가 되어 수행된 쿼리문이다.

select limit 1 2 this_.MOVIE_ID as MOVIE1_3_0_, this_.COUNTRY_CODE as COUNTRY2_3_0_,
    this_.TITLE as TITLE3_0_, this_.DIRECTOR as DIRECTOR3_0_, 
    this_.RELEASE_DATE as RELEASE5_3_0_ from PUBLIC.MOVIE this_ 

위의 코드에서 정의한 것처럼 첫번째로 조회해야 할 항목의 번호를 1, 조회 항목의 전체 개수를 2로 정의하였으므로 Hibernate에서는 HSQL DB의 특성에 맞게 'limit 1 2'가 추가된 SQL을 실행하여 페이징 처리를 수행하였다. 위의 코드는 HibernateCriteriaPaging.java 에서 확인할 수 있다.

49.7.Native SQL

Hibernate에서는 기본적으로 CRUD 작업을 할 때 Hibernate 기본 API를 사용하거나 Criteria를 사용하여 수행한다. 그러나 특정 DBMS에서 제공하는 기능을 사용할 수 있도록 하기 위해 Hibernate은 Native SQL 사용을 지원한다.

49.7.1.기본적인 사용 방법

session.createSQLQuery() 메소드를 이용하여 Native SQL을 실행할 수 있다.

49.7.1.1.Case 1. Basic

하나의 테이블을 대상으로 Native SQL을 이용한 조회 작업을 수행할 수 있다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT * ");
hqlBuf.append("FROM COUNTRY ");
hqlBuf.append("WHERE COUNTRY_NAME like :condition ");
hqlBuf.append("ORDER BY COUNTRY_NAME");

SQLQuery query = session.createSQLQuery(hqlBuf.toString());
query.addEntity(Country.class);
query.setParameter("condition", "%%");
List countryList = query.list();

session.createSQLQuery()를 사용하여 정의된 SQL문을 실행한다. 또한, 조회 결과값을 특정 Persistence 객체로 전달받고자 하는 경우 SQLQuery.addEntity()를 통해 특정 타입의 객체를 정의하여 사용한다.

49.7.1.2.Case 2. Join

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Native SQL(Inner Join)을 이용한 조회 작업을 수행할 수 있다.

hqlBuf.append("SELECT movie.* ");
hqlBuf.append("FROM MOVIE movie ");
hqlBuf.append("join MOVIE_CATEGORY moviecategory on movie.MOVIE_ID = moviecategory.MOVIE_ID ");
hqlBuf.append("join CATEGORY category on moviecategory.CATEGORY_ID = category.CATEGORY_ID ");
hqlBuf.append("WHERE category.CATEGORY_NAME = ?");
SQLQuery query = session.createSQLQuery(hqlBuf.toString());
query.addEntity(Movie.class);
query.setParameter(0, "Romantic");
List movieList = query.list();        

위의 코드와 같이 join 키워드를 사용하여 Inner Join을 수행할 수 있다.

또한, Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Native SQL(Right Outer Join)을 이용한 조회 작업을 수행할 수 있다. 작성 방법은 아래와 같다.

hqlBuf.append("SELECT distinct category.* ");
hqlBuf.append("FROM MOVIE movie ");
hqlBuf.append("right join MOVIE_CATEGORY moviecategory on movie.MOVIE_ID=moviecategory.MOVIE_ID ");
hqlBuf.append("right join CATEGORY category on moviecategory.CATEGORY_ID=category.CATEGORY_ID ");
hqlBuf.append("ORDER BY category.CATEGORY_NAME ASC ");
SQLQuery query = session.createSQLQuery(hqlBuf.toString());
 query.addEntity(Category.class); 
List categoryList = query.list();        

또한 Join하여 조회한 결과를 각각의 Join된 객체의 값으로 select 하기 위해서는 addJoin 메소드를 사용한다.

hqlBuf.append("SELECT distinct movie.*, country.* ");
hqlBuf.append("FROM MOVIE movie, COUNTRY country ");
hqlBuf.append("WHERE movie.COUNTRY_CODE = country.COUNTRY_CODE ");
hqlBuf.append("AND country.COUNTRY_ID = :condition1 ");
hqlBuf.append("AND movie.TITLE like :condition2 ");

SQLQuery query = session.createSQLQuery(hqlBuf.toString());
query.addEntity("movie", Movie.class);
query.addJoin("country", "movie.country");
query.setParameter("condition1", "KR");
query.setParameter("condition2", "%%");
List movieList = query.list();        

Movie와 Country 정보를 한꺼번에 조회하기 위해 위 Select문의 Movie 객체의 alias와 같은 'movie'를 이용하여 Movie 클래스를 addEntity()의 입력 인자로 정의한 다음 Join 대상이 되는 Country 또한 addJoin() 메소드에 정의해주어야 한다. 위의 코드에서는 Country 객체의 alias인 'country'와 이 객체의 값인 movie.country를 입력 인자로 지정해주었다. (movie.country의 movie는 addEntity() 메소드를 통해 정의된 Entity의 Key 값이다.) 이때 리턴되는 List는 Object Array 배열이 되며 Object Array에는 아래와 같이 movie와 country가 차례대로 저장되게 된다.

language="java">Object[] results1 = (Object[]) movieList.get(0);
Movie movie1 = (Movie)results1[0];
Country country1 = (Country)results1[1];

49.7.1.3.Case 3. 검색 조건 명시

두 개의 테이블을 대상으로 검색 조건을 별도 명시한 Native SQL을 이용하여 조회 작업을 수행할 수 있다.

hqlBuf.append("SELECT distinct movie.* ");
hqlBuf.append("FROM MOVIE movie, COUNTRY country ");
hqlBuf.append("WHERE movie.COUNTRY_CODE = country.COUNTRY_CODE ");
hqlBuf.append("AND country.COUNTRY_ID = :condition1 ");
hqlBuf.append("AND movie.TITLE like :condition2 ");

SQLQuery query = session.createSQLQuery(hqlBuf.toString());
query.addEntity(Movie.class); 
query.setParameter("condition1", "KR");
query.setParameter("condition2", "%%");
List movieList = query.list();

":"(Named Parameter 형태)으로 조건을 명시할 수 있으며 해당 조건의 값은 setParameter()를 통해 셋팅해 줄 수 있다. 위에서 설명한 기본적인 Native SQL 사용 코드는 HibernateNativeSQL.java 에서 확인할 수 있다.

49.7.2.XML에 Native SQL 정의하여 사용

Native SQL을 별도 Hibernate Mapping XML 파일 내에 정의하고 정의된 Native SQL문의 name을 입력하여 실행시킬 수 있다. 이는 Native SQL이 변경될 경우 소스 코드 변경없이 XML문에 정의된 HQL을 변경함으로써 재컴파일이 불필요하며 Native SQL문만을 따로 관리할 수 있도록 한다. org.hibernate.Session의 getNamedQuery() 메소드를 사용하면 Native SQL문의 name으로 정의된 Native SQL을 수행한다.

Query query = session.getNamedQuery("nativeFindCountryList");
query.setParameter("condition", "%%");
List countryList = query.list();

다음은 Native SQL이 정의되어 있는 Country.hbm.xml 의 일부이다.

<sql-query name="nativeFindCountryList">
	<return alias="country" class="anyframe.sample.model.bidirection.Country"/>
	<![CDATA[	
	SELECT * 
	FROM COUNTRY country
	WHERE country.COUNTRY_NAME like :condition 
	ORDER BY country.COUNTRY_NAME
	]]>
</sql-query>

Native SQL 작성을 위해 해당 XML에는 <sql-query> 태그를 사용하여 작성한다. 위에서 설명한 테스트 코드는 HibernateNamedNativeSQL.java 에서 확인할 수 있다.

49.7.3.Pagination

Pagination은 한 페이지에 보여줘야 할 조회 목록에 제한을 둠으로써 DB 또는 어플리케이션 메모리의 부하를 감소시키고자 하는데 목적이 있다. Native SQL 수행시 페이징 처리된 조회 결과를 얻기 위한 방법에 대해 알아보도록 한다. 특정 테이블을 대상으로(예에서는 MOVIE 테이블) Native SQL을 이용한 조회 작업을 수행한다. 이때, 조회를 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리가 가능해진다.

hqlBuf.append("SELECT * ");
hqlBuf.append("FROM MOVIE ");
SQLQuery query = session.createSQLQuery(hqlBuf.toString());
query.addEntity(Movie.class);
query.setFirstResult(1);
query.setMaxResults(2);
List movieList = query.list();

위와 같이 정의할 경우 Hibernate Configuration 파일에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞게 변경된 SQL이 수행한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다. 다음은 hibernate.dialect를 HSQL DB로 정의하였을 때 페이징 처리가 되어 수행된 쿼리문이다.

SELECT limit 1 2 * FROM MOVIE

위의 코드에서 정의한 것처럼 첫번째로 조회해야 할 항목의 번호를 1, 조회 항목의 전체 개수를 2로 정의하였으므로 Hibernate에서는 HSQL DB의 특성에 맞게 'limit 1 2'가 추가된 SQL을 실행하여 페이징 처리를 수행하였다. 위의 코드는 HibernateNativeSQLPaging.java 에서 확인할 수 있다.

49.7.4.Callable Statement

Hibernate를 이용하여 DB에 기 등록된 Procedure 또는 Function을 실행시킬 수 있다.

49.7.4.1.Case 1. XML에 정의한 Procedure 호출

Mapping XML 파일에 정의한 Procedure를 호출하여 결과값을 확인할 수 있다.

Query query = session.getNamedQuery("callFindCategoryList");
query.setParameter("condition", "%%");
List categoryList = query.list();

위 코드에서는 session.getNameQuery()를 호출하여 Mapping XML에 정의된 'callFindCategoryList'라는 이름의 query를 찾는다. 다음은 'callFindCategoryList'가 정의되어 있는 Category.hbm.xml 파일의 일부이다.

<sql-query name="callFindCategoryList" callable="true">
    <return alias="category" class="anyframe.sample.model.bidirection.Category"/>
    <![CDATA[	
        { call FIND_CATEGORY_LIST (?, :condition) }
    ]]>
</sql-query>

위의 코드에서는 해당 DB에 기 정의되어있는 FIND_CATEGORY_LIST 라는 Procedure를 호출하게 된다.

49.7.4.2.Case 2. Function을 이용한 HQL 실행

해당 DB에 생성한 Function을 이용하여 HQL을 실행하고 결과를 확인할 수 있다.

hqlBuf.append("FROM Movie movie ");
hqlBuf.append("WHERE movie.releaseDate > FIND_MOVIE(:condition)");
Query query = session.createQuery(hqlBuf.toString());
query.setParameter("condition", "MV-00002");
List movieList = query.list();

위 코드에서는 'FIND_MOVIE'라는 Function의 호출 결과를 이용하여 HQL을 수행하고 있다. 보다 자세한 코드는 HibernateProcedure.java 에서 확인한다.

49.8.Performance Strategy

Hibnernate은 성능 개선을 위해 Cache와 Fetch등의 Performance Strategy를 제공한다. 크게 Cache는 1 Level Cache와 2 Level Cache 등으로 구분되며 이는 매번 DB에 접근 없이 해당 Cache를 이용하여 객체를 조회 또는 보관할 수 있도록 한다. 또한 여러가지 Fetch 전략을 적절히 적용함으로써 Lazy Loading으로 발생할 수 있는 N+1 SELECT 이슈를 처리할 수 있다.

49.8.1.Cache

Hibernate을 사용하면 입력 인자로 전달된 객체를 정의된 테이블로 매핑시켜 데이터 액세스 처리를 수행해야 하는데 Hibernate에서는 이로 인해 발생 가능한 성능 이슈를 개선하기 위해 Cache를 활용한다. 특히, 어플리케이션의 조회 기능이 전체 실행 시간의 많은 비중을 차지하는 경우 매번 DB에 접근하지 않고 Cache에 저장된 객체를 사용함으로써 성능을 향상시킬 수 있게 되는 것이다.

49.8.1.1.1LC (1 Level Cache)

Hibernate Session 내부에 정의된 Cache로, Session의 시작과 종료 사이에서 사용되며 한 Session 내에서 Hibernate을 통해 읽혀진 객체들을 보관하는 역할을 수행한다. Hibernate은 하나의 Session 내에서 동일한 객체를 한 번 이상 Loading할 경우 2번째부터는 1LC로부터 해당 객체를 추출하고 또한, 한 Session 범위 내에서 객체의 속성 변경시 변경 사항은 Session 종료시에 자동적으로 DB에 반영하도록 한다. 즉, 하나의 Hibernate Session 내에서 동일한 객체에 대한 재조회가 이루어지는 경우 1LC를 이용함으로써 DB 접근 횟수를 줄여주기 때문에 어플리케이션 성능 향상에 도움이 되는 것이다. 1LC는 Hibernate에서 기본적으로 제공하는 Cache이므로 별도의 설정없이도 적용된다.

public void findMovie() throws Exception {
    newSession();
    // Add data to DB
    SetUpInitData.initializeData(session);	
    // 2. find a movie without accessing DB (using 1LC)
    /* #1 */ Movie movie = (Movie) session.get(Movie.class, "MV-00001");
	
    Set categories = movie.getCategories();
    categories.iterator();
	
    // 3. find a movie again without accessing DB (using 1LC)
    movie = (Movie) session.get(Movie.class, "MV-00001");
	
    categories = movie.getCategories();
    categories.iterator();
    closeSession();
}

위와 같이 작성할 경우 동일한 Session 내에서 SetUpInitData.initializeData(session)를 통해 save된 Persistence 객체는 1LC에 저장되므로 다음에 #1번 코드에서처럼 동일한 Persistence 객체 조회시 DB에 재접근하지 않고도, Cache를 통해 조회된다. findMovie() 메소드를 포함한 HibernateFirstLevelCache.java 테스트 소스를 DEBUG 모드로 실행시켜서 실행되는 쿼리를 콘솔창을 통해 확인해 보면 이를 확인할 수 있을 것이다. SetUpInitData.java에 대한 내용은 여기 에서 확인할 수 있다.

49.8.1.2.2LC (2 Level Cache)

2LC는 어플리케이션 단위의 Cache로, 어플리케이션 관점에서의 Cache 기능을 지원한다. 이는 여러 트랜잭션들을 통해 Load된 Persistence 객체를 Session Factory 레벨에서 저장하는 방법으로 처리된다.

hibernate.cache.use_second_level_cache, hibernate.cache.provider_class 등을 정의 하고, 2LC에 저장되어야 할 Persistence Class 매핑 파일의 <cache> 속성을 정의하면 해당 어플리케이션을 구성하는 특정 Persistence 객체들에 대해 2LC를 적용할 수 있다.

다음은 2LC에 대한 속성이 정의되어 있는 hibernate.cfg.xml 파일의 일부이다.

<property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
<property name="hibernate.cache.use_second_level_cache">true</property>

다음은 cache 속성이 read-write로 설정되어 있는 Persistence Class 매핑 파일 Country.hbm.xml 의 일부이다.

<class name="anyframe.sample.model.bidirection.Country" table="COUNTRY" lazy="true" schema="PUBLIC">
    <cache usage="read-write"/>
    <id name="countryCode" type="string">
        <column name="COUNTRY_CODE" length="12" />
    <generator class="assigned" />
    </id>
    <property name="countryId" type="string">
        <column name="COUNTRY_ID" length="2" not-null="true" />
    </property>
	...
 </class>

cache의 속성은 위에서 언급한 read-write외에도 다음과 같은 속성값으로 정의할 수 있다.

  • read-only : Persistence 객체가 변경되지 않는 경우에 사용 가능하다. 수정이 없으므로 분산 환경에서도 안전하게 사용 가능하며 가장 빠른 성능을 제공한다.

  • read-write : Persistence 객체가 변경되는 경우에 사용 가능하다. DBMS의 read-committed와 동일하게 동시 접근을 관리한다.

  • nonstrict-read-write : 트랜잭션 격리를 엄격히 적용할 필요가 없는 경우 사용 가능하다.

  • transactional : 완전한 트랜잭션을 보장하나 가장 느린 성능을 제공한다. JTA 환경 내에서만 사용된다.

위와 같은 설정을 기반으로 HibernateSecondLevelCache.java findCountry() 메소드를 실행해보면 다음의 #1번 코드에 의해 새로운 Session이 시작되었음에도 #2번 코드에서 DB에 접근하지 않고 이전 Session에서 Cache에 저장한 값을 가지고 사용한다는 것을 확인할 수 있다.

public void findCountry() throws Exception {
    newSession();
    SetUpInitData.initializeData(session);
    closeSession();
	
    // 2. find a movie without accessing DB (using 2LC)
    /* #1 */ newSession();
    /* #2 */ Country country = (Country) session.get(Country.class, "COUNTRY-0001");
	
    Set movies = country.getMovies();
    movies.iterator();
    closeSession();
	
    // 3. find a movie again without accessing DB (using 2LC)
    newSession();
    country = (Country) session.get(Country.class, "COUNTRY-0001");
	
    movies = country.getMovies();
    movies.iterator();
    closeSession();
}

DEBUG 모드에서 테스트케이스를 실행시켜보면서 DB에 접근하지 않고도 2LC를 통해 객체가 조회되는 것을 살펴볼 것을 권장한다. HibernateSecondLevelCache.java 의 findMovie()는 2LC 사용하지 않는 Persistence Class인 Movie에 대한 테스트로써 앞서 언급한 findCountry()와 달리 Session이 다를 경우 매번 DB에 접근하여 해당 Persistence 객체를 조회해오는 것을 알 수 있다.

단, 2LC를 적용하고자 할 경우 해당 어플리케이션을 통하지 않고, 외부에서 직접적으로 DB 정보가 수정될 가능성이 있다면 데이터의 동기화를 위해 세밀한 Cache 속성 제어가 필요함에 유의하도록 한다.

49.8.1.3.분산 Cache

하나의 어플리케이션을 대상으로 하는 경우 앞서 언급한 2LC를 사용하는데 문제가 없으나, 일반적인 Clustered 환경에서 실행된 여러 개의 어플리케이션에 속한 2LC 사이의 데이터 동기화는 중요한 사항이 될 것이다. 이를 위해 Hibernate는 분산 Cache를 지원하는 구현체를 통해 분산 어플리케이션에 대한 Cache 기능을 지원한다.

다음에서는 분산 Cache를 지원하는 구현체별로 설정 방법 및 실행 결과에 대해 살펴보기로 하자.

OSCacheProvider 이용

OSCache 2.0부터 분산 Cache를 지원한다. 현재 OSCache는 분산된 Cache들이 Caching하고 있는 데이터 동기화를 위해 JavaGroups 또는 JMS를 통해 Event를 처리할 수 있도록 구현체를 제공한다. 단, 분산 Cache 사이에서 flush Event 발생시(Caching된 객체를 Cache에서 지울때)에만 Message를 broadcast하는 기능이 지원된다.

본 페이지에서는 OSCacheProvider와 JMS 기능을 제공하는 오픈소스 ActiveMQ를 사용하여 분산 Cache를 관리하는 방법에 대해 알아볼 것이다. 다음은 Hibernate Configuration 파일로, hibernate.cache.provider_class 속성값으로 OSCacheProvider를 지정하고 있음을 알 수 있다.

language="xml"><session-factory>
    ...중략    	
    <property name="hibernate.format_sql">true</property>
    <property name="hbm2ddl.auto">create</property>
    <property name="hibernate.cache.use_second_level_cache">true</property>
    <property name="hibernate.cache.provider_class">
        com.opensymphony.oscache.hibernate.OSCacheProvider</property>
    <property name="hibernate.cache.region_prefix">
        hibernate.cache</property> 
    <property name="com.opensymphony.oscache.configurationResourceName">
        oscache-hibernate.properties</property>
    ...중략
</session-factory>				

또한, Hibernate Cache 영역에 대해 hibernate.cache.region_prefix 를 별도로 지정하였다. (hibernate.cache.region_prefix가 위와 같이 정의된 경우, Persistence Class인 anyframe.sample.model.bidirection.Country는 해당 2LC의 hibernate.cache.anyframe.sample.model.bidirection.Country 영역에 객체가 Caching된다.) 끝으로, com.opensymphony.oscache.configurationResourceName 속성에 OSCacheProvider가 분산 Cache들 사이의 데이터 동기화를 위해 필요로 하는 모든 속성 정보를 정의해 주어야 한다.

위에서 com.opensymphony.oscache.configurationResourceName의 속성값으로 정의한 oscache-hibernate.properties 파일 내용은 다음과 같다.

cache.event.listeners=anyframe.core.cache.impl.JMSBroadcastingListener
cache.cluster.jndi.config=jndi.properties

cache.cluster.jms.topic.factory=TopicConnectionFactory
cache.cluster.jms.topic.name=dynamicTopics/topic
cache.cluster.jms.node.name=node1

각 속성은 다음과 같은 의미를 지닌다.

  • cache.event.listeners : 한 Cache에 변경 사항이 발생한 경우 분산 Cache간 동기화를 위해 Event 처리가 필요하며, OSCache에서는 JMS를 통해 Event를 처리하기 위해 기본적으로 com.opensymphony.oscache.plugins.clustersupport.JMSBroadcastingListener를 제공한다. 그러나 이것은 앞서 언급했듯이 flush Event 발생시에만 Message를 broadcast하는 기능만 지원되므로 Caching된 객체에 대해 수정이 발생한 경우에는 Message Broadcasting되지 않는 취약점이 있다. 따라서, Anyframe에서는 이를 보완한 별도 Cache Event Listener 클래스(anyframe.core.cache.impl.JMSBroadcastingListener)를 제공 하고 있다. Anyframe의 JMSBroadcastingListener는 특정 어플리케이션을 통해 Caching된 객체에 수정이 발생한 경우 Clustering된 모든 어플리케이션의 Cache에서 해당 객체를 지우도록 Event를 보낸다.

  • cache.cluster.jndi.config : Cache Event Listener에서 JMS Server에 접근하기 위해 필요한 환경 정보를 정의하기 위한 파일이다. JMS Server의 InitialContextFactory 클래스를 정의하기 위한 java.naming.factory.initial 와 Provider URL 정의를 위한 java.naming.provider.url 를 정의해준다. 다음은 jndi.properties 파일 내용이다.

    java.naming.factory.initial=org.apache.activemq.jndi.ActiveMQInitialContextFactory
    java.naming.provider.url=tcp://localhost:61616					

  • cache.cluster.jms.topic.factory : JMS topic connection factory에 접근하기 위한 JNDI명을 정의한다. ActiveMQ의 경우, TopicConnectionFactory와 같이 정의하면 Topic을 사용하여 Messaging 처리를 수행하게 된다.

  • cache.cluster.jms.topic.name : OSCache에서 Message를 보내기 위해 사용할 Topic의 JNDI명을 정의한다. ActiveMQ의 경우, dynamicTopics/ 다음에 ActiveMQ에 생성한 Topic명을 정의해 주는데 만일 정의한 이름을 가진 Topic이 존재하지 않으면, 해당 Topic이 신규로 생성된다.

  • cache.cluster.jms.node.name : 분산 환경을 구성하는 여러 어플리케이션 중 해당 어플리케이션을 식별하기 위한 식별자를 정의한다. 분산 환경을 구성하고 있는 각 어플리케이션들은 모두 다른 node명을 갖도록 지정해야 한다. Cache Event가 발생할 때 해당 어플리케이션을 포함하여 분산 환경을 구성하고 있는 모든 어플리케이션의 Cache에 해당 Event가 Send되는데, Cache Event가 발생한 해당 어플리케이션에서는 Event를 받더라도 아무런 Action을 취할 필요가 없다. 따라서, cache.cluster.jms.node.name 는 Cache Event가 어느 어플리케이션에서 발생했는지 알 수 있는 정보로 활용된다.

위와 같은 설정이 모두 완료되었다면, 동일한 어플리케이션 2개를 각기 다른 WAS를 통해 시작시킨 후 다음과 같은 유형의 요청 수행시 Cache가 제대로 동작하는지 확인해보자. 이때, 사용하는 JMS 서버 라이브러리와 함께 jms spec., j2ee management spec. 라이브러리를 각 어플리케이션에 배포하여 구동시켜줘야 한다. 이 예제에서는 JMS 서버로 ActiveMQ를 사용하므로, 각 어플리케이션의 WEB-INF/lib 폴더에 activemq-core-x.x.x.jar와 jms spec jar, j2ee management spec jar 파일을 배포하여 테스트하였다.

  • 어플리케이션 A에서 특정 데이터 조회 후, 어플리케이션 B에서 동일한 데이터 조회시 별도 쿼리문 수행없이 해당 데이터가 조회되는지 확인한다. 즉, 어플리케이션 B는 DB에 접근하지 않고 Cache를 통해 데이터를 조회하는지 확인한다.

  • 어플리케이션 A에서 특정 데이터 수정 후, Event가 Send되고 어플케이션 B에서 해당 Event를 Receive하는지 확인한다.

    또한 어플리케이션 B에서 어플리케이션 A를 통해 수정한 데이터 조회시 해당 객체는 Cache Event Listener에 의해 Cache로부터 지워졌으므로, 기존에 Caching된 객체를 그대로 읽지 않고, DB에 접근하여 변경된 데이터를 읽어오는지 확인한다.

(* WAS가 Tomcat일 경우 spring-tomcat-weaver.jar 파일을 다운로드하여 Tomcat 설치 폴더\server\lib에 복사해야 한다.)

49.8.2.Fetch Strategy

Lazy Loading 이란 Hibernate에서 기본적으로 객체가 실제로 필요하기 전까지 SQL을 실행하지 않고 Proxy 객체로 리턴하는 것을 말한다. 이러한 Lazy Loading을 통해 불필요한 DB 접근을 줄이고 Session 내에 존재하는 Persistence 객체의 개수를 감소시킬 수 있다. 하지만 이러한 Lazy Loading을 처리하기 위해 다음과 같은 N+1 SELECT 이슈가 발생하게 된다. 다음은 Lazy Loading으로 발생할 수 있는 N+1 SELECT 문제를 테스트할 수 있는 HibernateFetchWithDefaultLazyLoading.java 파일의 일부이다.

hqlBuf.append("FROM Category category ");
hqlBuf.append("ORDER BY category.categoryName ASC");
Query query = session.createQuery(hqlBuf.toString());

/*1번의 쿼리 수행 :
select category0_.CATEGORY_ID as CATEGORY1_0_, 
category0_.CATEGORY_NAME as CATEGORY2_0_, category0_.CATEGORY_DESC as 
CATEGORY3_0_ from PUBLIC.CATEGORY category0_
 order by category0_.CATEGORY_NAME ASC */
List categoryList = query.list();

for (int i = 0; i < categoryList.size(); i++) {
    Category category = (Category) categoryList.get(i);

    if (i == 0) {
        Set movies = category.getMovies();
		
        /* n번의 쿼리 수행 :
        select movies0_.CATEGORY_ID as CATEGORY1_1_, movies0_.MOVIE_ID 
        as MOVIE2_1_, movie1_.MOVIE_ID as MOVIE1_3_0_, movie1_.COUNTRY_CODE 
        as COUNTRY2_3_0_, movie1_.TITLE as TITLE3_0_, movie1_.DIRECTOR 
        as DIRECTOR3_0_, movie1_.RELEASE_DATE as RELEASE5_3_0_ 
        from MOVIE_CATEGORY movies0_ left outer join PUBLIC.MOVIE movie1_ 
        on movies0_.MOVIE_ID=movie1_.MOVIE_ID
        where movies0_.CATEGORY_ID=? */
        
    } else if (i == 1) {
        Set movies = category.getMovies();
    ...
}

Category에 대한 조회 작업을 수행하며 특정 Category에 속한 Movie Set 조회시 Movie 정보 조회를 위한 SELECT문이 수행된다. 이를 해결하기 위해 Fetch 방식에 대한 제어가 필요하며 그 예는 다음과 같다.

49.8.2.1.Batch를 이용하여 데이터 조회

Hibernate Mapping XML 파일 내에 특정 객체에 대한 batch-size를 지정할 경우 지정한 개수만큼 해당 객체를 로딩하는 방식으로 쿼리 실행 회수가 n/batch size + 1로 감소한다. 다음은 batch-size 설정 예인 Country.hbm.xml 파일의 일부이다.

<hibernate-mapping>
    <class name="anyframe.sample.model.bidirection.Country" table="COUNTRY" 
        lazy="true" schema="PUBLIC">
        <id name="countryCode" type="string">
            ..
        </id>
        <property name="countryId" type="string">
            <column name="COUNTRY_ID" length="2" not-null="true" />
        </property>
        ..        
        <set name="movies" inverse="true" cascade="save-update" batch-size="2">
            <key>
                <column name="COUNTRY_CODE" length="12" />
            </key>
            <one-to-many class="anyframe.sample.model.bidirection.Movie" />
        </set>
    </class>
</hibernate-mapping>

위와 같이 정의할 경우 Country:Movie 관계에서 Movie Set에 대한 Fetch Strategy를 Batch Fetching한다.(여기서는 batch-size="2"로 정의함.) 특정 Country에 속한 Movie Set을 조회하고자 할 때 batch-size를 기반으로 SELECT문이 수행된다.

hqlBuf.append("FROM Country");
Query query = session.createQuery(hqlBuf.toString());
List countryList = query.list();

// 3. check result - country

for (int i = 0; i < countryList.size(); i++) {
    Country country = (Country) countryList.get(i);

    if (i == 0) {
        Set movies = country.getMovies();
		
        /* batch-size가 2이므로 2개씩 조회
        select movies0_.COUNTRY_CODE as COUNTRY2_1_, movies0_.MOVIE_ID as MOVIE1_1_,
        movies0_.MOVIE_ID	as MOVIE1_3_0_, movies0_.COUNTRY_CODE as COUNTRY2_3_0_,
        movies0_.TITLE as TITLE3_0_, movies0_.DIRECTOR as DIRECTOR3_0_, 
        movies0_.RELEASE_DATE as RELEASE5_3_0_ from PUBLIC.MOVIE movies0_ 
        where movies0_.COUNTRY_CODE in ('COUNTRY-0001', 'COUNTRY-0003')'*/        
    } else if (i == 1) {
        Set movies = country.getMovies();
		//쿼리 수행 안함.
		

위에 대한 테스트 코드는 HibernateFetchWithBatchSize.java 를 참고한다.

49.8.2.2.Sub-Query를 이용하여 데이터 조회

또다른 fetch 전략으로 subselect 속성을 주는 방법이 있다. subselect 속성 정의 방법은 Mapping XML 파일인 Movie.hbm.xml 에서 다음과 같이 확인할 수 있다.

<hibernate-mapping>
    <class name="anyframe.sample.model.bidirection.Movie" table="MOVIE" lazy="true"..>
        <id name="movieId" type="string">
            <column name="MOVIE_ID" />
            <generator class="assigned" />
        </id>
        <property name="title" type="string">
            <column name="TITLE" length="100" not-null="true" />
        </property>
		...
        <set name="categories" inverse="false" table="MOVIE_CATEGORY" fetch="subselect">
            <key>
                <column name="MOVIE_ID" length="8" not-null="true" />
            </key>
            ..
        </set>
    </class>           
</hibernate-mapping>

위와 같이 Movie 클래스 내의 categories set에 대해 fetch 속성의 값을 subselect로 정의할 경우 해당 데이터를 불러올때 Sub Query 형태의 SELECT 문이 수행되며 한번에 모두 로딩하게 된다.

for (int i = 0; i < movieList.size(); i++) {
    Movie movie = (Movie) movieList.get(i);

    if (i == 0) {
    ..
		
        Set categories = movie.getCategories();
		
        /* categories에 대한 Sub Query 형태의 SELECT문이 발생한다.
        select categories0_.MOVIE_ID as MOVIE2_1_, categories0_.CATEGORY_ID 
        as CATEGORY1_1_, category1_.CATEGORY_ID as CATEGORY1_0_0_, category1_.CATEGORY_NAME 
        as CATEGORY2_0_0_, category1_.CATEGORY_DESC as CATEGORY3_0_0_ 
        from MOVIE_CATEGORY categories0_ 
        left outer join PUBLIC.CATEGORY category1_ 
        on categories0_.CATEGORY_ID=category1_.CATEGORY_ID
        where categories0_.MOVIE_ID 
        in (select movie0_.MOVIE_ID from PUBLIC.MOVIE movie0_) */
				
	} else if (i == 1) {
 		..
		Set categories = movie.getCategories();
		//쿼리 수행 안함.
	...

하지만 최초로 필요한 순간에 모든 데이터를 로딩하므로 동시에 많은 데이터 요청이 있을 경우 메모리 사용량이 급격히 증가할 수 있음에 유의한다. 위의 테스트 코드는 HibernateFetchWithSubselect.java 에서 확인할 수 있다.

49.8.2.3.join fetch를 이용하여 데이터 한꺼번에 조회

특정 HQL문에 "join fetch"절을 사용하게 되면 해당 Join 객체에 대해서 Lazy Loading과 다른 방식으로 한 번에 필요한 데이터를 모두 로딩하게 된다. 다음은 join fetch가 적용된 HibernateFetchWithoutLazyLoading.java 파일의 일부이다.

StringBuffer hqlBuf = new StringBuffer();
hqlBuf.append("SELECT movie ");
hqlBuf.append("FROM Movie movie join fetch movie.categories category ");
hqlBuf.append("WHERE category.categoryName = ?");
Query query = session.createQuery(hqlBuf.toString());
query.setParameter(0, "Romantic");

/* fetch join된 categories의 데이터도 한꺼번에 모두 로드시킨다. (Lazy Loading이 아님)
select movie0_.MOVIE_ID as MOVIE1_3_0_, category2_.CATEGORY_ID as CATEGORY1_0_1_, 
movie0_.COUNTRY_CODE as COUNTRY2_3_0_, movie0_.TITLE as TITLE3_0_, 
movie0_.DIRECTOR as DIRECTOR3_0_, movie0_.RELEASE_DATE as RELEASE5_3_0_, 
category2_.CATEGORY_NAME as CATEGORY2_0_1_, category2_.CATEGORY_DESC as CATEGORY3_0_1_, 
categories1_.MOVIE_ID as MOVIE2_0__, categories1_.CATEGORY_ID as CATEGORY1_0__ 
from PUBLIC.MOVIE movie0_ inner join MOVIE_CATEGORY categories1_ 
on movie0_.MOVIE_ID=categories1_.MOVIE_ID inner join PUBLIC.CATEGORY category2_ 
on categories1_.CATEGORY_ID=category2_.CATEGORY_ID where category2_.CATEGORY_NAME='Romantic' */
List movieList = query.list();

// 3. check result - movie

for (int i = 0; i < movieList.size(); i++) {
    Movie movie = (Movie) movieList.get(i);

    if (i == 0) {
		..
        Set categories = movie.getCategories();
        //쿼리 수행 안함.
    } else if (i == 1) {
        ..
        Set categories = movie.getCategories();
        //쿼리 수행 안함.
    ...

이는 categories에 대한 fetch 속성을 "join"으로 준것과 같이 동작하게 된다. 하지만 Mapping XML에 정의할 경우 Movie를 조회할 때마다(Category 목록이 필요하지 않은 경우에도) 모든 Category 목록도 함께 초기화되어 메모리에 올라 오게 되므로 위와 같이 HQL문에 join fetch를 사용하여 필요한 경우에만 적용되도록 하는 것이 효율적이다.

49.9.Concurrency

Hibernate에서는 동시에 동일한 데이터에 접근할 때에 데이터에 대한 접근을 제어하기 위해 Optimistic Locking 또는 Pessimistic Locking 기법 등을 제공한다.

49.9.1.Optimistic Locking

public void updateMovieWithoutOptimisticLocking() throws Exception {
    // 1. insert a new country, movies information
    newSession(); // 첫번째 트랜잭션
    addCountryMovieAtOnce();
    closeSession();

    // 2. select a country
    newSession(); // 두번째 트랜잭션
    /* #1 */ Movie fstMovie = (Movie) session.get(Movie.class, "MV-00001");
    /* #2 */ Movie scdMovie = (Movie) session.get(Movie.class, "MV-00001");

    closeSession();

    // 3. set country name
    /* #3 */ fstMovie.setTitle("First : My Sassy Girl");


    // 4. select a country again with same id and update country name
    newSession(); // 세번째 트랜잭션
    /* #4 */ scdMovie.setTitle("Second : My Sassy Girl");

    closeSession();

    // 5. try to update with detached object
    newSession(); // 네번째 트랜잭션
    /* #5 */ session.update(fstMovie);

    closeSession();
}
        
위에서 제시한 updateMovieWithoutOptimisticLocking()의 로직에 대해 자세히 살펴보자.

  1. #1, #2번 코드에 의해 각각 동일한 식별자를 이용하여 같은 데이터 조회

  2. 두번째 트랜잭션이 종료된 후, #3번 코드에서는 Detached 상태의 fstMovie 객체의 title 변경

  3. 세번째 트랜잭션 내의 #4번 코드에서는 scdMovie 객체의 title 변경, 세번째 트랜잭션 종료시 변경 사항이 DB에 반영

  4. 네번째 트랜잭션 내에서 #3번 코드를 통해 변경된 fstMovie 객체에 대해 update 수행

  5. fstMovie에 대한 수정 작업 또한 성공적으로 처리

결론적으로 보면, MOVIE_ID가 "MV-00001"인 Movie의 Title은 "First : My Sassy Girl"이 되어 앞서 scdMovie에서 요청했던 수정 작업은 무시된 것이다. 이러한 현상을 Lost Update라고 하며, 이를 해결하기 위한 방법은 3가지가 있다.

  1. Last Commit Wins : Optimistic Locking 을 수행하지 않게 되면 기본적으로 수행되는 유형으로 2개의 트랜잭션 모두 성공적으로 commit된다. 그러므로 두번째 commit은 첫번째 commit 내용을 덮어쓸 수 있다.

  2. First Commit Wins : Optimistic Locking을 적용한 유형으로 첫번째 commit만이 성공적으로 이루어지며, 두번째 commit 시에는 Error를 얻게 된다.

  3. Merge : 첫번째 commit만이 성공적으로 이루어지며, 두번째 commit 시에는 Error를 얻게 된다. 그러나 First Commit Wins와는 달리 두번째 commit을 위한 작업을 처음부터 다시 하지 않고 개발자의 선택에 의해 선택적으로 변경될 수 있도록 한다. 가장 좋은 전략이나 변경 사항을 merge 할 수 있는 화면이나 방법을 직접 제공해 줄 수 있어야 한다.(추가 구현 필요함)

Hibernate에서는 Versioning 기반의 Automatic Optimistic Locking을 통해 First Commit Wins 전략을 취할 수 있도록 지원한다. Hibernate에서 Optimistic Locking을 수행하기 위해서는 해당 테이블에 Version 또는 Timestamp 컬럼을 추가해야 한다. 그러한 경우 해당 테이블과 매핑된 객체를 로드할 때 Version 또는 Timestamp 정보도 함께 로드되고 객체 수정시 테이블의 현재 값과 비교하여 처리 여부를 결정하게 된다.

다음은 Version을 이용하여 Optimistic Locking을 수행하는 예제이다.

Optimistic Locking의 대상이 되는 Persistence Class에 Version 관리를 위한 int 유형의 속성을 정의하고, Hibernate Mapping XML 파일 내의 <id> 태그 다음에 <version>을 이용하여 Version에 대한 매핑 정보를 정의하고 있다.

1. Country.java

public class Country implements java.io.Serializable {

    private int version;

    private String countryCode;
    private String countryId;
    private String countryName;
    private Set movies = new HashSet(0);
	
//...
}

2. Country.hbm.xml

<class name="anyframe.sample.model.bidirection.concurrency.optimistic.Country" table="COUNTRY" 
        lazy="true" schema="PUBLIC">
    <id name="countryCode" type="string">
        <column name="COUNTRY_CODE" length="12" />
        <generator class="assigned" />
    </id>
    <version name="version" access="field" column="COUNTRY_VERSION"/>
    ...중략
</class>
이와 같이 정의된 경우 다음의 updateCountryWithOptimisticLocking() 메소드를 수행하였을 때 첫번째 수정 작업은 성공적으로 이루어지나 두번째 수정 작업에 대해서는 #6번 코드에서처럼 StaleObjectStateException이 throw될 것이다.
1. HibernateOptimisticLocking.java
			
public void updateCountryWithOptimisticLocking() throws Exception {
    // 1. insert a new country, movies information
    newSession();
    addCountryMovieAtOnce();
    closeSession();

    // 2. select a country
    newSession();
    /* #1 */ Country fstCountry = (Country) session.get(Country.class,
            "COUNTRY-0001");

    /* #2 */ Country scdCountry = (Country) session.get(Country.class,
        "COUNTRY-0001");

    closeSession();

    // 3. set country name
    /* #3 */ fstCountry.setCountryName("First : Republic of Korea.");

    // 4. select a country again with same id and update country name
    newSession();
    /* #4 */ scdCountry.setCountryName("Second : Republic of Korea.");

    closeSession();

    // 5. try to update with detached object
    newSession();
    try {
        /* #5 */ session.update(fstCountry);
        closeSession();
    } catch (Exception e) {
        e.printStackTrace();
	}
}

Timestamp 사용은 Version에 비해 안전하지 않다. 일반적으로 JVM이 Millisecond 단위의 정확도를 가지지 않으므로 Timestamp 값으로 동시 제어를 위한 구분이 어려울 수 있다. 이러한 문제를 해결하기 위해 <timestamp> 내에 해당 컬럼에 대한 속성을 source="db"와 같이 정의함으로써 Timestamp 값을 DB에서 가져오도록 설정할 수 있으나 이 또한 Timestamp 값을 얻어낼 때마다 DB에 접속해야 하는 추가 비용이 발생하게 된다. 이러한 이유로 Hibernate에서는 Timestamp 보다 Version 사용을 권장한다. 이 외에도 <class> 내에 optimistic-lock 속성의 값을 "all" 또는 "dirtry"로 정의하면 별도 Version 또는 Timestamp 컬럼에 대한 추가 정의없이도 Optimistic Locking이 가능해진다. 그러나 이 또한 성능, 복잡성과 같은 이유로 권장하는 방법은 아니다.

  • optimistic-lock="all" : 해당되는 객체 조회 당시와 비교하여 변경되지 않은 속성들을 해당 객체를 조회하기 위한 조건(WHERE절)으로 명시하여 변경 작업을 시도함으로써 Optimistic Locking 적용.

  • optimistic-lock="dirty" : 두 트랜잭션에서 동일한 속성의 값에 대해 변경을 수행하였을 경우에 대해 Optimistic Locking 적용. 따라서, 두 트랜잭션이 서로 다른 속성의 값을 변경한 경우에는 해당되지 않는다.

49.9.2.Pessimistic Locking

동시 접근 제어를 위해 어플리케이션 전체의 isolation level을 read committed 이상으로 높이는 것은 어플리케이션의 확장성을 고려할 때 그리 추천하지 않는다. 특정 작업에 대해 isolation을 보다 잘 보장해 주는 것이 바람직하다. Hibernate 기반의 Pessimistic Locking은 다음과 같은 Locking Mode중, 개발자가 정의한 Locking Mode를 이용하여 특정 트랜잭션에 대해 Locking을 정의하는 방식으로 수행된다.

  • LockMode.NONE : 기본값으로 Locking이 수행되지 않으며 캐쉬에 객체가 존재하면 캐쉬 내의 객체를 사용한다.

  • LockMode.READ : Cache가 아닌 현재 트랜잭션에 포함되어 있는 DB로부터 데이터를 읽어 와서 메모리 상의 객체와 동일한 것인지 확인한다.

  • LockMode.UPGRADE : 조회시 SELECT .. FOR UPDATE와 같은 쿼리가 수행되므로 다른 쓰레드에서 동일한 객체에 접근하려고 할 때 행 단위로 Locking 한다. SELECT .. FOR UPDATE 기능을 제공하는 DBMS에 한해 지원된다. SELECT .. FOR UPDATE 문을 지원하지 않는 DB를 사용할 때는 LockMode.READ로 전환된다.

  • LockMode.UPGRADE_NOWAIT : 조회시 오라클의 SELECT .. FOR UPDATE NO WAIT와 같은 쿼리가 수행되므로 행 단위로 Locking을 걸며, 다른 쓰레드에서 동일한 객체에 접근하려고 할 때 Blocking 되지 않고 바로 Exception을 발생시킨다. SELECT .. FOR UPDATE NO WAIT를 지원하지 않으면 LockMode.UPGRADE로 전환된다.

  • LockMode.FORCE : 현재 트랜잭션에 의해 객체가 수정되었음을 인식할 수 있게 하기 위해 DB 내의 객체 버전을 강제로 증가시킨다.

  • LockMode.WRITE : Hibernate에 의해 현재 트랜잭션에서 행을 추가했을 때 자동으로 얻어진다. (Hibernate 내부에서 사용하는 mode로 개발자가 어플리케이션에서 명시적으로 사용하지 않도록 한다.)

    위의 그림을 살펴보면, 클라이언트 1과 2는 동일한 데이터에 접근하고 있으며, 이 때 클라이언트 1에서 먼저 lock()을 걸었으므로 그 이후 다른 Client에서는 클라이언트 1의 lock이 해제될 때까지 해당 데이터에 접근할 수 없게 된다. 즉, 클라이언트 1의 트랜잭션이 종료되고 난 이후에야 lock이 해제되어 다른 Client에서 해당 데이터에 접근할 수 있게 되는 것이다. 강력한 Locking 기법인 Pessimistic Locking은 데이터에 대한 접근이 먼저 이루어졌다 하더라도 수정 작업을 먼저 반영하지 않으면 Exception이 발생하는 Optimistic Locking 기법과는 달리 lock을 보유한 트랜잭션이 종료될 때까지 다른 트랜잭션의 해당 데이터에 대한 접근을 막기 때문에 안전한 데이터 수정이 가능해진다.

    다음에서는 Pessimistic Locking 수행을 테스트하기 위한 예제 코드 HibernatePessimisticLocking 를 이용하여, LockMode.NONE, LockMode.UPGRADE, LockMode.UPGRADE_NOWAIT 에 대해 상세히 비교해 보고자 한다.

    하나의 객체에 대한 동시 접근을 실현하기 위해 다음과 같은 Thread가 구현되었으며 모든 테스트 메소드에서는 두번째 Thread에 sleeptime을 줌으로써, 첫번째 Thread를 명시적으로 먼저 start시켜 하나의 객체에 대해 첫번째 Thread에서 먼저 접근할 수 있도록 강제하고 있다. 또한 첫번째 Thread의 변경 사항을 DB에 반영하기 전에는 sleep시켜 첫번째 Thread에 의한 변경 사항 반영을 지연시킨다.

    public class CountryThread extends Thread {
        ...중략 
        public void run() {
            try {
                Session session = initialSessionFactory.openSession();
                session.beginTransaction();
    
                Country country = (Country) session.get(Country.class,
                        "COUNTRY-0001", this.lockMode);
                this.beforeCountryName = country.getCountryName();
    
                country.setCountryName(id + " : Republic of Korea");
                this.sleep(sleepTime);
    
                session.flush();
    
                country = (Country) session.get(Country.class, "COUNTRY-0001");
                this.afterCountryName = country.getCountryName();
    
                session.getTransaction().commit();
                session.close();
            } catch (Exception e) {
                if (this.lockMode == LockMode.UPGRADE_NOWAIT
                    && id.equals("second")) {
                    e instanceof LockAcquisitionException);
                }
            }
        }
            //...
    }        

  • LockMode.NONE인 경우 : 첫번째 Thread를 통해 먼저 select ... 문이 수행되나 LockMode.NONE이므로, Lock이 걸리지는 않는다. 그리고 첫번째 Thread에서는 session.flush()를 수행하기 전에 주어진 시간만큼 sleep()하게 되므로, 뒤이어 시작한 두번째 Thread에서 select를 수행한 후, 바로 수정 작업을 commit한다. 첫번째 Thread에서는 주어진 시간만큼 sleep()한 후, 두번째 Thread의 변경 내용을 무시하고 수정 작업을 commit하게 된다. 즉, LockMode.NONE일 경우에는 Pessimistic Locking이 수행되지 않음을 알 수 있다.

  • LockMode.UPGRADE인 경우 : 첫번째 Thread를 통해 먼저 select ... for update 문이 수행되면서 해당 Row에 Lock이 생긴다. 그리고 첫번째 Thread에서는 session.flush()를 수행하기 전에 주어진 시간만큼 sleep()하게 되므로 뒤이은 두번째 Thread에서는 첫번째 Thread의 update 작업이 완료될 때까지 blocking되어 있다가 첫번째 Thread에서 변경한 값을 기반으로 하여 수정 작업을 commit한다.

  • LockMode.UPGRADE_NOWAIT인 경우 : 첫번째 Thread를 통해 먼저 select ... for update nowait 문이 수행되면서 해당 Row에 Lock이 생긴다. 그리고 첫번째 Thread에서는 session.flush()를 수행하기 전에 주어진 시간만큼 sleep()하게 되며, 뒤이은 두번째 Thread에서 select ... for update nowait를 시도하면 blocking 없이 바로 LockAcquisitionException이 throw되면서 두번째 Thread를 통한 수정 작업은 이루어지지 않게 된다.

49.9.3.Offline Locking

지금까지는 한 트랜잭션 내에서의 동시 접근 처리 기법에 대해 알아보았다. Offline Locking에서는 여러 개의 트랜잭션을 통해 하나의 작업이 이루어져야 하는 경우에서의 동시 접근 처리 기법에 대해 살펴보기로 하자. 웹어플리케이션의 일반적인 화면 구성을 가정해보자. 상영중인 영화 목록을 제공하는 웹어플리케이션에서 특정 영화 정보를 수정하는 작업을 수행하기 위해서는 먼저 선택된 영화 정보 조회가 이루어지고, 수정 작업이 뒤따라야 한다. 즉, 2개의 트랜잭션 수행을 통해 원하는 작업을 수행할 수 있게 되는 것이다. 동시 사용자가 이러한 작업을 수행한다고 했을때, 동시 제어가 제대로 이루어지지 않으면 어느 한 사용자의 작업 정보는 손실될 가능성이 존재하게 된다. 이와 같이 여러 트랜잭션을 통해 이루어지는 작업에서 동시 접근 제어를 수행하기 위해서는 다음과 같은 작업이 필요하다.

  • Offline Optimistic Locking : Optimistic Locking과 동일하게 Version을 사용하는 방법이다. 첫번째 트랜잭션을 통해 얻어온 Detached 상태의 객체(version 정보 포함하고 있음.)를 HTTP 세션에 저장해 둔다. 사용자가 수정 작업 반영을 요청하면 HTTP 세션에 저장된 Detached 객체를 꺼내 수정된 정보로 셋팅하고 두번째 트랜잭션에서 session.update() 메소드 호출시 입력 인자로 전달한다. 이렇게 하면 Optimistic Locking과 유사하게 Version 정보를 기반으로 동시 접근을 제어할 수 있게 된다.

  • Offline Pessimistic Locking : Pessimistic Locking과 동일한 동작 원리를 가지면서 DB 레벨이 아닌 어플리케이션 레벨에서 Locking을 관리할 수 있는 별도의 LockManager 구현이 필요하다.

49.10.Transaction Management

Hibernate에서 지원하는 Transaction 관리 방법에는 크게 JDBC, JTA, CMT 세 가지가 있다. 본 페이지에서는 일반적으로 가장 많이 사용하는 JDBC, JTA 기반의 Transaction 관리 방법에 대해서 설명하겠다.

49.10.1.JDBC - HibernateTransactionManager

HibernateTransactionManager는 DataSource를 사용하여 Local Transaction과 Hibernate Session을 관리한다. 따라서 HibernateTransactionManager는 LocalSessionFactory Bean에 의존성을 가지고 있으므로 반드시 LocalSessionFactory와 함께 사용되어야 한다.

  • Configuration

    다음은 Spring Framework의 org.springframework.orm.hibernate3.HibernateTransactionManager를 이용하여 Hibernate 기반에서 Transaction을 관리하기 위한 context-transaction.xml 파일의 일부이다.

    <bean id="transactionManager"
              class="org.springframework.orm.hibernate3.HibernateTransactionManager">
              <property name="sessionFactory" ref="sessionFactory" />
    </bean>        
    Spring의 TransactionManager 설정 방법에 대해서는 본 매뉴얼 >> Tech. Service >> Transaction 을 참고한다.

  • Test Case

    다음은 org.springframework.orm.hibernate3.HibernateTransactionManager를 이용하여 Transaction 관리 기능을 테스트하기 위한 HibernateJDBCTransactionManager.java 의 일부이다.

    public class HibernateJDBCTransactionManager {
    
    <!--중략 -->
    
    /**
     * [Flow #-1] HibernateTransactionManager Rollback을 이용하여,
     * 초기화 데이터의 입력 작업을 취소시킨 후, 데이터가 제대로 Rollback되었는지 검증한다.
     * 
     * @throws Exception
     *             throws exception which is from hibernate
     */
        public void rollback() throws Exception {
            // 1. insert init data
            Session session = sessionFactory.getCurrentSession();
            SetUpInitData.initializeData(session);
    
            // 2. rollback transaction
            isRollback();
            endTransaction();
    
            // 3. begin a new transaction
            startNewTransaction();
    
            // 4. check if insertion is rollbacked
            Movie movie = (Movie) sessionFactory.getCurrentSession().get(
            Movie.class, "MV-00001");
        }
    
    /**
     * [Flow #-2] HibernateTransactionManager Commit을 이용하여, 초기화
     * 데이터의 입력 작업을 DB에 반영시킨 후, 데이터가 제대로 Commit되었는지 검증한다.
     * 
     * @throws Exception
     *             throws exception which is from hibernate
     */
        public void commit() throws Exception {
            // 1. insert init data
            Session session = sessionFactory.getCurrentSession();
            SetUpInitData.initializeData(session);
    
            // 2. commit transaction
            setComplete();
            endTransaction();
    
            // 3. begin a new transaction
            startNewTransaction();
    
            // 4. check if insertion is successful
            Movie movie = (Movie) sessionFactory.getCurrentSession().get(
            Movie.class, "MV-00001");
        }
    }

49.10.2.JTA - JTATransactionManager

JTATransactionManager 서비스는 JTA를 사용한 Global Transaction 관리 부분을 추상화하여 해당 서비스가 JTA,JNDI 등에 종속적이지 않게 구현 가능 하도록 도와준다.

  • Configuration

    아래는 JTATransactionManager의 속성을 정의한 context-transaction.xml 파일의 일부이다.

    <bean id="transactionManager"
              class="org.springframework.transaction.jta.WebLogicJtaTransactionManager"/>        

    위에서 볼 수 있듯이 Hibernate 기반에서 JTA Transaction 관리는 SpringJDBC를 사용 할 때와 다르지 않다. 상세한 속성 정의 방법에 대해서는 JTA Transaction Service 를 참고한다.

  • Test Case

    다음은 WebLogicJtaTransactionManager를 이용해 Transaction 관리 기능을 테스트 하는 HibernateJTATransactionManager.java 의 일부이다.

    public class HibernateJTATransactionManager {
    
    ...중략
    
    /**
     * [Flow #-1] WebLogicJtaTransactionManager Rollback을 이용하여,
     * 초기화 데이터의 입력 작업을 취소시킨 후, 데이터가 제대로 Rollback되었는지 검증한다.
     * 
     * @throws Exception
     *             throws exception which is from hibernate
     */
        public void rollback() throws Exception {
            // 1. insert init data
            Session session = sessionFactory.getCurrentSession();
            SetUpInitData.initializeData(session);
    
            // 2. rollback transaction
            isRollback();
            endTransaction();
    
            // 3. begin a new transaction
            startNewTransaction();
    
            // 4. check if insertion is rollbacked
            Movie movie = (Movie) sessionFactory.getCurrentSession().get(
            Movie.class, "MV-00001");
        }
    
    /**
     * [Flow #-2] WebLogicJtaTransactionManager Commit을 이용하여,
     * 초기화 데이터의 입력 작업을 DB에 반영시킨 후, 데이터가 제대로 Commit되었는지 검증한다.
     * 
     * @throws Exception
     *             throws exception which is from hibernate
     */
        public void commit() throws Exception {
            // 1. insert init data
            Session session = sessionFactory.getCurrentSession();
            SetUpInitData.initializeData(session);
    
            // 2. commit transaction
            setComplete();
            endTransaction();
    
            // 3. begin a new transaction
            startNewTransaction();
    
            // 4. check if insertion is successful
            Movie movie = (Movie) sessionFactory.getCurrentSession().get(
            Movie.class, "MV-00001");
        }
    }

49.11.Spring Integration

Spring에서는 Hibernate 기반에서 DAO 클래스를 쉽게 구현할 수 있도록 하기 위해 HibernateTemplate을 제공하고 있다. (※ Spring 2.5 부터는 Hibernate 3 버전을 지원한다.) 또한, Anyframe에서는 Veloticy 문법을 이용하여 Dynamic HQL, Dynamic Native SQL문을 처리하기 위해서 DynamicHibernateService를 제공한다. Hibernate을 이용하여 데이터 액세스 처리를 수행하는 경우 하나의 비즈니스 서비스를 구성하는 요소들은 일반적으로 다음과 같이 구성될 수 있다.

Spring 기반에서 Hibernate을 통해 데이터 액세스 처리를 수행하기 위해서는 다음과 같은 절차에 따라 비즈니스 서비스를 개발할 수 있다.

49.11.1.Hibernate 속성 정의 파일 작성

Hibernate을 Spring과 연계하기 위해서는 SessionFactory 설정이 필요하다. 또한, Dynamic HQL, Dynamic Native SQL 실행을 위해서는 Anyframe에서 제공하는 DynamicHibernateService에 대한 설정도 필요하다.

49.11.1.1.Session Factory 속성 정의

Spring에서 제공하는 HibernateDaoSupport는 내부적으로 Hibernate 연계를 위해 HibernateTemplate을 생성하는데 이 클래스는 SessionFactory를 필요로 한다. 이를 위해 HibernateDaoSupport를 상속받은 클래스들은 SessionFactory를 필요로 하며, SessionFactory는 다음과 같은 속성 정보를 가질 수 있다. 다음은 SessionFactory의 속성을 정의한 context-hibernate.xml 파일의 일부이다.

<bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <!-- SessionFactory에서 사용할 dataSource 정의 -->
    <property name="dataSource" ref="dataSource" />
    <!-- Mapping XML의 위치 지정 -->
    <property name="mappingLocations">
        <list>
            <value>classpath:anyframe/sample/model/bidirection/Category.hbm.xml</value>
            <value>classpath:anyframe/sample/model/bidirection/Country.hbm.xml</value>
            <value>classpath:anyframe/sample/model/bidirectionMovie.hbm.xml</value>
        </list>
    </property>
<!-- Hibernate Property에 대한 속성 정의 -->
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.hbm2ddl.auto">create</prop>
            <!-- DBMS에 따른 dialect 설정-->
            <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
            <!-- hibernate을 이용한 sql문을 보여줄지 여부-->
            <prop key="hibernate.show_sql">false</prop>
            <prop key="hibernate.format_sql">true</prop>
        </props>
    </property>
</bean>

49.11.1.2.Dynamic HQL, Dynamic Native SQL 실행을 위한 DynamicHibernateService 속성 정의

조건에 따라 HQL문을 dynamic하게 생성해 주기 위해 Anyframe에서는 DynamicHibernateService를 제공한다. 이러한 기능을 사용하기 위해서는 다음과 같이 DynamicHibernateService 클래스에 대한 속성을 정의하고 특정 DAO 클래스 정의시 DynamicHibernateService를 참조하도록 할 수 있다. 다음은 dynamicHibernateService bean이 정의된 context-hibernate.xml 파일의 일부이다.

<bean id="dynamicHibernateService"
    class="anyframe.core.hibernate.impl.DynamicHibernateService">
    <!-- SessionFactory 지정  -->
    <property name="sessionFactory" ref="sessionFactory" />
    <!-- Velocity 문법이 적용된 dynamic한 HQL을 정의한 XML파일의 경로 지정 -->
    <property name="fileNames">
        <list>
            <value>classpath:anyframe/core/hibernate/spring/dynamic-hibernate.xml</value>
        </list>
    </property>
</bean>

위와 같이 정의할 경우 dynamicHibernateService bean은 sessionFactory bean을 SessionFactory로 가지며 fileNames에 정의된 XML들에서 해당되는 HQL또는 Native SQL을 찾게 될것이다.

49.11.2.Mapping XML 파일 작성

특정 비즈니스 서비스에서 사용할 객체와 테이블간의 매핑 정보를 Mapping XML 파일에 작성한다. 또한 Mapping XML 파일의 위치를 앞서 언급한 SessionFactory 속성 정의 파일에 아래와 같이 정의해 줘야한다.

<bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <!-- Mapping XML의 위치 지정 -->
    <property name="mappingLocations">
        <list>
            <value>classpath:anyframe/sample/model/bidirection/Movie.hbm.xml</value>
        </list>
    </property>
    <!-- Hibernate Property에 대한 속성 정의 -->
</bean>

자세한 Mapping File 작성은 Hibernate Mapping File 을 참고하도록 한다.

49.11.3.DAO 클래스 생성

Spring에서는 Hibernate을 보다 쉽게 연계하기 위해 HibernateDaoSupport 클래스를 제공하며 각 DAO 생성시 HibernateDaoSupport 클래스를 상속받아 구현할 수 있다. 각 DAO 클래스는 getHibernateTemplate()메소드를 호출함으로써 HibernateDaoSupport 클래스에서 제공하는 HibernateTemplate을 이용하여 기본 입력/수정/삭제/조회 작업을 수행할 수 있다. 또한, Dynamic HQL 처리를 위해 dynamicHibernateService를 사용해야 할 경우에는 위에서 언급한 바와 같이 dynamicHibernateService에 대한 참조가 필요하다.

49.11.3.1.DAO 속성 정의 파일 작성

DAO 클래스에 대한 속성 정의 파일을 작성한다. SessionFactory와 DynamicHibernateService를 참조하는 MovieDAOHibernateImpl 클래스에 대한 속성은 다음과 같이 정의할 수 있다.

<bean id="movieService" class="anyframe.sample.service.movie.impl.MovieServiceImpl">
    <property name="movieDAO">
        <bean class="anyframe.sample.service.movie.impl.MovieDAOHibernateImpl">
            <!-- Hibernate Template을 이용하기 위한 SessionFactory 정의 -->
            <property name="sessionFactory" ref="sessionFactory"/>
            <!-- Dynamic HQL문 지원을 위한 dynamicHibernateService 정의 
            (dynamicHibernateService를 사용할 때만 정의) -->
            <property name="dynamicHibernateService" ref="dynamicHibernateService"/>
        </bean>
    </property>
</bean>        

위 코드는 context-sample.xml 에서 확인할 수 있다.

49.11.3.2.DAO 클래스 개발

Spring에서 제공하는 HibnernateDaoSupport를 상속받아 DAO 클래스를 정의한다. 이 때, getHibernateTemplate() 메소드를 사용하여 HibernateTemplate을 이용한 데이터 입력/수정/삭제/조회가 가능하다.

public class MovieDAOHibernateImpl extends HibernateDaoSupportimplements
                  MovieDAO{

    private DynamicHibernateService dynamicHibernateService;

    //dynamicHibernateService Setter Injection
    public void setDynamicHibernateService(
                DynamicHibernateService dynamicHibernateService) {
            this.dynamicHibernateService = dynamicHibernateService;
        }

    public void createMovie(Movie movie) throws Exception {
        this.getHibernateTemplate().save(movie);
    }
    public Movie findMovie(String movieId) throws Exception {
        return (Movie) this.getHibernateTemplate().get(Movie.class, movieId);
    }

    public List findMovieList(int conditionType, String condition)
                throws Exception {
        Object[] args = new Object[3];
        if (conditionType == 0) {
            args[0] = "director=%" + condition + "%";
            args[1] = "sortColumn=movie.director";
        } else {
            args[0] = "title=%" + condition + "%";
            args[1] = "sortColumn=movie.title";
        }
            args[2] = "sortDirection=ASC";

        return dynamicHibernateService.findList("findMovieListAll", args);
    }

    public List findMovieListAll() throws Exception {
    return this.getHibernateTemplate().find(
            "FROM Movie movie ORDER BY movie.title");
    }

    public void removeMovie(Movie movie) throws Exception {
        this.getHibernateTemplate().delete(movie);
    }

    public void updateMovie(Movie movie) throws Exception {
        this.getHibernateTemplate().update(movie);
    }

    public void updateMovieByBulk(Movie movie) throws Exception {
        StringBuffer hqlBuf = new StringBuffer();
        hqlBuf.append("UPDATE Movie movie ");
        hqlBuf.append("SET movie.director = ? ");
        hqlBuf.append("WHERE movie.movieId = ? ");

        //HQL문을 이용한 CUD를 할 경우에는 
        //getHibernateTemplate().bulkUpdate() 메소드를 사용한다.
        this.getHibernateTemplate().bulkUpdate(hqlBuf.toString(),
                new Object[] { movie.getDirector(), movie.getMovieId() });
    }

    public void createCategory(Category category) throws Exception {
        this.getHibernateTemplate().save(category);
    }

    public void createCountry(Country country) throws Exception {
        this.getHibernateTemplate().save(country);
    }
}

위의 코드는 MovieDAOHibernateImpl.java 에서 확인할 수 있다

※ Dynamic Hibernate에 대한 자세한 사항은 본 매뉴얼 >> Hibernate >> Dynamic Hibernate 를 참고한다.

49.11.4.Test Code 작성

위와 같이 Spring과 Hibernate 연계 작업이 완료되었다면 Test Code를 작성해서 정상 동작 여부를 확인해 보도록 하자. 다음은 Test Code의 예인 HibernateSpringIntegration.java 파일의 일부이다.

public class HibernateSpringIntegration {
    private MovieService movieService;

        //Test 실행에 필요한 비즈니스 서비스 정의 파일의 위치를 지정해준다.
        protected String[] getConfigLocations() {
        return new String[] { "classpath:anyframe/core/hibernate/spring/context-*.xml" };
    }

    //MovieService Setter Injection
    public void setMovieService(MovieService movieService) {
        this.movieService = movieService;
    }

/**
 * [Flow #-1] Hibernate과 Spring Framework을 연계한 MovieService를
 * 통해 단건의 Movie 정보를 등록,수정,삭제,조회하여 본다.
 * 
 * @throws Exception
 *             throws exception which is from MovieService
 */
    public void movieService() throws Exception {
        Movie movie = new Movie();
        movie.setMovieId("MV-00001");
        movie.setDirector("Jaeyong Gwak");
        movie.setReleaseDate(DateUtil.string2Date("2001-07-27", "yyyy-MM-dd"));
        movie.setTitle("My Sassy Girl");
        //movie 객체 등록
        movieService.createMovie(movie);

        Movie result = movieService.findMovie("MV-00001");

        movie.setDirector("Update Jaeyong Gwak");
        //movie 객체 수정
        movieService.updateMovie(movie);

        //movie 객체 조회
        result = movieService.findMovie("MV-00001");
        result.getDirector());

        //movie 객체 삭제
        movieService.removeMovie(movie);

        //movie 객체 조회
        result = movieService.findMovie("MV-00001");
    }
}

위와 같은 코드로 MovieService를 통해 입력/수정/삭제/조회 관련 메소드들이 잘 작동되는지 확인할 수 있다.

49.11.5.선언적인 트랜잭션 관리

Hibernate을 사용할 시에도 Spring의 AOP를 이용한 선언적인 트랜잭션 관리가 가능하다. 이는 본 매뉴얼 >> Tech. Service >> Transaction >> declarative 에서 기본적인 내용을 확인할 수 있다. 단, Spring에서는 다음과 같이 Hibernate을 위한 TransactionManager인 org.springframework.orm.hibernate3.HibernateTransactionManager를 제공함으로써 Hibernate에 최적화된 형태로 트랜잭션을 관리할 수 있게 해주며 설정 방법의 예는 context-transaction.xml 의 일부인 다음과 같다.

<bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
         <tx:method name="*" propagation="REQUIRES_NEW" rollback-for="Exception" />
    </tx:attributes>
</tx:advice>

<aop:config proxy-target-class="true">
    <aop:pointcut id="executionMethods"
            expression="execution(* anyframe.sample..*Impl.*(..))" />
    <aop:advisor advice-ref="txAdvice" pointcut-ref="executionMethods" />
</aop:config>

기타 정의 방법은 기존 Spring TransactionManager를 사용할 때와 동일하다. Hibernate 기반의 트랜잭션 관리에 대한 자세한 내용은 본 매뉴얼 >> Hibernate >> Transaction Management 를 참고한다.

49.12.DynamicHibernateService 활용

Hibernate만을 사용하여 데이터 액세스 처리를 수행할 때 입력 조건에 따라 동적으로 변경되는 HQL, Native SQL을 만들기 위해서는 해당되는 자바 코드 내에 HQL또는 Native SQL문을 만들기 위한 로직이 포함되어야 한다. 이로 인해 쿼리문과 자바 코드가 뒤섞이게 되어, 변경 및 유지보수가 어려워질 수 있다. 따라서, Anyframe에서는 별도 XML에 동적으로 변경되는 HQL, Native SQL문을 정의하여 Hibernate을 이용하여 처리할 수 있도록 Hibernate와 Velocity를 연동한 DynamicHibernateService를 제공한다. DynamicHibernateService에 대한 구현체는 1가지이며, 다음은 각 구현체별 사용 방법이다.

49.12.1.DynamicHibernateService

49.12.1.1.DynamicHibernate Configuration

다음은 DynamicHibernateService를 사용하기 위해 필요한 설정 정보이다.

Property NameDescriptionRequiredDefault Value
sessionFactoryHibernate Session을 이용하여 HQL을 처리하는데 사용될 SessionFactory Bean의 idYN/A
fileNamesDynamic HQL이 정의된 파일 경로 또는 해당하는 디렉토리 정보YN/A

다음은 위에서 열거한 속성 정보를 포함한 context-hibernate.xml 파일의 일부이다.

<bean id="dynamicHibernateService"
         class="anyframe.core.hibernate.impl.DynamicHibernateService">
    <property name="sessionFactory" ref="sessionFactory" />
    <property name="fileNames">
      <list>
       <value>classpath:anyframe/core/hibernate/spring/dynamic-hibernate.xml</value>
      </list>
    </property>
</bean>        

49.12.1.2.Dynamic HQL 정의 파일

DynamicHibernateService 속성 정의 파일 내의 fileNames 값으로 정의한 Dynamic HQL 정의 파일은 다음과 같이 구성된다. <dynamic-hibernate> 태그는 여러 개의 query 태그를 포함할 수 있다. <query> 태그는 Velocity Rule을 접목시켜 Dynamic HQL문을 정의하기 위한 용도이며, 해당 HQL의 식별을 위해 name이라는 속성을 가져야 한다. <query> 태그 내에서는 text 치환과 named parameter 형태를 통해 Dynamic HQL 설정을 지원한다. 다음과 같은 syntax를 사용하면 다양한 형태로 운영시 조건 값에 따라 동적으로 HQL을 변환할 수 있다.

Dynamic Native SQL의 정의 방법도 Dynamic HQL과 같다.

  • :ParameterName : Named Parameter 형태로 변수를 지정할 때 사용한다.

  • {{치환 문자열 키}} : 키에 해당하는 문자열로 치환하여 Query를 수행한다.

  • #if ~ (#elseif) ~ #end : 조건 분기

  • # foreach ~ #end : Loop

  • $velocityCount : foreach 구문내의 Loop index를 체크하고자 하는 부분에 정의한다.

다음은 Dynamic HQL을 포함하고 있는 dynamic-hibernate.xml 파일의 일부로, 입력 조건에 director 정보가 포함되어 있으면, directo