달력

22025  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
Posted by tornado
|
[방문히트이벤트] 14000 히트를 잡아라! (이웃한정)
wingedzero님이 당첨되었습니다.

'이것저것 > 낙서장' 카테고리의 다른 글

노트북 ㅡㅡ; 고민이당...  (0) 2005.10.26
[link] window 용 tail utility  (0) 2005.10.25
웬 모기가 이리 많어 ㅡㅡ;  (0) 2005.10.11
음악이 끝장이군~ Larry's World  (0) 2005.09.29
13000 히트 이벤트!!!  (2) 2005.09.28
Posted by tornado
|

http://itcontents.com/sommar/dynamic_SQL.html

 

동적 SQL의 축복과 저주

원문 : SQL Server MVP Erland SommarskogSQL Text
번역 : quest, ASP MVP (microsoft.public.kr.asp.qna)

Microsoft SQL 서버에 대한 여러 뉴스그룹에서 왜 다음 쿼리가 불가능한지를 문의하는 사람들을 종종 보게 된다.:

 SELECT * FROM @tablename SELECT @colname FROM tbl SELECT * FROM tbl WHERE x IN (@list)

많은 경우에 간략한 예와 함께 동적 SQL을 사용하세요 라는 답변이 달리곤 하지만, 답변하는 사람들조차 동적 SQL 문의 사용시에 주의해야 할 사항에 대한 언급을 잊는 경우가 많다.

이 기사에서는 MS SQL Server의 저장 프로시저에서 동적 SQL의 사용에 대해 살펴보고, 조심스럽게 다루어야할 여러 강력한 특징들에 대해 얘기하고자 한다. 그런 특징들에 대해 설명하기 전에, 우선 왜 저장프로시저를 사용해야 하는지에 대해 논의할 것이다. 그 다음에 저장 프로시저를 사용함으로써 얻는 장점과 동적 SQL 효과간에 충돌에 대해 얘기해볼 것이다. SQL Injection이라고 알려진 일반적인 보안 문제에 대해 언급하고, 몇가지 좋은 코딩 습관에 대해 알아보고자 한다. 마지막으로, 동적 SQL이 좋은 해결책으로 활용되는 경우와 그렇지 못한 경우에 대해 살펴볼 것이며, 후자의 경우에는 대신 사용가능한 방법을 제안하고자 한다.

목 차:

   왜 저장 프로시저를 사용하는가?
   EXEC()와 sp_executesql
      EXEC()
      sp_executesql
      어느 것을 사용해야 할까?
      커서(Cursors)와 동적 SQL
   동적 SQL과 저장 프로시저
   SQL Injection - 심각한 보안 문제
   좋은 코딩 습관과 동적 SQL
   동적 SQL을 사용(하지 말아야)하는 일반적인 경우
      select * from @tablename
      select * from sales + @yymm
      update tbl set @colname = @value where keycol = @keyval
      select * from @dbname + '..tbl'
      select * from tbl where col in (@list)
      select * from tbl where @condition
      동적 검색 조건 (Dynamic Search Conditions)
      select * from table order by @col
      select top @n from table order by @col
      create table @tbl
      링크드 서버 (Linked servers)
      오픈쿼리 (OPENQUERY)
      열의 너비를 동적으로 변화시키고자 할 때 (Dynamic Column Widths)
   감사의 글


왜 저장 프로시저를 사용하는가?

동적 SQL이 실제로 무엇인가를 살펴보기 전에, 왜 저장 프로시저를 사용해야 하는지를 먼저 알아볼 필요가 있다. 저장 프로시저를 사용하지 않고 클라이언트(※ 역주: 여기서의 클라이언트는 SQL 서버에 대한 클라이언트를 의미합니다. 웹프로그램 개발의 경우 웹서버가 여기에 해당하며, 클라이언트 코드는 ASP와 같은 Server Side Script를 의미합니다.) 혹은 (COM+와 같은) 중간층(middle layer)에서 직접 SQL 문으로 이루어진 명령문을 내보내는 복잡한 응용프로그램을 작성할 수도 있다. 세련되어 보이기 때문에 저장 프로시저를 사용하는 것은 아니며, 이에는 분명 장점이 존재한다.

1. 권한 체계 (The Permission System)

저장 프로시저는 사용자들에게 데이타에 대한 제한적인 접근을 허용케하는 전통적인 수단이다. 쿼리 분석기와 같은 도구를 이용할 경우 원하는 어떠한 작업도 수행가능하므로, 사용자들은 SELECT, INSERT, UPDATE 혹은 DELETE 같은 문장을 직접 실행할 수 있는 권한을 가져서는 안된다. 예를 들어, 권한을 가진 사용자가 직원 데이타베이스에 접근하게 될 경우, 쉽게 봉급을 인상시킬 수 있다. 저장 프로시저를 사용하면, 해당 프로시저의 실행은 프로시저 소유자의 권한을 이용하게 되므로, 사용자들은 테이블에 대한 직접적인 권한을 필요로 하지 않게 된다.

요즘은 이 상황에 대해 몇가지 선택 가능한 사항이 더 존재한다. 사용자 계정에 직접 권한을 부여하기 보다는, 비밀번호가 필요한 고정 서버 역할(application role)에 접근권한을 부여할 수 있으며, 그런 비밀번호를 응용프로그램 안에 숨겨둘 수 있다. SQL 서버에서 고정 서버 역할은 7.0 버전부터 지원되었으며, COM+와 같은 중간층을 사용하면 사용자가 SQL 서버에 직접 접근할 필요가 없다는 점에서 더 안전하다.

하지만 고정 서버 역할이나 COM+ 등을 사용하지 않는다면, SQL 서버 보안 측면에서 저장 프로시저는 여전히 중요한 의미를 가진다.

2. 실행계획 캐쉬 (Caching Query Plans)

저장 프로시저를 사용하는 다른 중요한 이유는 성능을 향상시키기 위해서이다. 저장 프로시저가 최초로 실행되면, SQL 서버는 해당 프로시저에 대한 실행계획을 생성시키며, 이 실행계획이 캐쉬에 저장된다. 해당 저장프로시저가 재실행 요청을 받으면, SQL 서버는 저장된 실행계획을 재사용한다. 실행계획이 만료되거나 혹은 SQL 서버가 새로운 실행계획을 생성시켜야 할 이유가 생길 때까지 해당 실행계획은 캐쉬에 유지된다. (이 과정은 프로시저가 실행되는 동안에 일어나는데, 여기에 대한 논의는 이 기사의 범위를 범어난다.)

SQL 서버는 저장 프로시저 외부에서 실행되는 SQL 문장들에 대한 실행계획도 캐쉬한다. 자동 매개 변수화 과정(auto-parameterization) 또한 수행되어, 만약 다음과 같은 문을 실행시킬 경우, :

 SELECT * FROM pubs..authors WHERE state = 'CA' go SELECT * FROM pubs..authors WHERE state = 'WI'

SQL 서버가 쿼리를 다음과 같이 캐쉬하므로, 2번째 Select 문장은 첫번째 문장의 실행계획을 재사용한다.

 SELECT * FROM pubs..authors WHERE state = @1

SQL 문장이 좀더 복잡해 질 경우에는, SQL 서버가 자동 매개 변수화에 실패할 수도 있다. 심지어 유사한 쿼리문에서 공백 문자(white space)의 차이로 인해 SQL 서버가 캐쉬에서 해당 문장을 찾는데 실패하는 경우를 본 적도 있다. 결론은 SQL 서버가 순수 SQL 문장을 캐쉬하는데에도 좋은 성능을 발휘하지만, 저장 프로시저를 사용하는 경우 쿼리 실행계획의 재사용 확률이 더 높다는 것이다.

작고 짧은 실행시간을 가지며 자주 실행되는 SQL문 혹은 저장 프로시저의 경우에 캐쉬는 더욱 중요한 의미를 가진다. 실행계획을 만드는데 500ms 가 소요되면 상당한 과부하가 될 수도 있기 때문이다. 반면에, 20분동안 실행되는 프로시저의 실행계획을 세우는데 3초가 소요되는 것은 대단한 문제가 아니다. 만약, 아직도 SQL 6.5를 사용하고 있다면, 해당 버전의 SQL 서버는 순수한 SQL 문에 대한 캐쉬를 하지 않으며, 저장 프로시저에 대한 실행계획만을 캐쉬에 저장한다는 사실을 알아야 한다.

3. 네트워크 소통량 최소화 (Minimizing Network Traffic)

이것 역시 성능문제이다. 50줄 이상의 복잡한 Select 문장이 있으며, 각 실행시마다 Where 절 조건문이 조금씩 변한다고 가정해보자. 이 문장을 저장 프로시저에 넣을 경우, 네트워크를 통해 전달되는 데이타 소통량이 상당히 감소하게 되며, 해당 프로시저가 자주 실행될 수록 성능향상 효과가 증대된다.

서로 연관성을 가진 여러개의 SELECT / INSERT / UPDATE 문장으로 구성된 업무규칙이 존재할 경우 이러한 효과는 더욱 커진다. 저장 프로시저를 사용하면, 서버내부에서 이동하는 모든 데이타를 임시 테이블 혹은 변수에 저장할 수 있게 된다. 만약 순수 SQL 문장을 이용한다면, 서버와 클라이언트 혹은 중간층 사이에 데이타를 이동시켜야 한다. (사실, 이것이 전적으로 맞는 말은 아니다. SQL 문만을 사용해서도 임시 테이블을 생성시키고 거기에 데이타를 저장할 수 있다. 하지만 Connection pooling과 disconnected record sets에 대한 주의를 기울여야 한다.)

4. 출력매개변수 사용 (Using Output Parameters)

단일 행을 반환하는 SQL 문장을 실행시키고 싶을 경우에, SQL 문만 사용하면 결과 집합(record set)을 레코드셋으로 반환받아야 한다. 하지만, 저장 프로시저를 사용할 경우에는 성능이 월등한 출력매개변수의 사용이 가능하다. 단일 쿼리에 대한 실행시간의 차이는 무시할 수 있겠지만, 만약 다음과 같은 작업을 SQL 서버에 수천번 해야 한다면, 결과 집합으로 값을 받는 것에 비해 @key를 출력매개변수로 반환받는 경우의 이점은 엄청나게 커지게 된다.:

 INSERT tbl (...) VALUES (...) SET @key = @@identity

5. 업무규칙 모듈화 (Encapsulating Logic)

이것은 보안 혹은 성능향상과 관련된 것은 아니지만, 코드를 모듈화하는 방법의 하나이다. 저장 프로시저를 사용하면, SQL 문을 만들어내기 위해 클라이언트 코드와 씨름할 필요가 없다. 하지만, 이러한 이유때문에 저장 프로시저를 사용해야 한다고 말할 수는 없다. (SQL 코드가 클라이언트측 주요 언어의 문법에 묻혀 버리기는 하겠지만) 여전히 여러 매개변수들로부터 SQL 문을 조합하는 것이 가능하다.

좀 특별한 경우를 예로 들어 보겠다: 만약 쿼리 분석기 외의 다른 응용프로그램이 없다면, 관리자들을 위한 저장 프로시저를 작성하게 된다는 말이며, 이런 경우에는 저장 프로시저가 업무규칙 모듈화를 위한 유일한 수단이 된다.

6. 의존성 파악 (Keeping Track of what Is Used)

수백개의 테이블이 존재하는 복잡한 시스템에서, 간혹 어디에서 어떤 테이블 혹은 칼럼이 참조되었는지 알고 싶을 때가 있다. 이를테면, 만약 칼럼을 변경할 경우에 어떤 일이 일어날지 알고 싶은 경우가 있을 수 있다. 만약 모든 코드가 저장 프로시저에 보관되어 있다면, 참조된 개체를 찾기 위해 저장 프로시저의 코드만 살펴보면 된다. 또는 간단히 변경하고자 하는 칼럼 혹은 테이블을 누락시킨 데이타베이스를 생성시켜 어떤 일이 일어나는지 관찰할 수도 있다. 시스템 테이블 sysdepends와 시스템 저장 프로시저 sp_depends를 이런 목적에 사용할 수도 있지만, sysdepends내에 보관된 정보를 온전히 정확하게 유지시키기는 어렵다.

응용프로그램에서도 순수 SQL 문을 사용가능하도록 허용 한다면, 문제는 더욱 심각해진다. 훨씬 많은 양의 코드를 살펴봐야 하며, status와 같은 일반적인 이름을 가진 칼럼들은 놓치기도 쉽다. 그리고 sysdepends는 완전히 무의미하게 된다.


EXEC()와 sp_executesql

MS SQL 서버에서 동적 SQL을 실행시키는 2가지 방법은 EXEC()와 sp_executesql이다.

EXEC()

EXEC()는 다음 예제와 같이 그 사용법이 아주 간단하다.:

 SELECT @table = 'sales' + @year + @month EXEC('SELECT * FROM ' + @table)
비록 위의 예가 상당히 단순해 보이지만, 여기에는 놓쳐서는 안될 중요한 점이 존재한다. 첫번째 중요한 점은 비록 해당 문장이 저장 프로시저 내에 존재하더라도 현재 사용자의 권한으로 실행된다는 것이다. 두번째로 중요한 것은 EXEC()가 저장 프로시저의 실행을 위한 EXEC와 매우 유사하다는 점이다. 하지만 이 예제에서는 저장 프로시저를 호출하는 대신에, 단일 SQL 문을 일괄실행시켰다. 마치 저장 프로시저를 호출할 때처럼, 해당 일괄실행문은 호출하는 저장 프로시저와는 다른 실행범위(scope)를 가지게 된다. 여기에는 몇가지 중요한 의미가 내포되어 있다.:
  • SQL 일괄실행문 안에서는 호출하는 저장 프로시저의 지역변수 혹은 매개변수에 접근할 수 없다.
  • USE 문의 사용이 호출하는 저장 프로시저에 영향을 미치지 않는다.
  • SQL 일괄실행문에서 생성된 임시 테이블은 일괄실행문이 종료되면 삭제(drop)되기 때문에, 마치 저장프로시저가 종료된 경우와 마찬가지로, 호출하는 저장 프로시저에서 접근할 수 없다. 그러나, 일괄실행문 내부에서는 호출하는 저장 프로시저에서 생성된 테이블에 접근가능하다.
  • SQL 일괄실행문 내에서 SET 문장을 사용하면, SET 문장의 영향력은 일괄실행문 내부에서만 유지된다.
  • SQL 일괄실행문의 실행계획은 호출하는 저장 프로시저의 실행계획의 일부가 아니다. 해당 쿼리문의 캐쉬여부는 클라이언트 프로그램에서 SQL 문장만을 사용하는 경우와 동일하다.
  • SQL 일괄실행문이 (트리거내의 Rollback처럼) 일괄실행을 종료시키는 결과를 낳았을 때는, 동적 SQL의 일괄처리가 종료될 뿐만 아니라, 호출하는 저장 프로시저 (그리고 해당 프로시저를 호출한 다른 저장 프로시저도) 역시 종료된다.

정상적으로 저장 프로시저를 호출할 때와는 달리, EXEC()문에서는 매개변수 혹은 반환값을 사용할 수 없다. @@error 값은 일괄실행문의 마지막 문장의 실행결과에 관계가 있다. 그러므로, EXEC()내부에서 에러가 발생하더라도 뒤따르는 명령문이 성공적으로 수행되었다면, @@error는 0의 값을 가지게 된다.

EXEC()는 SQL 6.0에서 도입되었다.

EXEC(@sql)와 EXEC @sp를 혼돈하지 말아라. 후자는 이름이 @sp인 저장 프로시저를 실행시킨다.

sp_executesql

sp_executesql는 SQL 7에서 도입되었으며, 동적 SQL 문자열 내부로 입력과 출력을 위한 매개변수를 전달할 수 있다는 장점이 있다. 출력 매개변수를 사용하는 간단한 예는 다음과 같다. (※ 역주 : sysname은 nvarchar(128)과 같은 기능의 시스템 제공 사용자 정의 데이타 형식으로 데이타베이스 개체 이름을 참조할 때 사용됩니다.).:

 DECLARE @sql nvarchar(4000), @col sysname, @min varchar(20) SELECT @col = N'au_fname' SELECT @sql = N'SELECT @min = convert(varchar(20), MIN(' + @col + N')) FROM authors' EXEC sp_executesql @sql, N'@min varchar(20) OUTPUT', @min OUTPUT SELECT @min 

이 장점으로 인해, 동적 SQL 문을 사용할 때 EXEC()에 비하여 sp_executesql을 사용하면 지역변수로 값을 받아내기가 훨씬 쉬워졌다. (EXEC()에서도 INSERT EXEC()를 이용하여 동일한 작업을 수행할 수 있지만, 수월하지는 않다.)

sp_executesql의 첫번째 인자는 SQL 문(Unicode 문자열)으로, SQL 문법상 변수가 허용되는 곳에는 매개변수를 사용할 수 있다. (그러므로, 여전히 칼럼이름 혹은 테이블이름에 변수를 사용할 수는 없다). 매개변수의 데이타 형은 ntext이어야 하므로, nvarchar 형식의 변수를 사용하여야 한다. SQL 문이 상수로 전달되려면, Unicode 형식임을 나타내기 위해 N 접두어를 인용부호 앞에 붙여야 한다. SQL 문에는 @로 시작하는 매개변수가 포함될 수 있는데, 여기에 사용되는 매개변수들은 동적 SQL문의 외부에서 사용된 변수와는 전혀 별개의 변수이다. sp_executesql에서 사용되는 SQL 문에는 다른 곳에서 사용되는 SQL 문과 마찬가지의 문법이 적용되므로 여전히 칼럼명 혹은 테이블명에는 변수를 사용할 수는 없으므로, 동적으로 결정되는 경우에는 조합시킬 문자열 내에 포함시켜야 한다.

sp_executesql의 두번째 인자는 저장 프로시저 선언부와 마찬가지의 문법으로 매개변수 및 기본값이 정의된 매개변수 정의 목록이다. (온라인 도움말에는 매개변수에 대한 설명이 누락되어 있다.) 매개변수 정의 목록 역시 ntext 데이타 형이다. SQL 문장에서 사용된 모든 변수는 매개변수 선언목록에 명시되어야 한다.

sp_executesql의 나머지 인자는 매개변수 선언목록에서 선언된 것들로, 이들은 선언된 순서로 사용되거나 혹은 변수 이름과 같이 사용가능하다.

sp_executesql는 여러가지 이유로 인해 EXEC()보다 선호된다. sp_executesql 를 사용하면, 사용자가 직접 매개변수를 제공할 수 있기 때문에, SQL 서버가 매개변수 자동화를 수행하도록 의존할 필요가 없다는 것도 그 이유중의 하나이다. 그러므로, sp_executesql를 사용하면 캐쉬 사용의 가능성이 더 높아진다. (그래도 여전히 공백 문자의 사용은 주의를 기울여야 한다.) SQL injection좋은 코딩 습관에 대한 얘기를 하면서 sp_executesql의 다른 장점에 대해 다시 언급할 것이다.

EXEC()에 대해 얘기되었던 특징들은 sp_executesql에도 마찬가지로 적용된다.:

  • SQL 코드는 고유한 실행범위(scope)를 가지므로, 호출한 저장 프로시저에서 변수에 접근할 수 없다.
  • 현재 사용자의 권한이 적용된다.
  • USE 문장의 사용이 호출한 저장 프로시저에 영향을 미치지 않는다.
  • 호출한 저장 프로시저에서 SQL 일괄실행문에서 생성된 임시 테이블을 사용할 수 없다.
  • SQL 일괄실행문내에서 사용된 SET 문장은 일괄실행문 내에서 영향력을 가지지만, 호출한 저장 프로시저에는 영향을 미치지 않는다.
  • sp_executesql에 의해 실행된 일괄실행문이 종료되면 호출한 저장 프로시저도 종료된다.
  • @@error는 동적 SQL 코드 내부의 최종 실행문의 상태를 반영한다.

온라인 도움말(Books Online)에 의하면, sp_executesql은 성공했을 경우 0, 실패했을 경우 1의 반환값을 가지지만, 최소한 SQL 2000에서 반환값은 @@error의 값과 같다.

sp_executesql에 대한 더욱 자세한 정보는 온라인 도움말을 참조하기 바란다. KB Article 262499은 출력매개변수의 특징에 대해 기술하고 있다.

어느 것을 사용해야 할까? (Which to Use)

동적 SQL을 규칙적으로 사용하는 경우에는, sp_executesql이 최선의 선택이다. sp_executesql의 사용시에는, 실행계획이 재사용될 가능성이 높고, 매개변수를 사용할 수 있기 때문이다. 아직 SQL 6.5를 사용하고 있는 경우를 제외하면, EXEC()를 사용해야 하는 경우는 동적 SQL 문이 nvarchar(4000)의 범위를 넘어서는 경우 뿐이다. 다음과 같이 사용가능하다. :

 EXEC(@sql1 + @sql2)

T-SQL에서 저장 프로시저를 호출할 때 매개변수로 연산식을 사용할 수 없는 것처럼, sp_executesql를 호출할 때도 하나의 변수만 매개변수로 사용할 수 있다. 만약, 반드시 분리된 쿼리문을 써야 한다면, sp_executesql를 EXEC()내에 포함시켜 사용할 수 있다.:

 DECLARE @sql1 nvarchar(4000), @sql2 nvarchar(4000), @state char(2) SELECT @state = 'CA' SELECT @sql1 = N'SELECT COUNT(*)' SELECT @sql2 = N'FROM authors WHERE state = @state' EXEC('EXEC sp_executesql N''' + @sql1 + @sql2 + ''', N''@state char(2)'', @state = ''' + @state + '''')

이런 경우에 인용부호(')를 여러번 겹쳐 사용하는 것이 혼란스럽다면 EXEC()만 사용할 수도 있다. (뒷부분에 제시될 사용자 정의함수(UDF; User Defined Function)인 quotestring()을 사용하면 이런 문제점을 해소할 수 있다.)

커서(Cursors)와 동적 SQL

커서는 자주 사용될 뿐만 아니라, 동적 SQL에서의 커서 사용에 대한 질문도 자주 접하게 되므로, 완성도 측면에서 예를 들도록 하겠다. DECLARE CURSOR EXEC()와 같이 사용할 수는 없지만, Declare Cursor문 전체를 동적 SQL에 포함시켜 사용하는 것은 가능하다.:

 SELECT @sql = 'DECLARE my_cur CURSOR FOR SELECT col1, col2, col3 FROM ' + @table EXEC sp_executesql @sql

위 쿼리의 실행에는 로컬 커서(local cursor)를 사용할 수 없다는 것에 주목하라.(로컬 커서는 EXEC(@sql) 문이 종료되는 시점에서 접근불가능하게 되어 버리기 때문이다.) Anthony Faull이 다음 예제에서와 같이 사용할 경우, 로컬 커서를 동적 SQL과 함께 사용할 수 있다고 지적해 주었다.

 DECLARE @my_cur CURSOR EXEC sp_executesql N'SET @my_cur = CURSOR FOR SELECT name FROM dbo.sysobjects; OPEN @my_cur', N'@my_cur cursor OUTPUT', @my_cur OUTPUT FETCH NEXT FROM @my_cur

명명된 커서(Named Cursors)처럼 커서 변수에 접근가능하며, 예제에서 보이는 바와 같이 매개변수로 전달가능하다.


동적 SQL과 저장 프로시저

저장 프로시저를 사용하는 이유와 동적 SQL을 사용할 때 어떤 일이 일어나는지에 대해 살펴보자. 다음과 같은 프로시저를 사용하는 것으로 시작한다.:

 CREATE PROCEDURE general_select @tblname nvarchar(127), @key key_type AS -- key_type is char(3) EXEC('SELECT col1, col2, col3 FROM ' + @tblname + ' WHERE keycol = ''' + @key + '''')

앞으로 살펴 보겠지만, 이것은 전혀 의미없는 프로시저이다. 왜냐하면, 저장프로시저 사용시에 얻을 수 있는 거의 모든 장점을 살리지 못하기 되기 때문이다. 클라이언트 코드에서도 Select 문을 조합해서 SQL 서버에 바로 질의하는 것이 가능하다.

1. 권한 (Permissions)

사용자가 테이블에 직접 접근할 수 있는 권한이 없다면, 동적 SQL을 사용할 수 없다는 것은 너무나도 당연하다. 사용자가 Select 권한을 가지고 있는 환경도 있을 수 있다. 하지만, 권한이 문제되지 않는다는 것을 확신하지 못할 경우에는, 영구 테이블(permanent tables)에 대한 Insert, Update 및 Delete 문을 동적 SQL문에 사용해서는 안된다. 임시테이블을 사용하는 경우에는 아무런 권한 문제가 발생하지 않는다.

사용자들이 데이타베이스에 대한 직접적인 연결을 하지 않고, 고정 서버 역할(application roles)을 활용하거나 COM+와 같은 중간층을 사용하는 경우에는 이 문제에 대해 심각하게 생각할 필요는 없다. 하지만, SQL injection 절에서 여전히 고려해야할 다른 보안 문제에 대해 살펴볼 것이다.

Sysadmin 고정서버역할을 가진 사용자가 사용할 코드를 작성할 경우에는, 당연히 권한문제에 대해 걱정할 필요가 없다.

2. 실행계획 캐쉬 (Caching Query Plans)

살펴본 바와 같이, SQL 서버는 순수 SQL 문과 저장 프로시저 모두에 대해 실행계획을 캐쉬하지만, 저장 프로시저에 대한 실행계획을 재사용할 때 다소 더 정확하다. SQL 6.5 에서는 매실행시마다 재컴파일 되었기 때문에, 동적 SQL이 더 느리다고 확실히 말할수 있었으나, 그 이후 버전에서는 상황이 그렇게 명확하지는 않다.

앞에서 나왔던 general_select 프로시저를 보자. @tblname을 다르게 줄 경우, 실행계획은 캐쉬되고, @tblname에 대한 매개변수 자동화 과정이 일어난다. 이것은 클라이언트 코드에서 SQL 문을 생성시킨 경우에도 마찬가지이다.

이것이 의미하는 바는, 동적 SQL을 현명하게 사용한다면, 성능향상 효과를 얻을 수 있다는 것이다. 예를 들어, 저장 프로시저 내부에 복잡한 쿼리가 있고, 선호되는 실행계획이 실행 당시의 테이블에 있는 데이타에 의존한다고 가정하자. 해당 쿼리를 동적 SQL로 작성하고, SQL 서버가 충분히 똑똑해서 캐쉬된 정보를 전혀 쓰지 않기를 바랄 수 있다 (임시테이블이 사용되었다면, SQL서버가 똑똑하게 처리하지 못할 것이다.). 한편, 그 복잡한 쿼리를 각각의 목적에 맞는 저장 프로시저로 나누어서 같은 결과를 얻을 수도 있겠지만, 모든 로직이 한 군데에 위치한다면 코드는 훨씬 읽기 쉬울 것이다. 위의 내용은 사용자가 동적 SQL을 실행시킬 수 있도록 권한구성이 되어있을 경우를 가정하였다.(?)

3. 네트워크 소통량 최소화 (Minimizing Network Traffic)

앞의 두 절에서 저장프로시저내의 동적 SQL은 클라이언트에서의 평범한 SQL문에 비해 장점이 없다고 하였는데, 네트워크 소통량 문제에 대해서는 그렇지 않다. 저장 프로시저 내에 동적 SQL을 사용하면, 네트워크 비용이 들지 않는다. 예제 프로시져인 general_select의 사용시에는, 이러한 장점이 거의 없으며, 순수 SQL 코드의 크기와 저장 프로시져를 호출하기 위한 실행문의 크기가 거의 비슷하다.

그러나, 복잡한 조건에 따라 6개의 테이블을 조인시키는 복잡한 쿼리를 생각해 보자. 사용자가 필요로 하는 자료의 기간정보에 따라 필요한 테이블은 sales0101이 될 수도, sales0102 가 될 수도 있다. 사실 이런 테이블 디자인은 좋지 못한데, 이에 대해서는 다시 살펴보기로 하고, 아무튼 여러분이 이런 상황에 처해있다고 가정해 보자. 이런 문제를 해결하기 위해 동적 SQL과 저장프로시저내를 활용한다면, 전체 쿼리를 매번 질의하지 않고 기간정보만 매개변수로 전달하면 된다. 만약 쿼리가 시간당 한번 질의된다면, 이득은 무시할만 하겠지만 네트워크 사정이 그렇게 좋지 못한 환경에서 15초마다 한번씩 쿼리를 보내야 한다면 차이점을 느낄 수 있을 것이다.

4. 출력매개변수 사용 (Using Output Parameters)

출력매개변수를 얻을 목적만으로 저장프로시저를 사용하는 경우 동적 SQL의 사용과 별다른 관련성이 없다. 다른 말로, 클라이언트에서 직접 sp_executesql문을 사용할 수 있으므로, 저장 프로시저없이도 출력매개변수의 사용이 가능하다.

5. 업무규칙 모듈화 (Encapsulating Logic)

저장프로시저에 대한 이전 주제에서 다루어진 것들 외에 특별히 더할 내용은 없다. 그럼에도 불구하고 저장 프로시저를 사용하기로 결정한다면 SQL에 관련된 모든 숨겨야할 내용을 저장프로시저내에 포함시킬 수 있다는 점은 지적하고 싶다. 이러한 의미에서 general_select의 예에서 테이블 이름을 매개변수로 직접 전달하는 것은 좋지 못한 생각이다. (sysadmin 고정 서버 역할의 구성원을 위한 응용프로그램의 경우는 예외이다.)

6. 의존성 파악 (Keeping Track of what Is Used)

동적 SQL은 이 목적에 위배된다. 동적 SQL은 sysdepends를 사용하지 않으므로 참조되는 개체파악이 힘들어지며, 데이타베이스 내에 참조되는 개체가 존재하지 않는 경우에도 알아차리기 어렵다. 테이블이름 혹은 칼럼 이름을 매개변수로 사용하지 않을 경우에는, 어떤 테이블이 사용되었는지를 파악하려면 최소한 SQL 코드를 뒤져보는 작업을 해야 할 것이다. 그러므로, 동적 SQL을 사용할 때에는 테이블 이름과 칼럼 이름을 프로시켜 코드에 제한해서 사용하라.


SQL Injection - 심각한 보안 문제

SQL injection 은 공격자로 하여금 개발자가 의도하지 않은 SQL 문을 실행시킬 수 있게 하는 기술이다. 사용자가 입력한 값이 직접 SQL 코드로 전달될 때 (저장 프로시저에서 동적 SQL을 사용하거나 혹은 클라이언트 쪽에서 SQL문을 생성시키거나), SQL injection의 위험이 존재한다. 이 공격방법은 MS SQL 서버뿐만 아니라, 모든 관계형 데이타베이스 관리시스템(RDBMS)에 적용된다.

다음과 같은 저장 프로시저에 대해 생각해 보자.:

 CREATE PROCEDURE search_orders @custname varchar(60) = NULL, @prodname varchar(60) = NULL AS DECLARE @sql nvarchar(4000) SELECT @sql = 'SELECT * FROM orders WHERE 1 = 1 ' IF @custname IS NOT NULL SELECT @sql = @sql + ' AND custname LIKE ''' + @custname + '''' IF @prodname IS NOT NULL SELECT @sql = @sql + ' AND prodname LIKE ''' + @prodname + '''' EXEC(@sql)

매개변수 @custname와 @prodname 에 대한 입력은 사용자 입력필드로부터 직접 전달되는 값이다. 여기서, 심술궂은 사용자가 @custname에 전달된 입력필드에 다음과 같은 값을 전달한다고 가정해보자.

 ' DROP TABLE orders --
결과로 만들어지는 SQL문은 다음과 같다.:
 SELECT * FROM orders WHERE 1 = 1 AND custname LIKE '' DROP TABLE orders --'

붉은 색으로 표시된 문장이 보이는가? 이런 공격유형의 성공여부는 상황에 따라 다르다. SQL 서버에 직접 접속한 평범한 사용자가 테이블을 삭제(drop)할 수 있는 권한을 가진다고 보기는 어렵지만, 만약 그 사용자가 웹에서 접속한 사용자이고, 웹서버가 SQL서버에 관리자 권한으로 연결되어 있다면, 해당 공격은 성공하게 될 것이다. 이러한 정밀한 공격에 필요한 권한을 가지고 있지 않더라도, 공격자는 여전히 원하는 명령을 내릴 수 있는 수단을 갖게 된다.

공격자는 먼저 입력필드에 작은 따옴표(')를 넣었을 때 어떤 일이 일어나는지 살펴본다. 만약 문법 오류 (syntax error)가 발생한다면, 공격자는 취약점이 존재한다는 것을 알게 된다. 그런 다음, 공격자는 쿼리를 종료시킬 수 있는 다른 수단을 찾아내고, 결국 자기 자신이 작성한 SQL 명령을 더할 수 있게 된다. 마지막으로 공격자는 SQL 문장의 나머지를 무시하고 문법 오류를 피하기 위하여 주석 문자를 사용한다. 공격자가 세미콜론(;)과 같은 문자를 사용할 수도 있다. SQL 7 이후 버전에서는 세미콜론이 T-SQL 문장을 분리하기 위한 선택사항으로 사용된다. 세미콜론을 사용하여 오류가 발생한다면, 공격자는 general_select에서와 같은 문제점이 존재한다는 것을 알아차리게 된다. 만약 사용자가 입력한 값이 직접 매개변수 @tablename에 전달된다면, 다음과 같은 문장을 전달하는 것도 가능하다:

 some_table WHERE keycol = 'ABC' DELETE orders

사용자가 직접 값을 넣을 수 있는 입력필드만 공격에 이용되는 것이 아니라는 것을 기억해야 한다. 저장 프로시저에 직접 전달되는 값이 URL에 포함되어 있다면, 공격자가 이것을 이용할 수도 있다.

이런 공격에는 기술도 필요하겠지만, 운도 많이 작용할 거라고 생각할 수도 있을 것이다. 하지만, 인터넷에는 시간이 널널한 수많은 공격자가 존재한다는 걸 기억해야 한다. SQL injection은 심각한 보안문제이며, 이에 대항하기 위한 방법을 알아야 하다. 이를 위한 2가지 방법이 존재한다.

  • 사용자에게 SQL 서버에서 필요한 권한 이상을 부여하지 마라. 응용프로그램이 중간층(middle layer)을 이용하여 SQL 서버에 접속한다면, 테이블에 대한 Select 권한만을 가지는 평범한 사용자 계정으로 접속케 해라. 경험이 적거나, 적당히 얼버무리는 개발자들이 SQL injection이 가능케하는 헛점을 만들 수 있기 때문이다.
  • 간단히 적용가능한 코딩 습관이 있는데, 다음 절 "동적 SQL을 위한 코딩 습관"에서 이에 대해 살펴볼 것이다.

SQL injection 문제는 저장 프로시저에 제한된 문제만은 아니라는 것을 강조해야 겠다. 문자열 변수는 종종 제한없이 사용가능하므로, 클라이언트 코드에서 SQL 명령문을 생성시켜 전달할 때 더 큰 취약점이 존재할 수도 있다. 저장 프로시저를 사용하는 경우에도, 호출하기 위해 EXEC문을 텍스트로 전달해야 한다는 것을 기억해야 한다. 여기에 SQL injection 공격이 가능한 취약점이 존재한다.


좋은 코딩 습관과 동적 SQL

동적 SQL을 사용하는 것이 어렵지 않게 보이겠지만, 작성한 코드에 대한 통제를 잃는 경우를 피하기 위한 규칙들이 존재한다. 주의를 기울이지 않는다면, 작성해 놓은 코드가 지저분해지거나 읽기 어렵게 되고, 문제해결을 위한 시도나 유지보수가 어렵게 된다. 무시무시한 프로시져 general_select을 다시 살펴보자:

 CREATE PROCEDURE general_select @tblname nvarchar(127), @key key_type AS -- key_type is char(3) EXEC('SELECT col1, col2, col3 FROM ' + @tblname + ' WHERE keycol = ''' + @key + '''')

여기서 사용된 중복된 인용부호를 보고 이게 도대체 무슨 뜻이지?라며 자문할 수도 있을 것이다. SQL은 문자열 제한자를 문자열에 포함시키기 위해 해당 리터럴을 겹쳐써야 하는 언어중 하나이다. 따라서, 위의 네개의 작은 따옴표('''')는 하나의 작은 따옴표(')를 표현하기 위한 문자열 리터럴이다. 위에서는 간단한 예를 들었지만, 상황은 더 나빠질 수도 있다.

쉽게 저지르게 되는 다음과 같은 에러가 있다.:

 EXEC('SELECT col1, col2, col3 FROM' + @tblname + ' WHERE keycol = ''' + @key + '''')

FROM 다음에 공백이 생략된 것이 보이는가? 해당 프로시저를 컴파일 할때는 에러메시지가 보이지 않지만, 실행시키려로 하면 열 이름 'col1'이(가) 잘못되었습니다., 열 이름 'col2'이(가) 잘못되었습니다., 열 이름 'col3'이(가) 잘못되었습니다., 열 이름 'keycol'이(가) 잘못되었습니다.라는 에러메세지를 접하게 된다. 그러면, 입력된 테이블 이름이 정확하므로 칼럼이름이 잘못된 것으로 오해하게 되어, 혼란이 가중된다. 아래는 매개변수가 fooabc일때 실제로 생성된 코드이다.:

 SELECT col1, col2, col3 FROMfoo WHERE keycol = 'abc'
FROMfoocol3 칼럼에 대한 별칭(alias)으로 해석되므로, 문법적인 오류가 아니다.

앞서 테이블 이름이나 칼럼 이름을 매개변수로 사용해서는 안된다는 의견을 제시했었다. 그러나 여기는 좋은 코딩 습관에 관한 절이므로, 한번 더 강조하겠다. 저장 프로시저를 작성하면, 해당 프로시저는 SQL 개체를 참조하는 독점적인 공간이 된다. (save stored procedures that is! ?) 그럼에도 불구하고, 아래에 동적 SQL에 대한 좋은 코딩 습관의 장점을 보여주도록 general_select을 개선해 보았다.:

 CREATE PROCEDURE general_select @tblname nvarchar(127), @key key_type, @debug bit = 0 AS DECLARE @sql nvarchar(4000) SET @sql = 'SELECT col1, col2, col3 FROM ' + quotename(@tblname) + ' WHERE keycol = @key' IF @debug = 1 PRINT @sql EXEC sp_executesql @sql, N'@key key_type', @key = @key

보시는 바와 같이, 몇가지를 수정하였다:

  • @tblname이 SQL injection에 쓰이는 걸 방지하기 위해 quotename()을 사용하였다. quotename()에 대한 자세한 내용은 아래를 참조하라.
  • 매개변수 @debug를 첨가해서, 예기치 못한 에러가 나타나는 경우에는 @key를 사용하여 SQL 코드가 어떻게 작성되었는지 쉽게 나타낼 수 있다.
  • 문자열내에 @key 값을 포함시키지 않고, sp_executesql를 사용하고 @key를 매개변수로 전달하였다. 이런 방식은 SQL injection에 대한 대비도 된다.

quotename()은 SQL 7에서 처음 도입된 내장함수(built-in function)이다. 해당 함수는 구분 식별자가 되도록 추가된 구분 기호와 함께 유니코드 문자열을 반환한다. 이 함수의 제공목적이 원래 개체 이름을 인용하기 위한 것이므로, 기본 구분자는 각괄호(squeare brackets; [])이지만, 작은 따옴표(') 혹은 큰 따옴표(")로 지정할 수도 있다. 그러므로, EXEC()를 사용해야 할 때는 SQL injection을 막기 위해 quotename()을 사용할 수 있다. SQL injection절에서 예로 들었던 search_orders 프로시저에서 몇줄을 다음과 같이 수정할 수 있다.:

 IF @custname IS NOT NULL SELECT @sql = @sql + ' AND custname LIKE ' + quotename(@custname, '''')

quotename() 함수에 눈여겨 볼만한 점이 하나 더 있다.: quotename()의 입력인자는 nvarchar(129)이므로, 긴 문자열을 대상으로는 사용하지 못한다. SQL 2000에서는 다음과 같은 사용자 정의 함수를 사용할 수 있다.:

 CREATE FUNCTION quotestring(@str nvarchar(1998)) RETURNS nvarchar(4000) AS BEGIN DECLARE @ret nvarchar(4000), @sq char(1) SELECT @sq = '''' SELECT @ret = replace(@str, @sq, @sq + @sq) RETURN(@sq + @ret + @sq) END
사용법은 아래와 같다.:
 IF @custname IS NOT NULL SELECT @sql = @sql + ' AND custname LIKE ' + dbo.quotestring(@custname)

SQL 7에서는, 사용자정의 함수가 제공되지 않으므로, quotestring을 저장 프로시져로 만들어야 한다. SQL 6.5에서는 replace() 함수가 제공되지 않으므로, 별 다른 대안이 없다. (SQL 서버 MVP인 Steve Kass가 quotename() 혹은 사용자 정의함수에 대해 제안해주었음을 밝힌다.)

중복된 인용부호로 인해 야기되는 지저분함을 피하기 위한 다른 대안은, T-SQL이 큰 따옴표(")를 지원한다는 사실을 이용하는 것이다. QUOTED_IDENTIFIER를 OFF로 설정하면, 문자열 구분자로 큰 따옴표("))를 쓸 수 있다. 이 설정에 대한 가본값은 컨텐스트에 좌우되는데, 선호되는 설정값은 ON으로, 인덱스된 뷰(Indexed Views)와 계산된 열(computed column)에 대한 인덱스를 사용하기 위해서는 이 값이 반드시 ON으로 설정되어야 한다. 그러므로, 이 방법이 가장 좋은 해결책은 아니지만, 경고메시지를 보는게 싫을 경우에는, 다음과 같이 사용 가능하다.:

 CREATE PROCEDURE general_select @tblname nvarchar(127), @key key_type, @debug bit = 0 AS DECLARE @sql nvarchar(4000) SET @sql = 'SET QUOTED_IDENTIFIER OFF SELECT col1, col2, col3 FROM ' + @tblname + ' WHERE keycol = "' + @key + '"' IF @debug = 1 PRINT @sql EXEC(@sql)

두가지 다른 인용 부호가 쓰였으므로, 해당 코드의 가독성이 높아진다. SQL 문장을 위해 작은 따옴표가 쓰였고, 포함된 문자열 리터럴로 큰 따옴표가 사용되었다.

SQL injection에 대해 보호되지 못하므로, 이 방식은 sp_executesqlquotename()을 쓰는것 보다는 좋지 못한 방법이다. 그러나 sysadmin 을 위한 작업인 관계로 SQL injection이 문제되지 않을 경우에는 사용가능하며, SQL 6.5 환경에서는 아마도 최선의 방법일 것이다.

이 절에서 제시된 가이드라인을 따르더라도, SQL 코드에 동적 SQL을 사용함으로써 야기되는 복잡성은 상당하다. 따라서, 사용하기 전에 반드시 시용해야 하는지 재고해보기 바란다는 말로 끝을 맺겠다.


동적 SQL을 사용(하지 말아야)하는 일반적인 경우 (Common Cases when to (Not) Use Dynamic SQL)

SQL 서버에 관한 여러 뉴스그룹에서, 거의 매일 간단한 예와 함께 동적 SQL을 사용하라는 답변을 받는 사람들이 종종 있다. 하지만, 답변하는 사람들 조차도 권한 및 캐싱에 관련된 숨겨진 의미를 말해주는 것을 잊곤 한다. 많은 경우에 이런 질문들에 대해 동적 SQL이 유일한 해법이기는 하지만, 실제로는 완전히 다른 – 그러고 훨씬 더 좋은 – 해결책이 존재하는 경우도 있다.

이 절에서는 동적 SQL을 사용할 수 있는 몇가지 경우와, 동적 SQL이 적절한 해결책인 경우에 대해 살펴볼 것이다. 그리고, 다른 한편으로 동적 SQL이 좋지 못한 선택인 경우에 대해서도 살펴볼 것이다.

select * from @tablename

일반적인 질문은 왜 다음 쿼리가 작동하지 않는지에 관한 것이다:

 CREATE PROCEDURE my_proc @tablename sysname AS SELECT * FROM @tablename

이러한 경우에 동적 SQL을 이용하여 해결할 수 있다는 것을 이미 알고 있지만, 이런 식의 저장 프로시저의 사용은 의미없는 일이라는 것도 또한 알고 있다. 만약 SQL 프로그래밍을 이렇게 한다면, 저장 프로시저를 사용하기 위해 골치아파할 필요가 전혀 없다.

사람들이 이러한 작업을 하고 싶어하는데는 몇가지 이유가 있어 보인다. C++, VB등 다른 프로그래밍 언어에서의 경험이 있으나 SQL 프로그래밍이 처음인 사람들이 보통 이런 식으로 작업을 많이한다. (※ 역주 : ASP개발자들도 마찬가지죠 ^^;) 테이블 이름을 매개변수로 사용하는 것은 재사용 가능한 범용 코드(Generic Code)를 만들기 위해서, 그리고 유지보수 편의성을 높힌다는 측면에서 환영할만한 방식이다.

그러나 데이타베이스 개체에 대해서는, 이 오래된 진리가 통하지 않는다. 개발자는 각각의 테이블과 칼럼들을 유일하고 고정적인 개체로 보아야 한다. 왜 그럴까? 실행계획을 세울 때, 각각의 테이블은 고유의 통계값과 추정치를 가지고 있으며, SQL 서버에서 이러한 값은 상호 교환 가능한 값이 아니다. 복잡한 데이타 모델에서는, 현재 무엇이 사용되고 있는 가를 파악하는 것이 중요하다. 테이블 이름과 칼럼이름을 매개변수로 사용한다면, 이러한 관계를 파악하기기 어려워 진다.

코딩하면서 타이프하는 수고를 덜기 위한 목적으로 이런 식의 작업이 하고 싶다면(SELECT * 같은 코드는 실제 생산환경에서 사용되어서는 안된다는 것을 기억하라), 그것은 잘못된 선택이다. 이런 경우에는 서로 유사하더라도, 이름이 다른 10개 혹은 20개의 저장 프로시저를 작성하는 것이 훨씬 더 좋은 방법이다.

만약 SQL 문이 너무 복잡해서, 서로 다른 테이블들이 사용되더라도 한 군데서 관리하는 것이 유지보수 측면에 상당한 장점이 있다면, 고려될 수 있는 다른 실용적인 방법이 있다 : C/C++과 같은 전처리기를 사용하는 것이다. 테이블당 하나의 프로시저가 존재하더라도, 코드는 하나의 파일로 만들 수 있다.

select * from sales + @yymm

앞에서 든 예의 변형에 해당한다. 차이점은 앞 절에서는 유한한 갯수의 테이블이 있는 것으로 가정했다는 것이다. 만약 테이블들이 동적으로 생성되는 시스템이라면 어떻게 할까? 예를 들어 판매 자료를 위한 테이블이 매달 생성된다면? 이런 경우에, 테이블당 하나의 저장 프로시저를 생성시킨다는 것은 전처리기를 사용하더라도 사실상 불가능하게 된다.

그렇다면, 다른 대안이 없으므로 그냥 동적 SQL을 사용해야 할까? 아니다. 되돌아가서 이 상황을 다시한번 살펴보자. 사실 처음부터 잘못된 접근법이 사용되었다. 데이타 모델에 명백한 결함이 존재하는데, 월별로 하나의 테이블을 사용하는 것은 Access를 사용하는 시스템 혹은 파일 데이타 시스템에서 성능을 향상시키기 위해 사용가능한 방법이다. SQL 서버 혹은 기타 고급 RDBMS에서 이렇게 해야할 이유는 거의 존재하지 않는다. SQL 서버 혹은 그 경쟁제품은 막대한 양의 데이타를 처리하고 그 데이타를 키를 이용하여 효율적으로 관리하기 위해 고안되었다. 연(year) 혹은 월(month)은 sales 테이블의 PK(Primary Key)를 구성하는 요소일 뿐이다.

만약, 선임자로부터 이러한 시스템을 인계받은 경우에는, 리모델링을 위해 막대한 비용이 필요할 경우도 있다. (하지만 동적 SQL을 사용하므로써 발생되는 복잡한 코드에 소요되는 비용 또한 무시하지 못한다.) 만약 새로운 시스템을 개발하고 있다면, 동적으로 생성되는 테이블에 대해서는 잊어버려라. 그러한 테이블에 접근하거나 업데이트하기 위한 코드가 상당히 지저분해 질 것이다. 이를 테면 전자상거래 시스템에서 각 장바구니당 하나의 테이블을 생성시키는 것처럼 이러한 테이블을 자주 생성시킨다면, 시스템 테이블에 핫 스폿(※ 역주 : Hot Spot은 많은 Query들이 동시에 동일한 영역의 디스크에 데이터를 읽거나 쓰려고 하는 경우에 발생합니다. 이는 하드 디스크가 동시에 처리할 수 있는 것보다 많은 디스크 I/O 요청들을 받게 되기 때문에, 디스크 I/O 병목현상(Bottleneck)을 유발하게 됩니다. 참고: KB 601427)을 유도해 성능에 악영향을 미칠 수도 있다.

수백만개의 데이타가 있는데, 한 테이블에 모든 데이타를 저장해두면, 데이타베이스가 작동하지 않을꺼야 라며 아직도 수긍하지 못하고 궁시렁거릴 독자가 있을 것이다. 좋다. 테이블에 정말로 많은 행(rows)이 존재한다고 치자. 신경쓸 일이 많지? 그치만, 그건 수백만개의 데이타때문이 아니라, SQL 서버 관리를 위해 매일 해야 하는 당연한 업무이다. (인덱스가 현명하게 정의되었다고 가정한 것이다.) 일억개 이상의 행이 존재한다면, 고려해야할 다른 문제가 생긴다. 이런 목적을 위해, SQL 2000은 분할 뷰(partitioned views) 혹은 분산분할 뷰(distributed partitioned views)와 같은 몇가지 특성을 지원한다. 분할 뷰 혹은 분산분할 뷰를 이용하면, 큰 데이타 집합을 몇개의 테이블로 나눌 수 있고, 마치 하나의 테이블처럼 접근할 수 있게 해준다. (주의 : 정확한 표현을 위해서는 행의 갯수가 아닌 테이블의 크기(total size)에 대해 언급해야 한다. 물론 테이블의 크기는 행의 평균 크기와 밀접한 관계가 있다.)

update tbl set @colname = @value where keycol = @keyval

이 경우는, 실행시간에 선택되는 칼럼에 대한 update가 필요한 경우이다. 위의 T-SQL은 문법에 어긋나지 않지만, 실제 일어나는 일은 테이블에서 keycol의 값이 @keyval인 행들의 @value값이 변수 @colname에 대입되는 것 뿐이다.(※ 역주 : 실제로 실행시켜보면 에러메시지가 표시되지는 않지만, 테이블의 해당 레코드에 대한 update가 수행되는 것이 아니라, @colname 변수에 할당된 값이 update될 뿐입니다.)

이 경우에 동적 SQL을 사용하려면 사용자는 테이블에 대한 Update 권한을 갖고 있을 것이 요구된다. 이런 권한 설정은 가볍게 볼 수 있는 문제가 아니며 가능하면 피해야 하는 구성이다. 여기에는 상당히 간단한 해결책이 존재한다.:

 UPDATE tbl SET col1 = CASE @colname WHEN 'col1' THEN @value ELSE col1 END, col2 = CASE @colname WHEN 'col2' THEN @value ELSE col2 END, ...
Case에 익숙하지 않다면, 온라인 도움말을 참조하기 바란다. Case는 SQL의 상당히 강력한 특징중 하나이다.

여기서 왜 사람들이 이런 식의 작업을 하고 싶어하는지 살펴보자. 아마도 테이블이 다음과 같은 구조를 갖고 있어서일 것이다.:

 CREATE TABLE products (prodid prodid_type NOT NULL, prodname name_type NOT NULL, ... sales_1 money NULL, sales_2 money NULL, ... sales_12 money NULL, PRIMARY KEY (prodid))

이 경우에는 테이블을 분리하여 자식 테이블의 sales_n 칼럼을 이용하는 것이 보다 합리적이다.:

 CREATE TABLE product_sales (prodid prodid_type NOT NULL, month tinyint NOT NULL, sales money NOT NULL, PRIMARY KEY (prodid, month))

select * from @dbname + '..tbl'

이 경우는 테이블이 동적으로 결정되는 다른 데이타베이스에 있는 경우이다. 이런 작업방식에는 여러가지 이유가 있으며, 왜 이렇게 작업해야 하는가 하는 이유에 따라 해결책이 다르다.

다른 데이타베이스에서의 데이타 획득 만약 응용프로그램에서 사용하는 데이타가 어떤 이유로 2개이상의 데이타베이스에 분산되어 있다면, 데이타베이스 이름을 코드에 직접 참조시켜 고생할 필요가 없다. 왜냐하면, 테스트 환경에서 같은 서버에 존재하는 데이타베이스 이름이 실제 환경에서는 다른 서버에 존재할 수도 있기 때문이다. 이런 경우에는 설정 테이블에 다른 데이타베이스의 이름을 넣어두고 동적 SQL을 활용하는 것도 좋은 아이디어이지만, 다른 해결책 역시 존재한다. 만약 다른 데이타베이스에 대한 작업이 해당 저장프로시저 내에서 가능하다면, 다음과 같은 할 수 있다:

 SET @sp = @dbname + '..some_sp' EXEC @ret = @sp @par1, @par2...

저장 프로시저의 이름이 변수 @sp 값에 들어있다.

모든 데이타베이스를 대상으로 작업 이 경우는 아마도 sysadmin 고정서버 역할에 속한 사용자가 수행하는 작업일 것이다. 이런 경우에는 권한문제 혹은 캐쉬에 신경쓸 필요가 없기 때문에, 대개의 경우에 동적 SQL은 적절한 선택이다. 그럼에도 불구하고 다음 예제에서의 sp_MSforeachdb 같은 대안이 존재한다.:

 sp_MSforeachdb 'SELECT ''?'', COUNT(*) FROM sysobjects'

추측하는 바와 같이, sp_MSforeachdb는 동적 SQL 을 내부적으로 사용하므로, 개발자가 일일이 루프(loop) 코드를 작성하지 않아도 되는 장점이 있다. 덧붙여 말하고 싶은 것은, sp_MSforeachdb가 온라인 도움말에서 누락된 함수라는 점이다. 온라인 도움말에서 누락된 함수를 사용했을 때 문제가 발생하면 Microsoft로부터의 기술지원을 받을 수 없다.

"마스터" 데이타베이스 간혹, 동일한 테이블 구조를 가진 여러개의 데이타베이스를 관리하는 경우를 볼 수 있다. ASP 서비스(provider service)를 제공하거나, 혹은 고객 각각에 대해 별도의 데이타베이스가 존재하는 경우에 해당하며, 사업적인 이유로 모든 고객에 대한 데이타를 하나의 데이타베이스에 두는 것이 불가능하다. 이런 경우에 관리자들은 모든 데이타베이스에 대한 유지보수가 쉽지 않다고 느끼게 되며, 결과적으로 필요한 모든 프로시저를 저장해둘 "마스터" 데이타베이스가 필요하게 된다. 그러나, "마스터" 데이타베이스에 존재하는 저장프로시저는 동적 SQL을 필요로 하며, 또다른 끔찍한 유지보수 문제를 낳게 된다.

2가지 방법이 있는데, 하나는 SQL 서버에 기본 제공되는 Master 데이타베이스를 이용하여 사용자가 작성한 프로시저를 시스템 프로시저로 설치하는 것이다. (※ 역주: master 데이타베이스에 접두어 "sp_"로 시작하는 사용자 저장프로시저를 작성해 두면, 마치 시스템 저장프로시저처럼 다른 데이타베이스에서 사용가능하게 됩니다.) 하지만, 이렇게 하면 Microsoft에서 기술지원을 기대할 수 없으며, 보안 측면에서의 문제점도 존재하므로, 권장하고 싶은 방법은 아니다.

다른 방법? 저장 프로시저를 각각의 데이타베이스에 설치하고, SQL 개체에 대한 배포 루틴(Rollout Routines)을 개발하는 것이다. 테이블을 변경해야 할 필요가 생길 것으므로, 결국에는 이 방법이 필요하게 될 것이다. 게다가 개별 데이타베이스에 저장 프로시저를 만들어두면, 새로운 버젼으로 업그레이드하기를 꺼리는 고약한 고객들에 대한 대응도 가능해지며, 까다로운 고객을 위해 특정 목적에 맞는 프로시저를 제작하는 것이 가능해진다. 배포 루틴을 적용하는 방법은 설징 유지관리(configuration management)에 관한 주제까지 다루어야 하며, 이 기사의 범위를 벗어난다. 이에 대해서는 2가지 단서만 제공하겠다. SQL Server Resource Kit에 들어있는 Stored Procedure Builder를 사용하면, Visual SourceSafe를 이용하여 SQL 개체를 설치하는 것이 가능해진다. 내 경우에는 고객들에 대한 기술지원을 제공하기 위해 AbaPerls라는 툴을 몇년 동안 개발했으며, http://www.abaris.se/abaperls/에서 구할 수 있다. 이 툴은 프리웨어이다.

select * from tbl where col in (@list)

매우 흔한 질문으로, 동적 SQL을 사용하세요가 또한 일반적인 답변이다. 하지만 이 질문에 대해 동적 SQL을 사용하라는 것은 분명히 잘못된 답변으로, 이런 종류의 Select 권한이 필요하지도 않으며, @list에 많은 요소가 포함될 경우에는, 동적 SQL을 사용할 경우 틀림없이 성능저하를 경험하게 된다.

대안? 사용자 정의함수 혹은 저장프로시저를 사용해서 입력되는 문자열을 테이블에 나누어 저장해라. 이 기사에는 적당한 예가 포함되어 있지 않지만, 또다른 기사인 Arrays and Lists in SQL Server에서 이러한 문제를 다루는 방법과 여러 방법론들의 성능차이에 대한 자료를 제시하겠다. (제시된 방법들중 동적 SQL이 가장 하위에 있다!) 해당 기사는 매우 긴 분량으로, 기사의 첫부분에 각 SQL 버전에 적합한 내용으로 분기할 수 있는 링크를 제시하였다.)

select * from tbl where @condition

다음과 같은 프로시저를 작성하려 한다고 가정해 보자.

 CREATE PROCEDURE search_sp @condition varchar(8000) AS SELECT * FROM tbl WHERE @condition

그냥 관둬라. 이런 작업을 하고 있다면 저장 프로시저를 어중간한 상태로 사용하고 있는 것으로, 개발자가 여전히 클라이언트에서 SQL 코드를 조합하고 있는 것이다. 이 예는 다음 주제와 관련있다.

동적 검색 조건 (Dynamic Search Conditions)

사용자가 광범위한 매개변수로 부터 데이타를 검색하는 것은 드문 경우가 아니다. 입력 매개변수 각각의 조합에 대하여 최적화된 쿼리를 작성해내는 정적인 해결책을 만들어내는 것이 불가능하다는 데에는 논쟁의 여지가 없다. 그리고, 대부분의 프로그래머들이 모든 조건들을 "똑똑한" SQL을 이용하여 하나의 쿼리로 묶어낸 경우에 좋은 효율을 보여주리라고 기대하지도 않는다.

이런 유형의 문제에는 동적 SQL이 분명 더 좋은 해결책이다. 권한 문제만 해결할 수 있다면, 동적 SQL을 사용하는 것이 성능 및 유지비용 보수면에서 더 낫다. 분리된 다른 기사 동적 검색 조건 (Dynamic Search Conditions) 에서 동적 SQL을 사용하는 방법과 사용하지 않고 구현하는 방법에 대한 예를 들어보겠다.

select * from table order by @col

이 경우는 동

Posted by tornado
|

입력폼이 조금 많은데, 자바처럼 프라퍼티 카피 하는걸 몰라서리..

msdn, 닷넷책, 웹페이지 뒤져보니 System.Reflection 이라는 네임 스페이스가 있었고,

자바에 객체 내부 프라퍼티 복사하듯이 할 수 있었당..

메소드 호출도 잘 되고.. 필드값도 잘 대입됨..

 

사용은 걍 aspx 에서  대입될 객체 생성하고 메서드에 전달해 주면 된다.

 

MyCustomProperty dest = new MyCusotmProperty();

CopyProperty(Reqeust.Form, dest);

와 같이 사용하면 됨..

string , int double, boolean 만 대입 가능함.

 

 

public static void CopyProperty(NameValueCollection src, object dest)
  {
   if(src == null) throw new Exception("src is null..");
   if(dest == null) throw new Exception("dest obj is null..");

   Type t = dest.GetType();
   
   FieldInfo[] f = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);

   string key = "";
   for(int i = 0; i < src.Count; i++)
   {
    key = src.GetKey(i);

    for(int j = 0; j < f.Length; j++)
    {
     if(key.Equals(f[j].Name))
     {
      if("System.Int32".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, Int32.Parse(src.Get(key)));
      }
      else if("System.Single".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, Single.Parse(src.Get(key)));
      }
      else if("System.Double".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, Double.Parse(src.Get(key)));
      }
      else if("System.Boolean".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, Boolean.Parse(src.Get(key)));
      }
      else if("System.Decimal".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, Decimal.Parse(src.Get(key)));
      }
      else if("System.String".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, src.Get(key));
      }
      else if("System.Object".Equals(f[j].FieldType.ToString()))
      {
       f[j].SetValue(dest, src.Get(key));
      }       
     }     
    }    
   }
  } // end CopyProperty..

Posted by tornado
|

jetty site

JAVA/JSP_Servlet 2005. 10. 12. 11:13
Posted by tornado
|

[펌]가비지 콜렉션

JAVA/JSE 2005. 10. 12. 10:40

가비지 콜렉션

2004년 1월의 테크팁이었던 Monitoring Class Loading and Garbage Collection에서는 자바 커맨드 라인 툴 (자바 애플리케이션 런쳐)를 이용하여 커맨드 라인 옵션인 -verbose:gc에 대해서 알아보았다. 표준이 아니라는 이유로 -XX 옵션에 대해 신경 쓰지 않는다거나, 옵션의 균질성을 중요시한다거나, 또는 현재 사용하고 있는 옵션에 대해서 알고 싶다면 다음과 같이 -verbose:gc 를 입력해보자.

     -XX:+PrintGC -XX:+TraceClassUnloading

이것은 -verbose:gc 옵션이 어떻게 번역되는지를 보여준다.

이 옵션은 애플리케이션이 실행되는 동안 가비지 콜렉션 이벤트의 리포트를 요구한다. J2SE v1.4.2에서는 가비지 콜렉터를 컨트롤하는 다른 많은 커맨드 라인이 있다. 코드의 어느 라인도 변경하지 않고 애플리케이션의 성능을 최대한으로 활용하려면, 이 옵션들을 최소한 하나 이상 사용할 수가 있다. 이번 팁에서는 가비지 콜렉터를 컨트롤하는 부수적인 많은 자바 커맨드 라인을 다룰 것이지만, 가비지 콜렉션 튜닝 옵션의 전체를 말하지는 않는다.

자바 애플리케이션 런쳐는 표준 커맨드-라인 스위치와 비표준 커맨드-라인 스위치를 모두 동반한다. -verbose:gc 옵션에 관한 정보에 대해 알아보려면, 특정 플랫폼별로 다음 웹페이지를 참고하자.

위의 참고 페이지에는 몇몇 비표준 옵션들에 대해서도 나와있다. 자바 커맨드 라인 툴에서 표준 옵션인 -X을 실행하면 사용자의 플랫폼을 위한 비표준 옵션을 보여준다.

솔라리스 플랫폼이라면, java -X 을 실행했을 경우 출력되는 내용은 다음과 같다.

   -Xmixed           mixed mode execution (default)   -Xint             interpreted mode execution only   -Xbootclasspath:<directories and zip/jar files separated by :>                     set search path for bootstrap classes and                      resources   -Xbootclasspath/a:<directories and zip/jar files separated by :>                     append to end of bootstrap class path   -Xbootclasspath/p:<directories and zip/jar files separated by :>                     prepend in front of bootstrap class path   -Xnoclassgc       disable class garbage collection   -Xincgc           enable incremental garbage collection   -Xloggc:<file>    log GC status to a file with time stamps   -Xbatch           disable background compilation   -Xms<size>        set initial Java heap size   -Xmx<size>        set maximum Java heap size   -Xss<size>        set java thread stack size   -Xprof            output cpu profiling data   -Xrunhprof[:help]|[:<option>=<value>, ...]                     perform JVMPI heap, cpu, or monitor profiling   -Xdebug           enable remote debugging   -Xfuture          enable strictest checks, anticipating future                      default   -Xrs              reduce use of OS signals by Java/VM (see                      documentation)   -Xcheck:jni       perform additional checks for JNI functions     The -X options are non-standard and subject to change without   notice.

위의 출력값의 마지막 줄을 주목하자. 그러한 비표준 옵션들을 사용하는 것은 사용자의 책임 하에 있음을 분명히 나타내고 있다.

가비지 콜렉션을 위한 세 가지 특정한 비표준 옵션은 -Xnoclassgc, -Xincgc, 그리고 -Xloggc:<file>이다. -Xnoclassgc 옵션을 지정한다면, 콜렉션이 발생하지만 클래스들은 permanent generation으로부터 수집되지 않는다. 이는 콜렉션이 수거하는 메모리가 없음을 의미한다. 만약 클래스를 로딩하려고 할 때 이미 로딩된 클래스들을 위해 메모리를 써버렸다면 콜렉션은 문제를 해결하지 않을 것이다.

썬의 1.4.2 구현에서 -Xincgc-XX:+UseTrainGC 옵션과 동등한 값을 가진다. 즉, -Xincgc은 old generation을 위해서 디폴트 값에서 주어진 "serial" 콜렉터를 사용하기 보다는 "train"콜렉터를 사용한다. ("generations"의 개념은 이번 팁의 후반에서 다루게된다.) train 콜렉터는 약 10%의 퍼포먼스 부하를 발생시킨다. 또한 old generation 전체를 한 번에 수집할 수 없기 때문에 약간의 공간적인 부하(space overhead)도 발생한다. train 콜렉터는 점진적으로 작동하는 장점을 가지기 때문에 휴지 시간이 상당히 짧다. J2SE 1.5.0 베타 릴리즈에서는 -Xincgc 플래그는 train 콜렉터보다는 CMS(concurrent mark sweep) 콜렉터를 인보킹하게 된다. 그 이유는, CMS 콜렉터가 train 콜렉터보다 더 균일하기 때문이다.

-Xloggc 옵션을 지정해줌으로써 -verbose:gc 출력값의 등가를 파일로 출력할 수 있다. -verbose:gc 에 의한 출력값과 비교해 보면, -Xloggc 에 의한 출력값은 한 가지 추가적인 항목을 포함하는데, 그것은 애플리케이션에서의 첫 콜렉션이 일어난 시간에서부터 가비지 콜렉션이 발생한 시간에 대한 정보이다. 다음의 -Xloggc 샘플 출력값에서 살펴보자.

   0.000: [GC 512K->153K(1984K), 0.0198483 secs]   0.372: [GC 506K->281K(1984K), 0.0206428 secs]   0.393: [Full GC 281K->281K(1984K), 0.0888926 secs]   0.519: [GC 947K->941K(1984K), 0.0045715 secs]   0.524: [Full GC 941K->941K(1984K), 0.0797666 secs]   0.650: [GC 2107K->1597K(2808K), 0.0013546 secs]   0.838: [GC 2107K->1700K(2808K), 0.0116557 secs

파일에 재전송할 필요없이 -XX:+PrintGCTimeStamps 옵션을 지정해줌으로써 -Xloggc 출력값에 시간 소인을 나타낼 수 있다.

-Xnoclassgc, -Xincgc-Xloggc:<file> 이외에도 가비지 콜렉터를 컨트롤하는 다른 옵션들이 존재한다. 예를 들자면, 콜렉터가 실행할 때 간접적으로 영향을 끼치도록 -Xms -Xmx 옵션을 사용하여 메모리 할당의 풀 사이즈를 변경할 수 있다. -Xms의 값을 크게 지정해주면, 그만큼 큰 자바 객체 힙으로 작업하게 된다. 이것은 힙을 채우는 데에 더 많은 시간이 걸린다는 것을 의미하고 결과적으로 콜렉션을 피하지는 않지만 미루게 된다는 것이다. 큰 -Xms 값은 또한 더 많은 시스템 리소스를 소모하게 된다. 반면 -Xmx의 값을 크게 지정하면, 자바 객체 힙의 사이즈가 필요 시에 커질 수 있게 된다. 이렇게 값이 큰 객체 힙은 조금 덜 빈번하게 가비지 콜렉트된다는 점 외에는 모든 점이 동일하다. 따라서 이 옵션을 통해 가비지 콜렉션이 빈번하지만 짧은 시간동안 일어나는 것과, 횟수는 적지만 오랜 시간동안 가비지 콜렉션이 일어나는 것 중 하나를 선택할 수 있다. 하지만 각각의 콜렉션이 부하를 발생하기 때문에, 사용자가 정의하는 요구조건에 따라 더 나은 접근법은 달라지게 되어있다.

-Xms-Xmx 외에도 가비지 콜렉션에 영향을 주는 비표준 옵션들이 존재한다. 그 옵션들은 java -X 도움말에서 볼 수 없는데, 그 이유는 이 옵션들은 비표준 옵션이기 때문에 두개의 X를 사용해야하기 때문이다.

만약 힙의 최소값과 최대값을 지정해주고자 한다면 (이 때, 디폴트 값의 범위는 40%~70%), -XX:MinHeapFreeRatio=<Minimum> 옵션과 -XX:MaxHeapFreeRatio=<Maximum> 옵션을 사용하면 된다. 그러나 최소값을 너무 작게 설정해놓으면, 힙은 콜렉션 후에 충분한 여유공간을 확보하지 못하게 되므로, 단기간 내에 또다시 콜렉팅을 해야할 수도 있다. 반면, 최소값을 크게 설정하면, 더 많은 "head room"이 생기므로 다음 콜렉션 때까지 시간을 지연시킬 수 있을 것이다. -XX:MinHeapFreeRatio 의 값을 크게 했을 때의 단점은 시스템 메모리가 이 가상 장치에 고정되기 때문에 사용자의 또 다른 애플리케이션에 사용될 수 없다는 점이다.

-XX 세트내의 다른 비표준 옵션들은 가비지 콜렉터의 작동 방법에 영향을 끼친다. -XX:+UseConcMarkSweepGC 커맨드 라인 플래그("concurrent mark sweep garbage collection"의 줄임말)는 병행(concurrent) 가비지 콜렉터를 실행시킨다. -XX:+UseConcMarkSweepGC 콜렉터는 old generation을 동시에 콜렉팅한다. 그 이유는 전반적인 콜렉션 같은 old generation 들은 콜랙팅할 때 대체적으로 시간이 오래 걸려서 병행 가비지 콜렉터를 실행시키는 것이 더 효과적이기 때문이다. 이는 애플리케이션에서 가비지 콜렉션 휴지 시간을 짧게 할 수 있도록 프로세싱 파워를 활용한다는 것을 말한다. -XX:+UseParallelGC 커맨드 라인 플래그는 병렬 가비지 콜렉팅을 가능하게 한다. -XX:+UseParallelGC 콜렉터는 오직 다중 프로세서를 이용한 young generation만을 콜렉팅한다. 이 접근법은 처리율은 높이지만, 전반적인 콜렉션의 휴지 시간을 줄이지는 못한다.

또 하나의 흥미로운 옵션인 -XX:+PrintGCDetails 는 각각의 콜렉션에서 각 generation에 어떤 일이 발생했는지를 보여준다. (단, permanent generation 에 대한 자세한 사항들은 보여주지 않는다. 이 점은 JDK 1.5.0.에서 수정되었다.)

SwingSet2의 데모에 사용된 -XX:+PrintGCDetails의 예제를 살펴보자. (출력값의 라인들은 테크팁 페이지의 경계선에 맞추기 위해 나누어주었다.)

   java -Xloggc:details.out -XX:+PrintGCDetails -jar    /home/eo86671/j2sdk1.4.2/demo/jfc/SwingSet2/SwingSet2.jar   0.000: [GC 0.000: [DefNew: 511K->64K(576K), 0.0182344 secs]         511K->153K(1984K), 0.0185255 secs]   1.387: [GC 1.387: [DefNew: 417K->64K(576K), 0.0192086 secs]        1.407: [Tenured: 217K->281K(1408K), 0.0725645 secs]         506K->281K(1984K), 0.0923346 secs]   1.559: [GC 1.559: [DefNew: 10K->3K(576K), 0.0044098 secs]        1.564: [Tenured: 937K->941K(1408K), 0.0741569 secs]        948K->941K(1984K), 0.0790573 secs]   1.703: [GC 1.703: [DefNew: 510K->0K(576K), 0.0011627 secs]         2107K->1597K(2808K), 0.0013820 secs]   2.210: [GC 2.210: [DefNew: 509K->64K(576K), 0.0112637 secs]         2107K->1710K(2808K), 0.0115942 secs]   2.927: [GC 2.927: [DefNew: 575K->64K(576K), 0.0170128 secs]        2222K->1841K(2808K), 0.0173293 secs]   8.430: [GC 8.430: [DefNew: 576K->64K(576K), 0.0142839 secs]         2353K->2025K(2808K), 0.0156266 secs]   8.823: [GC 8.823: [DefNew: 494K->64K(576K), 0.0164915 secs]         2456K->2243K(2808K), 0.0166323 secs]   8.856: [GC 8.856: [DefNew: 569K->41K(576K), 0.0058505 secs]        8.862: [Tenured: 2341K->1656K(2360K), 0.1464764 secs]         2749K->1656K(2936K), 0.1526133 secs]

변경내용이 그 결과에 어떤 영향 (예를 들어서, 콜렉션간의 시간 연장이나 각각 특정 휴지 시간의 감소)을 주는지를 알아보기 위해 -Xms 옵션과 -Xmx 옵션을 조정해보자.

앞서 'generations'의 힙 공간에 대해 언급한 바 있다. 이 말이 무슨 의미인지 궁금하다면, 여기 짤막한 설명을 참조하자. JRE 1.4.2 에서 힙은 young, old, 그리고 permanet와 같이 세 개의 generation으로 구분된다. young generation은 객체가 생성된 장소이다. 이 generation의 사이즈는 -XX:NewSize 옵션과 -XX:MaxNewSize 옵션에 의해 컨트롤된다. 새로운 객체를 보유하게 되면, old generation으로 승격된다. old generation의 사이즈는 전체 힙 사이즈 (-Xms-Xmx) 에서 young generation의 사이즈 (-XX:NewSize-XX:MaxNewSize)를 감한 사이즈로 계산된다. young generation은 명시적 크기보다는 -XX:NewRatio=를 이용하여 더 잘 지정할 수 있는데, -XX:NewRatio= 옵션은 힙 설정의 전체 사이즈를 설정해주기 때문이다. 선택한 사이즈는 콜렉팅 되는 시간에 대해 콜렉션의 횟수를 결정하게 된다. 어떤 애플리케이션들은 짧은 휴지 시간을 요구하는 한편, 콜렉터의 효율성을 필요로 하는 애플리케이션들도 있다. (많은 애플리케이션에서 이 단계의 튜닝을 해줄 필요는 전혀 없다.)

마지막으로 언급할 GC옵션은 -XX:+DisableExplicitGC이다. System.gc()의 호출을 이용하여 가비지 콜렉터를 실행하라는 명령을 했을 때, 시스템이 이를 무시하기를 원한다면 이 옵션을 사용해보자. 가비지 콜렉터는 프로그래머가 명령했을 때가 아닌, 필요시에 계속해서 실행되고 있을 것이다. 하지만 프로그래머의 명령을 무시하는 것이 좋은 생각일까? 아마도 아닐 것이다. 왜냐하면, 프로그래머가 가비지 콜렉터를 실행하기 적당한 시점이라고 생각할 수 있기 때문이다. 물론, 만약 공유 라이브러리를 사용하고 있고, 실행 콘텍스트가 프로그래머의 원래의 계획과 다르다면, 이러한 새로운 상황에 유효하지 않을 수도 있다.

가비지 콜렉션에 관련된 옵션들에 대해 좀더 자세한 정보를 원한다면, Tuning Garbage Collection with the 1.4.2 Java Virtual Machine를 읽어보기 바란다. 튜닝을 할 때는 튜닝의 목적이 성능 향상인지, 짧은 휴지 시간인지 아니면 작은 범위의 ㄴ메모리 사용을 위한 것인지 그 목적을 확실히 해야 한다. JVM이 업데이트 되면서 선택할 수 있는 튜닝 옵션들도 다시 한번 살펴 보아야 한다. 이번 팁에서 다룬 옵션들의 대부분은 썬의 런타임 환경 1.4.2 릴리즈에 한정됨을 기억하자. 1.5.0 릴리즈는 고유의 컨트롤 셋과 디폴트 셋을 가진다.

Posted by tornado
|

애플리케이션 서버의 성능과 확장성을 개선하기 위한 Java HotSpot VM vl.4.x Turbo-Charging (1)

 

 
 
 

Java 프로그래밍 언어는 많은 데이터 중심적인 애플리케이션용 선택 언어로 널리 받아들여지고 있다. Java의 성공은 언어의 초기 단계부터 설계된 다음과 같은 중요 기능을 토대로 만들어졌기 때문이다.

- WORA : Write Once Run Anywhere
- 자동 메모리 할당과 컬렉션
- 객체 지향 디자인
- 상속

그러나 결정적으로 소스 코드를 컴파일해서 만들어진 Java 바이트 코드를 하위단인 JVM이 실행시키는 구조는 시간을 다투는 환경에서 Java를 선택하는 데 있어 고민을 안겨주었던 것도 사실이다. 전통적으로 가비지 컬렉션(GC)은 사용자의 애플리케이션을 일시적으로 중단시키고 시스템이 메모리를 재순환시킨다. 이런 GC 중단은 밀리초에서 심한 경우에는 수초일 수도 있다.

통신 회사 장비 공급자는 통신 회사가 기업 고객과 엄밀한 수준의 서비스 협약이 돼 있을 뿐더러 부동산 시장에서도 기본적인 수준의 신뢰성과 반응 시간을 가지고 있어야 한다는 것을 잘 알고 있다. 수화기를 든 다음 10 또는 20초를 기다려야 신호음이 난다는 것은 용납될 수 없다.

통신 회사들은 여러 가지 이유로 자사의 환경에 Java를 사용하길 원한다.


특히 이미 존재하는 광범위하고 풍부한 애플리케이션을 생성할 수 있는 능력있는 수백만의 프로그래머들과 회사들이 Java를 기본 기술로 표준화하고 있다. 이는 새로운 서비스와 기술을 많이 구할 수 있다는 것이다. 거기에다가 J2EE에는 보편적인 표준 환경과 기하급수적으로 확장할 수 있는 잠재력이 있다. 그렇지만 예측할 수 없는 ‘중단’과 그로 인해 애플리케이션의 반응이 지연되는 것 때문에 어떤 회사라도 자사 네트웍의 핵심 기술로 Java를 사용하는 것이 타당한지 묻게 한다.

통신 회사는 수익을 얻기 위해서 완전한 통화와 서비스 세션에 의존한다. 불완전하거나 버림받은 세션은(예를 들어 통화와 인스턴트 메시지) 네트웍 자원과 전혀 사용되지 않는 대역폭을 제공하는 비용을 낭비하는 것이다.

이러한 환경에서 통신 회사뿐만 아니라 차세대의 통신 기반을 선도하는 3rd generation(3G) 회사들에게도 표준으로 채용된 SIP 프로토콜이 이상적인 선택일 것이다. SIP는 UDP와 TCP를 기반으로 신호를 보내고, 패킷(UDP) 재전송이 내장돼 있는 프로토콜이다. 만약 UDP 요청 패킷이 특정 시간 내(주로 500ms)에 반응이 없으면 재전송된다. 네트웍 불능이나 패킷 손실 등 다른 에러들에 대한 계산도 돼 있어야 한다.

즉 JVM은 500ms에 근접한 시간만큼 애플리케이션을 중단시키면 안된다는 의미다. 왜냐하면 네트웍 패킷 지연(가는 데만 50~100ms)과 CPU의 프로세싱 시간이 계산되면 재전송이 일어나기까지 시간이 많지 않는다. 계단식(Cascade) 실패는 주로 애플리케이션 서버에 높은 로드에 대한 대비를 하지 않은 불완전한 디자인이나 단일 쓰레드, 애플리케이션 쓰레드를 모두 중지시키는 ‘Stop-the-world 가비지 컬렉션’ 정책을 사용하는 JVM(J2SE 1.4 버전 이하)을 사용할 때 일어난다. 그리고 이 JVM들은 단일 쓰레드이고, 여러 CPU가 있더라도 오직 하나의 CPU에서만 가비지 컬렉션을 실행시킨다.

Sun HotSpot JVM(JDKTM 1.3+)에 사용된 알고리즘은 메모리 재사용과 객체 노화에 효과적이다. JVM heap은 객체의 나이에 따라 ‘신세대’와 ‘구세대’로 나뉘어진다.

신세대는 ‘에덴’과 2개의 ‘생존 공간(Survivor)’으로 나눠졌다. 대부분의 애플리케이션에서는 2/3의 객체는 ‘단기 객체’로 빨리 죽고, ‘신세대’에서 컬렉션할 수 있다. 주로 신세대의 객체는 총 heap 크기에 비해 작다. 따라서 신세대에서 짧은 멈춤이 빈번하게 일어나지만 한번의 컬렉션에 더 많은 메모리를 컬렉션할 수 있다. 그러나 신세대에서 여러 번 생존 공간에 있게 되면, 그것은 ‘오래된’ 또는 ‘장기 객체’로 인식하고 ‘구세대’로 ‘승진(promoted)’ 또는 ‘보유(tenured)’하게 된다. 비록 대개 ‘구세대’의 크기가 크기만, 결국은 모두 사용하게 되면 컬렉션이 필요하게 된다. 이로 인해 구세대에서 빈도수는 낮지만, 장시간의 멈춤이 일어난다. 보다 자세한 것은 ‘Tuning Garbage Collection with the 1.3.1 Java Virtual Machine’이라는 Sun HotSpot JVM 가비지 컬렉션 테크놀로지를 참조한다.

Ubiquity의 SIP 애플리케이션 서버인 Application Services Broker(ASB)는 100% 순수 Java 기술이며, JVM과 가비지 컬렉션에 대한 완벽한 테스트 환경을 제공한다. SIP 애플리케이션 서버로서, ASB는 신호가 오는 네트웍으로부터 서비스 요청을 받은 다음 수정하고, 최종적으로 답변해주는 책임이 있다. SIP 트래픽을 신호가 오는 네트웍으로부터 서비스 요소(SIP Servlets)로 연결해 책임을 이행한다. 이 서비스 요소는 특정 신호 메시지에 관심이 있다고 ASB에 등록한다. ASB는 대규모 호출을 다양한 로드 분산으로 생성한다. 이는 클러스터 기반으로 구현되어 한 대나 여러 대에서나, 여러 프로세서에서 잘 확장된다. 이런 때 JVM 디자인이 한계점까지 압력을 받으며, 이런 상태를 관찰하면서 다중 쓰레드의 JVM을 개선시키는 데 많은 통찰력을 얻게 된다.

예를 들어 Ubiquity ASB는 호출되는 하나의 프로세스당(일반적으로 한 호출은 하나의 세션이다) 약 220KB의 가비지를 생성한다. 10%의 데이터는 40초 가까이 생존하고, 나머지 90%는 곧바로 죽어버린다. 이 10%의 데이터는 단시간이지만 평균 초당 100번을 호출한다고 하면 대단히 많은 메모리를 사용하게 된다. 40초 안에 최소한 88MB의 활성 데이터를 갖게 된다. 만약 가비지 컬렉션이 5분에 한번씩 ‘구세대’에서 일어난다면 heap의 크기는 최소한 660MB가 돼야 한다. 이전의 가비지 컬렉션 엔진이 스캔하기에 매우 커 컬렉션하는 데 100~150ms 정도 걸린다.

통신 애플리케이션 서버는 애플리케이션을 멈추는 시간을 엄격히 제한해줄 수 있는 결정적인 GC 모델을 요구하고 여러 프로세스에서도 잘 확장돼야 한다.

J2SE 1.2와 1.3에서 GC는 단일 쓰레드이며, stop-the-world 형식이었다. 이 JVM의 가비지 컬렉션으로 인해 생기는 멈춤은 애플리케이션에 지연을 더해 처리량과 확장성으로 본 성능이 떨어지게 된다. 이런 아키텍처는 한대의 컴퓨터의 여러 CPU에서 확장하기 어럽게 만든다. 가비지 컬렉션이 여러 프로세서로 분산되지 않기 때문에, 매우 다중 쓰레드인 애플리케이션이 JVM에서 실행될 때, 애플리케이션 레벨의 작업을 이행하기에 충분한 프로세서를 갖고 있음에도 불구하고 JVM으로부터 컨트롤을 얻기 위해 기다리며, JVM은 다중 CPU 환경에서 많은 압력을 받는다. 다중 프로세서 시스템에서 단일 쓰레드 GC의 영향은 병렬 애플리케이션일수록 크게 나타난다. 만약 GC를 제외하고 애플리케이션이 완벽하게 확장한다고 가정하면, GC가 일어나는 동안 작업을 수행할 수 있는 다른 프로세서가 유휴 상태에 있게 됨으로써, GC가 확장성을 저해하는 원인(Bottleneck)이 되는 것이다.

이런 내용에서 Java를 Turbo-charge한다는 것은 많은 CPU 사용과 많은 메모리에 접근, 많은 동시 발생 소켓 연결의 처리를 의미한다.

J2SE 1.4는 새로운 기능과 버퍼 관리, 네트웍 확장성, 파일 I/O의 성능을 개선한 논블록킹 new I/O API를 구현했다.
- 새로운 네트웍 I/O 패키지는 접속할 때마다 쓰레드를 배정하는 방식을 제거함으로써 서버에 동시 접속할 수
  있는 수를 극적으로 증가시켰다.
- 새로운 파일 I/O는 읽기와 쓰기, 복사, 이동 작업에서 현재의 파일 I/O보다 2배 가까운 속도를 낸다.
  새 것은 file locking과 memory-mapped files, 동시 여러 읽기/쓰기 작업을 지원한다.


J2SE 1.4의 64비트의 JVM은 4G 이상 큰 heap을 가질 수 있을 뿐더러 300G 이상 가질 수도 있다. 만약 가비지의 생성과 배당하는 비율이 일정하다면 heap이 클수록 GC로 인한 멈춤은 더욱 빈도수가 낮아지지만 그 시간은 길어질 것이다.

애플리케이션의 효율성과 확장성을 정하는 중요한 요소는 ‘GC 순차 비용(GC sequential overhead)’이다. 이는 애플리케이션의 실행 시간 중에 애플리케이션이 멈춘 상태에서 JVM에서 GC가 실행되는 시간의 비율을 뜻한다. 다음과 같이 계산할 수 있다.

Avg. GC pause * Avg. GC frequency * 100 %.
‘GC 빈도수’는 주기적인 GC나 한 단위의 시간에 일어나는 GC수이다. ‘신세대’와 ‘구세대’의 평균 GC 멈춤과 빈도수가 매우 다르기 때문에 GC 순차 비용을 구하는 계산이 다른데, 즉 둘을 합해서 애플리케이션의 총 GC 순차 비용을 구할 수 있다. GC 순차 비용은 다음과 같이 계산될 수 있다.

Total GC time/Total wall clock run time.

총 GC 시간은 다음과 같이 계산할 수 있다.

Avg. GC pause * total no. of GCs

분명 적은 GC 순차 비용은 애플리케이션의 높은 처리량과 확장성을 의미한다.

J2SE 1.4.1은 JVM이 더 많은 CPU와 메모리를 사용할 수 있게 해, 애플리케이션의 성능과 확장성을 높일 수 있게 디자인한 두 개의 새 가비지 컬렉터를 소개했다. 이 컬렉터는 통신 업체의 도전과제를 충족할 수 있게 해준다.

- 최대의 처리량을 위해 시스템의 자원을 확장성과 최적화로 사용하기 위해, 시스템의 GC 순차 비용은 10% 이상
  될 수 없다.
- 클라이언트와 서버 간에 정해진 프로토콜에 정의된 지연에 부합되고 서버의 양호한 답변 시간을 보장하기
  위해서, 애플리케이션에서 발생되는 모든 GC 멈춤은 200ms 이상이 되면 안된다.


J2SE 1.4.1에서 소개한 두 개의 새로운 컬렉터는 Parallel Collector와 Concurrent mark-sweep(CMS) Collector이다.

-Parallel collector: Parallel collector는 신세대를 대상으로 구현되었다. 이는 다중 쓰레드이며, stop-the-world이다. 이 컬렉터는 다중 프로세서 컴퓨터에서 더 좋은 성능을 내도록 다중 쓰레드에서 GC를 할 수 있게 한다. 비록 모든 애플리케이션 쓰레드를 중지시키지만 시스템의 모든 CPU를 사용해 주어진 양의 GC를 더욱 빠르게 처리할 수 있다. 신세대 부분에서의 GC 멈춤을 많이 감소시킨다. 따라서 Parallel collector는 여러 CPU뿐만 아니라 더 많은 메모리에 애플리케이션이 확장할 수 있게 해준다.

-Concurrent mark-sweep(CMS) collector: CMS collector는 구세대를 대상으로 구현되었다. CMS collector는 애플리케이션과 함께 ‘mostly concurrently’하게 실행되어, 때로는 ‘mostly-concurrent garbage collector’로 불린다. GC 멈춤 시간을 짧게 하기 위해서 애플리케이션을 위해 사용할 처리 능력을 이용한다. CMS collection은 Initial mark와 Concurrent marking, Remark, Concurrent sweeping의 4단계로 나뉜다.

‘initial mark’와 ‘remark’ 단계는 CMS collector가 모든 애플리케이션 쓰레드를 일시적으로 중지시키는 stop-the-world 단계이다. initial mark 단계는 시스템의 ‘roots’에서 곧바로 접근가능하게 모든 객체를 기록한다. ‘concurrent marking’ 단계에서 모든 애플리케이션 쓰레드는 재시작되고 concurrent marking 단계가 초기화된다. ‘remark’ 단계에서 애플리케이션 쓰레드는 다시 중지되고 마지막 마킹을 끝낸다. ‘concurrent sweeping’ 단계에서 애플리케이션 쓰레드는 다시 시작되며 heap을 동시 스위핑하며 표기되지 않은 모든 객체는 회수된다. initial mark와 remark 단계는 상당히 짧다. 구세대의 크기가 1G라고 해도 200ms 이하의 시간이 걸린다. concurrent sweeping 단계는 mark-compact collector 만큼의 시간이 걸릴 것이지만 애플리케이션 쓰레드가 중지되지 않기 때문에, 멈춤이 숨겨져 있다.

 
‘mostly concurrent’인 CMS collector는 JVM이 큰 heap과 여러 CPU에 확장할 수 있게 해, 지연과 mark-compact stop-the-world collector로 발생한 처리량 문제를 해결한다.

표 1은 J2SE 1.4.1에 있는 여러 컬렉터의 기능을 비교해본다.
그림 1과 2, 3은 다른 여러 가비지 컬렉터를 그림으로 나타낸 것이다. 초록 화살표는 다중 CPU에서 실행되는 다중 쓰레드 애플리케이션을 뜻한다. 빨간 화살표는 GC 쓰레드를 뜻한다. GC 쓰레드의 길이는 GC 멈춤 시간을 대략적으로 나타낸다.

표 1.다른 여러 가비지 컬렉터의 기능 요약
신세대 컬렉터구세대 컬렉터
Copying collector collector
Default
Stop-the-
world
Single
threaded
All J2SEs
Mark-compact collector
Default
Stop-the-world
Single threaded
All J2SEs
Parallel collectorConcurrent mark-sweep collector
Stop-the-
world
Multi-
threaded
J2SE 1.4.1+
Mostly-concurrent
Single threaded
J2SE 1.4.1+

그림 1과 2, 3이 보여주듯, Parallel collector를 신세대에서 concurrent mark-sweep collector를 구세대에서 같이 사용하면 중지 시간과 GC 순차 비용을 줄일 수 있다. 이 두 컬렉터는 애플리케이션이 더 많은 프로세서와 메모리에 확장할 수 있도록 도와준다.

Posted by tornado
|

애플리케이션 서버의 성능과 확장성을 개선하기 위한 Java HotSpot VM vl.4.x Turbo-Charging (2)

 

 
 



J2SE 1.3의 Sun HotSpot JVM에 있는 heap 크기와 GC 튜닝에 잘 알려진 스위치들은 다음과 같다. 스위치에 대한 더 자세한 것은 JVM HotSpot VM options을 참조한다.

일반 스위치들
-server
-Xmx, -Xms
-XX:NewSize=, -XX:MaxNewSize=
-XX:SurvivorRatio=

J2SE 1.4.1에서 2개의 새로운 가비지 컬렉터를 활성화시키기 위한 스위치는 다음과 같다.


Parallel Collector
-XX:+UseParNewGC : 이 플래그는 신세대에 병렬 가비지 컬렉션을 실행시키게 한다. 구세대에 CMS collector와 함께 활성화할 수 있다.

 

-XX:ParallelGCThreads=n : 이 스위치는 JVM이 신세대 GC를 실행할 때 몇개의 병렬 GC 쓰레드를 사용할 것인지 지정해준다. 기본값은 시스템의 CPU 개수와 같다. 그렇지만 어떤 경우에는 이 수를 변경해 성능을 개선할 수 있다는 것을 관찰할 수 있다. 예를 들면 다중 CPU 컴퓨터에서 여러 JVM 인스턴트를 실행할 때이다. 이런 경우, 각 JVM이 사용하는 병렬 GC 쓰레드수를 이 스위치를 사용해 전체 CPU수보다 적게 지정해줘야 할 것이다.

-XX:+UseParallelGC : 이 플래그도 신세대에 병렬 가비지 컬렉션 정책을 사용하게 하지만, 이것은 구세대에서 CMS collector를 같이 사용하지 못한다. 이는 매우 큰 신세대 heap을 사용하는 기업환경용 애플리케이션에 적합하다.

Concurrent Collector
-XX:+UseConcMarkSweepGC : 이 플래그는 구세대에 동시 수행 가비지 컬렉션을 사용하게 한다.

-XX:CMSInitiatingOccupancyFraction=x : CMS collection을 동작시키는 구세대 heap의 threshold 비율을 지정한다. 예를 들어 60으로 지정하면, CMS collector는 구세대의 60%가 사용될 때마다 실행될 것이다. 이 threshold는 실행시 기본값으로 계산되며, 대개의 경우 구세대의 heap이 80~90%가 사용될 때만 CMS collector가 실행된다. 이 값은 튜닝을 통해 대부분의 경우 성능을 개선할 수 있다. CMS collector는 스위핑하고 메모리를 반환할 때 애플리케이션 쓰레드를 중지시키지 않기 때문에, 많은 메모리를 요구하는 애플리케이션의 경우 신세대로부터 넘어오는 객체를 위한 여유 메모리 공간을 최대한 확보할 수 있도록 해준다. 이 스위치가 최적화되지 않았을 때, CMS collection은 수행에 실패하는 경우가 생길 수도 있으며, 이 경우 기본인 stop-the-world mark-compact collector를 실행하게 된다.
다음과 같이 다른 스위치를 사용해 성능을 튜닝할 수도 있다.

-XX:MaxTenuringThreshold=y : 이 스위치는 신세대의 객체가 얼마만큼의 시간이 지나야 구세대로 승진되는지 지정할 수 있다. 기본값은 31이다. 꽤 큰 신세대와 ‘생존 공간’은 오래된 객체는 구세대로 승진되기까지 생존 공간 사이에 31번 복사가 이뤄진다. 대부분의 통신 애플리케이션에서는 호출 또는 세션마다 만들어진 80~90%의 객체는 생성 즉시 죽고, 10~20%정도가 호출이 끝날 때까지 생존한다. XX:MaxTenuringThreshold=0는 애플리케이션의 신세대에 배정한 모든 객체가 한번의 GC 주기를 생존하면 신세대의 생존 공간 사이에 복사되지 않고 곧바로 구세대로 옮겨진다. 구세대에서 CMS collector를 사용할 때, 이 스위치는 두 가지로 도움이 된다.

- 신세대 GC는 10~20%의 장기 객체를 생존 공간 사이에 여러 번 복사할 필요가 없고 곧바로 구세대로 승진시킨다.
- 추가적으로 이 객체는 구세대에서 대부분의 동시발생적인 컬렉션을 행할 수 있다. 이것은 GC 순차 비용을 추가적으로 감소시킨다.

이 스위치가 사용되면 -XX:SurvivorRatio를 128 정도의 매우 큰 값으로 정하길 권한다. 이는 생존 공간이 사용되지 않고, 매 GC 주기마다 신생에서 곧바로 구세대로 승진되기 때문이다. 생존 비율을 높임으로써 신세대의 heap은 대부분 신생에 배정된다.

-XX:TargetSurvivorRatio=z : 이 플래그는 객체가 구세대로 승진되기 전에 사용해야 하는 생존 공간 heap의 희망 비율이다. 예를 들어 z를 90으로 지정하면 생존 공간의 90%를 사용해야 신세대가 꽉 찼다고 생각해 객체가 구세대로 승진된다. 이것은 객체가 승진되기까지 신세대에서 더 오래 머무를 수 있게 한다. 기본값은 50이다.

 

JVM은 GC에 관련된 많은 유용한 정보를 파일에 로그할 수 있다. 이 정보는 애플리케이션과 JVM을 GC 시각에서 튜닝하고 측정하는 데 사용할 수 있다. 이 정보를 로그하기 위한 스위치는 다음과 같다.

verbose:gc : 이 플래그는 GC 정보를 기록하게 한다.
-Xloggc=filename : 이 스위치는 ‘verbose:gc’로 인한 정보를 표준 출력 대신 기록할 로그 파일명을 지정한다.
-XX:+PrintGCTimeStamps : 애플리케이션의 시작을 시점으로 GC가 언제 실행되었는지 출력한다.
-XX:+PrintGCDetails : GC에 대해 GC하기 전과 후의 신세대와 구세대의 크기, 총 heap 크기, 신세대와 구세대에서 GC가 실행되기까지의 시간, GC 주기마다 승진된 객체 크기와 같은 자세한 정보를 준다.
-XX:+PrintTenuringDistribution : 신세대에서 배정된 객체의 나이 분포를 알려준다. 앞서 설명한 -XX:NewSize, -XX:MaxNewSize, -XX:SurvivorRatio, -XX:MaxTenuringThreshold=0의 튜닝은 바로 이 스위치에서 나오는 데이터를 사용해서 객체가 구세대로 승진할 정도로 시간이 지났는지 판단하게 된다.

 

다음의 커맨드라인 스위치를 사용해 나타나는 GC 관련 출력을 살펴보자,

java -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+UseParNewGC -XX:+UseConc MarkSweepGC -Xmx512m -Xms512m -XX:NewSize=24m -XX:MaxNew Size=24m -XX:SurvivorRatio=2 <app-name>
 
신세대 GC
311.649: [GC 311.65: [ParNew Desired survivor size 4194304 bytes, new threshold 3 (max 31)
- age 1: 1848472 bytes, 1848472 total
- age 2: 1796200 bytes, 3644672 total
- age 3: 1795664 bytes, 5440336 total
: 21647K->5312K(24576K), 0.1333032 secs] 377334K->362736K(516096K), 0.1334940 secs]
 
앞서의 GC 스냅샷은 신세대의 GC에 대한 설명이며, 이는 다음 정보를 제공한다.

- 이 GC를 실행할 때, 총 JVM heap은 516096K이며 신세대의 heap 크기는 24576K이다.
- 이 GC가 실행될 때, 구세대의 heap 크기는 516096K-24576K=491520K이다.
- 이 GC 주기의 마지막에 애플리케이션의 신세대에 배정받은 객체의 ‘나이 분포’를 설명한다. 이 GC 주기는 new threshold=3으로 설정돼 있으며, 이는 다음 GC 주기가 되기 전에 최대 3번 시간이 지나야 한다는 뜻이다.

- Desired survivor size가 4194304바이트라는 것은 SurvivorRatio=2를 통해 결정되었다. 이는 Eden/Survivor space=2로 지정한 것이다. 신세대의 heap 크기가 24MB이면, 24MB=2*Survivor space+Survivor space+Survivor space 또는 Survivor space=8MB를 뜻한다. 기본값인 TargetSurvivorRatio=50의 희망 생존 크기는 8*.5=4MB이다.
- 애플리케이션 시작을 시점으로 이 GC가 실행된 시간은 311.649초이다.
- 이 GC가 신세대에서 만든 멈춤 시간은 0.1334940초이다.
- GC를 하기 전에 사용중이던 신세대의 크기는 21647K이다.
- GC를 한 후 사용되는 신세대의 크기는 5312K이다.
- 신세대에서 377334K-362736K=14598K의 데이터가 GC되었다.
- 21647K-5312K-14598K=1737K가 구세대로 승진되었다.
- GC를 하기 전에 사용되고 있던 총 heap 크기는 377334K이었다.
- GC를 한 후에 사용되고 있는 총 heap 크기는 362736K이다.
- GC를 하기 전에 구세대에 사용중인 heap 크기는 377334K-21647K= 355687K이었다.
- GC를 한 후 구세대의 사용하는 heap 크기는 362736K-5312K= 357424K이다.
- 이 GC에 구세대로 승진한 객체의 총 크기는 357424K-355687K=1737K이다.
- ParNew 표기는 신세대에 Parallel collector를 사용했다는 것을 나타낸다. 기본인 Copying collector를 사용하면 ParNew 대신 DefNew로 표기한다.

verbose:gc 로그 형식에서 사용된 관례는 특정 세대에 대한 보고가 있다면 그 세대의 이름이 우선시되어 보고될 것이다.

[GC [gen1 info1] [gen2 info2] info]

info는 GC의 일반적인 것이고, 신세대와 구세대를 합한 것이다. info<n>는 gen<n>에 한정돼 있다. 그러므로 각 괄호의 배치를 잘 봐야 할 것이다.
 

구세대 GC

CMS Collector
구세대에서 concurrent mark-sweep collector를 사용하는 것을 스냅샷한 것이다. 앞서 언급했듯이 이 부분은 4단계로 나눠진다.

513.474: [GC [1 CMS-initial-mark: 335432K(491520K)] 340897K (516096K), 0.0482491secs]

- stop-the-world인 initial mark 단계는 0.048초 걸렸다.
- 애플리케이션 시작 시점에서 513.474초에 시작되었다.
- 사용중이었던 구세대의 heap 크기는 335432K이다.
- 신세대에서 사용하는 heap을 포함한 총 사용중인 heap 크기는 491520K이다.
- 구세대의 heap 총 크기는 340897K이다.
- 신세대 heap을 포함한 총 heap 크기는 516096K이다.
- 이 단계는 메모리를 재순환하지 않는다.
- ‘[GC’ 접두사는 stop-the-world 단계를 뜻한다.

513.523: [CMS-concurrent-mark-start]
514.337: [CMS-concurrent-mark: 0.814/0.814 secs]
514.337: [CMS-concurrent-preclean-start]
514.36: [CMS-concurrent-preclean: 0.023/0.023 secs]

이 concurrent mark 단계는 일초(0.814 + 0.023초)도 걸리지 않았다. 그러나 GC와 함께 애플리케이션도 동시에 실행되었다. 이 단계 또한 어떠한 가비지도 컬렉션되지 않았다.

514.361: [GC 514.361: [dirty card accumulation, 0.0072366 secs]
514.368: [dirty card rescan, 0.0037990 secs]
514.372: [remark from roots, 0.1471209 secs]
514.519: [weak refs processing, 0.0043200 secs] [1 CMS-remark: 335432K(491520K)] 352841K(516096K), 0.1629795 secs]

- stop-the-world인 remark 단계는 0.162초 걸렸다.
- 구세대에 사용중이던 heap 크기는 335432K이다.
- 구세대의 총 heap 크기는 491520K이다.
- 신세대를 포함한 총 사용중이던 heap 크기는 352841K이다.
- 신세대를 포함한 총 heap 크기는 516096K이다.
- 이 단계에서 어떠한 메모리도 재순환되지 않는다.
- 이미 말했듯이 ‘[GC’ 표기는 stop-the-world 단계라는 것을 알려준다.

514.525: [CMS-concurrent-sweep-start]
517.692: [CMS-concurrent-sweep: 2.905/3.167 secs]
517.693: [CMS-concurrent-reset-start]
517.766: [CMS-concurrent-reset: 0.073/0.073 secs]

이 concurrent sweep은 3초 걸렸다. 그러나 여러 프로세스가 있는 시스템에서 이 단계는 GC와 함께 애플리케이션 쓰레드가 실행될 수 있다. 4개의 CMS 단계에서 이 단계만이 heap을 스위핑하고 컬렉션한다.

Default Mark-Compact Collector
만약 CMS collector 대신에 구세대에서 기본인 mark-compact collector가 사용되었다면 GC 스냅샷은 다음과 같을 것이다.

719.2: [GC 719.2: [DefNew: 20607K->20607K(24576K), 0.0000341 secs]719.2: [Tenured: 471847K->92010K(491520K), 2.6654172 secs] 492454K->92010K(516096K), 2.6658030 secs]

- GC가 시작한 시작은 애플리케이션 시작 시점으로 719.2초였다.
- DefNew 표기는 신세대에서 기본인 copying collector가 사용되었음을 알려준다.
- ‘[GC’ 표기는 JVM이 stop-the-world GC를 사용했다는 것을 나타낸다. 구세대의 GC에서, 애플리케이션에서 System.gc()를 통해 시스템에 GC를 요청할 수 있는데, 이것은 ‘Full GC’로 표기된다.
- 신세대의 총 heap 크기는 24576K이다.
- 신세대에서 컬렉션은 단지 시도만 이뤄졌다. 구세대에서 잠재적인 데이터를 모두 흡수할 수 있다는 보장이 없어 컬렉션할 수 없다는 것을 알았기 때문이다. 그 결과 신세대의 컬렉션은 일어나지 않았고, 신세대에서는 어떠한 메모리 재순환도 이뤄지지 않았다고 보고했다(순간의 ms 내에 종료했다는 것을 주시한다). 이는 신세대에서 컬렉션이 이뤄지지 못한다는 것을 결정하는 것 외에는 별달리 한 것이 없기 때문이다.

[DefNew: 20607K->20607K(24576K), 0.0000341 secs]

신세대에서 승진되는 것을 흡수하지 못해 구세대가 꽉 찼기 때문에 GC가 필요하다는 알게 되었다. 그래서 full mark-compact collection이 실행되었다.

- 보유하고 있는 표기는 구세대에 full mark-compact GC가 실행되었다는 것을 나타낸다. ‘구세대’는 ‘Tenured generation’이라고도 한다.
- GC를 하기 전에 구세대의 사용중인 heap 크기는 471847K이었다.
- GC 후의 구세대의 사용 heap 크기는 92010K이다.
- GC가 실행될 때의 구세대의 총 heap 크기는 491520K이었다.
- GC 전에 신세대와 구세대의 합인 총 사용중인 heap 크기는 492454K이었다.
- GC 후에 신세대와 구세대의 합인 총 사용 heap 크기는 92010K이다.
- GC를 통해 컬렉션된 총 크기는 492454K-92010K=399837K이다.
- JVM의 총 heap 크기는 516096K이다.
- 이 GC로 총 2.6658030초동안 애플리케이션이 중지되었다.

 

verbose:gc 로그로부터 데이터 마이닝을 통해 애플리케이션
모델링하기


이 로그를 통해 가비지 컬렉션 시각에서 애플리케이션과 JVM 행동에 대한 다양한 정보가 파생될 수 있다. 이들은 다음과 같다.

- 신세대와 구세대에서의 평균 GC 멈춤 시간(Average GC pauses) : JVM에서 가비지 컬렉션이 이뤄지는 동안 애플리케이션이 중지되는 평균 시간이다.
- 신세대와 구세대에서의 평균 GC 빈도수(Average GC frequency) : 신세대와 구세대에서 실행된 가비지 컬렉터의 주기수이다. 이 값은 각 GC가 로그에 기록된 시간을 보면 구할 수 있다.
- GC 순차 비용(GC sequential overhead) : 가비지 컬렉션이 이뤄지면서 애플리케이션이 중지된 시스템 시간의 비율이다. Avg. GC pause*Avg. GC frequency*100%로 계산할 수 있다.
- GC concurrent overhead : 애플리케이션과 함께 가비지 컬렉션이 일어난 시스템 시간의 비율이다. Avg. concurrent GC time (sweeping phase) * Avg. concurrent GC frequency / no. of CPUs로 계산할 수 있다.
- 신세대와 구세대에서 각 GC에서 재순환된 메모리 : 각 GC에서 컬렉션된 총 가비지
- 할당 비율 : 애플리케이션의 신세대에서 데이터가 할당받는 비율을 말한다. 만약 heap이 occupancy_at_start_of_current_gc=x , occupancy_at_end_of_ previous_gc = y, GC 빈도수가 초당 1이면, 할당 비율은 약 초당 x-y이다.
- 승진 비율(Promotion rate) : 데이터가 구세대로 승진되는 비율. ‘신세대 GC’에서 설명한 것처럼 GC당 승진된 객체의 크기가 계산되었고, 예를 들어 신세대 GC 빈도수가 초당 1일 때 그 스냅샷에 의하면 승진 비율은 초당 1737K이다.
- 호출당 애플리케이션에 의해 할당된 총 데이터 : 이것은 앞서와 같이 할당 비율을 구하고, 애플리케이션 서버의 로드가 호출 비율이라고 할 때, 할당 비율/호출 비율(Allocation Rate/Call rate)로 계산될 수 있다. 즉 호출 비율은 요청 또는 오는 호출을 서버가 처리하는 비율을 말한다.
- 모든 데이터는 단기 데이터(short term)와 장기 데이터(long term data)를 나눌 수 있다 : 장기 데이터는 신세대 GC에서 생존해서 구세대로 승진한 것을 말한다. 승진 비율/호출 비율(Promotion Rate/Call Rate)로 계산할 수 있다.
- 호출당 단기 데이터(Short Term) : 단기 데이터는 매우 빨리 소멸하며, 신세대에서 컬렉션할 수 있다. 이것은 총 데이터-장기 데이터(Total Data-Long Term Data)로 계산할 수 있다.
- 호출당 총 활성화 데이터 : JVM heap 크기를 결정하는 데 매우 중요한 정보이다. 예를 들어 이 SIP 애플리케이션의 경우와 같이, 초당 100 호출의 로드에 초당 50K의 장기 데이터가 최소 40초 이상 지속되면 구세대의 최소 메모리 크기는 50K*40s*100=200M이어야 한다.
- ‘메모리 누출(Memory leaks)’ : 이것은 감지할 수 있으며 로그가 보여주는 각 GC를 모니터링에서 나오는 ‘out of memory’ 에러로 보다 쉽게 이해할 수 있을 것이다.

이런 종류의 정보는 GC의 시각에서 애플리케이션과 JVM 행동을 보다 잘 이해하고 가비지 컬렉션의 실행 성능을 튜닝하는 데 사용할 수 있다.

 

PrintGCStats(http://developer.java.sun.com/servlet/Turbo1EntryServlet에서 다운받을 수 있다)는 ‘verbose:gc’ 로그를 마이닝하는 쉘 스크립트이고 신세대와 구세대의 GC 중단 시간(총, 평균, 최대, 표준편차)의 통계를 낸다. 이는 GC 순차적 오버헤드, GC 동시발생 오버헤드, 데이터 배정, 승진 비율, 총 GC와 애플리케이션 시간과 같은 다른 중요한 GC 매개변수도 계산한다. 통계 외에도 PrintGCStats는 사용자가 설정한 주기에 맞춰 애플리케이션 실행에 대해 GC를 시간에 따라 분석한다.

 

입력
이 스크립트의 입력은 HotSpot VM이 다음의 하나 이상의 플래그를 사용했을 때 나오는 출력이다.

-verbose:gc : 최소의 출력을 생성한다. 제한적인 통계를 내지만, 모든 JVM에서 사용할 수 있다.
-XX:+PrintGCTimeStamps : 시간 단위의 통계를 활성화한다. 예를 들어 할당 비율과 주기 등이 있다.
-XX:+PrintGCDetails : 더 많은 통계 정보를 얻을 수 있다.
J2SE 1.4.1 이후부터 다음 명령어를 권한다.
java -verbose:gc -XX:+PrintGCTimeStamps -XX:+ PrintGCDetails ...

 

사용법
PrintGCStats -v ncpu=<n> [-v interval=<seconds>] [-v verbose=1] <gc_log_file >
·ncpu
Java 애플리케이션이 실행된 컴퓨터의 CPU 개수이다. 사용할 수 있는 CPU 시간과 GC ‘로드’ 요소를 계산하는 데 사용된다. 기본값이 없기 때문에 명령어에서 필히 지정해줘야 한다(기본값 1은 틀릴 때가 많다).

·interval
시간 단위 분석을 위해서 매 주기의 마지막에 통계를 출력한다 : -XX:+PrintGCTimeStamps의 출력이 필요하다. 기본값은 0(비활성화)

·verbose
만약 non-zero이면 통계 요약 후 추가적으로 각 아이템은 다른 줄에 출력한다.

출력 통계치
표 1은 PrintGCStats로부터의 통계치를 설명한 것이다.

 
표 1. PrintGCStats이 출력한 통계 요약
항목명 정의
  gen0(s) 신세대의 GC 시간을 초단위로 나타낸다.
  cmsIM(s) CMS의 initial mark 단계의 멈춤 시간을 초단위로 나타낸다.
  cmsRM(s) CMS의 remark 단계의 멈춤 시간을 초단위로 나타낸다.
  GC(s) 모든 stop-the-world GC의 멈춤 시간을 초단위로 나타낸다.
  cmsCM(s) CMS concurrent mark 단계를 초단위로 나타낸다.
  cmsCS(s) CMS concurrent sweep 단계를 초단위로 나타낸다.
  alloc(MB) 신세대에 객체 할당한 용량(MB)
  promo(MB) 구세대에 승진된 객체 용량(MB)
  elapsed_time(s) 애플리케이션이 실행되며 경과된 총 시간(초)
  tot_cpu_time(s) 총 CPU 시간 = CPU 개수 * 경과 시간
  mut_cpu_time(s) 애플리케이션이 사용한 총 CPU 시간.
  gc0_time(s) 신세대를 GC할 때 총 멈춰진 시간
  alloc/elapsed_time(MB/s) 경위 시간당 할당 비율(MB/초)
  alloc/tot_cpu_time(MB/s) 총 CPU 시간당 할당 비율(MB/초)
  alloc/mut_cpu_time(MB/s) 애플리케이션 총 시간당 할당 비율(MB/초)
  promo/gc0_time(MB/s) GC 시간당 승진 비율(MB/초)
  gc_seq_load(%) stop-the-world GC에 사용된 총 시간의 비율
  gc_conc_load(%) 동시발생적 GC에 사용된 총 시간의 비율
  gc_tot_load(%) GC 시간(순차적과 동시발생적)의 총 비율

- Solaris 8 이후로 사용할 수 있는 alternate thread library를 사용해라.
Solaris 8에서 LD_LIBRARY_PATH=/usr/lib/lwp:/usr/lib을 설정해서 사용할 수 있고, Solaris 9에서는 기본 라이브러리로 설정돼 있다. 이는 Java 쓰레드와 커널 쓰레드 사이에 ‘one-to-one threading model’을 사용한다. 이 라이브러리를 사용해서 대부분의 경우에 처리량이 5~10% 또는 그 이상으로 성능이 향상됐다.

- prstat -Lm -p <jvm process id>로 light-weight-process(LWP) 단위로 한 프로세스의 자원 사용을 분석하고 확장성과 성능 면에서의 병목 지점을 찾을 수 있게 한다. 예를 들어 애플리케이션의 확장성에 있어 병목 지점이 GC인지 확인하는 데 도움을 준다. 대부분의 경우, 애플리케이션이 자체적으로 제대로 쓰레드화하지 못해 큰 시스템으로 확장되지 못한다. 이 명령어는 시스템 자원 사용과 주어진 프로세스에서 LWP 단위의 활동을 보고한다. 더 자세한 사항은 man prstat에서 참조한다. 이 명령어의 결과 출력물의 단점은 보고된 LWP id가 어떤 Java 쓰레드인지 확인하기 어렵다는 것이다.

- ThreadAnalyser는 Java 프로세스를 분석하는 쉘 스크립트이고, Solaris의 LWPIDs 대신 prstat의 출력물에 있는 ‘쓰레드명’을 생성한다. Java 애플리케이션의 시스템 사용을 ‘쓰레드명’ 단위로 확인할 수 있게 해준다. 이 스크립트를 앞서 언급한 alternate thread library와 함께 사용하는 것이 가장 이상적이다.

ThreadAnalyser는 이미 실행돼 있는 Java 프로세스에 붙일 수 있거나(파일로 전환된 프로세스의 stderr 출력에 접근할 수 있는 한) Java 프로세스를 시동시킬 수 있는 스크립트 파일을 사용해 동작시킬 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
사용 예
Java 프로세스를 시작하고 표준 에러를 java <app_name>>/ tmp/java.out 2>&1 으로 파일로 전환시킨다. Java 프로세스의 PID를 주의하고 다음과 같이 ThreadAnalyser(http://developer.java.sun.com/servlet/Turbo2EntryServlet에서 다운받을 수 있다.)을 붙인다.

ThreadAnalyser -f /tmp/java.out -p PID <interval>
또는
ThreadAnalyser -f /tmp/java.out <interval>


이 컴퓨터에 다른 Java 프로세스가 없으면 ps(1)을 사용해 PID를 구한다. ThreadAnalyser로 Java 프로세스를 실행하고자 한다면 다음과 같이 한다.

ThreadAnalyser -s <app_start_script_file> <interval>

이것은 이 시스템에 다른 Java 프로세스가 동작하고 있지 않을 때만 사용할 수 있다. app_start_script_file이 Java 프로세스을 실행하는 실행 스크립트 파일명이다. Interval은 스크립트로부터 출력되는 정보 갱신 주기이고, 기본값은 5이다.

보다 많은 옵션과 사용법은 ThreadAnalyser 스크립트의 README 부분을 읽어보기 바란다.
 

man prstat 명령어를 입력하면 다음과 같은 출력의 각 열이 의미하는 것을 자세히 알 수 있다.



- 실행되는 Java 애플리케이션의 모든 쓰레드의 덤프(dump)를 얻으려면, SIGQUIT 신호를 JVM 프로세스에 보낸다. 이는 kill - QUIT <JVM process pid> 명령어로 신호를 보낼 수 있다.
- JVM 프로세스 안의 각 LWP의 hex와 symbolic stack trace를 얻으려면 명령어 pstack <JVM process id>을 사용하면 된다. 이 출력은 LWP id와 Java 애플리케이션 쓰레드 id와의 관계를 알려준다. 만약 alternate thread library를 JVM에서 사용하면, 출력물은 프로세스의 각 LWP와 해당 묶여 있는 Java 애플리케이션 쓰레드의 일대일 관계를 보여준다. 명령어 man pstack을 입력하면 보다 자세한 것을 알 수 있다.
- JVM에 있는 -Xrunhprof 플래그를 사용해 불필요한 객체 보유(때로는 막연하게 ‘메모리 누수’라 부른다.)를 확인할 수 있다.
- UDP 기반의 애플리케이션에서는 DatagramSocket.connect (InetAddress address, int port)와 DatagramSocket.disconnect()의 호출 횟수를 최소화한다. J2SE 1.4 버전 이상의 JVM부터는 이것들이 네이티브한 호출이기 때문에, 여러 다른 이점을 가져다 주지만, 잘못된 사용은 애플리케이션의 성능 저하를 야기한다. API를 잘못 사용하면 애플리케이션의 애플리케이션 ‘시스템 시간’ 시점에서 매우 비싼 비용을 지불하게 된다.

 

다음에 보여줄 결과값은 2개의 새로운 GC 정책과 앞서 언급한 튜닝 기법을 사용해 얻은 것이다. 이 사례를 위해서 Ubiquity의 Application Services Broker(ASB)인 SIP Application Server가 사용되었다. 지난 호에 언급했듯이 ASB는 GC 시점에서 JVM을 광범위하게 사용하는 일반적인 통신 애플리케이션 서버를 대표한다.

 

실험적인 플랫폼
4개의 900MHz UltraSPARC Ⅲ 프로세서를 장착하고, Solaris 8이 설치된 Sun Fire V480을 사용했다. 물론 논의된 튜닝 기술들은 Linux와 Windows, Solaris(인텔 아키텍처)와 같은 다른 플랫폼에도 적용된다.
구세대 heap의 크기는 ASB 서버의 최대 로드에 기반해서 결정되었고, JVM은 튜닝이 적용되었다. 신세대 heap의 크기는 12MB에서 128MB 사이에 경험을 통해 계속적으로 변경해보았다. GC 순차적 오버헤드가 가장 낮고 신세대에 허용할 수 있는 GC 멈춤 정도를 갖는 최적의 크기는 24MB였다. 24MB 이상의 신세대는 GC 멈춤이 증가했고 성능의 차이가 없었다. 24MB 이하는 신세대의 GC 빈도수가 많아지면서 GC 순차적 오버헤드가 급증하고 짧은 생명주기를 갖은 데이터도 구세대로 승진되고 만다. 그림 1은 신세대의 heap 크기에 따른 다양한 GC 순차적 오버헤드와 GC 멈춤, GC 빈도수를 보여준다.

512MB의 구세대와 24MB의 신세대에서 여러 가지의 GC 정책과 튜닝 기술을 사용해 나온 결과값을 보여준다.

그림 1. 신세대의 다양한 GC 순차적 오버헤드와 GC 멈춤, GC 빈도수를 신세대의 heap 크기에 비례해서 보여준다.

단계 1 : J2SE 1.4.1에서 신세대와 구세대의 기본 GC를 사용할 때 가장 좋은 결과다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=2 <application>

구세대의 평균 GC pause: 3 secs.
신세대의 평균 GC pause: 110 ms.
GC sequential overhead: 18.9%

단계 2 : J2SE 1.4.1에서 구세대의 GC를 CMS 컬렉터로 설정하고 나온 가장 좋은 값이다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=128 -XX:+UseConcMarkSweepGC -XX:MaxTenuring Threshold=0 -XX:CMSInitiatingOccupancyFraction=60 <application>

-XX:MaxTenuringThreshold=0로 설정하면 성능이 증가한다. -XX:CMSInitiatingOccupancyFraction의 적절한 값은 60이다. 이보다 작으면 더 많은 CMS 가비지 컬렉션이 일어나고 많으면 CMS 컬렉션의 효율이 떨어진다.

구세대의 평균 GC 멈춤(stop-the-world init mark와 동시발생 컬렉션의 remark phase) : 115ms
신세대의 평균 GC 멈춤 : 100ms
GC 순차적 오버헤드 : 8.6%

단계 3 : J2SE 1.4.1에서 구세대는 CMS 컬렉터를 신세대는 새로운 패러럴 컬렉터를 사용할 때 가장 좋은 값이다.

java -Xmx512m -Xms512m -XX:MaxNewSize=24m -XX:NewSize=24m -XX:SurvivorRatio=128 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=0 -XX:CMSInitiatingOccupancyFraction=60 <application>

구세대의 평균 GC 멈춤(stop-the-world init mark와 동시발생 컬렉션의 remark phase) : 142ms
신세대의 평균 GC 멈춤 : 50ms
GC 순차적 오버헤드 : 5.9%

 
표 2. 사례를 통해 얻은 값을 정리
Summary Table 구세대의 평균
GC 멈춤(ms)
신세대의 평균
GC 멈춤(ms)
순차적
오버헤드(%)
  기본 컬렉터3000 110 18.9%
  CMS 컬렉터1151008.6%
  CMS와 패러럴 컬렉터142 50 5.9%
 

결과
CMS 컬렉터를 사용함으로써 구세대에서 GC 멈춤은 2000% 감소하고 GC 순차적 오버헤드는 220% 감소했다. GC 순차적 오버헤드의 감소는 애플리케이션 처리량의 성능 향상에 직접적으로 영향을 준다. 패러럴 컬렉터를 사용해 신세대에서 GC 멈춤은 100% 감소했다. 4개의 CPU가 장착된 시스템에서 4배 성능의 패러럴 컬렉터를 기대했을지도 모르겠다. 그러나 패러럴 컬렉션에 연관된 간접 비용에 의해, 가속도는 선형을 그리지 않지만 JVM이 선형을 그리도록 연구하고 있다.

기업에 적용
성능 튜닝 기술과 소개된 새로운 GC 정책은 통신업체에만 국한된 것이 아니고 웹 서버와 포털 서버, 애플리케이션 서버와 같이 비슷한 요구사항을 갖는 대기업용으로도 적용될 수 있다.

미래에는...
썬 JVM에서 하위 레벨의 명령어 플래그를 없애려고 연구중이다. 신세대와 구세대의 크기와 GC 정책 등을 설정하기보다는 최고 멈춤 시간 또는 지연, JVM의 메모리 사용량, CPU 사용법 등과 같은 상위 레벨의 요구사항을 설정하면 JVM이 자동적으로 그 설정에 맞게 애플리케이션을 위해 여러 하위 레벨의 값과 정책을 변경한다.

HotSpot JVM에 JVM의 조작 면에서 상위 레벨 단의 성능 정보를 제공할 수 있는 가볍고 항상 실행될 수 있는 것을 포함하려고 한다. 향후 출시되는 JVM에서 사용할 수 있겠지만, 현재로서는 툴과 기술은 아직 실험적이다.

Java Specification Request(JSR) 174는 JVM을 모니터링하고 관리하는 API 스펙이고, JSR 163은 시간과 메모리에 대한 프로파일링의 지원으로 실행되는 JVM의 프로파일링 정보를 추출해내는 API 스펙이다. 이 API는 프로파일에 영향을 주지 않는 구현이 되도록 설계될 것이다. 이 API는 프로파일링의 상호 운용성과 향상된 가비지 컬렉션 기술과 최대한 많은 JVM에서도 안정적으로 동작하는 구현을 고려할 것이고, Java 애플리케이션, 시스템 관리 툴, RAS 관련 툴에 JVM의 상태를 모니터할 수 있는 능력을 부여하며, 많은 런타임 컨트롤을 관리할 수 있게 할 것이다.

통신 애플리케이션 서버의 특수한 요구사항을 충족하기 위한 연구가 Sun ONE Application Server 7에서도 진행되고 있다. 통신업체의 경우 서비스와 데이터에 더 높은 가용성이 요구되는 것과 서비스 업그레이드를 제외하면 대기업과 유사하다. Sun ONE Application Server Enterprise Edition 7은 상태 확인 저장소와 세션, 빈 상태의 높은 유용성을 주는 Clustra ‘Always ON’ 기술이 포함돼 있다. Sun ONE Application server 7는 JAIN과 OSA/Parlay 프레임웍 기반의 호출 프로세싱 서버를 구축하는 데 사용할 수 있다. EJB 컨테이너 향상을 위해 SLEE 구현은 Sun ONE Application server에 통합될 수 있다. 추후 EJB를 사용할 때 지속적인 유용성과 어느 정도의 실시간, QoS 능력을 제공하기 위해 JSR 117 API 뿐만 아니라 통합된 SIP 스택도 포함될 수 있다.

통신 업체 사이에 Java의 인기가 증가하면서 썬 JVM은 그들의 요구 사항에 맞추어 발전해가고 있다. 대형 서버의 요구사항을 충족시키는 데 필요한 해결책으로 64비트 JVM과 대형 하드웨어에서(4G에 제한되지 않은) 대용량의 heap 크기를 설정할 수 있다. 여러 CPU 시스템에서 패러럴 컬렉터는 신세대에서 멈춤을 줄일 수 있고 동시발생 컬렉션은 구세대에 mark-compact collector로 인해 생긴 큰 GC 멈춤을 숨길 수 있다. 이로 인해 GC 순차적 오버헤드를 상당히 감소시켜서, 다수의 CPU가 장착된 시스템에서 애플리케이션의 성능과 확장성을 높여줄 수 있다.

JVM의 가비지 컬렉션은 verbose:gc의 로그를 보고 모니터하고, 분석하고 튜닝할 수 있다. GC 로그의 정보는 JVM의 크기를 조정하고 애플리케이션의 최적의 성능을 내기 위한 스위치와 매개변수를 설정하기 위해 분석된다.

머지않아 Sun ONE Application Server 7 Enterprise Edition은 최적의 통신 애플리케이션 서버로 구현될 것이다.

참고로 이글은 썬의 Alka Gupta 씨와 Ubiquity Software Corporation의 CTO인 Michael Doyle 씨가 쓴 ‘Turbo-Charging the Java HotSpot VM, 1.4.X to Improve the Performance and Scalability of Application Servers’를 정리한 것이다.

 

Posted by tornado
|

 

출처 : MSDN

ASP.NET 마스터하기: 사용자 지정 엔터티 클래스 소개

 

Karl Seguin
Microsoft Corporation

요약: 경우에 따라 형식화되지 않은 DataSet가 데이터 조작을 위한 최선의 솔루션이 되지 못하는 상황이 있을 수 있습니다. 이 가이드에서는 사용자 지정 엔터티 및 컬렉션이라는 DataSet의 대안을 살펴도록 하겠습니다.

목차

소개
DataSet의 문제
사용자 지정 엔터티 클래스
개체 관련 매핑
사용자 지정 컬렉션
관계 관리
중급 단계
결론

소개

ADODB.RecordSet과 곧잘 잊혀지던 MoveNext의 시대는 가고 이제는 그 자리를 Microsoft ADO.NET의 강력하고 유연한 기능이 대신하게 되었습니다. Microsoft에서 내놓은 새로운 방법은 탁월한 속도의 DataReader와 풍부한 기능의 DataSet를 갖추고 뛰어난 개체 지향 모델에 패키지화되는 System.Data 네임스페이스입니다. 이러한 도구를 마음대로 사용할 수 있는 상황은 충분히 예견된 일입니다. 모든 3 계층 아키텍처는 강력한 DAL(데이터 액세스 계층)을 사용하여 데이터 계층을 비즈니스 계층에 안정적으로 연결합니다. 양질의 DAL은 코드 재사용률을 높이고 뛰어난 성능을 위한 핵심 역할을 수행할 뿐 아니라 완전히 투명하게 나타납니다(이 기사에는 영문 페이지 링크가 포함되어 있습니다).

도구가 발전을 거듭함에 따라 일정한 개발 패턴을 갖게 되었습니다. MoveNext와 작별을 고한 것은 성가신 구문으로부터 벗어난 수준을 뛰어넘어 연결이 끊어진 데이터에 눈을 돌리게 한 것은 물론 응용 프로그램을 빌드하는 방법에도 막대한 영향을 미쳤습니다.

DataReader에 익숙해지자(RecordSet과 유사하게 작동) 얼마 지나지 않아 DataAdapter, DataSet, DataTableDataView에도 과감히 달려들어 살펴보게 되었습니다. 이는 개발 방식에 변화를 주는 새 개체를 활용하는 능력이 향상된 것으로 볼 수 있습니다. 연결이 끊어진 데이터를 사용하면 새로운 캐싱 기법을 활용할 수 있어 응용 프로그램의 성능이 크게 향상됩니다. 게다가 이러한 클래스 기능을 통해 보다 세련되고 강력한 함수를 작성할 수 있게 된 동시에 때로는 일반적인 작업에 필요한 코드의 양을 눈에 띌 만큼 줄이게 되었습니다.

DataSet가 특히 적합한 상황은 프로토타입, 소형 시스템 및 지원 유틸리티를 비롯하여 다양합니다. 하지만 출시 시간보다 유지 관리의 편의성이 중요한 엔터프라이즈 시스템에 사용하면 최상의 효과를 발휘하지 못할 수도 있습니다. 이 가이드의 목표는 이러한 작업 유형을 위해 조정된 DataSet를 대신할 사용자 지정 엔터티 및 컬렉션이라는 대안을 살펴보는 것입니다. 다른 대안도 있기는 하지만 기능이 동일하지 않거나 지원 수준이 떨어집니다. 가장 먼저 할 일은 DataSet의 단점을 확인하고 해결할 문제를 이해하는 것입니다.

모든 솔루션은 저마다 장단점이 있으므로 사용자 지정 엔터티의 단점(이후 설명 참조)보다 DataSet의 단점에 더 친숙해질 수 있습니다. 따라서 여러분과 팀 구성원은 해당 프로젝트에 보다 적합한 솔루션을 결정해야만 합니다. 또한 변경할 요구 사항의 특성 및 실제 코드 개발보다 생산 후에 더 많은 시간이 소요될 가능성을 비롯하여 총 솔루션 비용을 반드시 고려해야 합니다. 마지막으로, 여기서 언급하는 DataSet는 형식화되지 않은 DataSet의 일부 단점을 해결한 형식화된 DataSet를 말하는 것이 아님을 유의하십시오.

DataSet의 문제

추상화의 부재

DataSet의 대안을 고려해야 할 첫 번째이자 가장 확실한 이유는 코드와 데이터베이스 구조를 분리할 수 없다는 점에 있습니다. DataAdapter는 기본 데이터베이스 공급업체(Microsoft, Oracle, IBM 등) 종류에 관계 없이 코드를 작성하는 데는 효과적이지만 테이블, 열 및 관계 같은 핵심 데이터베이스 구성 요소를 추상화하지는 못합니다. 이러한 핵심 데이터베이스 구성 요소는 DataSet의 핵심 구성 요소이기도 합니다. DataSet와 데이터베이스는 일반적인 구성 요소 이상의 것을 공유하며 아쉽게도 스키마까지 공유합니다. 다음과 같은 Select 문이 있다고 가정합시다.

SELECT UserId, FirstName, LastName   FROM Users

다들 알겠지만 값은 DataSet 내의 UserId, FirstNameLastName DataColumn에 있습니다.

이것이 어째서 문제가 되는 것일까요? 기본적인 일반 예제를 살펴봅시다. 먼저 다음과 같이 간단한 DAL 함수를 만듭니다.

'Visual Basic .NETPublic Function GetAllUsers() As DataSet Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As SqlCommand = New SqlCommand("GetUsers", connection) command.CommandType = CommandType.StoredProcedure Dim da As SqlDataAdapter = New SqlDataAdapter(command) Try  Dim ds As DataSet = New DataSet  da.Fill(ds)  Return ds Finally  connection.Dispose()  command.Dispose()  da.Dispose() End TryEnd Function//C#public DataSet GetAllUsers() { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("GetUsers", connection); command.CommandType = CommandType.StoredProcedure; SqlDataAdapter da = new SqlDataAdapter(command); try {  DataSet ds = new DataSet();  da.Fill(ds);  return ds; }finally {  connection.Dispose();  command.Dispose();  da.Dispose(); }            }

그런 다음 아래와 같이 모든 사용자의 이름을 표시하는 반복기가 있는 페이지를 만듭니다.

<HTML> <body>   <form id="Form1" method="post" runat="server">     <asp:Repeater ID="users" Runat="server">        <ItemTemplate>           <%# DataBinder.Eval(Container.DataItem, "FirstName") %>           <br />        </ItemTemplate>     </asp:Repeater>   </form> </body></HTML><script runat="server">  public sub page_load     users.DataSource = GetAllUsers()     users.DataBind()  end sub</script>

위에 나온 것처럼 ASPX 페이지는 반복기의 DataSource에 대해 DAL 함수 GetAllUsers를 사용합니다. 어떠한 이유로든(예: 성능 향상을 위한 비정규화, 명확도 향상을 위한 정규화, 요구 사항의 변화) 데이터베이스 스키마가 변경되면 변경 내용은 항상 "FirstName" 열 이름을 사용하는 ASPX 즉, Databinder.Eval 줄로 전달됩니다. 이렇게 되면 즉시 '데이터베이스 스키마의 변경 내용이 항상 ASPX 코드로 전달될까?'와 같은 위험한 의문이 머리 속에 떠오르게 됩니다. N 계층의 장점이 무색해지는 대목입니다.

해야 할 작업이 간단한 열 이름 바꾸기 뿐이라면 이 예제에서의 변경 작업은 간단하게 이루어집니다. 그러나 GetAllUsers를 수많은 위치에 사용하거나 설상가상으로 웹 서비스로 노출하여 수없이 많은 소비자에게 공급한다면 어떻게 될까요? 얼마나 쉽게 또는 안전하게 변경 내용을 전파할 수 있을까요? 이 기본 예제에서는 저장 프로시저가 추상화 계층 역할을 수행하는 것으로 충분하지만 가장 기본적인 보호의 용도 이외에 모든 부분에서 저장 프로시저에 의존하면 향후 더 큰 문제가 발생하게 됩니다. 그러면 이러한 형태를 하드 코딩이라고 가정해 봅시다. 본질적으로 DataSet를 사용하면 데이터베이스 스키마(열 이름을 사용하든 순서를 사용하든 관계없이)와 응용 프로그램/비즈니스 계층 사이에 긴밀한 연결을 만들게 됩니다. 이전의 경험(또는 논리)을 통해 하드 코딩이 유지 관리 및 향후 개발에 미치는 악영향을 알고 있을 것입니다.

DataSet가 적절한 추상화를 제공하지 못하는 또 다른 이유는 개발자가 기본 스키마를 알고 있어야 하기 때문입니다. 여기서 말하는 스키마란 기본 지식을 의미하는 것이 아니라 열 이름, 형식 및 관계에 대한 전체 지식을 의미하는 것입니다. 이러한 요구 사항을 없애면 위에서처럼 코드가 잘못될 위험이 줄어들 뿐 아니라 작성 및 유지 관리도 용이해집니다. 간단히 나타내면 다음과 같습니다.

Convert.ToInt32(ds.Tables[0].Rows[i]["userId"]);

위의 코드는 읽기 어려울 뿐만 아니라 열 이름 및 해당 형식에 대해 자세히 알고 있어야 합니다. 이상적인 경우라면 비즈니스 계층에서는 기본 데이터베이스, 데이터베이스 스키마 또는 SQL에 대해 전혀 알 필요가 없습니다. DataSet를 이전 코드 문자열에 나타난 대로 사용하면(CodeBehind를 사용해도 효과가 없음) 비즈니스 계층이 매우 얇아질 수 있습니다.

약한 형식

DataSet는 오류가 자주 발생하여 개발 노력에 영향을 줄 수 있는 약한 형식입니다. 다시 말해 DataSet에서 값을 검색할 때마다 System.Object 형식으로 반환되므로 이를 변환해야 합니다. 여기서 직면하는 위험은 변환에 실패하는 상황입니다. 안타깝게도 이러한 실패 상황은 컴파일 타임이 아닌 런타임에 발생합니다. 또한 Microsoft VS.NET(Visual Studio.NET) 같은 도구는 개발자가 약한 형식의 개체를 작업하는 데 있어 그다지 많은 도움이 되지 못합니다. 바로 이러한 이유 때문에 앞에서 스키마에 대해 풍부한 지식을 갖추고 있어야 한다고 언급한 것입니다. 다음은 매우 일반적인 예제입니다.

'Visual Basic.NETDim userId As Integer =        Convert.ToInt32(ds.Tables(0).Rows(0)("UserId"))Dim userId As Integer = CInt(ds.Tables(0).Rows(0)("UserId"))Dim userId As Integer = CInt(ds.Tables(0).Rows(0)(0))//C#int userId = Convert.ToInt32(ds.Tables[0].Rows[0]("UserId"));

이 코드는 DataSet에서 값을 검색할 수 있는 방법을 나타내며 아마도 수 많은 위치에 이 코드가 있을 것입니다(변환을 수행하지 않고 현재 Visual Basic .NET을 사용하는 경우 Option Strict를 비활성화했을 것이며 이 경우 문제는 훨씬 심각해집니다).

아쉽게도 위의 각 코드 줄은 다음과 같은 수많은 런타임 오류를 발생시킬 수 있습니다.

  1. 변환에 실패하는 원인은 다음과 같습니다.
    • 값이 null일 수 없습니다.
    • 개발자가 기본 데이터 형식을 잘못 알고 있을 수 있습니다(데이터베이스 스키마에 대해 자세히 알고 있어야 함).
    • 순서 값을 사용하는 경우, X 위치에 어떤 열이 있는지 알 수 없습니다.
  2. ds.Tables(0)가 null 참조를 반환할 수 있습니다(DAL 메서드 또는 저장 프로시저의 일부가 실패한 경우).
  3. "UserId"의 열 이름이 올바르지 않은 원인은 다음과 같습니다.
    • 이름이 변경되었을 수 있습니다.
    • 저장 프로시저에 의해 반환되지 않을 수 있습니다.
    • 오타가 있을 수 있습니다.

null/nothing에 대한 확인과 try/catch를 변환 과정에 추가하는 방식으로 코드를 수정하여 좀 더 방어적으로 작성할 수 있더라도 개발자에게는 도움이 되지 않습니다.

가장 나쁜 상황은 앞에서 언급했듯이 추상화되지 않는다는 점입니다. 이렇게 되면 DataSet에서 userId를 제거할 때마다 앞에서 언급한 위험을 겪게 되거나 동일한 방어 단계를 다시 프로그래밍해야 합니다(이 문제를 완화하는 데는 유틸리티 함수가 도움이 됨). 약한 형식의 개체는 오류를 항상 자동으로 발견하여 손쉽게 수정하는 디자인 타임이나 컴파일 타임에서 위험이 생산 단계에 노출되어 잡아내기가 어려운 런타임으로 옮깁니다.

비개체 지향

DataSet가 개체이고 C# 및 Visual Basic .NET이 OO(개체 지향) 언어라고 해서 이를 사용할 때 개체 지향이 자동으로 이루어지는 것은 아닙니다. OO 프로그래밍의 "Hello World"는 일반적으로 Person 클래스의 하위 클래스인 Employee 클래스 입니다. 그러나 DataSet는 이러한 상속 유형이나 대부분의 다른 OO 기법을 가능한(최소한 자연스럽게/직관적으로) 만들지 않습니다. 클래스 엔터티의 열렬한 지지자인 Scott Hanselman은 이를 다음과 같이 잘 설명하고 있습니다.

"DataSet는 물론 개체입니다. 그러나 도메인 개체도 아니고 'Apple' 또는 'Orange'도 아닌 'DataSet' 형식의 개체입니다. DataSet는 일종의 그릇입니다(백업 데이터 저장소에 대한 정보가 있는). DataSet는 행과 열을 저장하는 방법을 알고 있는 개체이기도 합니다. 또한 이 개체는 데이터베이스에 대해서도 많은 부분을 알고 있습니다. 그러나 저는 그릇은 반환하고 싶지 않으며 'Apples' 같은 도메인 개체를 반환하고 싶습니다."1

DataSet는 데이터를 관계 형식으로 유지하므로 강력한 특성을 나타내고 관계형 데이터베이스와 함께 사용하기 편리합니다. 하지만 아쉽게도 이렇게 되면 OO의 이점을 놓치게 됩니다.

DataSet는 도메인 개체 역할을 할 수 없으므로 기능을 추가할 수 없습니다. 일반적으로 개체에는 클래스 인스턴스에 대해 동작하는 필드, 속성 및 메서드가 있습니다. 예를 들어 Promote 또는 CalcuateOvertimePay 함수가 someUser.Promote() 또는 someUser.CalculateOverTimePay()를 통해 명확하게 호출할 수 있는 User 개체와 연결되어 있을 수 있습니다. DataSet에 메서드를 추가할 수 없으므로 유틸리티 함수를 사용하고 약한 형식의 개체를 처리하며 하드 코딩된 값의 인스턴스를 코드 전체에 추가로 분배해야 합니다. 또한 기본적으로 절차 코드로 마무리하여 DataSet에서 계속 데이터를 제거하거나 이를 로컬 변수에 저장하여 전달합니다. 두 메서드 모두 단점은 있지만 어느 쪽도 이점은 없습니다.

DataSet 사례

데이터 액세스 계층이 DataSet를 반환하기 위한 것이라는 생각을 가지고 있으면 몇 가지 중요한 이점을 놓칠 수 있습니다. 한 가지 이유는 특히 추상화 능력을 제한하는 얇거나 존재하지 않는 비즈니스 계층을 사용할 수 있기 때문입니다. 또한 미리 빌드된 일반적인 솔루션을 사용하기 때문에 OO 기법을 사용하기가 어렵습니다. 마지막으로 Visual Studio.NET 같은 도구는 DataSet 같은 약한 형식의 개체를 사용하는 개발자의 능률을 손쉽게 끌어올리지 못해 생산성을 떨어뜨리고 버그의 발생 가능성을 높이게 됩니다.

이러한 모든 요소가 이런 저런 방식으로 코드의 관리 용이성에 직접적으로 영향을 미칩니다. 추상화를 수행하지 않으면 기능 변경 및 버그 수정의 복잡성과 위험이 높아집니다. 또한 코드 재사용이나 OO에서 제공하는 향상된 가독성을 완전히 활용할 수 없게 됩니다. 게다가 개발자는 비즈니스 논리를 작업하든 프레젠테이션 논리를 작업하든 간에 기본 데이터 구조에 대해 자세히 알고 있어야 합니다.

사용자 지정 엔터티 클래스

DataSet와 관련된 대부분의 문제는 효율적으로 정의된 비즈니스 계층 내에 OO 프로그래밍의 풍부한 기능을 활용하여 해결할 수 있습니다. 일단 기본적으로 필요한 것은 관계에 따라 구성된 데이터(데이터베이스)를 얻어 개체(코드)에서 사용하는 것입니다. 개념적인 측면에서 보면 자동차에 대한 정보를 저장하는 DataTable을 가지는 대신에 실제로 자동차 개체(사용자 지정 엔터티 또는 도메인 개체라고 함)를 가지는 것입니다.

사용자 지정 엔터티를 살펴보기 전에 먼저 당면한 과제를 짚고 넘어가겠습니다. 가장 분명하게 드러나는 부분은 필요한 코드의 양입니다. 데이터를 가져와 DataSet를 자동으로 채우는 대신 데이터를 가져와 먼저 만들어야 하는 사용자 지정 엔터티에 수동으로 매핑합니다. 이렇게 되면 반복 작업을 수행하게 되므로 코드 생성 도구 또는 O/R 매퍼를 사용하여 이를 줄여야 합니다. 이에 대해서는 나중에 자세히 다룰 것입니다. 보다 큰 문제는 데이터를 관계 영역에서 개체 영역으로 매핑하는 실제 프로세스입니다. 단순한 시스템에서는 매핑이 가장 간단한 작업이지만 시스템이 복잡해지면 두 영역 간의 차이가 벌어져 문제가 발생할 수 있습니다. 예를 들어 개체 영역에서 코드 재사용 및 관리 용이성에 도움이 되는 주요 기법에는 상속이 있습니다. 하지만 아쉽게도 상속은 관계형 데이터베이스에서 낯선 개념입니다. 이러한 차이점의 또 다른 예는 개체 영역은 개별 개체에 대한 참조를 관리하고, 관계 영역은 외래 키를 사용한다는 점입니다.

이렇게 하면 마치 이 접근 방식이 코드의 양이 많고 관계형 데이터와 개체 간의 불일치로 인해 복잡한 시스템에는 적합하지 않는 것처럼 들리지만 실제로는 정반대입니다. 복잡한 시스템은 단일 계층에서 격리하는 데(매핑 프로세스) 어려움이 있으므로 이 접근 방식이 도움이 됩니다(자동화 가능). 또한 이 접근 방식은 이미 상당히 널리 사용되고 있으므로 추가되는 복잡성을 명확하게 처리할 수 있는 다양한 디자인 패턴이 나와 있습니다. 앞에서 복잡한 시스템의 단점과 함께 다룬 DataSet의 단점을 좀 더 자세히 살펴보면 결국 시스템을 빌드하는 데 따르는 어려움은 변경 불가능한 특성만 뛰어넘는 수준으로 마무리 될 것입니다.

사용자 지정 엔터티의 정의

사용자 지정 엔터티는 비즈니스 도메인을 나타내는 개체로 비즈니스 계층의 기초가 됩니다. 사용자 인증 구성 요소(이 가이드 전체에서 사용할 예제)가 있다면 아마도 UserRole 개체가 있을 것입니다. 또한 전자 상거래 시스템이라면 SupplierMerchandise 개체가, 부동산 회사에는 Houses, RoomsAddresses가 있을 수 있습니다. 사용자 지정 엔터티는 코드 내에서 단순한 클래스입니다(엔터티와 클래스는 OO 프로그래밍에 사용될 때 상당히 밀접한 상관 관계를 가짐). 일반적인 User 클래스는 다음과 같습니다.

'Visual Basic .NETPublic Class User#Region "Fields and Properties" Private _userId As Integer Private _userName As String Private _password As String Public Property UserId() As Integer  Get   Return _userId  End Get  Set(ByVal Value As Integer)    _userId = Value  End Set End Property Public Property UserName() As String  Get   Return _userName  End Get  Set(ByVal Value As String)   _userName = Value  End Set End Property Public Property Password() As String  Get   Return _password  End Get  Set(ByVal Value As String)   _password = Value  End Set End Property#End Region#Region "Constructors" Public Sub New() End Sub Public Sub New(id As Integer, name As String, password As String)  Me.UserId = id  Me.UserName = name  Me.Password = password End Sub#End RegionEnd Class//C#public class User {#region "Fields and Properties" private int userId; private string userName; private string password; public int UserId {  get { return userId; }  set { userId = value; }  } public string UserName {  get { return userName; }  set { userName = value; } } public string Password {  get { return password; }  set { password = value; } }#endregion#region "Constructors" public User() {} public User(int id, string name, string password) {  this.UserId = id;  this.UserName = name;  this.Password = password; }#endregion}

이점 세부 사항

사용자 지정 엔터티를 통해 얻게 되는 중요한 이점은 컨트롤에서는 완전히 개체라는 단순한 사실에서 비롯됩니다. 즉, 사용자 지정 엔터티를 사용하면 다음을 수행할 수 있습니다.

  • 상속 및 캡슐화 같은 OO 기법을 사용합니다.
  • 사용자 지정 동작을 추가합니다.

예를 들어 User 클래스는 클래스에 UpdatePassword 함수를 추가하여 효과적으로 사용할 수 있습니다(외부/유틸리티 함수를 사용하면 DataSet로도 가능하지만 가독성과 관리 용이성이 희생됨). 또한 강력한 형식이므로 IntelliSense가 지원됩니다.

그림 1. User 클래스의 IntelliSense

마지막으로 사용자 지정 엔터티는 강력한 형식이므로 다음과 같이 오류에 취약한 캐스트가 덜 필요합니다.

Dim userId As Integer = user.UserId'vsDim userId As Integer =          Convert.ToInt32(ds.Tables("users").Rows(0)("UserId"))

개체 관련 매핑

앞에서 언급했듯이 이 접근 방식의 한 가지 큰 난제는 관계형 데이터와 개체 간의 차이를 처리하는 것입니다. 관계형 데이터베이스에는 데이터가 영구적으로 저장되기 때문에 두 영역을 연결하는 것 외에 다른 선택은 없습니다. 앞의 User 예제에서 예상되는 데이터베이스의 사용자 테이블 모양은 다음과 같습니다.

그림 2. User의 데이터 뷰

이 관계형 스키마에서 사용자 지정 엔터티로 매핑하는 작업은 다음과 같이 매우 간단하게 이루어집니다.

'Visual Basic .NETPublic Function GetUser(ByVal userId As Integer) As User Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("GetUserById", connection) command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId Dim dr As SqlDataReader = Nothing Try  connection.Open()  dr = command.ExecuteReader(CommandBehavior.SingleRow)  If dr.Read Then   Dim user As New User   user.UserId = Convert.ToInt32(dr("UserId"))   user.UserName = Convert.ToString(dr("UserName"))   user.Password = Convert.ToString(dr("Password"))   Return user  End If  Return Nothing Finally  If Not dr is Nothing AndAlso Not dr.IsClosed Then   dr.Close()  End If  connection.Dispose()  command.Dispose()  End TryEnd Function//C#public User GetUser(int userId) { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("GetUserById", connection); command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId; SqlDataReader dr = null; try {  connection.Open();  dr = command.ExecuteReader(CommandBehavior.SingleRow);  if (dr.Read()){   User user = new User();   user.UserId = Convert.ToInt32(dr["UserId"]);   user.UserName = Convert.ToString(dr["UserName"]);   user.Password = Convert.ToString(dr["Password"]);   return user;              }  return null; }finally{  if (dr != null && !dr.IsClosed){   dr.Close();  }  connection.Dispose();  command.Dispose(); }}

connection 및 command 개체는 여전히 평소와 마찬가지로 설정하지만 User 클래스의 새로운 인스턴스를 만들고 DataReader에서 이를 채웁니다. 또한 이 함수 내에서 계속해서 DataSet를 사용하여 이를 사용자 지정 엔터티에 매핑할 수 있지만 DataReader에 대한 DataSet의 주된 이점은 연결이 끊어진 데이터 뷰를 제공한다는 것입니다. 이 경우 User 인스턴스는 이처럼 연결이 끊어진 뷰를 제공하여 DataReader의 속도를 활용할 수 있게 해줍니다.

잠깐, 아직 아무 것도 해결되지 않았습니다!

주의 깊은 독자라면 DataSet에 대해 지적한 문제 중 한 가지가 강력한 형식이 아닌 관계로 생산성이 떨어지고 런타임 오류 발생 가능성이 높은 점이라는 것을 알 수 있습니다. 또한 개발자들은 기본 데이터 구조에 대해 세부적인 지식을 갖추고 있어야 합니다. 앞의 코드를 보면 이와 똑같은 함정이 숨어 있음을 알 수 있을 것입니다. 그러나 이러한 문제들은 완전히 격리된 코드 영역에 캡슐화되어 있으므로 클래스 엔터티(웹 인터페이스, 웹 서비스 소비자 및 Windows Form)의 소비자는 이러한 문제를 완전히 알 수 없다는 점을 고려해야 합니다. 이와 반대로 DataSet를 사용하면 코드 전체에 이러한 문제가 확산됩니다.

향상 부분

앞의 코드는 매핑 개념을 설명하기 위한 것으로, 두 가지 주요 부분을 향상시켜 이를 개선할 수 있습니다. 첫째, 채우기 코드를 자체 함수로 끌어내어 재사용이 쉽도록 합니다.

'Visual Basic .NETPublic Function PopulateUser(ByVal dr As IDataRecord) As User Dim user As New User user.UserId = Convert.ToInt32(dr("UserId")) 'NULL 검사 예제 If Not dr("UserName") Is DBNull.Value Then  user.UserName = Convert.ToString(dr("UserName")) End If user.Password = Convert.ToString(dr("Password")) Return userEnd Function//C#public User PopulateUser(IDataRecord dr) { User user = new User(); user.UserId = Convert.ToInt32(dr["UserId"]); //NULL 검사 예제 if (dr["UserName"] != DBNull.Value){  user.UserName = Convert.ToString(dr["UserName"]);    } user.Password = Convert.ToString(dr["Password"]); return user;}(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는
 설명을 위해 번역문으로 제공됩니다.)

두 번째로 확인할 사항은 매핑 함수에 SqlDataReader를 사용하는 대신 IDataRecord를 사용했다는 점입니다. 이것은 모든 DataReader가 구현하는 인터페이스입니다. IDataRecord를 사용하면 매핑 프로세스를 공급업체와 무관하게 실행할 수 있습니다. 즉, 앞의 함수가 OleDbDataReader를 사용하더라도 이를 통해 Access 데이터베이스에서 User를 매핑할 수 있습니다. 이러한 특정 접근 방식과 Provider Model Design Pattern(링크 1 , 링크 2 )을 조합하면 서로 다른 데이터베이스 공급업체에 대해 손쉽게 사용할 수 있는 코드를 얻게 됩니다.

마지막으로 위의 코드는 캡슐화가 얼마나 강력한지를 보여 줍니다. DataSetNULL을 처리하기가 쉽지 않은 이유는 값을 추출할 때마다 NULL인지 확인해야 하기 때문입니다. 우리는 위의 채우기 메서드를 사용해 이를 단일 위치에서 편리하게 관리하여 소비자가 이를 직접 처리해야 하는 수고를 덜어주었습니다.

매핑 위치

이러한 데이터 액세스 및 매핑 함수가 개별 클래스의 일부인지 아니면 해당 사용자 엔터티의 일부인지에 대한 논란이 일부에서 제기되고 있습니다. 모든 사용자 관련 작업(데이터 얻기, 업데이트 및 매핑)을 User 사용자 지정 엔터티의 일부로 사용하면 확실한 이점을 얻을 수 있습니다. 이러한 특성은 데이터베이스 스키마가 사용자 지정 엔터티와 매우 비슷한 경우에 확실한 효과를 나타냅니다(이 예제에서처럼). 시스템의 복잡성이 늘어나면서 두 영역 간의 차이가 드러나기 시작함에 따라 데이터 계층과 비즈니스 계층을 명확하게 구분하면 유지 관리를 단순화하는 데 큰 도움이 될 수 있습니다(이를 데이터 액세스 계층이라고 함). 자체 계층인 DAL 내에 액세스 및 매핑 코드를 두었을 때 얻어지는 부수적인 효과는 다음과 같은 명확한 계층 분리를 위한 훌륭한 규칙을 제공한다는 점입니다.

"System.Data에서 클래스를 반환하거나 DAL에서 자식 네임스페이스를 반환해서는 안 됩니다."

사용자 지정 컬렉션

지금까지는 개별 엔터티를 처리하는 부분만 살펴보았지만 단일 개체를 둘 이상 처리해야 하는 경우도 비일비재할 것입니다. 이를 위한 단순한 솔루션은 Arraylist 같은 일반적인 컬렉션 내에 여러 값을 저장하는 것입니다. 하지만 DataSet에 대해 겪었던 다음과 같은 몇 가지 문제를 다시 유발하므로 이상적인 솔루션이라고 하기에는 부족합니다.

  • 강력한 형식이 아니며
  • 사용자 지정 동작을 추가할 수 없습니다.

여기서의 요구에 가장 적합한 솔루션은 사용자 지정 컬렉션을 만드는 것입니다. 다행히도 Microsoft .NET Framework는 다음과 같이 이를 위해 상속하도록 설계된 클래스인 CollectionBase를 제공합니다. CollectionBase는 전용 Arraylists 내에 모든 개체 형식을 저장하지만 User 개체 같은 특정 형식만 사용하는 메서드를 통해 이들 전용 컬렉션에 대한 액세스를 노출하는 방식으로 작동합니다. 즉, 약한 형식의 코드가 강력한 형식의 API 내에 캡슐화되는 것입니다.

사용자 지정 컬렉션은 코드가 많은 것처럼 보이지만 대부분은 코드 생성 또는 잘라내기 및 붙여넣기를 쉽게 수행할 수 있으며 찾아서 바꾸기는 한 번만 수행하면 되는 경우가 많습니다. 다음과 같이 User 클래스의 사용자 지정 컬렉션을 구성하는 다양한 부분을 살펴보겠습니다.

'Visual Basic .NETPublic Class UserCollection   Inherits CollectionBase Default Public Property Item(ByVal index As Integer) As User  Get   Return CType(List(index), User)  End Get  Set   List(index) = value  End Set End Property Public Function Add(ByVal value As User) As Integer  Return (List.Add(value)) End Function Public Function IndexOf(ByVal value As User) As Integer  Return (List.IndexOf(value)) End Function Public Sub Insert(ByVal index As Integer, ByVal value As User)  List.Insert(index, value) End Sub Public Sub Remove(ByVal value As User)  List.Remove(value) End Sub Public Function Contains(ByVal value As User) As Boolean  Return (List.Contains(value)) End FunctionEnd Class//C#public class UserCollection : CollectionBase { public User this[int index] {  get {return (User)List[index];}  set {List[index] = value;} } public int Add(User value) {  return (List.Add(value)); } public int IndexOf(User value) {  return (List.IndexOf(value)); } public void Insert(int index, User value) {  List.Insert(index, value); } public void Remove(User value) {  List.Remove(value); } public bool Contains(User value) {  return (List.Contains(value)); }}

CollectionBase를 구현하면 더 많은 작업을 수행할 수 있지만 여기서는 사용자 지정 컬렉션에 필요한 핵심 기능만을 나열했습니다. Add 함수를 살펴보면 User 개체만 허용되는 함수에서 List.Add(Arraylist)에 대한 호출을 어떤 방식으로 간단히 래핑하는지 알 수 있습니다.

사용자 지정 컬렉션 매핑

관계형 데이터를 사용자 지정 컬렉션에 매핑하는 프로세스는 사용자 지정 엔터티에 대해 살펴본 프로세스와 매우 유사합니다. 단일 엔터티를 만들어 반환하는 대신 컬렉션에 엔터티를 추가하고 다음 항목으로 반복합니다.

'Visual Basic .NETPublic Function GetAllUsers() As UserCollection Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("GetAllUsers", connection) Dim dr As SqlDataReader = Nothing Try  connection.Open()  dr = command.ExecuteReader(CommandBehavior.SingleResult)  Dim users As New UserCollection  While dr.Read()   users.Add(PopulateUser(dr))  End While  Return users Finally  If Not dr Is Nothing AndAlso Not dr.IsClosed Then   dr.Close()  End If  connection.Dispose()  command.Dispose() End TryEnd Function//C#public UserCollection GetAllUsers() { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command =new SqlCommand("GetAllUsers", connection); SqlDataReader dr = null; try {  connection.Open();  dr = command.ExecuteReader(CommandBehavior.SingleResult);  UserCollection users = new UserCollection();  while (dr.Read()){   users.Add(PopulateUser(dr));  }  return users; }finally{  if (dr != null && !dr.IsClosed){   dr.Close();  }  connection.Dispose();  command.Dispose(); }}

여기서는 데이터베이스에서 데이터를 가져오고 사용자 지정 컬렉션을 만들며 결과를 순환하여 각 User 개체를 만들고 이를 컬렉션에 추가합니다. 또한 PopulateUser 매핑 함수를 어떻게 재사용하는지 확인해 보십시오.

사용자 지정 동작 추가

사용자 지정 엔터티에 대해 설명할 때 사용자 지정 동작을 클래스에 추가하는 기능에 대해서는 피상적으로만 언급했습니다. 엔터티에 추가할 기능의 유형은 주로 구현하는 비즈니스 논리의 유형에 따라 달라지지만 몇 가지 일반 기능을 사용자 지정 컬렉션에 구현해야 할 수 있습니다. 이에 대한 한 가지 예는 일정한 키를 토대로 단일 엔터티를 반환하는 것인데 예를 들어 userId를 기반으로 사용자를 반환할 수 있습니다.

'Visual Basic .NETPublic Function FindUserById(ByVal userId As Integer) As User For Each user As User In List  If user.UserId = userId Then   Return user  End If Next Return NothingEnd Function//C#public User FindUserById(int userId) { foreach (User user in List) {  if (user.UserId == userId){   return user;  } } return null;}

또 다른 예는 다음과 같이 부분 사용자 이름 등의 특정 기준을 토대로 사용자 하위 집합을 반환하는 것입니다.

'Visual Basic .NETPublic Function FindMatchingUsers(ByVal search As String) 
As UserCollection If search Is Nothing Then  Throw New ArgumentNullException("search cannot be null") End If Dim matchingUsers As New UserCollection For Each user As User In List  Dim userName As String = user.UserName  If Not userName Is Nothing And userName.StartsWith(search) Then   matchingUsers.Add(user)  End If Next Return matchingUsersEnd Function//C#public UserCollection FindMatchingUsers(string search) { if (search == null){  throw new ArgumentNullException("search cannot be null"); } UserCollection matchingUsers = new UserCollection(); foreach (User user in List) {  string userName = user.UserName;  if (userName != null && userName.StartsWith(search)){   matchingUsers.Add(user);  } } return matchingUsers;}

DataSet를 사용하면 DataTable.Select로도 동일한 방법을 수행할 수 있습니다. 자신의 기능을 만들면 코드를 완전히 제어할 수 있으며 Select 메서드는 매우 편리하고 자유로운 코딩 방식으로 이 기능을 제공합니다. 한편 Select는 강력한 형식이 아니므로 이를 사용하려면 개발자가 기본 데이터베이스에 대해 알고 있어야 합니다.

사용자 지정 컬렉션 바인딩

우리가 살펴본 첫 번째 예제는 DataSet를 ASP.NET 컨트롤에 바인딩한 것이었습니다. 이 작업이 상당히 자주 이루어진다는 점을 고려한다면 사용자 지정 컬렉션이 그만큼 쉽게 바인딩된다는 사실에 반가움을 느낄 것입니다(이는 CollectionBase가 바인딩에 사용되는 Ilist를 구현하기 때문임). 다음과 같이 사용자 지정 컬렉션은 이를 노출하는 모든 컨트롤에 대해 DataSource 역할을 수행할 수 있으며 DataBinder.EvalDataSet에서처럼 사용할 수 있습니다.

'Visual Basic .NETDim users as UserCollection = DAL.GetallUsers()repeater.DataSource = usersrepeater.DataBind()//C#UserCollection users = DAL.GetAllUsers();repeater.DataSource = users;repeater.DataBind();<!-- HTML --><asp:Repeater onItemDataBound="r_IDB" ID="repeater" Runat="server"> <ItemTemplate>  <asp:Label ID="userName" Runat="server">   <%# DataBinder.Eval(Container.DataItem, "UserName") %><br />  </asp:Label> </ItemTemplate></asp:Repeater>

열 이름을 DataBinder.Eval의 두 번째 매개 변수로 사용하는 대신 표시할 속성 이름을 지정하며 이 경우에는 UserName입니다.

많은 데이터 바인딩된 컨트롤에 의해 노출되는 OnItemDataBound 또는 OnItemCreated에서 처리를 수행하는 경우 e.Item.DataItemDataRowView로 캐스팅할 수 있습니다. 다음과 같이 사용자 지정 컬렉션에 바인딩하는 경우 e.Item.DataItem은 대신 사용자 지정 엔터티로 캐스팅하며 이 예제에서는 User 클래스입니다.

'Visual Basic .NETProtected Sub r_ItemDataBound (s As Object, 
e As RepeaterItemEventArgs) Dim type As ListItemType = e.Item.ItemType If type = ListItemType.AlternatingItem OrElse    type = ListItemType.Item Then  Dim u As Label = CType(e.Item.FindControl("userName"), Label)  Dim currentUser As User = CType(e.Item.DataItem, User)  If Not PasswordUtility.PasswordIsSecure(currentUser.Password) Then   ul.ForeColor = Drawing.Color.Red  End If End IfEnd Sub//C#protected void r_ItemDataBound(object sender, 
RepeaterItemEventArgs e) { ListItemType type = e.Item.ItemType; if (type == ListItemType.AlternatingItem ||      type == ListItemType.Item){  Label ul = (Label)e.Item.FindControl("userName");  User currentUser = (User)e.Item.DataItem;  if (!PasswordUtility.PasswordIsSecure(currentUser.Password)){   ul.ForeColor = Color.Red;  } }}

관계 관리

아무리 단순한 시스템이라도 엔터티 간에 관계가 존재하기 마련입니다. 관계형 데이터베이스의 관계는 외래 키를 통해 관리되며 개체를 사용하는 경우 관계는 다른 개체에 대한 참조에 해당합니다. 예를 들어 앞의 예제를 기반으로 설명하면 User 개체에 다음과 같은 Role이 만들어질 것으로 예측할 수 있습니다.

'Visual Basic .NETPublic Class User Private _role As Role Public Property Role() As Role  Get   Return _role  End Get  Set(ByVal Value As Role)   _role = Value  End Set End PropertyEnd Class//C#public class User { private Role role; public Role Role {  get {return role;}  set {role = value;} }}

또는 다음과 같은 Role의 컬렉션일 수도 있습니다.

'Visual Basic .NETPublic Class User Private _roles As RoleCollection Public ReadOnly Property Roles() As RoleCollection  Get   If _roles Is Nothing Then    _roles = New RoleCollection   End If   Return _roles  End Get End PropertyEnd Class//C#public class User { private RoleCollection roles; public RoleCollection Roles {  get {   if (roles == null){    roles = new RoleCollection();   }   return roles;  } }}

위의 두 예제에 사용된 Role 클래스 또는 RoleCollection 클래스는 가상의 것으로, 이는 UserUserCollection 클래스와 같이 사용자 지정 엔터티 또는 컬렉션 클래스의 하나 입니다.

관계 매핑

실질적인 문제는 관계를 매핑하는 방법에 있습니다. 간단한 예제를 살펴보고 역할과 함께 userId를 기반으로 사용자를 검색하겠습니다. 먼저, 다음과 같은 관계형 모델을 살펴봅니다.

그림 3. Users 및 Roles 간의 관계

이제 Users 테이블과 Roles 테이블 모두 간단한 방식으로 사용자 지정 엔터티에 매핑할 수 있는지 확인해 보겠습니다. 여기에는 UsersRoles 사이에 다대다 관계를 나타내는 UserRoleJoin 테이블도 있습니다.

그런 다음 아래와 같이 저장 프로시저를 사용하여 두 개의 개별 결과를 가져오는데 다음과 같이 첫 번째는 User용이고 두 번째는 사용자의 Role을 위한 것입니다.

CREATE PROCEDURE GetUserById(  @UserId INT)ASSELECT UserId, UserName, [Password]  FROM Users  WHERE UserId = @UserIDSELECT R.RoleId, R.[Name], R.Code  FROM Roles R INNER JOIN     UserRoleJoin URJ ON R.RoleId = URJ.RoleId  WHERE  URJ.UserId = @UserId

마지막으로 다음과 같이 관계형 모델에서 개체 모델로 매핑합니다.

'Visual Basic .NETPublic Function GetUserById(ByVal userId As Integer) As User Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("GetUserById", connection) command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId Dim dr As SqlDataReader = Nothing Try  connection.Open()  dr = command.ExecuteReader()  Dim user As User = Nothing  If dr.Read() Then   user = PopulateUser(dr)   dr.NextResult()   While dr.Read()    user.Roles.Add(PopulateRole(dr))   End While  End If  Return user Finally  If Not dr Is Nothing AndAlso Not dr.IsClosed Then   dr.Close()  End If  connection.Dispose()  command.Dispose() End TryEnd Function//C#public User GetUserById(int userId) { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("GetUserById", connection); command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId; SqlDataReader dr = null; try {  connection.Open();  dr = command.ExecuteReader();  User user = null;  if (dr.Read()){   user = PopulateUser(dr);   dr.NextResult();   while(dr.Read()){    user.Roles.Add(PopulateRole(dr));   }              }  return user; }finally{  if (dr != null && !dr.IsClosed){   dr.Close();  }  connection.Dispose();  command.Dispose(); }}

User 인스턴스가 만들어져 채워지면 다음 결과로 이동하고 선택 및 순환하여 Roles를 채우고 이를 User 클래스의 RolesCollection 속성에 추가합니다.

중급 단계

이 가이드의 목적은 사용자 지정 엔터티 및 컬렉션의 개념과 사용 방법을 소개하는 것입니다. 사용자 지정 엔터티의 사용은 업계에서 널리 사용되는 방식이며 그로 인해 다양한 시나리오를 처리하는 수많은 패턴이 문서화되어 있습니다. 디자인 패턴이 유용한 이유는 다양합니다. 첫째, 특정 상황을 처리하는 데 있어 아마 주어진 문제를 처음 겪지는 않을 것입니다. 디자인 패턴을 사용하면 이미 시도된, 그리고 테스트된 솔루션을 주어진 문제에 다시 사용할 수 있습니다(설계 패턴을 완전히 잘라내어 붙여넣을 수는 없지만 대개 솔루션을 위한 훌륭한 기초가 됩니다). 또한 널리 사용되는 접근 방식이고 체계적으로 문서화되어 있으므로 시스템을 복잡성의 정도에 따라 확장할 수 있다는 안정감을 느끼게 해줍니다. 디자인 패턴은 또한 일반적인 어휘를 제공하여 정보의 전달 및 교육이 매우 용이하게 이뤄질 수 있습니다.

물론 디자인 패턴은 사용자 지정 엔터티에만 적용되는 것이 아니며 실제로 다양한 분야에 사용됩니다. 하지만 사용자 지정 엔터티와 매핑 프로세스에 적용할 수 있는 문서화된 패턴이 얼마나 되는지 확인하면 깜짝 놀라게 될 것입니다.

이 마지막 섹션은 보다 크고 복잡한 시스템을 실행할 수 있는 일부 고급 시나리오를 설명하기 위한 것입니다. 대부분의 항목은 개별 가이드만으로 충분할 수 있지만 여기서는 최소한 몇 가지 시작 리소스를 제공할 예정입니다.

처음에 활용하기 좋은 자료로는 Martin Fowler의 Patterns of Enterprise Application Architecture 가 있는데 일반적인 디자인 패턴을 위한 효과적인 참조(자세한 설명과 많은 샘플 코드가 있는) 역할 밖에 못하지만 처음 100페이지를 잘 읽어보면 전체적인 개념을 이해하는 데 많은 도움이 됩니다. 또한 Fowler의 온라인 catalog of patterns 는 이미 개념에 친숙하지만 간단한 참조가 필요한 사람에게 유용합니다.

동시성

앞에서 소개한 예제들은 모두 데이터베이스에서 데이터를 가져오고 이 데이터에서 개체를 만드는 부분을 다루고 있습니다. 또한 대부분의 경우 데이터의 업데이트, 삭제 및 삽입이 간단히 이루어집니다. 여기서 소개한 비즈니스 계층은 개체를 만들고 이를 데이터 액세스 계층으로 전달하며 관계 영역에 대한 매핑을 처리합니다. 예를 들면 다음과 같습니다.

'Visual Basic .NETPublic sub UpdateUser(ByVal user As User) Dim connection As New SqlConnection(CONNECTION_STRING) Dim command As New SqlCommand("UpdateUser", connection) '역방향 맵핑을 위한 재사용 함수도 만들 수 있음 command.Parameters.Add("@UserId", SqlDbType.Int) command.Parameters(0).Value = user.UserId command.Parameters.Add("@Password", SqlDbType.VarChar, 64) command.Parameters(1).Value = user.Password command.Parameters.Add("@UserName", SqlDbType.VarChar, 128) command.Parameters(2).Value = user.UserName Try  connection.Open()  command.ExecuteNonQuery() Finally  connection.Dispose()  command.Dispose() End TryEnd Sub//C#public void UpdateUser(User user) { SqlConnection connection = new SqlConnection(CONNECTION_STRING); SqlCommand command = new SqlCommand("UpdateUser", connection); //역방향 맵핑을 위한 재사용 함수도 만들 수 있음 command.Parameters.Add("@UserId", SqlDbType.Int); command.Parameters[0].Value = user.UserId; command.Parameters.Add("@Password", SqlDbType.VarChar, 64); command.Parameters[1].Value = user.Password;  command.Parameters.Add("@UserName", SqlDbType.VarChar, 128); command.Parameters[2].Value = user.UserName; try {  connection.Open();  command.ExecuteNonQuery(); }finally{  connection.Dispose();  command.Dispose(); }}

그러나 동시성을 처리하는 경우는 간단하지 않습니다. 즉, 두 명의 사용자가 동시에 동일한 데이터를 업데이트하면 어떤 일이 발생할까요? 기본 동작(아무 것도 하지 않는 경우)은 데이터를 마지막으로 커밋한 사람이 이전의 모든 작업을 덮어쓰는 것입니다. 이러한 동작은 사용자 한 명의 작업을 자동으로 덮어쓰게 되므로 이상적이지는 않을 수 있습니다. 충돌을 완전히 피하는 한 가지 방법은 비관적 동시성을 사용하는 것이지만 이 방식을 사용하려면 확장 가능한 방식으로 구현하기 힘든 특정 유형의 잠금 메커니즘을 사용해야 합니다. 이에 대한 대안은 낙관적 동시성 기법을 사용하는 것입니다. 첫 번째 커밋에 우선 순위를 부여하고 이후의 사용자에게 알리는 것은 일반적으로 보다 순조롭고 사용자 친화적인 접근 방식입니다. 이를 위해 타임스탬프 같은 특정 유형의 행 버전 관리를 사용합니다.

참고 자료

성능

적절한 유연성 및 성능에 대한 염려와는 달리 사소한 성능 차이에 대해 걱정하는 경우가 너무나도 많습니다. 성능은 물론 중요하지만 가장 간단한 솔루션을 제외한 모든 부분에 대해 일반화된 지침을 제공하기란 어렵기 마련입니다. 사용자 지정 컬렉션과 DataSet를 예로 들어봅시다. 어느 쪽이 더 빠를까요? 사용자 지정 컬렉션을 사용하면 DataReader를 많이 사용하여 데이터베이스에서 데이터를 보다 신속하게 가져올 수 있습니다. 하지만 여기서 주의할 점은 이를 어떤 데이터 형식과 함께 어떻게 사용하느냐에 따라 해답이 달라지므로 포괄적인 설명은 아무런 소용이 없다는 것입니다. 보다 중요한 사항은 절감할 수 있는 처리 시간이 어느 정도이든 관계없이 관리 용이성과의 차이에 비해 그다지 많지 않을 것이라는 사실입니다.

물론 관리하기 용이한 고성능 솔루션을 가질 수 없다는 말은 아닙니다. 이를 사용하는 방법에 크게 좌우된다고 다시 말하지만 여기에는 성능을 극대화할 수 있는 몇 가지 패턴이 있습니다. 먼저 사용자 지정 엔터티 및 컬렉션 캐시와 DataSetHttpCache 같은 동일한 메커니즘을 사용할 수 있다는 점을 알아야 합니다. DataSet의 한 가지 이점은 Select 문을 작성하여 필요한 정보만 포함시킬 수 있는 기능에 있습니다. 사용자 지정 엔터티를 사용하면 전체 엔터티와 자식 엔터티까지 모두 채워야 한다는 느낌을 받는 경우가 많습니다. 예를 들어 DataSet를 사용하여 Organization 목록을 표시하려면 OganizationId, NameAddress를 가져와 이를 반복기에 바인딩할 것입니다. 필자의 경우 사용자 지정 엔터티를 사용할 때 다른 모든 Organization 정보를 가져와야 할 것 같은 느낌까지 듭니다. 또한 이러한 정보에는 ISO 인증 여부, 모든 직원 컬렉션, 추가 연락처 정보 등이 포함될 수 있습니다. 다른 사람은 이러한 고민거리를 공유하지 않을 수도 있지만 다행히도 우리는 원하는 경우 사용자 지정 엔터티를 세부적으로 제어할 수 있습니다. 가장 일반적인 접근 방식은 처음 필요할 때만 정보를 가져오는 레이지 로드(lazy-load) 패턴 형식을 사용하는 것입니다(속성에 효과적으로 캡슐화할 수 있음). 개별 속성을 이런 방식으로 제어하면 다른 방식으로는 얻기 힘든 엄청난 유연성을 발휘하게 됩니다(DataColumn 수준에서 유사한 작업을 수행한다고 가정해 보십시오).

참고 자료

정렬 및 필터링

DataView의 기본 정렬 및 필터링 지원은 SQL 및 기본 데이터 구조에 대해 알아야 한다는 단점은 있지만 편리한 기능이며 사용자 지정 컬렉션에는 없는 기능이기도 합니다. 정렬 및 필터링은 계속 수행할 수 있지만 이렇게 하려면 기능을 작성해야 합니다. 고급 기법이라고 할 수는 없지만 전체 데모 코드는 이 섹션의 범위를 벗어납니다. 하지만 필터 클래스로 필터링하거나 비교 클래스로 정렬하는 것 같은 대부분의 기법은 전과 상당히 비슷하며 분명 방법이 있습니다. 다음 리소스를 참조하십시오.

코드 생성

개념적인 문제를 지나쳤다면 사용자 지정 엔터티 및 컬렉션의 중요한 단점은 이러한 모든 유연성, 추상화 및 낮은 유지 관리 비용을 제공하는 추가 코드의 양을 들 수 있습니다. 실제로 지금까지 언급한 줄어든 유지 관리 비용과 버그보다 추가 코드가 더 부담스러울 수도 있습니다. 어떤 솔루션도 완벽하지는 않으므로 이것은 분명히 올바른 지적이지만 디자인 패턴 및 CSLA.NET 같은 프레임워크가 장기적으로 이러한 문제를 점차 완화하고 있습니다. 또한 패턴 및 프레임워크와는 별도로 코드 생성 도구가 실제로 작성하는 데 필요한 코드의 양을 현저히 줄여줄 수 있습니다. 이 가이드는 처음에 무료로 널리 사용되는 CodeSmith 같은 코드 생성 도구를 자세히 설명하려고 했지만 필자의 지식 범위를 넘어서는 너무 많은 리소스가 있어 제외하였습니다.

코드 생성이 마치 꿈 같은 일로 들릴 수도 있습니다. 그러나 적절히 사용하고 이해하면 사용자 지정 엔터티 뿐만 아니라 다른 분야에서도 강력한 무기가 될 수 있습니다. 코드 생성이 사용자 지정 엔터티에만 적용되는 것은 아니지만 대부분 이러한 목적으로만 조정되어 있습니다. 이유는 간단합니다. 사용자 지정 엔터티를 사용하려면 많은 양의 반복 코드가 필요하기 때문입니다.

간단히 말해 코드 생성은 어떤 식으로 작동할까요? 이러한 개념은 진로를 한참 벗어났거나 역효과를 나타내는 것처럼 들리겠지만 기본적으로는 코드(템플릿)를 작성하여 코드를 생성하게 됩니다. 예를 들어 CodeSmith는 다음과 같이 데이터베이스를 활용하

Posted by tornado
|

집에 8시에 도착해서... 지금 8시 40분...

 

현재 잡은 모기 ==> 8 마리.

한마리는 화장실 쪽으로 튀었으나, 포획하지 못함..

 

일요일날 바다낚시 갔다왔는데.. 바닷가에서 쓰고다닌

밀집모자에서 해변가 해병대 모기 나옴 ... 크헉 ~

이넘이 화장실 쪽으로 도망갔으나 현재 생포하지 못함.

 

가을인데 웬 모기가 이리 극성이냐~ 여름보다 더하네....

 

물어도 꼭 긁기 뭐한곳만 무네..

발바닥.. 등 한가운데...

잡히면 화형에 처하리라~~~

 

 

 

 

'이것저것 > 낙서장' 카테고리의 다른 글

[link] window 용 tail utility  (0) 2005.10.25
14000 히트 이벤트!!!  (0) 2005.10.14
음악이 끝장이군~ Larry's World  (0) 2005.09.29
13000 히트 이벤트!!!  (2) 2005.09.28
[펌] NHK 다큐멘터리 - 블랙홀  (0) 2005.09.09
Posted by tornado
|

[Oracle]오라클 어드민 팁  

Oracle Administration을 정리하다가 간단히 찾아고 조금이나마 도움이 되시라고 정리해서 올립니다.
너무 단시간에 두서없이 써서 보기도 좋지 않지만 필요하신 분들 심심할때 하나씩  
해보세요.(다들 아시는거지만~~)
아래 tips 는 하나의 database에서 작성한 것이 아니므로 각종 정보들(file들의 위치등)이  
tip마다 다를 수 있습니다. 각 tip은 개개의 것으로 생각하시고 응용하시기 바랍니다.
혹시 틀린 내용 발견되면 mail주세요. 바로 수정하겠습니다.
편집 이쁘게 못해서 죄송합니다.
나름대로 사연있는 글입니다.
정리하다가 날려먹어서 한 몇일 더 고생해서 작성한겁니다.^^;

님들도 좋은 정보 있으시면 공유하시죠.


================================================================================================  
1. DBMS = database(data file & control file & redo log file) +  
                                                   instance(memory & background processes)
================================================================================================  


2. Oracle Architecture Component

================================================================================================  

* Oracle Instance 확인 : v$instance

SQL> select instance_name from v$instance;

INSTANCE_NAME
----------------
IBM

================================================================================================  

* datafile들의 경로 및 정보 : v$datafile
SQL> select name from v$datafile;

NAME
--------------------------------------------------------------------------------
/oracle/ora_data/system/system01.dbf
/oracle/ora_data/data/tools01.dbf
/oracle/ora_data/data/rbs01.dbf
/oracle/ora_data/data/temp01.dbf
/oracle/ora_data/data/users01.dbf

================================================================================================  

* control file의 경로 및 정보 : v$controlfile;

SQL> select name from v$controlfile;

NAME
--------------------------------------------------------------------------------
/oracle/ora_data/contr1/ora_control1
/oracle/ora_data/contr2/ora_control2

================================================================================================  

* logfile의 경로 및 정보 : v$logfile

SQL> select member from v$logfile;

MEMBER
--------------------------------------------------------------------------------
/oracle/ora_data/redolog_a/redo1a.log
/oracle/ora_data/redolog_b/redo1b.log
/oracle/ora_data/redolog_a/redo2a.log
/oracle/ora_data/redolog_b/redo2b.log
/oracle/ora_data/redolog_a/redo3a.log
/oracle/ora_data/redolog_b/redo3b.log

================================================================================================  

* System Global Area 내용을 조회

SQL> select * from v$sga;

NAME                      VALUE
-------------------- ----------
Fixed Size               108588
Variable Size          27631616
Database Buffers        2252800
Redo Buffers              77824

SQL> show sga

Total System Global Area   30070828 bytes
Fixed Size                   108588 bytes
Variable Size              27631616 bytes
Database Buffers            2252800 bytes
Redo Buffers                  77824 bytes

================================================================================================  

* 현재 수행중인 background process들을 확인

SQL> select paddr,name,description from v$bgprocess where paddr>'00';

PADDR            NAME  DESCRIPTION
---------------- ----- ----------------------------------------------------------------
070000000139ABC0 PMON  process cleanup
070000000139AFD0 DBW0  db writer process 0
070000000139B3E0 LGWR  Redo etc.
070000000139B7F0 CKPT  checkpoint
070000000139BC00 SMON  System Monitor Process
070000000139C010 RECO  distributed recovery

SQL> !ps -ef|grep ora|grep

 oracle 25148     1   0  19:25:34      -  0:00 ora_reco_IBM
 oracle 60576     1   0  19:25:34      -  0:00 ora_smon_IBM
 oracle 60782     1   0  19:25:34      -  0:00 ora_pmon_IBM
 oracle 70166     1   0  19:25:34      -  0:00 ora_lgwr_IBM
 oracle 72248     1   0  19:25:34      -  0:00 ora_ckpt_IBM
 oracle 84918     1   0  19:25:34      -  0:00 ora_dbw0_IBM
 
================================================================================================  

* 초기화 파라미터 파일 : init.ora

================================================================================================  

* database log 모드 확인

SQL> connect internal
Connected.

SQL> archive log list
Database log mode              No Archive Mode
Automatic archival             Disabled
Archive destination            /oracle/app/oracle/product/8.1.6/dbs/arch
Oldest online log sequence     20
Current log sequence           22

SQL> select log_mode from v$database;

LOG_MODE
------------
NOARCHIVELOG

================================================================================================  


3. Managing an Oracle Instance

================================================================================================  

단계별 :
shutdown : oracle이 내려가 있는 상태
nomount : instance started(SGA, B.G process를 시작 init.ora에서 읽어서)
alert, trace file open
- 이 단계에서 할 수 있는 것은  
 a. db creation
- 이 상태에서도 볼수있는 view
 v$parameter
 v$dga
 v$option
 v$process
 v$session
 v$version
 v$instance

mount : control file opened for this instance
- 이 단계에서 할 수 있는 것은 control file의 내용을 변경하는것
 a. archivelog mode로 변환
 b. data file/redo log file rename시
 c. recovery시

- SQL>alter database open read only;
 로 하게되면 data file에 writing을 허용 안함.

open : control file에 기술된 모든 files open

================================================================================================  

* parameter 변경 종류

a. init.ora 에서 변경
b. alter session set ~
c. alter system set ~    => shutdown 될때까지 변경된것 유효
  alter system deffered set ~ => 현재 session에서만 변경된것 유효

================================================================================================  

* 특정 session 죽이기

SQL> select sid, serial#,username,status from v$session; => (특정 user는 where username='SCOTT'로)
      SID    SERIAL# USERNAME                       STATUS
---------- ---------- ------------------------------ --------
        1          1                                ACTIVE
        2          1                                ACTIVE
        3          1                                ACTIVE
        4          1                                ACTIVE
        5          1                                ACTIVE
        6          1                                ACTIVE
        7          1 SYS                            ACTIVE

SQL> alter system kill session '7,3'    -- 7은 sid, 3은 serial#

================================================================================================  

* alert file 과 trace file
- alert file은 꼭 1개, 중요한사건,시간순으로 (startup,shutdown,recovery)
- trace file은 여러개 가능, background process는 background_dump_dest에 생기고 server process는
 user_dump_dest에 생성된다.

================================================================================================  


4. Creating a Database

================================================================================================  

* Create a Database Manually

a. OS Environment setting

.profile에 ORACLE_HOME,ORACLE_SID,ORA_NLS33,PATH,(ORACLE_BASE) 등을 편집한다.

ex)
DISPLAY=swsvrctr:0.0
ORACLE_HOME=/oracle/app/oracle/product/8.1.7
PATH=$ORACLE_PATH/bin:/usr/ccs/bin:$PATH
NLS_LANG=AMERICAN_AMERICA.KO16KSC5601
ORA_NLS33=$ORACLE_HOME/ocommon/nls/admin/data
ORACLE_SID=IBM

b. init.ora file을 copy하고 편집한다.
file
db_name=KYS
control_files = (/home/oracle/data02/control/control01.ctl,/home/oracle/data02/control/control02.ctl)
db_block_size = 8192

기본적으로 위 두개 parameter외에
rollback_segments=(rbs1,rbs2,..)  =>나중에 rollback segment생성후 DB start시 Online되는 rbs지정
background_dump_dest=/home/oracle/data02/bdump
user_dump_dest=/home/oracle/data02/udump
core_dump_dest=/home/oracle/data02/cdump

c. Starting the Instance

SQL> startup nomount
SQL> startup nomount pfile=initKYS.ora

SQL> create database KYS
 2     maxlogfiles 5
 3     maxlogmembers 5
 4     maxdatafiles 100
 5     maxloghistory 100
 6  logfile
 7     group 1 ('/home/oracle/data02/redolog/log1a.rdo','/home/oracle/data02/redolog2/log1b.rdo') size 1m,
 8     group 2 ('/home/oracle/data02/redolog/log2a.rdo','/home/oracle/data02/redolog2/log2b.rdo') size 1m
 9  datafile
10     '/home/oracle/data02/data/system01.dbf' size 50m autoextend on
11  character set "KO16KSC5601";

일단 여기까지 database는 생성이 되었다.
이후부터는 추가적인 작업이다.

d. 추가 system rollback segment 생성

SQL> create rollback segment r0 tablespace system
 2  storage (initial 16k next 16k minextents 2 maxextents 10);

 
e. rollback sement online

SQL> alter rollback segment r0 online;


f. rollback segment tablespace 생성 & datafile 저장위치, 크기 및 초기값 지정

SQL> create tablespace rbs
 2  datafile '/home/oracle/data02/data/rbs01.dbf' size 300m
 3  default storage(
 4  initial            4M
 5  next               4M
 6  pctincrease        0
 7  minextents         10
 8  maxextents         unlimited);

g. rollback segment 생성

SQL> create rollback segment r01 tablespace rbs
 2  storage (minextents 10 optimal 40M);
SQL> create rollback segment r02 tablespace rbs
 2  storage (minextents 10 optimal 40M);
SQL> create rollback segment r03 tablespace rbs
 2  storage (minextents 10 optimal 40M);
SQL> create rollback segment r04 tablespace rbs
 2  storage (minextents 10 optimal 40M);


h. rollback segment online

SQL> alter rollback segment r01 online;
SQL> alter rollback segment r02 online;
SQL> alter rollback segment r03 online;
SQL> alter rollback segment r04 online;


i. 추가 system rollback segment off-line 및 삭제  

SQL> alter rollback segment r0 offline;
SQL> drop rollback segment r0;

j. sorting 작업시 필요한 temporary tablespace 생성 & datafile 저장 위치, 크기 및 초기값 지정

SQL> create tablespace temp
 2  datafile '/home/oracle/data02/data/temp01.dbf' size 300 temporary
 3  default storage(
 4  initial            4M
 5  next               4M
 6  maxextents         unlimited
 7  pctincrease        0);
 
 
k. 추가 tablespace 생성 & data file 저장 위치 및 크기 지정

SQL> create tablespace tools
 2  datafile '/home/oracle/data02/data/tools.dbf' size 50m
 3  default storage(
 4  maxextents 505
 5  pctincrease 0);
 
SQL> create tablespace users
 2  datafile '/home/oracle/data02/data/user01.dbf' size 30M
 3  default storage(
 4  maxextents 505
 5  pctincrease 0);
 
l. 작업 환경에서 추가적으로 필요한 tablespace는 위의 방법으로 생성한다.


================================================================================================  


5. Data Dictionary and Standard Package

================================================================================================  

* database 생성후 돌려줘야 할 script

$ORACLE_HOME/rdbms/admin/catalog.sql ==> dictionary views, export utility views 생성
$ORACLE_HOME/rdbms/admin/catproc.sql ==> procedures, functions 생성
$ORACLE_HOME/rdbms/admin/catdbsyn.sql ==> synonyms 생성

================================================================================================  

* Dictionary list 확인

SQL> col table_name format a30
SQL> col comments format a45
SQL> set pages 800
SQL> spool dictionary.lst
SQL> select * from dictionary order by 1 ==> 전체 dictionary의 list를 볼 수 있다.
SQL> spool off
SQL> ed sictionary.lst
SQL> select * from dictionary where table_name like '%TABLE%'; ==> table 관련 dictionary  
SQL> select * from dictionary where table_name like '%INDEX%';  ==> index 관련 dictionary

================================================================================================  

* 유용한 dictionary  

TABLE_NAME                     COMMENTS
------------------------------ ---------------------------------------------
DBA_USERS                      Information about all users of the database
DBA_TABLESPACES                Description of all tablespaces
DBA_DATA_FILES                 Information about database data files
DBA_FREE_SPACE                 Free extents in all tablespaces
DBA_OBJECTS                    All objects in the database
DBA_SEGMENTS                   Storage allocated for all database segments
DBA_ROLLBACK_SEGS              Description of rollback segments
DBA_EXTENTS                    Extents comprising all segments in the database
DBA_TABLES                     Description of all relational tables in the d
                              atabase
DBA_INDEXES                    Description for all indexes in the database
DBA_VIEWS                      Description of all views in the database
DBA_TRIGGERS                   All triggers in the database
DBA_SOURCE                     Source of all stored objects in the database

================================================================================================  

* sample Query

SQL> select username,default_tablespace,temporary_tablespace from dba_users;
SQL> select tablespace_name,bytes,file_name from dba_data_files;
SQL> select tablespace_name,count(*),sum(bytes) from dba_free_space
 2  group by tablespace_name;
 
================================================================================================  


6. Maintiaining the Contorol File

================================================================================================  

* Control File 리스트 조회

SQL> select name from v$controlfile;

NAME
--------------------------------------------------------------------------------
/home/oracle/data01/oradata/IBM/control01.ctl
/home/oracle/data01/oradata/IBM/control02.ctl
/home/oracle/data01/oradata/IBM/control03.ctl

================================================================================================  

* Control File 을 하나 추가해보자

a. database shutdown
SQL> shutdown immediate

b. control file 복사(os상 물리적인 복사)
/home/oracle/data01/oradata/IBM> cp control03.ctl control04.ctl ==> 실제는 다른 disk로 복사해야함
   문제발생을 대비해 분리하는것임.

c. Parameter File 편집
control_files = ("/home/oracle/data01/oradata/IBM/control01.ctl",  
"/home/oracle/data01/oradata/IBM/control02.ctl",  
"/home/oracle/data01/oradata/IBM/control03.ctl",  
"/home/oracle/data01/oradata/IBM/control04,ctl")

d. database startup & 확인
SQL> startup
SQL> select name from v$controlfile;

NAME
--------------------------------------------------------------------------------
/home/oracle/data01/oradata/IBM/control01.ctl
/home/oracle/data01/oradata/IBM/control02.ctl
/home/oracle/data01/oradata/IBM/control03.ctl
/home/oracle/data01/oradata/IBM/control04.ctl ==> 하나 더 추가되었지요...(실제는 다른disk로)

================================================================================================  


7. Multiplexing Redo Log Files

================================================================================================  

* Redo Log File 리스트 조회

SQL> select group#,sequence#,bytes,members,status from v$log;

   GROUP#  SEQUENCE#      BYTES    MEMBERS STATUS
---------- ---------- ---------- ---------- --------------------------------
        1        862     512000          1 CURRENT
        2        860     512000          1 INACTIVE
        3        861     512000          1 INACTIVE

SQL> select * from v$logfile;

   GROUP# STATUS         MEMBER
---------- -------------- --------------------------------------------------
        1                /home/oracle/data01/oradata/IBM/redo03.log
        2                /home/oracle/data01/oradata/IBM/redo02.log
        3                /home/oracle/data01/oradata/IBM/redo01.log          

================================================================================================  

* Log Group 추가(기존 로그 파일과 동일한 사이즈로)

SQL> alter database add logfile
 2  '/home/oracle/data01/oradata/IBM/redo04.log' size 200k;

SQL> select group#,sequence#,bytes,members,status from v$log;

   GROUP#  SEQUENCE#      BYTES    MEMBERS STATUS
---------- ---------- ---------- ---------- --------------------------------
        1        862     512000          1 CURRENT
        2        860     512000          1 INACTIVE
        3        861     512000          1 INACTIVE
        4          0     204800          1 UNUSED

SQL> select * from v$logfile;

   GROUP# STATUS         MEMBER
---------- -------------- --------------------------------------------------
        1                /home/oracle/data01/oradata/IBM/redo03.log
        2                /home/oracle/data01/oradata/IBM/redo02.log
        3                /home/oracle/data01/oradata/IBM/redo01.log
        4                /home/oracle/data01/oradata/IBM/redo04.log

================================================================================================  

* Log Group 별 멤버 파일 추가    ==> backup 시 risk줄이기 위해 실제는 다른 disk에 해야함.

SQL> alter database add logfile member
 2  '/home/oracle/data01/oradata/IBM/redo01b.log' to group 1,
 3  '/home/oracle/data01/oradata/IBM/redo02b.log' to group 2,
 4  '/home/oracle/data01/oradata/IBM/redo03b.log' to group 3,
 5  '/home/oracle/data01/oradata/IBM/redo04b.log' to group 4;

================================================================================================  

* 확인

SQL> !ls /home/oracle/data01/oradata/IBM/*.log
/home/oracle/data01/oradata/IBM/redo01.log   /home/oracle/data01/oradata/IBM/redo03.log
/home/oracle/data01/oradata/IBM/redo01b.log  /home/oracle/data01/oradata/IBM/redo03b.log
/home/oracle/data01/oradata/IBM/redo02.log   /home/oracle/data01/oradata/IBM/redo04.log
/home/oracle/data01/oradata/IBM/redo02b.log  /home/oracle/data01/oradata/IBM/redo04b.log

SQL> select group#,sequence#,bytes,members,status from v$log;

   GROUP#  SEQUENCE#      BYTES    MEMBERS STATUS
---------- ---------- ---------- ---------- --------------------------------
        1        862     512000          2 CURRENT
        2        860     512000          2 INACTIVE
        3        861     512000          2 INACTIVE
        4          0     204800          2 UNUSED ==> 아직 한번도 사용되지 않음

SQL> select * from v$logfile;

   GROUP# STATUS         MEMBER
---------- -------------- --------------------------------------------------
        1                /home/oracle/data01/oradata/IBM/redo03.log
        2                /home/oracle/data01/oradata/IBM/redo02.log
        3                /home/oracle/data01/oradata/IBM/redo01.log
        4                /home/oracle/data01/oradata/IBM/redo04.log
        1 INVALID        /home/oracle/data01/oradata/IBM/redo01b.log
        2 INVALID        /home/oracle/data01/oradata/IBM/redo02b.log
        3 INVALID        /home/oracle/data01/oradata/IBM/redo03b.log
        4 INVALID        /home/oracle/data01/oradata/IBM/redo04b.log


==> 현재 사용되고 있는 log group 은 group 1이고 나중에 추가한 member들은 invalid 한 상태이다.
강제로 log switch를 일으켜서 valid하게 바꾸자.

SQL> alter system switch logfile;

SQL> select group#,sequence#,bytes,members,status from v$log;

   GROUP#  SEQUENCE#      BYTES    MEMBERS STATUS
---------- ---------- ---------- ---------- --------------------------------
        1        862     512000          2 ACTIVE
        2        860     512000          2 INACTIVE
        3        861     512000          2 INACTIVE
        4        863     204800          2 CURRENT ==> unused에서 바뀜.

SQL> select * from v$logfile;

   GROUP# STATUS         MEMBER
---------- -------------- --------------------------------------------------
        1                /home/oracle/data01/oradata/IBM/redo03.log
        2                /home/oracle/data01/oradata/IBM/redo02.log
        3                /home/oracle/data01/oradata/IBM/redo01.log
        4                /home/oracle/data01/oradata/IBM/redo04.log
        1 INVALID        /home/oracle/data01/oradata/IBM/redo01b.log
        2 INVALID        /home/oracle/data01/oradata/IBM/redo02b.log
        3 INVALID        /home/oracle/data01/oradata/IBM/redo03b.log
        4                /home/oracle/data01/oradata/IBM/redo04b.log ==> valid하게 바뀜
         
================================================================================================  


Log Miner

================================================================================================  

* Parameter File 의 utl_file_dir 편집

a. 확인

SQL> select name,value from v$parameter
 2  where name='utl_file_dir';
 
NAME                 VALUE
-------------------- ------------------------------
utl_file_dir

SQL> !mkdir $ORACLE_HOME/LOG

b. LogMiner사용을 위해 init.ora file 편집
utl_file_dir=/oracle/app/oracle/product/8.1.7/LOG

c. restart

SQL> shutdown immediate
SQL> startup

확인
SQL> select name,value from v$parameter
 2  where name='utl_file_dir';
 
NAME                 VALUE
-------------------- ------------------------------
utl_file_dir         /oracle/app/oracle/product/8.1.7/LOG ==> LogMiner 준비를 위한 parameter set

d. LogMiner setting - 반드시 트랜잭션의 첫번째 명령이어야 함

SQL> commit;
SQL> exec dbms_logmnr_d.build('v817dict.ora','/oracle/app/oracle/product/8.1.7/LOG');
BEGIN dbms_logmnr_d.build('v817dict.ora','/oracle/app/oracle/product/8.1.7/LOG'); END;

*
ERROR at line 1:
ORA-06532: Subscript outside of limit
ORA-06512: at "SYS.DBMS_LOGMNR_D", line 793
ORA-06512: at line 1

SQL> !ls $ORACLE_HOME/LOG

SQL> exec dbms_logmnr.add_logfile('/home/oracle/data01/oradata/IBM/redo01.log',DBMS_LOGMNR.NEW);
SQL> exec dbms_logmnr.add_logfile('/home/oracle/data01/oradata/IBM/redo02.log',DBMS_LOGMNR.ADDFILE);
SQL> exec dbms_logmnr.add_logfile('/home/oracle/data01/oradata/IBM/redo03.log',DBMS_LOGMNR.ADDFILE);
SQL> exec dbms_logmnr.add_logfile('/home/oracle/data01/oradata/IBM/redo04.log',DBMS_LOGMNR.ADDFILE);
SQL> exec dbms_logmnr.start_logmnr('/oracle/app/oracle/product/8.1.7/LOG/v817dict.ora');


e. 트랜잭션 수행

SQL> descc scott.dept
SQL> select * from scott.dept;
SQL> insert into scott.dept values(99,'test','test');
SQL> update scott.dept set loc='TEST' where deptno=99;
SQL> commit;

f. log miner 정보 분석
SQL> select timestamp,username,sql_redo from v$logmnr_contents
 2  where seg_name='DEPT';

g. 로그마이닝 종료

SQL> exec dbms_logmnr.end_logmnr;

================================================================================================  


8. Managing TableSpace and Data Files

================================================================================================  

* tablespace와 datafile 조회

SQL> col tablespace_name format a15
SQL> col file_name format a45
SQL> select tablespace_name,status,contents from dba_tablespaces;

TABLESPACE_NAME STATUS             CONTENTS
--------------- ------------------ ------------------
SYSTEM          ONLINE             PERMANENT
TOOLS           ONLINE             PERMANENT
RBS             ONLINE             PERMANENT
TEMP            ONLINE             TEMPORARY
USERS           ONLINE             PERMANENT
INDX            ONLINE             PERMANENT
DRSYS           ONLINE             PERMANENT

SQL> select tablespace_name,bytes,file_name from dba_data_files;

TABLESPACE_NAME      BYTES FILE_NAME
--------------- ---------- ---------------------------------------------
TOOLS             10485760 /home/oracle/data01/oradata/IBM/tools01.dbf
DRSYS             20971520 /home/oracle/data01/oradata/IBM/drsys01.dbf
USERS             20971520 /home/oracle/data01/oradata/IBM/users01.dbf
INDX              20971520 /home/oracle/data01/oradata/IBM/indx01.dbf
RBS               52428800 /home/oracle/data01/oradata/IBM/rbs01.dbf
TEMP              20971520 /home/oracle/data01/oradata/IBM/temp01.dbf
SYSTEM           283115520 /home/oracle/data01/oradata/IBM/system01.dbf

================================================================================================  

* tablespace 생성 및 사이즈 변경

SQL> create tablespace data05
 2  datafile '/home/oracle/data01/oradata/IBM/data05_01.dbf' size 1m;

Tablespace created.

1m 짜리 datafile 하나를 가진 tablespace data05를 추가하였다. 확인.

SQL> select tablespace_name,bytes,file_name from dba_data_files
 2  where tablespace_name='DATA05';

TABLESPACE_NAME      BYTES FILE_NAME
--------------- ---------- ---------------------------------------------
DATA05             1048576 /home/oracle/data01/oradata/IBM/data05_01.dbf

tablespace가 부족할때 늘리는 방법은 두가지가 있다.  
하나는 datafile을 추가하는 방법이고 다른하나는 datafile의 size를 늘리는 방법이다.

a. datafile을 하나 추가해보자.

SQL> alter tablespace data05
 2  add datafile '/home/oracle/data01/oradata/IBM/data05_02.dbf' size 1m;
 
SQL> select tablespace_name,bytes,file_name from dba_data_files
 2  where tablespace_name='DATA05';

TABLESPACE_NAME      BYTES FILE_NAME
--------------- ---------- ---------------------------------------------
DATA05             1048576 /home/oracle/data01/oradata/IBM/data05_01.dbf
DATA05             1048576 /home/oracle/data01/oradata/IBM/data05_02.dbf

제대로 추가되었다.


b. 그렇다면 하나의 사이즈를 변경해보자.

SQL> alter database datafile
 2  '/home/oracle/data01/oradata/IBM/data05_02.dbf' resize 2m;
 
SQL> select tablespace_name,bytes,file_name from dba_data_files
 2  where tablespace_name='DATA05';

TABLESPACE_NAME      BYTES FILE_NAME
--------------- ---------- ---------------------------------------------
DATA05             1048576 /home/oracle/data01/oradata/IBM/data05_01.dbf
DATA05             2097152 /home/oracle/data01/oradata/IBM/data05_02.dbf

2m로 제대로 변경이 되었다.

다시 원상복구
SQL> alter database datafile
 2  '/home/oracle/data01/oradata/IBM/data05_02.dbf' resize 1m;

전체를 다시 확인해보자

SQL> select tablespace_name,bytes,file_name from dba_data_files;

TABLESPACE_NAME      BYTES FILE_NAME
--------------- ---------- ---------------------------------------------
TOOLS             10485760 /home/oracle/data01/oradata/IBM/tools01.dbf
DRSYS             20971520 /home/oracle/data01/oradata/IBM/drsys01.dbf
USERS             20971520 /home/oracle/data01/oradata/IBM/users01.dbf
INDX              20971520 /home/oracle/data01/oradata/IBM/indx01.dbf
RBS               52428800 /home/oracle/data01/oradata/IBM/rbs01.dbf
TEMP              20971520 /home/oracle/data01/oradata/IBM/temp01.dbf
SYSTEM           283115520 /home/oracle/data01/oradata/IBM/system01.dbf
DATA05             1048576 /home/oracle/data01/oradata/IBM/data05_01.dbf
DATA05             1048576 /home/oracle/data01/oradata/IBM/data05_02.dbf

================================================================================================  

* tablespace 삭제 : Dictionary에서만 삭제되는것으로 실제 물리적으로 파일은 os command로 삭제해야한다.


SQL> select tablespace_name from dba_tablespaces
 2  where tablespace_name like 'DATA%'
 3  minus
 4  select distinct tablespace_name from dba_segments;

TABLESPACE_NAME
---------------
DATA05

SQL> drop tablespace data05;
SQL> select tablespace_name,bytes,file_name from dba_data_files;

TABLESPACE_NAME      BYTES FILE_NAME
--------------- ---------- ---------------------------------------------
TOOLS             10485760 /home/oracle/data01/oradata/IBM/tools01.dbf
DRSYS             20971520 /home/oracle/data01/oradata/IBM/drsys01.dbf
USERS             20971520 /home/oracle/data01/oradata/IBM/users01.dbf
INDX              20971520 /home/oracle/data01/oradata/IBM/indx01.dbf
RBS               52428800 /home/oracle/data01/oradata/IBM/rbs01.dbf
TEMP              20971520 /home/oracle/data01/oradata/IBM/temp01.dbf
SYSTEM           283115520 /home/oracle/data01/oradata/IBM/system01.dbf

SQL> !ls //home/oracle/data01/oradata/IBM/*.dbf
//home/oracle/data01/oradata/IBM/data05_01.dbf  //home/oracle/data01/oradata/IBM/system01.dbf
//home/oracle/data01/oradata/IBM/data05_02.dbf  //home/oracle/data01/oradata/IBM/temp01.dbf
//home/oracle/data01/oradata/IBM/drsys01.dbf    //home/oracle/data01/oradata/IBM/tools01.dbf
//home/oracle/data01/oradata/IBM/indx01.dbf     //home/oracle/data01/oradata/IBM/users01.dbf
//home/oracle/data01/oradata/IBM/rbs01.dbf

dictionary에서는 삭제되었으나 여전히 물리적인 file은 존재한다. 삭제하면 된다.
(tablespace생성시에는 file이 그냥 생성되나 삭제시는 dictionary삭제후 강제로 삭제해줘야 한다.)

SQL> !rm /home/oracle/data01/oradata/IBM/data05*

================================================================================================  

* tablespace 의 online/offline, read only/read write

SQL> select tablespace_name, status from dba_tablespaces;

TABLESPACE_NAME                                              STATUS
------------------------------------------------------------ ------------------
SYSTEM                                                       ONLINE
TOOLS                                                        ONLINE
RBS                                                          ONLINE
TEMP                                                         ONLINE
USERS                                                        ONLINE
INDX                                                         ONLINE
DRSYS                                                        ONLINE

7 rows selected.

SQL> select tablespace_name from dba_tables
 2  where table_name ='DEPT' and owner='SCOTT';

TABLESPACE_NAME
------------------------------------------------------------
SYSTEM

default로 생성시 scott user의 data가 system tablespace에 생성되었으나 이렇게 쓰면 안된다.
하나 생성해볼까?

SQL> create tablespace data01
 2  datafile '/home/oracle/data01/oradata/IBM/data01.dbf' size 1m;

Tablespace created.

SQL> connect scott/tiger
Connected.

SQL> create table dept_tmp tablespace data01
 2  as select * from dept;
 
SQL> connect internal
Connected.
SQL> select tablespace_name from dba_tables
 2  where table_name ='DEPT_TMP' and owner='SCOTT';

TABLESPACE_NAME
------------------------------------------------------------
DATA01

SQL> select * from scott.dept_tmp;

   DEPTNO DNAME                        LOC
---------- ---------------------------- --------------------------
       10 ACCOUNTING                   NEW YORK
       20 RESEARCH                     DALLAS
       30 SALES                        CHICAGO
       40 OPERATIONS                   BOSTON
       
제대로 된다. 그렇다면 tablespace를 offline으로...

SQL> alter tablespace data01 offline;
SQL> select tablespace_name, status from dba_tablespaces
 2  where tablespace_name='DATA01';

TABLESPACE_NAME                                              STATUS
------------------------------------------------------------ ------------------
DATA01                                                       OFFLINE

SQL> select * from scott.dept_tmp;
select * from scott.dept_tmp
                   *
ERROR at line 1:
ORA-00376: file 8 cannot be read at this time
ORA-01110: data file 8: '/home/oracle/data01/oradata/IBM/data01.dbf'

위와 같이 error가 발생한다.
다시 online으로 해두자.
SQL> alter tablespace data01 online;

이번엔 read only로 변경
SQL> alter tablespace data01 read only;

SQL> select tablespace_name, status from dba_tablespaces
 2  where tablespace_name='DATA01';

TABLESPACE_NAME                                              STATUS
------------------------------------------------------------ ------------------
DATA01                                                       READ ONLY

변경되었다.

SQL> insert into scott.dept_tmp values(80,'new_dept','new_loc');
insert into scott.dept_tmp values(80,'new_dept','new_loc')
                 *
ERROR at line 1:
ORA-00372: file 8 cannot be modified at this time
ORA-01110: data file 8: '/home/oracle/data01/oradata/IBM/data01.dbf'

insert같은 DML(write성) 수행시 위와 같은 error 발생

원상복구
SQL> alter tablespace data01 read write;
SQL> insert into scott.dept_tmp values(80,'test','test');

제대로 된다.
================================================================================================  


9. Storage Structure and Relationships

================================================================================================  

* Extent 정보 조회 : 다음과 같이 각종 extent,segment 등의 정보를 조회해 볼 수 있다.

SQL> col owner format a10
SQL> col segment_type format a12
SQL> col segment_name format a12
SQL> col tablespace_name format a10

SQL> select owner,segment_name,segment_type, tablespace_name,max_extents,extents,pct_increase
 2  from dba_segments
 3  where max_extents - extents <= 10 and owner !='SYS';

no rows selected

SQL> select owner,segment_name,segment_type, tablespace_name,max_extents,extents,pct_increase
 2  from dba_segments
 3  where owner='SCOTT';

OWNER      SEGMENT_NAME SEGMENT_TYPE TABLESPACE MAX_EXTENTS    EXTENTS PCT_INCREASE
---------- ------------ ------------ ---------- ----------- ---------- ------------
SCOTT      DEPT_TMP     TABLE        DATA01             505          1           50
SCOTT      DEPT         TABLE        SYSTEM      2147483645          1           50
SCOTT      EMP          TABLE        SYSTEM      2147483645          1           50
SCOTT      BONUS        TABLE        SYSTEM      2147483645          1           50
SCOTT      SALGRADE     TABLE        SYSTEM      2147483645          1           50
SCOTT      PK_DEPT      INDEX        SYSTEM      2147483645          1           50
SCOTT      PK_EMP       INDEX        SYSTEM      2147483645          1           50


SQL> select segment_name,extents, initial_extent, next_extent,pct_increase
 2  from dba_segments
 3  where owner='SCOTT' and segment_name='EMP';

SEGMENT_NAME    EXTENTS INITIAL_EXTENT NEXT_EXTENT PCT_INCREASE
------------ ---------- -------------- ----------- ------------
EMP                   1          65536       65536           50


SQL> select segment_name,extent_id,block_id,bytes,blocks
 2  from dba_extents
 3  where owner='SCOTT' and segment_name='EMP';
 4  order by 2,3;
 
SEGMENT_NAME  EXTENT_ID   BLOCK_ID      BYTES     BLOCKS
------------ ---------- ---------- ---------- ----------
EMP                   0      33945      65536          8

================================================================================================  

* Free space 관리

tablespace내에 free space를 먼저 확인해본다.
SQL> select * from dba_free_space
 2  where tablespace_name ='DATA01' order by 1,2,3;

TABLESPACE    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO
---------- ---------- ---------- ---------- ---------- ------------
DATA01              8          7     999424        122            8

테이블을 여러개 생성해보자.
SQL> create table scott.dept2 tablespace data01 as select * from scott.dept;
SQL> create table scott.dept3 tablespace data01 as select * from scott.dept;
SQL> create table scott.dept4 tablespace data01 as select * from scott.dept;
SQL> create table scott.dept5 tablespace data01 as select * from scott.dept;
SQL> create table scott.dept6 tablespace data01 as select * from scott.dept;

SQL> select * from dba_free_space
 2  where tablespace_name ='DATA01' order by 1,2,3;

TABLESPACE    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO
---------- ---------- ---------- ---------- ---------- ------------
DATA01              8         32     794624         97            8

사용함에 따라 tablespace내 free space 가 줄어듦을 알 수 있다.

SQL> drop table scott.dept2;
drop table dept2
          *
ERROR at line 1:
ORA-04098: trigger 'SYS.JIS$ROLE_TRIGGER$' is invalid and failed re-validation
이건 또 뭐야 ? trigger가 걸려있네요...  
table drop 을 위해
SQL> alter trigger SYS.JIS$ROLE_TRIGGER$ disable;
drop table scott.dept3; ==> dept4 만 빼고 전부 drop
drop table scott.dept5;
drop table scott.dept6;

SQL> select * from dba_free_space
 2  where tablespace_name ='DATA01' order by 1,2,3;

TABLESPACE    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO
---------- ---------- ---------- ---------- ---------- ------------
DATA01              8          7      40960          5            8
DATA01              8         32     794624         97            8

tablespace의 free space가 늘긴 했는데 쪼개졌네요..
빈공간을 병합하자
SQL> alter tablespace data01 coalesce;

SQL> select * from dba_free_space
 2  where tablespace_name ='DATA01' order by 1,2,3;

TABLESPACE    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO
---------- ---------- ---------- ---------- ---------- ------------
DATA01              8          7      40960          5            8
DATA01              8         32     794624         97            8

그래도 두개로 쪼개져 있는 이유는? 중간에 dept4 가 사용하는 space가 coalesce 되지 않았기 때문

SQL> drop table scott.dept4;
SQL> alter tablespace data01 coalesce;

완전히 병합되었다.

================================================================================================  



10. Managing Rollback Segments

================================================================================================  

* rollback segment의 정보 조회

SQL> col owner format a10
SQL> col segment_name format a12
SQL> col segment_type format a12
SQL> col tablespace_name format a10
SQL> col status format a7

SQL> select segment_name,tablespace_name,status,initial_extent,next_extent,min_extents
 2  from dba_rollback_segs;

SEGMENT_NAME TABLESPACE STATUS  INITIAL_EXTENT NEXT_EXTENT MIN_EXTENTS
------------ ---------- ------- -------------- ----------- -----------
SYSTEM       SYSTEM     ONLINE           57344       57344           2
RBS0         RBS        ONLINE          524288      524288           8
RBS1         RBS        ONLINE          524288      524288           8
RBS2         RBS        ONLINE          524288      524288           8
RBS3         RBS        ONLINE          524288      524288           8
RBS4         RBS        ONLINE          524288      524288           8
RBS5         RBS        ONLINE          524288      524288           8
RBS6         RBS        ONLINE          524288      524288           8

================================================================================================  

* rollback segment 생성

SQL> create rollback segment rbs99
 2  tablespace rbs
 3  storage(initial 20k next 20k minextents 2 optimal 80k);

Rollback segment created.

SQL> select segment_name,tablespace_name,status,initial_extent,next_extent,min_extents
 2  from dba_rollback_segs;

SEGMENT_NAME TABLESPACE STATUS  INITIAL_EXTENT NEXT_EXTENT MIN_EXTENTS
------------ ---------- ------- -------------- ----------- -----------
SYSTEM       SYSTEM     ONLINE           57344       57344           2
RBS0         RBS        ONLINE          524288      524288           8
RBS1         RBS        ONLINE          524288      524288           8
RBS2         RBS        ONLINE          524288      524288           8
RBS3         RBS        ONLINE          524288      524288           8
RBS4         RBS        ONLINE          524288      524288           8
RBS5         RBS        ONLINE          524288      524288           8
RBS6         RBS        ONLINE          524288      524288           8
RBS99        RBS        OFFLINE          24576       32768           2

추가되었다. online으로 전환하자.

SQL> alter rollback segment rbs99 online;


SQL> create table emp2 as select * from emp;


SQL> select name,extents,xacts,shrinks,optsize
 2  from v$rollname n, v$rollstat s
 3  where n.usn = s.usn;

NAME                      EXTENTS      XACTS    SHRINKS    OPTSIZE
---------------------  ----------- ---------- ---------- ----------
SYSTEM                          9          0          0
RBS0                            8          0          0    4194304
RBS1                            8          0          0    4194304
RBS2                            8          0          0    4194304
RBS3                            8          0          0    4194304
RBS4                            8          0          0    4194304
RBS5                            8          0          0    4194304
RBS6                            8          0          0    4194304
RBS99                           2          0          0      81920   ==> extents,xacts의 변화 관찰


SQL> set transaction use rollback segment rbs99;
SQL> update emp2 set hiredate=sysdate;


SQL> select name,extents,xacts,shrinks,optsize
 2  from v$rollname n, v$rollstat s
 3  where n.usn = s.usn;

NAME               EXTENTS      XACTS    SHRINKS    OPTSIZE
--------------- ---------- ---------- ---------- ----------
SYSTEM                   9          0          0
RBS0                     8          0          0    4194304
RBS1                     8          0          0    4194304
RBS2                     8          0          0    4194304
RBS3                     8          0          0    4194304
RBS4                     8          0          0    4194304
RBS5                     8          0          0    4194304
RBS6                     8          0          0    4194304
RBS99                    2          1          0      81920 ==> transaction이 시작됨


SQL> update emp2 set hiredate=sysdate-1;  
sql> insert into emp2 select * from emp2; ==> 엄청 많이 수행 하자.


SQL> select name,extents,xacts,shrinks,optsize
 2  from v$rollname n, v$rollstat s
 3  where n.usn = s.usn;

NAME               EXTENTS      XACTS    SHRINKS    OPTSIZE
--------------- ---------- ---------- ---------- ----------
SYSTEM                   9          0          0
RBS0                     8          0          0    4194304
RBS1                     8          0          0    4194304
RBS2                     8          0          0    4194304
RBS3                     8          0          0    4194304
RBS4                     8          0          0    4194304
RBS5                     8          0          0    4194304
RBS6                     8          0          0    4194304
RBS99                    3          1          0      81920 ==> extents 증가


SQL> rollback;
SQL> set transaction use rollback segment rbs99; <

Posted by tornado
|

sql server 온라인 도움말 에 있는것.

 

 

계층 확장

데이터베이스에 계층 정보가 포함될 경우도 있습니다. 예를 들어, 다음 데이터는 전세계 지역에 대한 계층을 표현한 것입니다. 그러나 데이터에 함축되어 있는 구조를 명확하게 알 수 없습니다.

Parent                             Child                             ---------------------------------- ----------------------------------World                              Europe                            World                              North America                     Europe                             France                            France                             Paris                             North America                      United States                     North America                      Canada                            United States                      New York                          United States                      Washington                        New York                           New York City                     Washington                         Redmond                           

다음 예제와 같이 정리하면 더 이해하기 쉽습니다.

World   North America      Canada      United States         Washington            Redmond         New York            New York City   Europe      France         Paris

다음의 Transact-SQL 프로시저는 인코딩된 계층을 임의의 깊이로 확장합니다. 비록 Transact-SQL이 재귀를 지원하지만 진행 중인 처리를 위해 모든 항목을 추적하는 스택으로 임시 테이블을 사용하는 것이 보다 효율적입니다. 특정한 항목의 처리가 끝나게 되면 스택에서 제거되고 새로운 항목이 발견되면 스택에 추가됩니다.

CREATE PROCEDURE expand (@current char(20)) asSET NOCOUNT ONDECLARE @level int, @line char(20)CREATE TABLE #stack (item char(20), level int)INSERT INTO #stack VALUES (@current, 1)SELECT @level = 1WHILE @level > 0BEGIN   IF EXISTS (SELECT * FROM #stack WHERE level = @level)      BEGIN         SELECT @current = item         FROM #stack         WHERE level = @level         SELECT @line = space(@level - 1) + @current         PRINT @line         DELETE FROM #stack         WHERE level = @level            AND item = @current         INSERT #stack            SELECT child, @level + 1            FROM hierarchy            WHERE parent = @current         IF @@ROWCOUNT > 0            SELECT @level = @level + 1      END   ELSE      SELECT @level = @level - 1END -- WHILE

입력 매개 변수 (@current)는 시작할 계층의 위치를 지정합니다. 이 매개 변수는 주 루프의 현재 항목 역시 추적합니다.

계층에서 현재 수준을 계속 추적하는 @level 과 들여쓰여진 줄을 만드는 데 이용되는 작업 공간인 @line의 두 가지 지역 변수가 사용됩니다.

SET NOCOUNT ON문은 각 SELECT의 ROWCOUNT 메시지로 인해 출력이 혼동되는 것을 방지합니다.

계층의 시작점에서 항목 식별자로 임시 테이블인 #stack이 만들어지고 초기화되며 @level이 이에 맞게 설정됩니다. #stacklevel 열은 데이터베이스의 복수의 수준에 같은 항목이 나타나게 합니다. 비록 이러한 상황이 예제의 지리 데이터에 응용되진 않지만 다른 예제에 응용할 수 있습니다.

이 예제에서 @level이 0보다 크면 프로시저는 다음 단계를 따릅니다.

  1. 현재 수준(@level)에서의 스택에 항목이 남아 있으면 프로시저는 하나를 선택하고 @current로 호출합니다.

  2. 항목을 @level 공백만큼 들여 쓴 후 인쇄합니다.

  3. 더 이상 처리되지 않는 항목을 스택에서 삭제한 후 다음 수준(@level + 1)의 스택에 모든 하위 항목을 추가합니다. 이 곳이 계층 테이블(#stack)이 유일하게 사용되는 곳입니다..

    상용 프로그래밍 언어를 사용할 경우 각 하위 항목을 찾아서 일일이 스택에 추가해야 합니다. Transact-SQL을 사용하면 별도의 중첩 루프를 쓰지 않고도 하나의 문으로 모든 하위 항목을 찾고 추가할 수 있습니다.

  4. 하위 항목이 존재할 경우(IF @@ROWCOUNT > 0), 처리를 위해 수준을 한 단계 낮추고(@level = @level + 1) 그 외에는 현재 수준에서 처리를 진행합니다.

  5. 현재 수준에서 처리 대기 중인 스택의 항목이 없으면 이전 수준(@level = @level - 1)에서 대기 중인 처리가 있는지 알아보기 위해 한 수준 뒤로 갑니다. 이전 수준이 없으면 확장은 완료됩니다.

©1988-2000 Microsoft Corporation. All Rights Reserved.

Posted by tornado
|
JAVA IDE
 Download News & Updates
 Support Contact Us
 FAQ Tutorials and Demos
 Search Member Services
  

MyEclipse XDoclet Web Development Tutorial

Eric Glass, Independent Consultant
karicg@worldnet.att.net

 

Overview

This tutorial provides a detailed example for using the XDoclet support within MyEclipse to speed web development through the use of attribute-oriented programming. Using XDoclet greatly reduces development time since it automates the generation of deployment descriptors and other support code. This tutorial is based on the concepts that are explained more fully in the IBM developerWorks article Enhance J2EE component reuse with XDoclets by Rick Hightower. If you're new to XDoclet, reading the developerWorks tutorial is highly recommended to provide you the background information needed to fully understand what is described here. Additionally, once you've read how to do it the "hard way" you'll really appreciate the integrated facilities provided by MyEclipse as we develop a sample web application composed of an HTML page, a servlet, a JSP, and a custom taglib.

   
   

Steps to Easy Web Development

This section will take you through the step-by-step process of web project creation, XDoclet configuration, deployment descriptor generation, and Tomcat 5 deployment.

1) Create a New Web Project

  1. Change to or open the MyEclipse Perspective (Window > Open Perspecitve > Other... > MyEclipse) .
  2. Select File > New... > Project > J2EE > Web Module Project, then press the Next button.
  3. For the project name use MyXDocletWeb and for the context root specify /MyXDocletWeb, as shown in Figure 1 below, and press the Finish button.  The resulting project structure will be the same as that shown in Figure 2.

    Creating the Web Project
    Figure 1. Creating the Web Project

    Web Project Layout
    Figure 2. Web Project Layout

2) Create a Servlet

  1. Select the project MyXDocletWeb in the Package Explorer
  2. Select File > New... > Servlet,
  3. Populate it with the package name com.myeclipse.tutorial.servlet and class name BasicServlet as shown in Figure 3, then press the Next button.

    Creating the Servlet
    Figure 3. Creating the Servlet

  4. When the second page of the servlet wizard is displayed, deselect the checkbox labeled Generate/Map web.xml File and select the finish button as shown in Figure 4.  We don't need to have the wizard map the web.xml file since we'll be generating it based on our XDoclet settings later.

    Creating the Servlet - Page 2
    Figure 4. Creating the Servlet - Page 2

  5. After the servlet is generated, it will be opened in the Java editor. Replace the generated source code completely with the following contents and save the file.

/*
 * BasicServlet.java
 * Created on Aug 7, 2003
 */
package com.myeclipse.tutorial.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Basic Servlet Using XDoclet.
 *
 * @author Administrator
 * @version 1.0, Aug 7, 2003
 *
 * @web.servlet name = "BasicServlet"
 *              display-name = "Basic Servlet"
 *              load-on-startup = "1"
 * @web.servlet-init-param name = "hi"
 *                         value = "${basic.servlet.hi}"
 * @web.servlet-init-param name = "bye"
 *                         value = "${basic.servlet.bye}"
 * @web.servlet-mapping url-pattern = "/Basic/*"
 * @web.servlet-mapping url-pattern = "*.Basic"
 * @web.servlet-mapping url-pattern = "/BasicServlet"
 * @web.resource-ref description = "JDBC resource"
 *                   name = "jdbc/mydb"
 *                   type = "javax.sql.DataSource"
 *                   auth = "Container"
 */
public class BasicServlet extends HttpServlet {
    /**
     * Constructor of the object.
     */
    public BasicServlet() {
        super();
    }

    /**
     * Destruction of the servlet.
     */
    public void destroy() {
        super.destroy(); // Just puts "destroy" string in log
    }

    /**
     * The doGet method of the servlet.
     *
     * @param request the request send by the client to the server
     * @param response the response send by the server to the client
     *
     * @throws ServletException if an error occurred
     * @throws IOException if an error occurred
     */
    public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        processRequest(request, response);
    }

    /**
     * The doPost method of the servlet.
     *
     * @param request the request send by the client to the server
     * @param response the response send by the server to the client
     *
     * @throws ServletException if an error occurred
     * @throws IOException if an error occurred
     */
    public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        processRequest(request, response);
    }

    /**
     * Returns information about the servlet.
     *
     * @return String information about this servlet
     */
    public String getServletInfo() {
        return "Basic Servlet Using XDoclet";
    }

    /**
     * Initialization of the servlet.
     *
     * @throws ServletException if an error occurred
     */
    public void init() throws ServletException {
        // Put your code here
    }

    /**
     * Initialization of the servlet with the servlet's configuration.
     *
     * @param config the servlet's configuration
     *
     * @throws ServletException if an error occurred
     */
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    /**
     * Processes requests for both HTTP GET and POST methods.
     *
     * @param request servlet request
     * @param response servlet response
     *
     * @throws ServletException if an error occurred
     * @throws java.io.IOException if an I/O error occurred
     */
    protected void processRequest(HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {
        ServletConfig config = this.getServletConfig();
        String hi = config.getInitParameter("hi");
        String bye = config.getInitParameter("bye");

        try {
            response.setContentType("text/html");

            PrintWriter out = response.getWriter();
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Basic Servlet</title>");
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>hi:  " + hi + "</h1>");
            out.println("<h1>bye:  " + bye + "</h1>");
            out.println("</body>");
            out.println("</html>");
            out.close();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
}

Explanation of XDoclet Tags Used in the Servlet
The example tag illustrates the use of several helpful XDoclet tags. The tags and what they generate are displayed in the table below.
XDoclet TagGenerated Code
@web.servlet name = "BasicServlet"     display-name = "Basic Servlet"     load-on-startup = "1" Generated into the web.xml:

<servlet>
   <servlet-name>BasicServlet</servlet-name>
   <display-name>Basic Servlet</display-name>
   <servlet-class>com.myeclipse.tutorial.servlet.BasicServlet</servlet-class>
   ...
   <load-on-startup>1</load-on-startup>
</servlet>
@web.servlet-init-param name = "hi"
    value = "${basic.servlet.hi}"
@web.servlet-init-param name = "bye"
    value = "${basic.servlet.bye}"
Generated into the web.xml when Ant properties are set to: (basic.servlet.hi = Ant is cool!) and (basic.servlet.bye = XDoclet Rocks!):

<servlet>
   ...
   <init-param>
      <param-name>hi</param-name>
      <param-value>Ant is cool!</param-value>
   </init-param>
   <init-param>
      <param-name>bye</param-name>
      <param-value>XDoclet Rocks!</param-value>
   </init-param>
   ...
</servlet>
@web.servlet-mapping url-pattern = "/Basic/*"
@web.servlet-mapping url-pattern = "*.Basic"
@web.servlet-mapping url-pattern = "/BasicServlet"
Generated into web.xml:

<servlet-mapping>
   <servlet-name>BasicServlet</servlet-name>
   <url-pattern>/Basic/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
   <servlet-name>BasicServlet</servlet-name>
   <url-pattern>*.Basic</url-pattern>
</servlet-mapping>
<servlet-mapping>
   <servlet-name>BasicServlet</servlet-name>
   <url-pattern>/BasicServlet</url-pattern>
</servlet-mapping>
@web.resource-ref
    description = "JDBC resource"
    name = "jdbc/mydb"
    type = "javax.sql.DataSource"
    auth = "Container"
Generated into web.xml:

<resource-ref>
   <description>JDBC resource</description>
   <res-ref-name>jdbc/mydb</res-ref-name>
   <res-type>javax.sql.DataSource</res-type>
   <res-auth>Container</res-auth>
</resource-ref>

3) Create a Custom Tag Class

  1. Select the project MyXDocletWeb in the Package Explorer
  2. Select File > New... > Class,
  3. Populate it with the package name com.myeclipse.tutorial.customtag, class name BasicTag, and subclass javax.servlet.jsp.tagext.TagSupport as shown in Figure 5, then press the Finish button.

    Creating a Custom Tag
    Figure 5. Creating a Custom Tag

  4. After the tag is generated, it will be opened in the Java editor. Replace the generated source code completely with the following contents and save the file.

/*
 * BasicTag.java
 */
package com.myeclipse.tutorial.customtag;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;

/**
 * Basic Custom Tag Using XDoclet.
 *
 * @jsp.tag name="BasicTag"
 * @jsp.variable name-given="currentIter"
 *               class="java.lang.Integer"
 *               scope="NESTED"
 *               declare="true"
 * @jsp.variable name-given="atBegin"
 *               class="java.lang.Integer"
 *               scope="AT_BEGIN"
 *               declare="true"
 * @jsp.variable name-given="atEnd"
 *               class="java.lang.Integer"
 *               scope="AT_END"
 *               declare="true"
 *
 */
public class BasicTag extends TagSupport {
    /** Holds value of property includePage. */
    private boolean includePage = false;

    /** Holds value of property includeBody. */
    private boolean includeBody = false;

    /** Holds value of property iterate. */
    private int iterate = 0;

    /**
     * Creates a new BasicTag object.
     */
    public BasicTag() {
        super();
    }

    /**
     * @see javax.servlet.jsp.tagext.TagSupport#doStartTag()
     */
    public int doStartTag() throws JspException {
        pageContext.setAttribute("currentIter", new Integer(iterate));
        pageContext.setAttribute("atBegin", new Integer(0));

        return includeBody ? EVAL_BODY_INCLUDE : SKIP_BODY;
    }

    /**
     * @see javax.servlet.jsp.tagext.TagSupport#doEndTag()
     */
    public int doEndTag() throws JspException {
        pageContext.setAttribute("atEnd", new Integer(iterate));

        return includePage ? EVAL_PAGE : SKIP_PAGE;
    }

    /**
     * @see javax.servlet.jsp.tagext.TagSupport#doAfterBody()
     */
    public int doAfterBody() throws JspException {
        iterate -= 1;
        pageContext.setAttribute("currentIter", new Integer(iterate));

        if (iterate <= 0) {
            return SKIP_BODY;
        } else {
            return EVAL_BODY_AGAIN;
        }
    }

    /**
     * Getter for property includePage.
     *
     * @return Value of property includePage.
     *
     * @jsp.attribute required="true"
     *                rtexprvalue="true"
     *                description="The includePage attribute"
     */
    public boolean isIncludePage() {
        return includePage;
    }

    /**
     * Setter for property includePage.
     *
     * @param includePage New value of property includePage.
     */
    public void setIncludePage(boolean includePage) {
        this.includePage = includePage;
    }

    /**
     * Getter for property includeBody.
     *
     * @return Value of property includeBody.
     *
     * @jsp.attribute required="true"
     *                rtexprvalue="true"
     *                description="The includeBody attribute"
     */
    public boolean isIncludeBody() {
        return includeBody;
    }

    /**
     * Setter for property includeBody.
     *
     * @param includeBody New value of property includeBody.
     */
    public void setIncludeBody(boolean includeBody) {
        this.includeBody = includeBody;
    }

    /**
     * Getter for property iterate.
     *
     * @return Value of property iterate.
     *
     * @jsp.attribute required="true"
     *                rtexprvalue="true"
     *                description="The iterate attribute"
     */
    public int getIterate() {
        return iterate;
    }

    /**
     * Setter for property iterate.
     *
     * @param iterate New value of property iterate.
     */
    public void setIterate(int iterate) {
        this.iterate = iterate;
    }
}

Explanation of XDoclet Tags Used in the Custom Tag
The example tag  illustrates the use of several helpful XDoclet tags. The tags and what they generate are displayed in the table below.
XDoclet TagGenerated Code
@jsp.tag name="BasicTag" Generated into the taglib's .tld file:

<tag>
   <name>BasicTag</name>
   <tag-class>
       com.myeclipse.tutorial.customtag.BasicTag
   </tag-class>
   ...
</tag>
@jsp.variable 
    name-given="currentIter"
    class="java.lang.Integer"
    scope="NESTED"
    declare="true"
@jsp.variable 
    name-given="atBegin"
    class="java.lang.Integer"
    scope="AT_BEGIN"
    declare="true"
@jsp.variable 
    name-given="atEnd"
    class="java.lang.Integer"
    scope="AT_END"
    declare="true"
Generated into the taglib's .tld file:

<tag>
   ...
   <variable>
      <name-given>currentIter</name-given>
      <variable-class>java.lang.Integer</variable-class>
  <declare>true</declare>
      <scope>NESTED</scope>
   </variable>
   <variable>
      <name-given>atBegin</name-given>
      <variable-class>java.lang.Integer</variable-class>
  <declare>true</declare>
      <scope>AT_BEGIN</scope>
   </variable>
   <variable>
      <name-given>atEnd</name-given>
      <variable-class>java.lang.Integer</variable-class>
  <declare>true</declare>
      <scope>AT_END</scope>
   </variable>
   ...
</tag>
@jsp.attribute required="true"
    rtexprvalue="true"
    description="The includePage attribute"
@jsp.attribute required="true"
    rtexprvalue="true"
    description="The includeBody attribute"
@jsp.attribute required="true"
    rtexprvalue="true"
    description="The iterate attribute"
Generated into the taglib's .tld file:

<tag>
   ...
   <attribute>
      <name>includePage</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
      <description><![CDATA[The includePage attribute]]></description>
   </attribute>
   <attribute>
      <name>includeBody</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
      <description><![CDATA[The includeBody attribute]]></description>
   </attribute>
   <attribute>
      <name>iterate</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
      <description><![CDATA[The iterate attribute]]></description>
   </attribute>
</tag>

4) Add a Folder Called 'merge' to the Project

  1. Select the project MyXDocletWeb in the Package Explorer
  2. Select File > New... > Folder,
  3. Populate it with the name merge, as shown in Figure 6, then press the Finish button.

    Creating the Merge Folder
    Figure 6. Creating the Merge Folder

5) Create the taglibs.xml file

  1. Select the folder MyXDocletWeb/merge in the Package Explorer
  2. Select File > New... > XML,
  3. Populate it with the name taglibs.xml, as shown in Figure 7, then press the Finish button.

    Creating the taglibs.xml file
    Figure 7. Creating the taglibs.xml file

Once taglibs.xml has been created and opened in the XML editor, completely replace the contents with the following:

<taglib>
   <taglib-uri>/mytaglib</taglib-uri>
   <taglib-location>/WEB-INF/mytaglib.tld</taglib-location>
</taglib>

6) Create the welcomefiles.xml file

  1. Select the folder MyXDocletWeb/merge in the Package Explorer
  2. Select File > New... > XML,
  3. Populate it with the name welcomefiles.xml then press the Finish button as you did for taglibs.xml.
Once welcomefiles.xml has been created and opened in the XML editor, completely replace the contents with the following:

<welcome-file-list>
   <welcome-file>index.jsp</welcome-file>
   <welcome-file>index.html</welcome-file>
</welcome-file-list>

7) Create the xdoclet-build.properties file

  1. Select the folder MyXDocletWeb in the Package Explorer
  2. Select File > New... > File,
  3. Populate it with the name xdoclet-build.properties, as shown in Figure 8, then press the Finish button.

    Creating the xdoclet-build.properties file
    Figure 8. Creating the xdoclet-build.properties file

    Once xdoclet-build.properties has been created and opened in the Text editor, completely replace the contents with the following:

basic.servlet.hi = MyEclipse Rocks!
basic.servlet.bye = Feel the power of XDoclet!

8) Configure the Project for XDoclet Usage

  1. Right-click on the folder MyXDocletWeb in the Package Explorer
  2. Select Properties > XDoclet Configurations
  3. Right-click and select Add Standard as shown in Figure 9.
  4. Select Standard Web from the resulting dialog box. The resulting configuration is shown in Figure 10.

    Adding a Standard XDoclet Configuration
    Figure 9. Adding a Standard XDoclet Configuration

    Adding Standard Web Configuration
    Figure 10. Adding Standard Web Configuration

  5. Select deploymentdescriptor under the webdoclet entry and select and set the following values as shown in the table below and in Figure 11.
    PropertyValue
    Servletspec2.3
    destDirWebRoot/WEB-INF
    displayNameXDoclet Web Tutorial
    mergeDirmerge

    Deployment Descriptor Settings
    Figure 11. Deployment Descriptor Settings

  6. Select jsptaglib under the webdoclet entry and select and set the following values as shown in the table below and in Figure 12.
    PropertyValue
    Jspversion1.2
    destDirWebRoot/WEB-INF
    destinationFilemytaglib.tld
    shortnamebasic

    JSP Taglib Settings
    Figure 12. JSP Taglib Settings

  7. Select the OK button at the bottom of the Properties dialog to save the XDoclet Configurations. This will generate the file xdoclet-build.xml into the project folder.

9) Setting XDoclet Ant Properties

  1. Right-click on the generated xdoclet-build.xml file in the Package Explorer and select Run Ant....
  2. On the dialg, titled MyXDocletWeb xdoclet-build.xml, select the Properties tab.
  3. Select the Add... button next to the Property files: list
  4. Browse to the project folder select xdoclet-build.properties.
  5. The resultant configuration will look like that in Figure 13.

    Adding Xdoclet Properties File
    Figure 13. Adding the Properties File

  6. Select the Refresh tab on the dialog and ensure that Refresh references after running tool is checked.

    Checking the Refresh Settings
    Figure 14. Checking the Refresh Settings

  7. Select the Apply button to save the changes.
  8. Select the Run button to process the XDoclet JavaDoc statements in the Java source with the Ant webdoclet task.
  9. After XDoclet runs, the files web.xml and mytaglib.tld will have been added to the WEB-INF directory as shown in Figure 15 below.

    Generated Files
    Figure 14. Generated Files

10) Adding a JSP Page

  1. Select the project MyXDocletWeb in the Package Explorer
  2. Right-click and select New... > JSP,
  3. Populate the wizard page with the file name TestJSP.jsp, as shown in Figure 15, then press the Finish button.

    Creating a JSP
    Figure 15. Creating a JSP

  4. After the JSP is generated, it will be opened in the JSP editor. Replace the generated source code completely with the following contents and save the file.

<%@ page language="java" %>
<%@ taglib uri="/mytaglib" prefix="mytag" %>

<html>
  <head>
    <title>I am a happy JSP page. Yeah!</title>
  </head>
  <body>
    <mytag:BasicTag includePage="true" includeBody="true" iterate="3">
      Current iteration is <%=currentIter%> <br/>
    </mytag:BasicTag>
  </body>
</html>

10) Adding an HTML Page

  1. Select the project MyXDocletWeb in the Package Explorer
  2. Right-click and select New... > HTML,
  3. Populate the wizard page with the file name index.html, as shown in Figure 16, then press the Finish button.

    Creating an HTML Page
    Figure 16. Creating an HTML Page

  4. After the HTML page is generated, it will be opened in the HTML editor. Replace the generated source code completely with the following contents and save the file.

<html>
  <head>
    <title>XDoclet Web Tutorial</title>
  </head>
  <body>
    <br />
    <blockquote>
      <h3>XDoclet Web Tutorial</h3>
      <ul>
        <li><a href="TestJSP.jsp">Test Basic JSP Custom Tag</a></li>
        <li><a href="BasicServlet">Test Basic Servlet</a></li>
      </ul>
    </blockquote>
  </body>
</html>

11) Verify Project

The project is now complete. To verify that the structure is complete, please compare your project to the one shown in Figure 17.

Final Project Structure
Figure 17. Final Project Structure

12) Deploy the Project and Test

  1. Right-click on the MyXDocletWeb project and select MyEclipse > Add and Remove Project Deployments as shown in Figure 18.

    Opening the Deployment Dialog
    Figure 18. Opening the Deployment Dialog

  2. Select the Add button as shown in Figure 19.

    Adding a Deployment
    Figure 19. Adding a Deployment

  3. Select whatever server you've got configured as an exploded archive as shown in Figure 20.

    Picking the Application Server
    Figure 20. Picking a Server

  4. Select the OK button as shown in Figure 21.

    Completing Deployment
    Figure 21. Completing Deployment

  5. Start the server as shown in Figure 22.

    Starting the Application Server
    Figure 22. Starting the Server

  6. Open a browser and test the application. The results are shown in Figure 23.

    Testing the Application 1 Testing the Application 2 Testing the Application 3
    Figure 23. Testing the Application

    Conclusion

    This has been an introduction to XDoclet use for web applications within MyEclipse. Although the example application was a simple one, it was a complete application that included an HTML page, a JSP page, an XDoclet-based Servlet, and and XDoclet-based Tag library. Now that you know how the basics work, get out there and start simplifying your web development with MyEclipse and XDoclet!
 
Genuitec Offers a broad range of consulting services including an Eclipse practice for plugin development, training and customization. Genuitec Eclipse services cover all aspects of the Eclipse framwork and include:
Genuitec Solutions and Services
  • Custom Plugin Development for Eclipse-Based Applications
  • Eclipse Development Courses and Workshops
  • Custom Widget Development and SWT/JFaces Extensions
  • Business Desktop Application or Suite Development
  • Productivity Tools Development and Branding
  • J2EE Consulting and Out-sourcing

 
MyEclipse mission is to deliveran affordable and a true end-to-end seamless development environment for Web, J2EE, XML, JSP, Struts, and application server integration.
MyEclipse Features
  • Web Development Tools
    • Smart editors - JSP, HTML, Struts, XML, CSS, and J2EE deployment descriptors
    • XML editing
    • Struts Support
    • XDoclet Support
  • Productivity Wizards
    • Creation of Web, EAR and EJB projects.
    • Java Project to Web Project enablements.
    • EJB Wizards
    • Automated deployment.
  • Application Server Integration
    • 20 application server connectors
    • Integrated controls
    • Full hot swap debugging
 
  
   
   

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] apache.common.DbUtil & Oracle 용 RowProcess  (0) 2005.10.17
jetty site  (0) 2005.10.12
[펌] 웹개발문서모음 - iHelpers  (0) 2005.08.24
[openlaszlo] 아래에 푸터 붙어있을때...  (0) 2005.07.05
jwebunit  (0) 2005.06.03
Posted by tornado
|

핑거스타일 까페에 갔다가 알게된 곡인데..

듣고 있으면 너무 편하다..

치라고 하면 절대로~ 못치겠지만...

흉내라도 꼭 내보고 싶은.. 맑은 소리다.

 

나이가 점점 먹어가면서 음악 스타일도... 점점 바뀌어간다..

10대~ 20 대에는 메탈이 이세상 최고의 음악인줄 알고 지냈지만..

요즘은 이런 식의 편안한 느낌의 곡이 좋다.

 

 

'이것저것 > 낙서장' 카테고리의 다른 글

14000 히트 이벤트!!!  (0) 2005.10.14
웬 모기가 이리 많어 ㅡㅡ;  (0) 2005.10.11
13000 히트 이벤트!!!  (2) 2005.09.28
[펌] NHK 다큐멘터리 - 블랙홀  (0) 2005.09.09
12000 히트 이벤트!!!  (3) 2005.09.06
Posted by tornado
|
[방문히트이벤트] 13000 히트를 잡아라! (이웃한정)
리오님이 당첨되었습니다.
Posted by tornado
|

A look inside the Glassbox Inspector with AspectJ and JMX

 

Level: Intermediate

Ron Bodkin (ron.bodkin@newaspects.com), Founder, New Aspects of Software

13 Sep 2005

Say goodbye to scattered and tangled monitoring code, as Ron Bodkin shows you how to combine AspectJ and JMX for a flexible, modular approach to performance monitoring. In this first of two parts, Ron uses source code and ideas from the Glassbox Inspector open source project to help you build a monitoring system that provides correlated information to identify specific problems, but with low enough overhead to be used in production environments.
About this series

The AOP@Work series is intended for developers who have some background in aspect-oriented programming and want to expand or deepen what they know. As with most developerWorks articles, the series is highly practical: you can expect to come away from every article with new knowledge that you can put immediately to use.

Each of the authors contributing to the series has been selected for his leadership or expertise in aspect-oriented programming. Many of the authors are contributors to the projects or tools covered in the series. Each article is subjected to a peer review to ensure the fairness and accuracy of the views expressed.

Please contact the authors individually with comments or questions about their articles. To comment on the series as a whole, you may contact series lead Nicholas Lesiecki. See Resources for more background on AOP.

Modern Java™ applications are typically complex, multithreaded, distributed systems that use many third-party components. On such systems, it is hard to detect (let alone isolate) the root causes of performance or reliability problems, especially in production. Traditional tools such as profilers can be useful for cases where a problem is easy to reproduce, but the overhead imposed by such tools makes them unrealistic to use in production or even load-test environments.

A common alternative strategy for monitoring and troubleshooting application performance and failures is to instrument performance-critical code with calls to record usage, timing, and errors. However, this approach requires scattering duplicate code in many places with much trial and error to determine what code needs to be measured. This approach is also difficult to maintain as the system changes and is hard to drill into. This makes application code challenging to add or modify later, precisely when performance requirements are better known. In short, system monitoring is a classic crosscutting concern and therefore suffers from any implementation that is not modular.

As you will learn in this two-part article, aspect-oriented programming (AOP) is a natural fit for solving the problems of system monitoring. AOP lets you define pointcuts that match the many join points where you want to monitor performance. You can then write advice that updates performance statistics, which can be invoked automatically whenever you enter or exit one of the join points.

In this half of the article, I'll show you how to use AspectJ and JMX to create a flexible, aspect-oriented monitoring infrastructure. The monitoring infrastructure I'll use is the core of the open source Glassbox Inspector monitoring framework (see Resources). It provides correlated information that helps you identify specific problems but with low enough overhead to be used in production environments. It lets you capture statistics such as total counts, total time, and worst-case performance for requests, and it will also let you drill down into that information for database calls within a request. And it does all of this within a modest-sized code base!

In this article and the next one, I'll build up from a simple Glassbox Inspector implementation and add functionality as I go along. Figure 1 should give you an idea of the system that will be the end result of this incremental development process. Note that the system is designed to monitor multiple Web applications simultaneously and provide correlated statistical results.


Figure 1. Glassbox Inspector with a JConsole JMX client
Glassbox Inspector monitoring multiple applications

Figure 2 is an overview of the architecture of the monitoring system. The aspects interact with one or more applications inside a container to capture performance data, which they surface using the JMX Remote standard. From an architectural standpoint, Glassbox Inspector is similar to many performance monitoring systems, although it is distinguished by having well-defined modules that implement the key monitoring functions.


Figure 2. The Glassbox Inspector architecture
Glassbox Inspector architecture

Java Management Extensions (JMX) is a standard API for managing Java applications by viewing attributes of managed objects. The JMX Remote standard extends JMX to allow external client processes to manage an application. JMX management is a standard feature in Java Enterprise containers. Several mature third-party JMX libraries and tools exist, and JMX support has been integrated into the core Java runtime with Java 5. Sun Microsystems's Java 5 VM includes the JConsole JMX client.

You should download the current versions of AspectJ, JMX, and JMX Remote, as well as the source packet for this article (see Resources for the technologies and Download for the code) before continuing. If you are using a Java 5 VM, then it has JMX integrated into it. Note that the source packet includes the complete, final code for the 1.0 alpha release of the open source Glassbox Inspector performance monitoring infrastructure.

The basic system

I'll start with a basic aspect-oriented performance monitoring system. This system captures the time and counts for different servlets processing incoming Web requests. Listing 1 shows a simple aspect that would capture this performance information:


Listing 1. An aspect for capturing time and counts of servlets
/** * Monitors performance timing and execution counts for  * <code>HttpServlet</code> operations */public aspect HttpServletMonitor {   /** Execution of any Servlet request methods. */  public pointcut monitoredOperation(Object operation) :     execution(void HttpServlet.do*(..)) && this(operation);   /** Advice that records statistics for each monitored operation. */  void around(Object operation) : monitoredOperation(operation) {      long start = getTime();       proceed(operation);       PerfStats stats = lookupStats(operation);      stats.recordExecution(getTime(), start);  }   /**   * Find the appropriate statistics collector object for this   * operation.   *    * @param operation   *            the instance of the operation being monitored   */  protected PerfStats lookupStats(Object operation) {      Class keyClass = operation.getClass();      synchronized(operations) {          stats = (PerfStats)operations.get(keyClass);          if (stats == null) {                              stats = perfStatsFactory.                createTopLevelOperationStats(HttpServlet.class,                        keyClass);              operations.put(keyClass, stats);          }      }      return stats;          }   /**   * Helper method to collect time in milliseconds. Could plug in   * nanotimer.   */  public long getTime() {      return System.currentTimeMillis();  }   public void setPerfStatsFactory(PerfStatsFactory     perfStatsFactory) {      this.perfStatsFactory = perfStatsFactory;  }   public PerfStatsFactory getPerfStatsFactory() {      return perfStatsFactory;  }   /** Track top-level operations. */   private Map/*<Class,PerfStats>*/ operations  =     new WeakIdentityHashMap();  private PerfStatsFactory perfStatsFactory;} /** * Holds summary performance statistics for a  * given topic of interest * (e.g., a subclass of Servlet). */ public interface PerfStats {  /**   * Record that a single execution occurred.   *    * @param start time in milliseconds   * @param end time in milliseconds   */  void recordExecution(long start, long end);   /**   * Reset these statistics back to zero. Useful to track statistics   * during an interval.   */  void reset();   /**   * @return total accumulated time in milliseconds from all   *         executions (since last reset).   */  int getAccumulatedTime();   /**   * @return the largest time for any single execution, in   *         milliseconds (since last reset).   */  int getMaxTime();   /**    * @return the number of executions recorded (since last reset).    */  int getCount();} /** * Implementation of the *  * @link PerfStats interface. */public class PerfStatsImpl implements PerfStats {  private int accumulatedTime=0L;  private int maxTime=0L;  private int count=0;   public void recordExecution(long start, long end) {      int time = (int)(getTime()-start);      accumulatedTime += time;      maxTime = Math.max(time, maxTime);      count++;  }   public void reset() {      accumulatedTime=0L;      maxTime=0L;      count=0;  }   int getAccumulatedTime() { return accumulatedTime; }  int getMaxTime() { return maxTime; }  int getCount() { return count; }} public interface PerfStatsFactory {    PerfStats       createTopLevelOperationStats(Object type, Object key);} 

As you can see, this first version is fairly basic. HttpServletMonitor defines a pointcut called monitoredOperation that matches the execution of any method on the HttpServlet interface whose name starts with do. These are typically doGet() and doPost(), but it also captures the less-often-used HTTP request options by matching doHead(), doDelete(), doOptions(), doPut(), and doTrace().

Managing overhead

I'll focus on techniques to manage the monitoring framework's overhead in the second half of the article, but for now, it's worth noting the basic strategy: I'll do some in-memory operations that take up to a few microseconds when something slow happens (like accessing a servlet or database). In practice, this adds negligible overhead to the end-to-end response time of most applications.

Whenever one of these operations executes, the system executes an around advice to monitor performance. The advice starts a stop watch, and then it lets the original request proceed. After this, it stops the stop watch and looks up a performance-statistics object that corresponds to the given operation. It then records that the operation was serviced in the elapsed time by invoking recordExecution() from the interface PerfStats. This simply updates the total time, the maximum time (if appropriate), and a count of executions of the given operation. Naturally, you could extend this approach to calculate additional statistics and to store individual data points where issues might arise.

I've used a hash map in the aspect to store the accumulated statistics for each type of operation handler, which is used during lookup. In this version, the operation handlers are all subclasses of HttpServlet, so the class of the servlet is used as the key. I've also used the term operation for Web requests, thus distinguishing them from the many other kinds of requests an application might make (e.g., database requests). In the second part of this article, I'll extend this approach to address the more common case of tracking operations based on the class or method used in a controller, such as an Apache Struts action class or a Spring multiaction controller method.



Back to top


Exposing performance data

Thread safety

The statistics-capturing code for the Glassbox Inspector monitoring system isn't thread safe. I prefer to maintain (potentially) slightly inaccurate statistics in the wake of rare simultaneous access to a PerfStats instance by multiple threads, rather than adding extra synchronization to program execution. If you prefer improved accuracy, you can simply make the mutators synchronized (for example, with an aspect). Synchronization would be important if you were tracking accumulated times more than 32 bits long, since the Java platform doesn't guarantee atomic updates to 64-bit data. However, with millisecond precision, this would give you 46 days of accumulated time. I recommend aggregating and resetting statistics far more frequently for any real use, so I've stuck with the int values.

Once you've captured performance data, you have a wide variety of options for how to make it available. The easiest way is to write the information to a log file periodically. You could also load the information into a database for analysis. Rather than add the latency, complexity, and overhead of summarizing, logging, and processing information, it is often better to provide direct access to live system performance data. I'll show you how to do this in this next section.

I want a standard protocol that existing management tools can display and track, so I'll use the JMX API to share performance statistics. Using JMX means that each of the performance-statistics instances will be exposed as a management bean, thus yielding detailed performance data. Standard JMX clients like Sun Microsystems's JConsole will also be able to show the information. See Resources to learn more about JMX.


Figure 3 is a screenshot of the JConsole showing data from the Glassbox Inspector monitoring the performance of the Duke's Bookstore sample application (see Resources). Listing 2 shows the code that implements this feature.


Figure 3. Using Glassbox Inspector to view operation statistics
Using Glassbox Inspector to view operation statistics

Traditionally, supporting JMX involves implementing patterns with boilerplate code. In this case, I'll combine JMX with AspectJ, which enables me to write the management logic separately.


Listing 2. Implementing the JMX management feature
/** Reusable aspect that automatically registers *  beans for management */public aspect JmxManagement {    /** Defines classes to be managed and      *  defines basic management operation     */    public interface ManagedBean {        /** Define a JMX operation name for this bean.          *  Not to be confused with a Web request operation.         */        String getOperationName();        /** Returns the underlying JMX MBean that          *  provides management         *  information for this bean (POJO).         */        Object getMBean();    }     /** After constructing an instance of      *  <code>ManagedBean</code>, register it     */    after() returning (ManagedBean bean):       call(ManagedBean+.new(..)) {        String keyName = bean.getOperationName();        ObjectName objectName =           new             ObjectName("glassbox.inspector:" + keyName);        Object mBean = bean.getMBean();        if (mBean != null) {            server.registerMBean(mBean, objectName);        }    }    /**      * Utility method to encode a JMX key name,      *  escaping illegal characters.     * @param jmxName unescaped string buffer of form      * JMX keyname=key      * @param attrPos position of key in String     */     public static StringBuffer       jmxEncode(StringBuffer jmxName, int attrPos) {        for (int i=attrPos; i<jmxName.length(); i++) {            if (jmxName.charAt(i)==',' ) {                jmxName.setCharAt(i, ';');            } else if (jmxName.charAt(i)=='?'                 || jmxName.charAt(i)=='*' ||                 jmxName.charAt(i)=='\\' ) {                jmxName.insert(i, '\\');                i++;            } else if (jmxName.charAt(i)=='\n') {                jmxName.insert(i, '\\');                i++;                jmxName.setCharAt(i, 'n');            }        }        return jmxName;    }    /** Defines the MBeanServer with which beans     *   are auto-registered.     */    private MBeanServer server;    public void setMBeanServer(MBeanServer server) {        this.server = server;    }    public MBeanServer getMBeanServer() {        return server;    }}

JMX tools

Several good JMX implementation libraries support remote JMX. Sun Microsystems provides reference implementations of JMX and JMX Remote under a free license. Some open source implementations also exist. MX4J is a popular one that includes helper libraries and tools like a JMX client. Java 5 integrates JMX and JMX remote support into the virtual machine. Java 5 also introduced management beans for VM performance in the javax.management package. Sun's Java 5 virtual machines include the standard JMX client JConsole.

You can see that this first aspect is reusable. With it, I can automatically register an object instance for any class that implements an interface, ManagedBean, using an after advice. This is similar to the AspectJ Marker Interface idiom (see Resources) in that it defines the classes whose instances should be exposed through JMX. However, unlike a true marker interface, this one also defines two methods.

The aspect provides a setter to define which MBean server should be used for managing objects. This is an example of using the Inversion of Control (IOC) pattern for configuration and is a natural fit with aspects. In the full listing of the final code, you'll see I've used a simple helper aspect to configure the system. In a larger system, I would use an IOC container like the Spring framework to configure classes and aspects. See Resources for more about IOC and the Spring framework and for a good introduction to using Spring to configure aspects.


Listing 3. Exposing beans for JMX management
/** Applies JMX management to performance statistics beans. */public aspect StatsJmxManagement {    /** Management interface for performance statistics.      *  A subset of @link PerfStats     */    public interface PerfStatsMBean extends ManagedBean {        int getAccumulatedTime();        int getMaxTime();        int getCount();        void reset();    }        /**      * Make the @link PerfStats interface      * implement @link PerfStatsMBean,      * so all instances can be managed      */    declare parents: PerfStats implements PerfStatsMBean;    /** Creates a JMX MBean to represent this PerfStats instance. */     public DynamicMBean PerfStats.getMBean() {        try {            RequiredModelMBean mBean = new RequiredModelMBean();            mBean.setModelMBeanInfo              (assembler.getMBeanInfo(this, getOperationName()));            mBean.setManagedResource(this,               "ObjectReference");            return mBean;        } catch (Exception e) {            /* This is safe because @link ErrorHandling            *  will resolve it. This is described later!            */            throw new               AspectConfigurationException("can't                 register bean ", e);        }    }    /** Determine JMX operation name for this     *  performance statistics bean.      */        public String PerfStats.getOperationName() {                StringBuffer keyStr =           new StringBuffer("operation=\"");        int pos = keyStr.length();        if (key instanceof Class) {            keyStr.append(((Class)key).getName());        } else {            keyStr.append(key.toString());        }                JmxManagement.jmxEncode(keyStr, pos);                keyStr.append("\"");        return keyStr.toString();    }    private static Class[] managedInterfaces =       { PerfStatsMBean.class };    /**      * Spring JMX utility MBean Info Assembler.      * Allows @link PerfStatsMBean to serve      * as the management interface of all performance      * statistics implementors.      */    static InterfaceBasedMBeanInfoAssembler assembler;    static {        assembler = new InterfaceBasedMBeanInfoAssembler();        assembler.setManagedInterfaces(managedInterfaces);    }}

Listing 3 contains the StatsJmxManagement aspect, which defines concretely which objects should expose management beans. It outlines an interface, PerfStatsMBean, that defines the management interface for any performance-statistics implementation. This consists of the statistics values for counts, total time, maximum time, and the reset operation, which is a subset of the PerfStats interface.

PerfStatsMBean itself extends ManagedBean, so that anything that implements it will automatically be registered for management by the JmxManagement aspect. I've used the AspectJ declare parents form to make the PerfStats interface extend a special management interface, PerfStatsMBean. As a result, the JMX Dynamic MBean approach manages these objects, which I prefer to using JMX Standard MBeans.

Using Standard MBeans would require me to define a management interface with a name based on each implementation class of performance statistics, such as PerfStatsImplMBean. Later, when I add subclasses of PerfStats to the Glassbox Inspector, the situation would get worse because I would be required to create corresponding interfaces such as OperationPerfStatsImpl. The standard MBeans convention would make the interfaces depend on the implementations and represents a needless duplication of the inheritance hierarchy for this system.

Deploying these aspects

The aspects used in this article need to be applied only to each application they are monitoring, not to third-party libraries or container code. As such, you could integrate them into a production system by compiling them into an application, by weaving into an already compiled application, or by using load-time weaving, which I prefer for this use case. You'll learn more about load-time weaving in the second half of this article.

The rest of the aspect is responsible for creating the right MBean and object name with JMX. I reuse a JMX utility from the Spring framework, the InterfaceBasedMBeanInfoAssembler, which makes it easier to create a JMX DynamicMBean that manages my PerfStats instance using the PerfStatsMBean interface. At this stage, I've exposed only PerfStats implementations. This aspect also defines helper methods using inter-type declarations on managed bean classes. If a subclass of one of these classes needed to override the default behavior, it could do so by overriding the method.

You may be wondering why I've used an aspect for management rather than simply adding support directly into the PerfStatsImpl implementation class. While adding management to this one class wouldn't scatter my code, it would entangle the performance-monitoring system's implementation with JMX. So, if I wanted to use the system in an application without JMX, I would be forced to include the libraries and somehow disable the service. Moreover, as I expand the system's management functionality, I expect to expose more classes for management with JMX. Using aspects keeps the system's management policy modular.



Back to top


Database request monitoring

Distributed calls are a common source of slow application performance and errors. Most Web-based applications do a significant amount of database work, making monitoring queries and other database requests an especially important area for performance monitoring. Common issues include ill-written queries, missing indexes, and excessive numbers of database requests per operation. In this section, I'll expand the monitoring system to track database activity, correlated with operations.

Distributed calls

In this section, I present an approach to handling distributed calls to a database. While databases are typically hosted on a different machine, my technique also works for a local database. My approach also extends naturally to other distributed resources, including remote object invocations. In the second part of the article, I'll show you how to apply this technique to Web services invocations using SOAP.

To start with, I'll monitor database connection times and the execution of database statements. To support this effectively, I need to generalize my performance monitoring information and allow for tracking performance nested within an operation. I'll want to extract the common elements of performance into an abstract base class. Each base class is responsible for tracking performance before and after certain operations and will need to update system-wide performance statistics for that information. This lets me track nested servlet requests and will be important to support tracking controllers in Web application frameworks (discussed in Part 2).

Because I want to update database performance by request, I'll use a composite pattern to track statistics held by other statistics. This way, statistics for operations (such as servlets) hold performance statistics for each database. The database statistics hold information about connection times and aggregate additional statistics for each individual statement. Figure 4 shows how the overall design fits together. Listing 4 has the new base monitoring aspect that supports monitoring different requests.


Figure 4. Generalized monitoring design
Revised monitoring design

Listing 4. The base monitoring aspect
/** Base aspect for monitoring functionality.  *  Uses the worker object pattern. */public abstract aspect AbstractRequestMonitor {    /** Matches execution of the worker object     *  for a monitored request.     */    public pointcut       requestExecution(RequestContext requestContext) :        execution(* RequestContext.execute(..))           && this(requestContext);        /** In the control flow of a monitored request,     *  i.e., of the execution of a worker object.      */    public pointcut inRequest(RequestContext requestContext) :        cflow(requestExecution(requestContext));    /** establish parent relationships     *  for request context objects.     */     // use of call is cleaner since constructors are called    // once but executed many times    after(RequestContext parentContext)       returning (RequestContext childContext) :       call(RequestContext+.new(..)) &&         inRequest(parentContext) {        childContext.setParent(parentContext);    }    public long getTime() {        return System.currentTimeMillis();    }    /** Worker object that holds context information     *  for a monitored request.     */    public abstract class RequestContext {        /** Containing request context, if any.          *  Maintained by @link AbstractRequestMonitor         */        protected RequestContext parent = null;                /** Associated performance statistics.          *  Used to cache results of @link #lookupStats()         */        protected PerfStats stats;        /** Start time for monitored request. */         protected long startTime;        /**          * Record execution and elapsed time          * for each monitored request.         * Relies on @link #doExecute() to proceed         * with original request.          */        public final Object execute() {            startTime = getTime();                        Object result = doExecute();                        PerfStats stats = getStats();            if (stats != null) {                stats.recordExecution(startTime, getTime());            }                        return result;        }                /** template method: proceed with original request */        public abstract Object doExecute();        /** template method: determines appropriate performance         *  statistics for this request          */        protected abstract PerfStats lookupStats();                /** returns performance statistics for this method */         public PerfStats getStats() {            if (stats == null) {                stats = lookupStats(); // get from cache if available            }            return stats;        }        public RequestContext getParent() {            return parent;                    }                public void setParent(RequestContext parent) {            this.parent = parent;                    }    }}

As you might expect, I had a number of choices for how to store the shared performance statistics and per-request state for the base monitoring aspect. For example, I could have used a singleton with lower-level mechanisms like a ThreadLocal holding a stack of statistics and context. Instead, I chose to use the Worker Object pattern (see Resources), which allows for a more modular, concise expression. While this imposes a bit of extra overhead, the additional time required to allocate a single object and execute the advice is generally negligible compared to the time servicing Web and database requests. In other words, I can do some processing work in the monitoring code without adding overhead because it runs only relatively infrequently and generally is dwarfed by the time spent sending information over networks and waiting for disk I/O. This would be a bad design for a profiler, where you would want to track data about many operations (and methods) per request. However, I'm summarizing statistics about requests, so the choice is a reasonable one.

In the above base aspect, I've stored transient state about the currently monitored request in an anonymous inner class. This worker object is used to wrap any execution of monitored requests. The worker object, a RequestContext, is defined in the base class and provides a final execute method that defines the flow for monitoring any request. The execute method delegates to an abstract template method, doExecute(), which is responsible for proceeding with the original join point. The doExecute() method also acts as a natural point to set up statistics before proceeding with the monitored join point based on context information, such as the data source being connected to, and to associate returned values such as database connections after returning from the join point.

Each monitor aspect is also responsible for providing an implementation of an abstract method, lookupStats(), to determine what statistics object is updated for a given request. lookupStats() needs to access information based on the monitored join point. In general, the context captured must vary in each monitoring aspect. For example, in HttpServletMonitor, the context needed is the class of the currently executing operation object. For a JDBC connection, the context needed is the data source being acquired. Because the requirements will differ according to context, the advice to set up the worker object is best contained in each sub-aspect rather than in the abstract base aspect. This arrangement is cleaner, it allows type checking, and it performs better than writing one piece of advice in the base class that passes the JoinPoint to all the children.



Back to top


Servlet request tracking

The AbstractRequestMonitor does contain one concrete after advice to track the parent contexts of request contexts. This allows me to associate the operation statistics for nested requests with the statistics of their parents (for example, what servlet request caused this database access). For my monitoring example system, I explicitly do want nested worker objects and do not limit myself to handling just top-level requests. For example, all the Duke's Bookstore Servlets call a BannerServlet as part of rendering a page. It is useful that I can break out the times for these calls separately, as Listing 5 shows. I don't show the supporting code for looking up nested statistics in an operation statistics here (you can see it in the article source code). In part two, I'll revisit this topic to show how I update the JMX support to display nested statistics like this.


Listing 5. Updated servlet monitoring
Listing 5 should now readpublic aspect HttpServletMonitor extends AbstractRequestMonitor {    /** Monitor Servlet requests using the worker object pattern */  Object around(final Object operation) :     monitoredOperation(operation) {      RequestContext requestContext = new RequestContext() {          public Object doExecute() {              return proceed(operation);          }                    public PerfStats lookupStats() {                              if (getParent() != null) {                  // nested operation                  OperationStats parentStats =(OperationStats)getParent().getStats();                  returnparentStats.getOperationStats(operation.getClass());              }              return lookupStats(operation.getClass());          }        };        return requestContext.execute();    } ... 

Listing 5 shows the revised monitoring advice for servlet request tracking. All the rest of the code remains the same as in Listing 1: it is either pulled up into the base AbstractRequestMonitor aspect or remains the same.



Back to top


JDBC monitoring

Having set up my performance monitoring framework, I am now ready to track database connection times and the time for database statements. Moreover, I want to be able to correlate database statements with the actual database I have connected to (in the lookupStats() method). To do this, I create two aspects to track information about JDBC statements and connections: JdbcConnectionMonitor and JdbcStatementMonitor.

One of the key responsibilities for these aspects is to follow chains of object references. I would like to track requests by the URL I used to connect to the database, or at least the database name. This requires me to track the data source used to acquire a connection. I would further like to track performance of prepared statements by the SQL strings that was prepared prior to executing to them. Finally, I need to track the JDBC connection associated with statements that are executing. You'll note that JDBC statements do provide an accessor for their connection; however, application servers and Web application frameworks frequently use the decorator pattern to wrap JDBC connections. I want to make sure I can correlate statements with the connection I have a handle to and not a wrapped connection.

The JdbcConnectionMonitor is responsible for measuring performance statistics for connections to the database, and it also associates connections with their metadata (for example, a JDBC URL or a database name) from data sources or connection URLs. The JdbcStatementMonitor is responsible for measuring performance statistics for executing statements, tracks the connections used to acquire statements, and tracks the SQL strings associated with prepared (and callable) statements. Listing 6 shows the JdbcConnectionMonitor aspect.


Listing 6. The JdbcConnectionMonitor aspect
/**  * Monitor performance for JDBC connections,  * and track database connection information associated with them.  */ public aspect JdbcConnectionMonitor extends AbstractRequestMonitor {        /** A call to establish a connection using a      *  <code>DataSource</code>      */    public pointcut dataSourceConnectionCall(DataSource dataSource) :         call(Connection+ DataSource.getConnection(..))           && target(dataSource);    /** A call to establish a connection using a URL string */    public pointcut directConnectionCall(String url) :        (call(Connection+ Driver.connect(..))  || call(Connection+           DriverManager.getConnection(..))) &&         args(url, ..);    /** A database connection call nested beneath another one     *  (common with proxies).     */        public pointcut nestedConnectionCall() :         cflowbelow(dataSourceConnectionCall(*) ||           directConnectionCall(*));        /** Monitor data source connections using     *  the worker object pattern      */    Connection around(final DataSource dataSource) :       dataSourceConnectionCall(dataSource)         && !nestedConnectionCall() {        RequestContext requestContext =           new ConnectionRequestContext() {            public Object doExecute() {                                accessingConnection(dataSource);                 // set up stats early in case needed                Connection connection = proceed(dataSource);                return addConnection(connection);            }                    };        return (Connection)requestContext.execute();    }    /** Monitor url connections using the worker object pattern */    Connection around(final String url) : directConnectionCall(url)       && !nestedConnectionCall() {        RequestContext requestContext =           new ConnectionRequestContext() {            public Object doExecute() {                accessingConnection(url);                Connection connection = proceed(url);                                return addConnection(connection);            }        };        return (Connection)requestContext.execute();    }    /** Get stored name associated with this data source. */     public String getDatabaseName(Connection connection) {        synchronized (connections) {            return (String)connections.get(connection);        }    }    /** Use common accessors to return meaningful name     *  for the resource accessed by this data source.     */    public String getNameForDataSource(DataSource ds) {        // methods used to get names are listed in descending        // preference order         String possibleNames[] =           { "getDatabaseName",               "getDatabasename",               "getUrl", "getURL",               "getDataSourceName",               "getDescription" };        String name = null;        for (int i=0; name == null &&           i<possibleNames.length; i++) {            try {                            Method method =                   ds.getClass().getMethod(possibleNames[i], null);                name = (String)method.invoke(ds, null);            } catch (Exception e) {                // keep trying            }        }        return (name != null) ? name : "unknown";    }        /** Holds JDBC connection-specific context information:     *  a database name and statistics     */    protected abstract class ConnectionRequestContext      extends RequestContext {        private ResourceStats dbStats;                /** set up context statistics for accessing         *  this data source         */         protected void           accessingConnection(final DataSource dataSource) {            addConnection(getNameForDataSource(dataSource),               connection);        }                /** set up context statistics for accessing this database */         protected void accessingConnection(String databaseName) {            this.databaseName = databaseName;            // might be null if there is database access            // caused from a request I'm not tracking...            if (getParent() != null) {                OperationStats opStats =                   (OperationStats)getParent().getStats();                dbStats = opStats.getDatabaseStats(databaseName);                            }        }        /** record the database name for this database connection */         protected Connection           addConnection(final Connection connection) {            synchronized(connections) {                connections.put(connection, databaseName);            }            return connection;        }        protected PerfStats lookupStats() {            return dbStats;        }    };        /** Associates connections with their database names */        private Map/*<Connection,String>*/ connections =       new WeakIdentityHashMap();}

Listing 6 shows my aspect for tracking database connections using AspectJ and the JDBC API. It has a map to associate database names with each JDBC connection.

Inside jdbcConnectionMonitor

Inside the JdbcConnectionMonitor shown in Listing 6, I've defined pointcuts to capture two different ways of connecting to a database: through a data source or directly through a JDBC URL. The connection monitor contains monitoring advice for each case, both of which set up a worker object. The doExecute() methods start by proceeding with the original connection and then pass the returned connection to one of two helper methods named addConnection. In both cases, the pointcut being advised excludes connection calls that result from another connection (for example, if connecting to a data source results in establishing a JDBC connection).

The addConnection() for data sources delegates to a helper method, getNameForDataSource(), to try to determine the name of a database from the data source. The DataSource interface doesn't provide any such mechanism, but almost every implementation provides a getDatabaseName() method. getNameForDataSource() uses reflection to try this and a few other common (and less common) methods that provide a useful identifier for a data source. This addConnection() method then delegates to the addConnection() method that takes a string for a name.

The delegated addConnection() method retrieves the operational statistics from its parent-request context and looks up database statistics based on the database name (or other description string) associated with the given connection. It then stores that information in the dbStats field on the request-context object to update performance information about acquiring the connection. This lets me track the time required to connect to the database (often this is really the time required to get a connection from a pool). The addConnection() method also updates the connections map of connections to database names. This map is used subsequently when JDBC statements are executed to update statistics for the appropriate request. The JdbcConnectionMonitor also provides a helper method, getDatabaseName(), which looks up the string name for a connection from the connections map.

Weak identity maps and aspects

The JDBC monitoring aspects use weak identity hash maps. These maps hold weak references to allow tracked objects like connections to be garbage collected when they are referenced only by the aspect. This is important because the singleton aspects are typically not garbage collected. If the references weren't weak, the application would have a memory leak. The aspects use identity maps to avoid calling the hashCode or equals methods on connections or statements. This is important because I want to track the connections and statements regardless of their state: I don't want to encounter exceptions from the hashCode method, nor should I rely on the hash code of the object remaining the same when its internal state changes (for example, when closed). I experienced this issue when working with dynamic proxy-based JDBC objects (like those from iBatis), which threw exceptions when any method on them was called after a connection had closed. This led to errors in trying to record statistics after finishing an operation.

One lesson to learn from this is to minimize the assumptions you make about third-party code. Using identity maps is a good way to avoid making assumptions about the implementation logic in advised code. In this case, I'm using an open source implementation of a WeakIdentityHashMap from the Emory DCL Java Utilities (see Resources). Tracking metadata information about connections or statements lets me group statistics across each request for equivalent connections or statements. This means I can track based on just object instances; I don't need to use object equality to track these JDBC objects. Another lesson to keep in mind is that JDBC objects are frequently wrapped by decorators (increasingly with dynamic proxies) by various frameworks. It's a bad idea to assume that you're working with a simple, raw implementation of such an interface!

Inside jdbcStatementMonitor

Listing 7 shows the JdbcStatementMonitor aspect. This aspect has two primary responsibilities: tracking information about creating and preparing statements and then monitoring performance statistics for executing JDBC statements.


Listing 7. The JdbcStatementMonitor aspect
/** * Monitor performance for executing JDBC statements,  * and track the connections used to create them,  * and the SQL used to prepare them (if appropriate). */public aspect JdbcStatementMonitor extends AbstractRequestMonitor {        /** Matches any execution of a JDBC statement */    public pointcut statementExec(Statement statement) :         call(* java.sql..*.execute*(..)) &&           target(statement);        /**     * Store the sanitized SQL for dynamic statements.      */    before(Statement statement, String sql,       RequestContext parentContext):       statementExec(statement) && args(sql, ..)         && inRequest(parentContext) {        sql = stripAfterWhere(sql);        setUpStatement(statement, sql, parentContext);    }        /** Monitor performance for executing a JDBC statement. */    Object around(final Statement statement) :      statementExec(statement) {        RequestContext requestContext =           new StatementRequestContext() {            public Object doExecute() {                return proceed(statement);            }        };        return requestContext.execute();    }           /**     * Call to create a Statement.     * @param connection the connection called to     * create the statement, which is bound to      * track the statement's origin      */    public pointcut callCreateStatement(Connection connection):        call(Statement+ Connection.*(..))           && target(connection);    /**     * Track origin of statements, to properly      * associate statistics even in     * the presence of wrapped connections      */    after(Connection connection) returning (Statement statement):      callCreateStatement(connection) {        synchronized (JdbcStatementMonitor.this) {            statementCreators.put(statement, connection);        }    }    /**      * A call to prepare a statement.     * @param sql The SQL string prepared by the statement.      */    public pointcut callCreatePreparedStatement(String sql):        call(PreparedStatement+ Connection.*(String, ..))           && args(sql, ..);    /** Track SQL used to prepare a prepared statement */    after(String sql) returning (PreparedStatement statement):       callCreatePreparedStatement(sql) {        setUpStatement(statement, sql);    }                protected abstract class StatementRequestContext       extends RequestContext {        /**          * Find statistics for this statement, looking for its          * SQL string in the parent request's statistics context          */        protected PerfStats lookupStats() {            if (getParent() != null) {                Connection connection = null;                String sql = null;                synchronized (JdbcStatementMonitor.this) {                    connection =                       (Connection) statementCreators.get(statement);                    sql = (String) statementSql.get(statement);                }                if (connection != null) {                    String databaseName =                       JdbcConnectionMonitor.aspectOf().                        getDatabaseName(connection);                    if (databaseName != null && sql != null) {                        OperationStats opStats =                           (OperationStats) getParent().getStats();                        if (opStats != null) {                            ResourceStats dbStats =                               opStats.getDatabaseStats(databaseName);                            return dbStats.getRequestStats(sql);                        }                    }                }            }            return null;        }    }    /**      * To group sensibly and to avoid recording sensitive data,     * I don't record the where clause (only used for dynamic     * SQL since parameters aren't included     * in prepared statements)     * @return subset of passed SQL up to the where clause     */    public static String stripAfterWhere(String sql) {        for (int i=0; i<sql.length()-4; i++) {            if (sql.charAt(i)=='w' || sql.charAt(i)==              'W') {                if (sql.substring(i+1, i+5).equalsIgnoreCase(                  "here"))                  {                    sql = sql.substring(0, i);                }            }        }        return sql;    }        private synchronized void       setUpStatement(Statement statement, String sql) {        statementSql.put(statement, sql);    }    /** associate statements with the connections     *  called to create them     */    private Map/*<Statement,Connection>*/ statementCreators =       new WeakIdentityHashMap();    /** associate statements with the     *  underlying string they execute     */    private Map/*<Statement,String>*/ statementSql =       new WeakIdentityHashMap();}

The JdbcStatementMonitor maintains two weak identity maps: statementCreators and statementSql. The first tracks the connections used to create a statement. As I noted earlier, I don't want to rely on the getConnection method of the statement because it could refer to a wrapped connection for which I don't have metadata. Note the callCreateStatement pointcut, which I advise to monitoring JDBC statement execution. This matches any call to a method defined on a JDBC connection that returns a Statement or any subclass thereof. This matches the 12 different ways that a statement can be

Posted by tornado
|

http://www.aquafold.com/

 

한글 폰트 깨져서 폰트를 바꿔줘야함..

생각보다 빠르고.. 좋음..

'SQL' 카테고리의 다른 글

[펌] [Oracle]오라클 어드민 팁  (0) 2005.10.11
[mssql] 계층적 구조 표현하기.  (0) 2005.10.07
[펌] window 2000, 2003에서 오라클 삭제하기  (0) 2005.09.08
[mssql] 날짜 형식 convert  (0) 2005.08.26
MySQL 간단백업  (0) 2005.08.24
Posted by tornado
|

피싱 사이트 -.-

OS/LINUX 2005. 9. 22. 16:31

회사 네임서버가 레드햇 리눅스 9 이다.

네임서버에는 hosts.allow, hosts.deny 가 걸려 있고,

iptables 에서 몇몇 포트(53번, 22번 등)을 뺀 나머지는 다 걸러내구 있다.


흠.. 그런데.. 22번 포트가 문제였다.

이 구멍을 통해 우리 네임서버가 fising site 가 되 버렸다 ㅡㅡ;


/usr/include 디렉토리에 보니 못보던 파일들이 있다.

log.h 라는게 뭐지? 열어봤더니,

ssh 를 통해 들어온 계정들의 패스워드가 죄다 적혀있다.

허걱...


허미.. 잽싸게 다른 서버에 Linux 설치하고, 원래 서버 복구에 들어갔다.

그리고 새로 설치한 리눅스 서버에 iptables 로 C 클래스 중 울 회사 IP 대역만 열어놓구

시스템을 켰다..


음.. 다시 외부에서 들어온 흔적이 보인다..

/dev 디렉토리에 작업하네 ㅡㅡ;

범인은 회사내의 Informix 서버였다. MS-SQL 쓰니까.. 내버려 두고 있었더니, 이곳이 활동무대가 된듯 하다..

그래서 그 서버죽이고, ssh 도 죽이고 서버 다시 시작하니 잘 된다.


좀 써보다가 또 뚫으려고 하는 시도가 보이거나, 뚫렸을 때는 할수 없이

방화벽을 앞단에 세우고, 브릿지로 연결해서 쓰던지 해야겄다...


정말 골때리는거는... 아파치가 설치되어있으니까, 거기에 PHP 깔고... 서비스를 하더라는것이다.

대단한 넘들.....

Posted by tornado
|


 

전 세계적으로 비디오게임을 상징하는 아이콘으로 성장한 日닌텐도社의 대표 액션 게임 ‘슈퍼마리오’의 배경 음악을 아카펠라로 부른 음악 그룹의 동영상이 공개돼 화제다.

이 동영상은 한 공연장에서 이름을 알 수 없는 아카펠라 그룹이 ‘슈퍼마리오’의 오프닝부터 보스전 등 배경 음악을 무(無)반주 및 육성으로 부르는 장면을 담고 있다. 플레이 타임은 4분 39초이며 전체 파일 용량은 5MB다.

 

참고 :모 사이트 모 인물의 부연설명

일단 팩 불고. 성 들어가는 거 먼저 나오고. 마리오 1스테이지. 닥터마리오. 마리오 3. 마리오 별 먹고. 테트리스. 모탈컴뱃. 마리오 보스. 젤다 메인테마, 마리오 게임오버... 의 순. 이놈들 존내 매니아셈. 링크 회전베기 완벽재현에 모~탈~컴~배앳~까지

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

======================================================================================

 

용량제한때문에 링크로 걸었습니다.

 

지금 10대들도 슈퍼마리오를 기억할지는 모르겠지만, 20대라면 모르는 분들이 없을것 같네요.

 

이거 보면서 완전 감동했습니다. 세계?오 ㅠ.ㅠ 완전 감동입니다. 중간에 퍼포먼스까지

 

주의깊게 보시면 아주 재미있습니다. ㅎㅎ

 

 

peace out!

'이것저것 > 음악' 카테고리의 다른 글

[펌] 락이 세상을 구원한다.  (0) 2006.01.27
[펌] Stryper - In God We Trust  (0) 2006.01.24
[펌] Incubus - Drive  (0) 2005.08.31
[펌] Mind Revolution - Skyfire  (0) 2005.08.22
[펌] Metallica - Creeping death (Moscow Live)  (0) 2005.08.22
Posted by tornado
|
 

 

================================ 화성 ================================

 

 


 

화성의 지표면 (화성탐사선 스피릿호가 보내온 컬러 사진)

 

이때 스피릿호가 있던 곳은 화성에서 홍수가 났다고 추정되는 지역입니다.

 

잘 보시면 사진에 있는 돌과 바위들이 오른쪽으로 기울어져 있는 걸 발견할 수 있는데,

 

이게 화성에 정말 홍수가 있었었다는 사실을 입증하는 중요한 증거라고 합니다.

 

과학자들의 가설에 따르면, 화성은 내부에 많은 물을 가지고 있다고 합니다. 

 

그 물들은 화성의 화산이 활동할 때의 압력과 열로 인해 때때로 지표 밖으로 분출되는데,

 

이것이 화성의 대 홍수라고 합니다. 스피릿호와 화성탐사위성이 찍어온 지표사진과 위성사진에서

 

가설을 뒷밤침해줄 중요한 증거를 찾아 냈는데, 그건 바로 화산 부근에만

 

다량 분포하는 특이지형이었습니다. 바로 홍수가 일어났던 지형이죠.

 

 

 
 

화성의 저녁입니다.

 

미량 존재하는 대기 속에 먼지입자들이, 빛을 산란시켜 푸른 빛의 노을을 만들어 낸다고 합니다.


정말 장관이죠? 직접 가서 봤으면 정말 좋겠지만 제가 살아있을 동안에는 불가능할 것 같네요.
 
 
 
 
================================블랙홀의 존재===============================
 
 
 
 
미국과 일본에서 쏘아올린 X선 위성이 발견한
 
백조자리의 목부근에 있는 미지의 천체 X-1 입니다.
 
그 부근에선 강력한 X선이 포착되는데도 불구하고,
 
별들의 모습은 마치 우주에 구멍이 난 듯 까맣게 보이지 않는다고 합니다.
 
이유가 무엇인지 고민하던 중에, 한가지 단서가 발견되었습니다.
 
 
X-1 천체의 위치.
 
 
 
 
 
바로 X-1 부근에 있던 태양보다 30배나 무거운 거대한 항성의 존재죠.
 
이 항성은 그 검은 천체 X-1의 주위를 5.6일에 한바퀴 맹렬한 속도로 공전하고 있었습니다.
 
 
 
 
보이지 않는, 마찬가지로 엄청나게 무거운 어떤 무언가에 이끌려서 말이죠.
 
눈에는 보이지 않지만 엄청나게 무거운 무언가..
 
 
 
 
 
과학자들은 이것이 바로 블랙홀이라고 결론지었습니다.
 
 
 
우리 은하계에서 지금까지 발견된 11개의 블랙홀들.
 
블랙홀은 단순히 주변 물체를 잡아당기는 기이한 천체로만 여겨졌었습니다.
 
하지만, 우리 은하 중심에서 엄청난 양의 X선이 관측되고 나선 이야기가 달라졌죠.
 
 
오랜 관찰 결과, 은하 중심부의 별들이
 
시속 500km의 맹렬한 속도로 이 중심부를 공전하고 있다는 것이 발견되었습니다.
 
이것은 은하 중심에 태양 질량의 300만배나 되는 천체가 있음을 말합니다.
 
이것은 블랙홀 이외에 다른 것일 수가 없다는 주장이 나왔고,
 
지금까지 블랙홀과는 비교도 안되게 거대한 것이란 결론으로 이어졌습니다.
 
 
 
백조자리 X-1 블랙홀의 반지름은 불과 수십키로미터에 불과합니다.
 
 
 
하지만 은하수 중심의 블랙홀은
 
반지름이 900만 키로미터나 되는 상상을 뛰어넘는 거대한 천체입니다.
 
 
이를 근거로 은하는 블랙홀을 핵으로 가지고 있다는 가설이 나왔고,
 
허블 우주 망원경을 이용해 촬영한 결과,
 
 
 
 
우리 이웃 은하인 안드로메다에서도,
 
 
 
 
또 그 이웃인 M32 에서도,
 
 
무려 40개에 이르는 은하에서 모두 블랙홀이 발견?營윱求?
 
 
그리고 이제는 거의 모든 은하에서 블랙홀의 존재가 증명되어 있습니다.
 
 
 
어떤가요? 정말 우주는 참 신비하고 매력적이지 않나요?
 
아니라면 할 수 없구요 -.-;;
Posted by tornado
|