저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

회원 정보 목록 화면과 회원 정보 상세 화면을 반복해서 왔다 갔다 하는 직원이 있다고 하자. 또, 특정 조건으로 회원 목록을 검색해서 1페이지, 2페이지, 3페이지 등 페이지 이동을 하는 직원이 있다고 하자. 이 두 경우 회원 정보를 지속적으로 조회하는 것으로서, 회원 정보를 몰래 수집하는 이상 행동으로 의심해 볼 수 있다. 이런 이상 행동에는 일정 패턴이 반복되는 경우가 많은데, 이런 상황을 발견할 때 유용하게 사용할 수 있는 것이 EPL 패턴이다.


관련 글:

EPL 패턴


EPL은 발생하는 이벤트들이 특정 패턴에 매칭되는지 찾아준다. 다음은 매우 간단한 EPL 패턴의 사용 예다.


EPStatement eps = epService.getEPAdministrator().createEPL(

        "select l from pattern[every l=List ]"

);

eps.addListener(new UpdateListener() {

    @Override

    public void update(EventBean[] newEvents, EventBean[] oldEvents) {

        MemberList l = (MemberList)newEvents[0].get("l"));

        ...

    }

});


위 코드에서 from 절 뒤에 "pattern [ 패턴 ]" 형식으로 패턴을 지정하고 있다. 위 패턴은 매우 단순한 패턴으로, 모든 List 이벤트에 대해 매칭되는 패턴이다. 위 EPL을 실행하면 List 이벤트가 발생할 때 마다 UpdateListener가 실행된다.


위 패턴은 "select l from List l" EPL과 동일한 결과를 발생시키는데, 실제 위와 같이 단순한 패턴을 사용하지는 않는다. EPL은 다양한 패턴 연산자와 구성 요소를 제공하고 있으며 이들을 조합해서 흥미로운 패턴을 만들어낼 수 있다.


패턴의 구성 Atom과 연산자


패턴을 구성하는 주요 Atom(EPL 레퍼런스 문서를 보면 Atom이라고 부름)에는 다음의 두 가지가 있다.


패턴 Atom 

이벤트 필터 Atom

이벤트: List

조건에 맞는 이벤트: List(userId = 'madvirus')

시간 기반 Atom

타이머: timer:interval(10 sec)

스케줄: timer:at(*, 6, *, *, *)


Atom에 대한 연산자는 다음의 4가지 종류가 존재한다.


종류

연산자

반복을 제어하는 연산자

every, [num], until 등

논리 연산자

and, or, not

이벤트의 발생 순서

 ->

표현식 유효 조건

where timer:within, where timer:withinmax, while 등 


연산자의 우선순위는 다음과 같다.

  1. 조건 접미사: where timer:within, where timer:withinmax, while
  2. 단항 연산자: every, not, every distinct
  3. 반복: [num], until
  4. and
  5. or
  6. 순서: ->

연산자 우선순위를 이해하지 않으면 패턴을 다른 의미로 읽거나 작성하게 되므로, 우선순위에 유의해서 패턴을 작성해야 한다.


이벤트 필터


다음 EPL을 보자.


select L from pattern [L=List]


위 EPL에서는 List는 이벤트를 위한 Atom 이다. 패턴에 명시한 이벤트를 참조하려면 '태그이름=이벤트Atom'의 형식으로 태그를 사용해야 한다. 위 코드에서는 'L'이 태그가 된다.


위 EPL을 실행하면 최초에 발생한 List 이벤트만 패칭되며, 이후 발생하는 List 이벤트에 대해서는 매칭되지 않는다. 따라서, List 타입 이벤트가 L1, L2, L3의 순서로 발생하면 L1 이벤트에 대해서만 UpdateListener를 통해 통지 받을 수 있다.


모든 List 이벤트에 대해 매칭되려면 every 연산자를 사용해야 한다.


select L from pattern [every L=List]


위 EPL은 모든 List 이벤트에 매칭된다.


특정 조건을 만족하는 이벤트를 걸러내고 싶다면 다음과 같이 필터 구문을 사용하면 된다.


select L from pattern [every L=List(uri='/member/list', userId='bkchoi')]


"->"를 이용한 발생 순서 매칭


-> 연산자는 매우 유용한 연산자이다. 이벤트의 발생 순서를 지정할 때 -> 연산자를 사용할 수 있다. 아래 패턴은 List 이벤트 다음에 Detail 이벤트가 발생한 경우 매칭된다.


select L, D from pattern [L=List -> D=Detail]


다음은 이벤트 순서와 위 패턴에 매칭되어 선택된 결과를 표시한 것이다.

  • L1, L2, D1, D2, L3, D3 : (L1, D1)

위 결과를 보면 첫 번째로 패턴에 매칭된 이벤트 집합(L1, D1)만 선택된 것을 알 수 있다. 


모든 이벤트 List 이벤트에 대해 매칭되도록 하려면 every 연산자를 사용하면 된다.


select L, D from pattern [every L=List -> D=Detail]


위 패턴은 모든 List 이벤트에 대해 뒤에 Detail이 오면 매칭된다. every 연산자가 -> 보다 우선순위가 높으므로 위 패턴은 [(every L=List) -> D=Detail]과 같다. 다음은 이벤트 흐름에 대해 매칭되어 선택되는 이벤트 집합을 표시한 것이다.

  • L1, L2, D1, D2, L3, D3 : (L1, D1), (L2, D1), (L3, D3)

위 매칭 결과를 보면 (L1, D1)과 (L2, D1)이 선택된 것을 알 수 있다. L1과 L2에 대해 D1이 모두 매칭됐는데, 겹치는 경우 매칭되지 않도록 하고 싶다면 다음과 같이 @SuppressOverlappingMatches 패턴 어노테이션을 사용한다.


select L, D from pattern @SuppressOverlappingMatches [every L=List -> D=Detail]


위 패턴을 사용하면 이벤트 발생 순서에 따라 다음과 같이 (L1, D1)만 선택되고, (L1, D1)과 겹쳐지는 (L2, D1)은 매칭되지 않는다.

  • L1, L2, D1, D2, L3, D3 : (L1, D1), (L3, D3)

-> 연산자를 사용할 때 주의할 점은 -> 연산자 앞 뒤에 지정한 이벤트가 반드시 바로 직후에 발생되야 하는 것은 아니라는 점이다. 예를 들어, 다음과 같은 패턴과 이벤트 순서를 생각해보자.

  • 패턴: select L, D from pattern [every L=List -> D=Detail]
  • 이벤트: L1, V1, D1, D2, L3, D3
이 경우 L1과 D1 사이에 다른 타입의 이벤트 V1이 발생했지만, L1 이벤트 이후에 D1 이벤트가 발생했으므로 (L1, D1) 이벤트 쌍이 매칭되어 선택된다. 즉, '->' 연산자는 바로 뒤에의 의미가 아니라 '이후에'라는 의미이다.


and와 or 연산자


and는 순서에 상관없이 두 표현식이 모두 true면 매칭된다. 예를 들어, 다음 패턴은 List 이벤트와 Detail 이벤트가 도착하면 매칭된다.


select L, D from pattern [L=List and D=Detail]


순서가 상관이 없기 때문에 다음의 두 순서로 이벤트가 들어와도 매칭되서 선택되는 이벤트는 L2, D1 이다. 한 번만 선택되는 이유는 패턴에 every가 없기 때문이다.

  • L2, D1, D2, L3, D3
  • D1, L2, L3, D3
동일 패턴을 사용하면서 다음의 순서로 이벤트가 발생하면 어떻게 될까?
  • L1, L2, D1, D2, L3, D3
이 경우 선택되는 이벤트 조합은 (L1, D1)이다.

아래와 같이 every를 붙이면 어덯게 될까? (every의 우선순위가 and 보다 높기 때문에 and 쪽에 괄호를 넣었다.)

select L, D from pattern [every (L=List and D=Detail)]


다음 순서로 이벤트가 발생할 경우,
  • L1, L2, D1, D2, L3, D3

매칭되는 결과 이벤트는 (L1, D1)과 (L3, D2) 이다.


or는 두 표현식 중 하나만 true면 된다. 즉, 아래 이벤트는 List 이벤트나 Detail 이벤트 중 하나만 발생해도 매칭된다. every가 붙어 있으므로 결과적으로 모든 List 이벤트와 Detail 이벤트가 매칭된다.

select L, D from pattern [every (L=List or D=Detail)]


not 연산자


not은 표현식이 false인 경우 true가 된다. 처음 Statement가 시작될 때에 "not 표현식"은 true 상태로 시작하며, 표현식이 true가 될 때 false가 된다. not은 단독으로 사용되기 보다는 and와 함께 사용된다. 예를 들어, 발권을 하한 뒤 상영관에 입장하기 전에, 표에 있는 할인 쿠폰으로 간신을 구매하지 않은 사용자를 찾고 싶다고 해 보자. 이는 다음과 같은 패턴으로 찾아낼 수 있다.


select i from pattern [

  every i=IssuedTicket -> (Entering(ticketId = i.id) and not UseTicketCoupon(couponId = i.couponId))

]


위 패턴은 모든 영화표발권(IssuedTicket) 이벤트에 대해 이후 같은 표에 대한 입장(Entering) 이벤트가 발생하고, 그 사이에 표의 쿠폰을 사용함(UseTicketCoupon) 이벤트가 발생하지 않으면 매칭된다. 따라서, 이 패턴으로 매칭된 IssuedTicket 이벤트를 이용해서 실시간으로 쿠폰이 사용되지 않은 표를 구매한 고객을 찾아낸다면, 고객에게 SMS를 이용해서 쿠폰 사용을 유도하는 프로모션을 진행할 수도 있을 것이다. (예를 들어, 같은 건물에 있는 커피숍에 가서 500원을 할인 받으라는 등의 프로모션)


Every 연산자


every 연산자는 동작 방식을 잘 이해해서 사용해야 한다. every 연산자를 어떻게 사용하느냐에 따라 패턴 매칭 결과가 달라지는데, 이에 대한 설명은 레퍼런스 문서에 잘 나와 있다. 그래서 레퍼런스 문서에 있는 내용을 발췌해서 아래 표로 정리해 본다.


* 이벤트가 A1, B1, C1, B2, A2, D1, A3, B3, E1, A4, F1, B4 순서로 발생했다고 가정

every 사용 예

설명

매칭 결과 

every (A -> B)

A 이벤트 발생 후, B 이벤트가 발생하면 매칭된다. 매칭된 후 새로운 매처(매칭 검사기)를 시작한다. A가 매칭된 후, 다음 B가 나올 때 까지 새로운 매처가 시작되지 않는다. 즉, A 이벤트 이후에 B 이벤트가 발생하기 전까지의 다른 A 이벤트는 매칭 대상에서 제외된다.

(A1, B1), (A2, B3),

(A4, B4)

every A -> B

(every A) -> B

모든 A에 대해, A 이벤트 발생 후 B 이벤트가 발생하면 매칭된다.

(A1, B1), (A2, B3),

(A3, B3), (A4, B4) 

A -> every B 

A -> (every B)

패턴 Statement가 시작된 뒤, 첫 번째 A 이벤트에 대해 모든 B가 매칭된다.

(A1, B1), (A1, B2),

(A1, B3), (A1, B4) 

(every A) -> (every B)

모든 A 이벤트에 대해, A 이벤트 발생 후 모든 B 이벤트에 매칭된다.

(A1, B1), (A1, B2),
(A1, B3), (A1, B4)
(A2, B3), (A2, B4)
(A3, B3), (A3, B4)
(A4, B4) 


반복 연산자


특정 표현식이 지정한 횟수만큼 반복되면 매칭되도록 하고 싶다면 반복 연산자를 사용하면 된다. 반복 연산자는 '[횟수]' 형식으로 사용한다. 아래 코드는 예이다.


EPStatement eps = epService.getEPAdministrator().createEPL(

        "select s from pattern [every [2] s=SlowResponse ]"

);


eps.addListener(new UpdateListener() {

    @Override

    public void update(EventBean[] newEvents, EventBean[] oldEvents) {

        for (EventBean eb : newEvents) {

            // 길이가 2인 배열

            SlowResponse[] responses = (SlowResponse[]) eb.get("s");

            ...

        }

    }

});


위 패턴을 사용하면 SlowResponse 이벤트가 2번 발생할 때 마다 매칭된다. 매칭 결과를 받는 리스너의 코드를 보면 "s"에 보관된 값이 SlowResponse 배열임을 알 수 있다.


반복 연산자와 not을 사용하면 다음과 같은 패턴을 만들 수도 있다.


select s, o from pattern [

     every [3] ( (s=SlowResponse or o=OvertimeResponse) and not NormalResponse)"

]


위 패턴은 SlowResponse나 OvertimeResponse가 3회 발생하고, 그 사이에 NormalResponse가 발생하지 않은 경우 매칭된다. 즉, 느린 응답이 연속해서 3회 발생하는 상황을 찾아내주는 패턴이다.


반복 연산자와 until


until 연산자는 until 뒤의 표현식이 충족될 때 매칭된다. 아래 패턴은 NormalResponse가 발생하기 전까지 SlowResponse 이벤트를 반복한다.


select s from pattern [ every (s=SlowResponse until NormalResponse) ]


NormalResponse 이벤트 발생 전에 SlowResponse 이벤트가 5번 발생하면 s는 SlowReponse 객체 5개를 갖는 배열이 할당된다.


범위를 갖는 반복 연산자는 다음과 같은 형식을 갖는다.

  • [3:8] - 최소 3번에서 최대 8번까지 반복
  • [3:] - 최소 3번 반복
  • [:8] - 최대 8번 반복
until과 범위를 갖는 반복 연산자를 함께 사용해도 된다. 다음은 예이다.


* S1 S2 N1 S3 S4 S5 N2 순서로 이벤트가 발생했다고 가정 (S:Slow, N:Normal)

 패턴 예

 결과

 every ([2:] s=SlowResponse until NormalResponse)

(S1, S2) *N1 발생 시점에 생성

(S3, S4, S5) *N2 발생 시점에 생성 

 every ([:3] s=SlowResponse until NormalResponse)

(S1, S2) *N1 발생 시점에 생성 

(S3, S4, S5) *N2 발생 시점에 생성

 every ([:2] s=SlowResponse until NormalResponse)

(S1, S2) *N1 발생 시점에 생성

(S3, S4) *N2 발생 시점에 생성

 every ([3:] s=SlowResponse until NormalResponse)

(S3, S4, S5) *N2 발생 시점에 생성


타이머 패턴 가드


where timer:within 가드는 표현식 뒤에 위치하며, 일정 시간 안에 표현식이 true가 되면 매칭되고 지정한 시간이 지나면 매칭되지 않는다. 다음은 타이머 가드의 사용 예이다.


select s from pattern [s=SlowResponse where timer:within(2 sec)]


위 패턴은 최초 2초 안에 SlowResponse 이벤트가 발생하는지 여부를 확인한다. 최초 2초 안에 이벤트가 발생하면 해당 SlowResponse 이벤트가 선택되고 타이머가 종료된다. 2초가 지나면 타이머가 종료되고 더 이상 패턴은 사용되지 않는다.


최근 2초간 발생한 SlowResponse 이벤트를 구하고 싶다면 다음과 같이 every 연산자를 함께 사용한다. 아래 패턴을 실행하면 2초 안에 SlowResponse가 이벤트가 발생할 때 마다 select 결과를 받게 된다.


select s from pattern [(every s=SlowResponse) where timer:within(2 sec)]


아래 코드는 어떤 의미일까?


select s from pattern [every ((s=SlowResponse -> SlowResponse) where timer:within(2 sec))]


위 코드는 최초 시작시 타이머가 시작되어 (SlowResponse -> SlowResponse)가 2초 이내에 발생하는지 여부를 확인한다. 타이머가 종료되면 every 연산자에 의해 새로운 타이머가 시작된다. 위 패턴을 사용했을 때 이벤트 발생 시점과 select 결과는 아래 그림과 같이 이뤄진다. 아래 그림에서 화살표는 타이머의 시작과 끝을 나타낸다.



위 그림과 동일한 시점에 이벤트가 발생했을 때, 아래 코드는 어떤 결과를 만들까?


select s from pattern [every s=SlowResponse -> SlowResponse where timer:within(2 sec)]

// 우선순위에 따라 evern s=SlowResponse -> (SlowResponse where timer:within(2 sec)) 와 동일


이 패턴은 모든 SlowResponse 이벤트가 발생한 후 2초 이내에 SlowResponse 이벤트가 발생하면 매칭된다.


아래 패턴은 어떨까?


select s from pattern [every s=SlowResponse -> (not NormalResponse where timer:within(2 sec)) ]


얼핏 생각하면 SlowResponse 이벤트 발생 후 2초 이내에 NormalResponse가 발생하지 않으면 매칭될 것 같지만, 실제로는 SlowResponse 이벤트가 발생하는 순간에 바로 매칭된다. 실제로 특정 이벤트가 발생하고 나서 일정 시간 안에 다른 이벤트가 발생하지 않는 패턴을 만들고 싶다면 다음에 설명할 time:interval Atom을 사용해야 한다.


시간 Atom


패턴의 Atom은 이벤트 필터 외에 시간 간격 Atom인 timer:interval 을 제공하고 있다. 다음은 time:interval을 사용한 패턴 예이다.

select o from pattern [
    every o=OrderForm -> timer:interval(10 min) and not OrderComplete(id=o.id)
]

위 코드는 OrderForm 이벤트가 발생한 후, 같은 id를 갖는 OrderComplete 이벤트가 10분 안에 발생하지 않으면 매칭된다. 즉, 주문 양식까지 들어왔는데 10분 동안 결제를 하지 않은 경우를 찾아내는 패턴이다.


Posted by 최범균 madvirus

댓글을 달아 주세요

Singleton 패턴을 적용한 매니저 클래스를 이용하여 모델 1 구조보다 견고하고 확장가능한 웹 어플리케이션을 개발하는 것에 대해 알아본다.

JSP 모델 1 구조의 한계

최근에 자바를 사용하여 웹 어플리케이션을 구축하는 개발자들은 JSP/Servlet/EJB 에 대해서 한두번씩은 경험하고 있을 것이다. 특히, 이제 막 웹 어플리케이션 개발 세계에 발을 들인 사람들은 JSP를 공부하고 있을 것이다. 하지만, 아쉽게도 많은 개발자들이 JSP를 이용하여 웹 어플리케이션을 개발할 때, JSP와 자바의 장점을 살리지 못하고 있는 것 같다. 특히, JSP 페이지에서 모든 필요한 비지니스 로직을 구현하는 경우가 많으며, 증권 사이트와 같은 대형 사이트를 구축할 경우에는 종종 JSP 페이지의 소스 코드가 1000 줄이 넘는 경우도 발생하고 있다. 즉, JSP 페이지에서 표현을 위한 HTML 부분을 제외한 나머지 (비지니스) 로직 부분이 소스 코드의 반절 이상을 차지한다는 것이다. 특히, EJB를 사용하지 않는 경우 그 정도는 더 심하다고 할 수 있다. 실제로 대규모가 아닌 사이트의 경우 대부분 EJB를 사용하지 않으며, 따라서 EJB를 사용하지 않는 중소규모의 웹 어플리케이션의 경우 많은 개발자들이 다음과 같은 구조로 개발하곤 한다.


위 그림은 JSP에서 모든 필요로 하는 비지니스 로직을 구현하는 것을 보여주고 있다. 이러한 형태로 웹 어플리케이션을 개발하는 것의 장점은 쉽다는 점이며, 개발 기간이 상대적으로 짧다는 점이다. 하지만, 이러한 모델1 구조는 다음과 같은 단점을 갖게 된다.

  • 동기화 문제: 여러 JSP 페이지에서 동시에 시스템의 동일한 자원에 접근하므로 동기화가 문제가 될 수 있으며, JSP 페이지에서 직접적으로 동기화를 처리할 수 있다고 해도 그리 간단하게 해결되지는 않는다.
  • 디버깅의 어려움: 같은 작업 단위(예를 들어, 게시판 글쓰기/글읽기/목록 보기)에 속한 것들을 여러 개의 JSP 페이지에서 나누어 처리하므로 각 페이지 별로 디버깅해야 한다.
  • 유지/보수의 어려움: 같은 작업 단위에 속한 JSP 페이지들이 각각 비지니스 로직을 갖고 있기 때문에 내부 로직이 변경될 경우 영향을 받는 모든 JSP 페이지를 손봐야 한다.
물론, 이러한 문제는 'JSP-서블릿'으로 구성된 모델2 구조와 EJB를 사용함으로써 많은 부분 해결할 수 있다. 하지만, 서블릿과 JSP의 관계를 견고하고 확장성있게 설계하는 것은 그리 쉽지 않으며, EJB를 사용하여 개발하는 것은 더더욱 쉽지 않다. 또한, 모델 2 구조와 EJB를 사용하기 위해서는 세밀한 컴포넌트 설계가 필요하며 따라서 개발기간이 그 만큼 길어지게 된다.

이러한 문제는 이 글에서 설명할 매니저 클래스를 사용함으로써 해결될 수 있다. 여러분은 매니저 클래스를 사용함으로써 모델 1 구조와 모델 2 구조의 중간적인 형태를 구현할 수 있으며, 따라서 모델 1 구조의 단점을 극복하면서 동시에 모델 2 구조의 장점을 취할 수 있게 된다. 매니저 클래스는 Singleton 패턴을 사용하여 구현하며, 먼저 Singleton 패턴에 대해서 알아보기로 하자.

Singleton 패턴

여러 패턴 중에서 Singleton 패턴은 비교적 구현이 간단하며, 정의는 다음과 같다. (이 글에서는 Singleton 패턴이 왜 필요한지에 대한 설명은 하지 않겠으며, Singleton 패턴의 실제 구현에 초점을 맞추도록 하겠다).

"한 클래스의 인스턴스가 오직 한 개만 존재한다."

이제 좀 더 쉽게 이 말을 설명해보자. 우리는 흔히 특정한 클래스의 인스턴스(또는 객체)를 생성할 때 다음과 같이 new 키워드를 사용한다.

Date date1 = new Date();
Date date2 = new Date();

위 코드에서 첫번째 new 연산과 두번째 new 연산은 서로 다른 Date 클래스의 인스턴스를 생성하며, 따라서 date1과 date2는 서로 다른 인스턴스를 참조하게 된다. 즉, Date 클래스의 인스턴스가 여러개 존재할 수 있는 것이다. 반면에 Singleton 패턴을 적용한 클래스의 경우는 new 키워드를 사용하여 인스턴스를 생성하는 일반적인 객체 생성 방법을 사용하지 않는다. Singleton 패턴을 적용한 클래스는 getInstance()라는 메소드를 사용하여 인스턴스를 구한다. 보통 다음과 같은 형태로 Singleton 패턴을 사용하는 클래스의 인스턴스를 구한다.

SingletonClass sac1 = SingletonClass.getInstance();
SingletonClass sac2 = SingletonClass.getInstance();

여기서 SingletonClass 클래스의 static 메소드인 getInstance()는 단 한 개의 인스턴스를 리턴하며, 따라서 sac1과 sac2는 같은 인스턴스를 참조하게 된다.

Singleton 패턴의 구현

Singleton 패턴을 적용한 클래스는 오직 한 개의 인스턴스만을 갖는다고 하였다. 이 말은 아무곳에서나 new 키워드를 통해서 인스턴스를 생성할 수 있으면 안 된다는 것을 의미한다. 따라서, Singleton 패턴을 구현하기 위해서는 먼저 생성자를 private으로 선언해야 한다. Singleton 패턴을 적용한 클래스가 SingletonClass라고 할 경우 전체적인 클래스의 정의는 다음과 같은 형태를 취한다.

class SingletonClass {
private static SingletonClass instance = new SingletonClass();

public static SingletonClass getInstance() {
return instance;
}
private SingletonClass() {
// 초기화
}

// 기타 다른 메소드의 정의가 온다.
.....
}

위 코드를 보면 static 필드로 instance가 정의되어 있는 것을 알 수 있다. static으로 선언되었기 때문에, instance 필드는 SingletonClass 클래스에 대해서 오직 한 개만 존재하며 따라서 Singleton 패턴의 정의를 만족시킨다. getInstance() 메소드는 단순히 instance 필드를 리턴하면 된다. 이렇게 구현함으로써 SingletonClass 클래스를 사용하는 모든 것들은 오직 한 개의 인스턴스를 공유하게 된다. 여기서 한 가지 짚고 넘어갈 것이 있다면, SingletonClass 클래스가 실제로 사용되는 지의 여부에 상관없이 항상 SingletonClass 클래스의 인스턴스가 생성된다는 점이다. 실제로 인스턴스를 생성할 필요가 있는 시점은 처음으로 getInstance() 메소드가 호출되는 때이며, 따라서 다음과 같이 코드를 변경할 수 있다.

class SingletonClass {
private static SingletonClass instance = null;

public static SingletonClass getInstance() {
if (instance == null)
instance = new SingletonClass();

return instance;
}
private SingletonClass() {
// 초기화
}

// 기타 다른 메소드의 정의가 온다.
.....
}

이제 getInstance()가 처음으로 호출되는 순간에 SingletonClass의 인스턴스가 생성된다. (이처럼 처음부터 인스턴스를 생성하지 않고 필요한 순간에 인스턴스를 생성하는 것을 'Lazy Instantiation' 이라고 하며, 시스템 자원을 효율적으로 사용하기 위해서 많이 사용된다). 하지만, 위 코드를 살펴보면 문제점을 발견할 수 있을 것이다. 바로 동기화이다. 동시에 두 개의 쓰레드가 getInstace()를 호출한다면 어떻게 될까? 알맞게 동기화 처리가 되어 있지 않기 때문에, 두 개의 인스턴스가 생성될 수 있다. 이는 Singleton 패턴의 정의에 어긋나는 것이며, 따라서 멀티 쓰레드 환경에서 문제가 발생하지 않도록 동기화 처리를 해 주어야 한다. 동기화 처리를 하도록 변형된 getInstance() 메소드는 다음과 같다.

public static SingletonClass getInstance() {
if (instance == null)
synchronized(SingletonClass.class) {
if (instance == null)
instance = new SingletonClass();
}
return instance;
}

위 코드에서 if 문을 두번한 것을 알 수 있다. 이 처럼 두 번 체크 하는 것을 'double-check idiom'이라 한다. 이와 관련된 내용은 Doug Lea가 쓴 'Concurrent Programming in Java, Second Edition: Design Principles and Patterns'(Addison Wesley)에 자세한 내용이 있으니 참조하기 바란다. 또한, Lazy Instantiation에 대한 내용은 JavaWorld Tip 65에서 참고할 수 있다.

Singleton 패턴을 이용한 매니저 클래스

여러분이 쇼핑몰을 구축한다고 해보자. 이 웹 어플리케이션은 제품 주문/주문 취소/주문 제품 목록 표시 등의 기능을 제공해야 한다. 여기서 나열한 기능은 모두 "제품 구매"라는 하나의 처리 과정에 속하며, 따라서 하나의 처리 과정에 속하는 기능을 하나의 클래스에서 관리하도록 할 수 있다. 이처럼 특정한 기능들을 제공하고 관리하는 클래스를 매니저 클래스라 할 수 있으며, 보통 클래스 이름에 Manager 라는 단어를 붙이곤 한다. 예를 들어, 회원 관리 기능을 제공하는 클래스를 MemberManager라고 이름지을 수 있다.

웹 어플리케이션은 이처럼 일련의 기능들이 하나의 매니저 클래스로 묶이는 경우가 많다. 예를 들어, 회원 관리/구매 관리/게시판/방명록/배너 광고 관리 등은 각각 별도의 매니저 클래스를 통해서 필요한 기능들을 제공할 수 있을 것이다. 여기서 중요한 점은 매니저 클래스에서 모든 중요한 기능-거의 대부분은 DB 처리일 것이다-을 수행한다는 것이다. 예를 들어, 회원 관리를 생각해보자. 회원 관리에는 회원 추가/회원 탈퇴/회원 정보 변경 등의 기능이 필요하며, MemberManager 클래스가 이러한 기능을 제공한다고 해보자. 이 경우 전체적인 구조는 다음과 같을 것이다.


즉, 회원과 관련된 모든 기능은 MemberManager 클래스를 통해서 이루어지는 것이다. 즉, 매니저 클래스가 모든 기능의 엔트리 포인트(Entry Point; 진입 지점)가 되는 것이다. 이처럼 매니저가 중요한 모든 기능을 제공하는 경우 매니저 클래스의 인스턴스가 두 개 이상 생성되는 것은 문제가 될 수 있으며, 동기화 처리에 있어서 특히 문제가 될 수 있다. 따라서 매니저 클래스는 Singleton 패턴을 적용하여 구현해야 하며, 따라서 대부분의 매니저 클래스는 다음과 같은 형태로 정의된다.

public SomeManager extends SuperClass implements SuperInterface {
private static SomeManager instance = null;

public static SomeManager getInstance() { // Singleton 패턴
if (instance == null)
synchronized(syncObj) {
if (instance == null)
instance = new SingletonClass();
}
return instance;
}
private SomeManager() {
// 초기화
}
public void doSomeFunction1(...) {
// 어떤 기능을 수행한다.
}
public AnyBean getSomeInfo2(...) {
// 어떤 정보를 구한다.
return abean;
}
.....
}

필요에 따라 어떤 기능을 하는 doSomeFunction1()이나 getSomeInfo2()와 같은 메소드의 선언부분에 동기화를 위한 목적으로 synchronized 키워드를 추가할 수 있으며, 또는 메소드 내부의 필요한 곳에서 synchronized() 블럭을 사용하여 동기화를 할 수도 있다. 매니저 클래스는 다음과 같은 형태로 사용하면 된다.

SomeManager sMgr = SomeManager.getInstance();
// 필요한 기능을 하는 메소드 호출
sMgr.doSomeFunction1();
.....

JSP 페이지와 매니저 클래스

이제 마지막으로 JSP 페이지에서 매니저 클래스를 사용하는 것에 대해서 알아보자. JSP 페이지와 매니저 클래스의 관계는 다음 그림과 같다.


위 그림에서 실제 내부 로직은 매니저 클래스에서 담당하며 JSP는 매니저에 데이터를 넘겨주거나(보통 자바빈 컴포넌트를 넘겨준다) 매니저로부터 필요한 데이터를 넘겨받을 것이다. JSP 페이지는 단지 필요한 매니저를 사용하면 될 뿐, 매니저 클래스에서 내부적으로 어떤 자원을 사용하는 지, 내부 로직이 어떻게 되는 지에 대해선 전혀 알 필요가 없다. 또한, 동기화 문제 역시 매니저 클래스 내부에서 처리되므로 JSP는 동기화에 대한 특별한 처리를 할 필요가 없다.

실제로 JSP 페이지에서 매니저를 사용하는 것은 매우 간단하다. 먼저 특정 매니저 클래스의 getInstance()를 호출하여 인스턴스를 구한 후, 필요로 하는 기능을 제공하는 메소드를 호출하면 된다. 예를 들어, 신규 회원 가입을 처리하는 JSP 페이지가 있다고 해 보자. 이 JSP 페이지는 HTML 폼을 통해서 회원의 이름, 사용할 ID, 암호, 주소 등 다양한 정보를 입력받을 것이다. 이때, 신규 회원 가입 JSP 페이지는 다음과 같은 순서로 신규 회원 가입을 처리할 것이다. (여기서, MemberManager 클래스는 회원과 관련된 기능을 제공하는 매니저 클래스이며, MemberBean 클래스는 사용자가 입력한 정보를 저장하는 자바빈 컴포넌트라고 가정한다).

  1. <jsp:useBean>와 <jsp:setProperty> 액션 태그를 사용하여 사용자가 입력한 정보를 자바빈 컴포넌트에 저장한다.
  2. MemberManager 클래스의 Singleton 인스턴스를 구한다.
  3. 매니저의 회원 가입 메소드를 호출한다. 이 때, 1에서 생성한 자바빈 컴포넌트를 인자로 넘겨준다.
이를 JSP 코드로 표시하면 다음과 같을 것이다.

<%@ page import = "com.mypackage.member.MemberManager" %>
<%@ page errorPage = "error_page.jsp" %>
<%-- 자바빈 컴포넌트를 생성해서 사용자가 입력한 값을 저장한다. --%>
<jsp:useBean id="newData" class="com.mypackage.member.bean.MemberBean">
<jsp:setProperty name="newData" property="*" />
</jsp:useBean>
<% MemberManager mMgr = MemberManager.getInstance();
mMgr.registerNewMember(newData); // 새로운 회원 추가
// 예외가 발생할 경우 에러 페이지인 error_page.jsp로 이동
%>
<!-- 실제 표현 부 -->
<html>
....
</html>

회원의 정보를 변경하는 페이지 역시 다음과 같은 구조를 이룰 것이다.

<%@ page import = "com.mypackage.member.MemberManager" %>
<%@ page errorPage = "error_page.jsp" %>
<%-- 자바빈 컴포넌트를 생성해서 사용자가 입력한 값을 저장한다. --%>
<jsp:useBean id="updateData" class="com.mypackage.member.bean.MemberBean">
<jsp:setProperty name=" updateData" property="*" />
</jsp:useBean>
<% MemberManager mMgr = MemberManager.getInstance();
mMgr.updateMemberInfo(newData); // 새로운 회원 추가
// 예외가 발생할 경우 에러 페이지인 error_page.jsp로 이동
%>
<!-- 실제 표현 부 -->
<html>
....
</html>

이 두 JSP가 매우 간단하다는 것을 알 수 있다. 동기화를 비롯한 모든 처리가 매니저를 통해서 이루어지기 때문에 웹 어플리케이션의 전체적인 구조가 견고해진다. 또한, 내부 로직이 변경되거나 MemberBean 클래스가 변경되더라도 단지 MemberManager 클래스만 변경하면 될 뿐, JSP 페이지는 거의 영향을 받지 않는다. 따라서 유지/보수가 JSP에서 비지니스 로직을 직접적으로 구현하는 것에 비해 훨씬 용이하다. 그리고 매니저 클래스를 사용할 경우 JSP에서 모든 걸 구현할 때에 비해 좀 더 손쉽게 모델 2 구조로 확장할 수 있으며, EJB로의 확장 역시 용이하다.

하지만, 아쉽게도 디버깅에 있어서는 아직 별다른 진전을 보이지 못한다. 그저 여러 JSP 페이지가 아닌 하나의 .java 파일을 검사하면 된다는 것에 위안을 삼아야 할 처지이다. 자바 개발자에게 있어서 디버깅은 정말로 골치덩어리인거 같다. 하루라도 빨리 개발자들을 조금이라도 편하게 해줄 자바 디버깅 툴이 나오길 바랄 뿐이다.

결론

Singleton 패턴을 적용한 매니저 클래스는 JSP에서 로직과 표현을 담당하는 모델 1 구조와 로직은 서블릿에서 담당하고 표현은 JSP에서 담당하는 모델 2 구조의 중간 형태를 구현할 수 있도록 해 준다. 매니저 클래스를 사용함으로써 모델 1 구조의 단점을 극복하고 좀 더 견고한 웹 어플리케이션을 개발할 수 있게 되었으며, 차후에 좀 더 손쉽게 웹 어플리케이션을 확장할 수 있게 되었다. 또한 매니저 클래스는 웹 어플리케이션 뿐만 아니라 대부분의 어플리케이션에서 사용할 수 있으므로 여러분의 어플리케이션에서 손쉽게 활용할 수 있을 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요