'Framework'에 해당되는 글 84건

  1. 2009.04.23 AL : Property
  2. 2009.04.23 AL : PropertyDefinition
  3. 2009.04.23 AL : NodeType
  4. 2009.04.23 AL : Lock
  5. 2009.04.23 AL : sequence
  6. 2009.04.17 자동화 테스트 - 자바스크립트
  7. 2009.04.14 여섯번째 계 - Encapsulation
  8. 2009.04.09 와~~~
  9. 2009.04.04 AL - Extreme
  10. 2009.03.26 예외 수집기
Framework/Another Lore2009. 4. 23. 11:17

A Property object represents the smallest granularity of content storage.  A property must have one and only one parent node. A property does not have children. When we say that node A "has" property B it means that B is a child of A. A property consists of a name and a value.

All data stored within a AL repository is ultimately stored as the values of properties.


Property Types

STRING properties store instances of java.lang.String.

BOOLEAN properties store instances of the Java primitive type boolean.

LONG properties store instances of the Java primitive type long.

DOUBLE properties store instances of the Java primitive type double.

BINARY properties store instances of File

DATE properties store instances of java.util.Calendar.

NAME properties store instances of AlphaNumUnderBar

PATH properties store instances of AL paths and serve as pointers to locations within the workspace.PATH properties do not enforce referential integrity.

REFERENCE properties serve as pointers to referenceable nodes by storing their identifiers. REFERENCE properties do not enforce referential integrity

TEXT properties store instances of long length java.lang.String.

VIRTUAL_TEXT properties store template of string type


Property Type Conversion

When the value of a property is read or written using a type different from that declared for the property, the repository attempts a type conversion according to the following rules. Note that even in cases where the JCR type conversion is
defined in terms of standard JDK type conversion method, failure of conversion must only ever cause a JCR ValueFormatException to be thrown and never any exception defined in the JDK API.


Value Length

The length of a value is defined as follows:
For a BINARY value, its length is equal to its length in bytes. This number is returned both by Binary.getSize and by
Property.getLength and Property.getLengths

For other types, the length is the same value that would be returned by calling java.lang.String.getByte("UTF-8"). This number is returned by Property.getLength


[Example Code]
  IDString _string = IDString.propId("string");
  IDString _binary = IDString.propId("binary");
  IDString _long = IDString.propId("long");
  IDString _double = IDString.propId("double");
  IDString _date = IDString.propId("date");
  IDString _boolean = IDString.propId("boolean");
  IDString _name = IDString.propId("name");
  IDString _path = IDString.propId("path");
  IDString _reference = IDString.propId("reference");
  IDString _text = IDString.propId("text");
  IDString _integer = IDString.propId("integer");
  IDString _ftext = IDString.propId("ftext");
  PropertyDefinition[] pds = new PropertyDefinition[]{
    createProperty(_string, Property.Type.STRING), 
    createProperty(_binary, Property.Type.BINARY), 
    createProperty(_long, Property.Type.LONG), 
    createProperty(_double, Property.Type.DOUBLE), 
    createProperty(_date, Property.Type.DATE), 
    createProperty(_boolean, Property.Type.BOOLEAN), 
    createProperty(_name, Property.Type.NAME), 
    createProperty(_path, Property.Type.PATH), 
    createProperty(_reference, Property.Type.REFERENCE), 
    createProperty(_text, Property.Type.TEXT), 
    createProperty(_integer, Property.Type.INTEGER), 
    createProperty(_ftext, Property.Type.VIRTUAL_TEXT)
  } ;
  
  NodeType allType = createNodeType(objectType, "allType", pds) ;

  Node node = createNode(root, allType, "allType") ;
  node.setProperty(_string, (Object)"123") ;
  node.setProperty(_binary, (Object)TestValue.binaryValue) ;
  node.setProperty(_long, (Object)12311212121212L) ;
  node.setProperty(_double, (Object)123.3) ;
  Calendar cal = Calendar.getInstance();
  cal.setTime(DateUtil.string2Date("20060504-141224")) ;
  node.setProperty(_date, (Object)cal) ;
  node.setProperty(_boolean, (Object)true) ;
  node.setProperty(_name, (Object)"namedd") ;
  node.setProperty(_path, (Object)"path...") ;
  node.setProperty(_reference, (Object)root.getIdentifier()) ;
  node.setProperty(_text, (Object)new StringBuffer("abcdd")) ;
  node.setProperty(_integer, (Object)123) ;
  node.setProperty(_ftext, (Object)"$node.getUUIDString()") ;
  session.save() ;
  
  
  GenericIterator<Property> properties = node.getProperties() ;
  while(properties.hasNext()){
   Property property = properties.next() ;
   assertEquals(allType.getMappedDefinition(property.getId()).requiredType(), property.getValue().getPropertyType()) ;
  }
  
  assertEquals("123", node.getProperty(_string).getString()) ;
  assertEquals("111.jpg", node.getProperty(_binary).getString()) ;
  assertEquals("12311212121212", node.getProperty(_long).getString()) ;
  assertEquals("123.3", node.getProperty(_double).getString()) ;
  assertEquals("20060504-141224", node.getProperty(_date).getString()) ;
  assertEquals("true", node.getProperty(_boolean).getString()) ;
  assertEquals("namedd", node.getProperty(_name).getString()) ;
  assertEquals("path...", node.getProperty(_path).getString()) ;
  assertEquals(root.getIdentifier(), node.getProperty(_reference).getString()) ;
  assertEquals("abcdd", node.getProperty(_text).getString()) ;
  assertEquals("123", node.getProperty(_integer).getString()) ;
  assertEquals("$node.getUUIDString()", node.getProperty(_ftext).getString()) ;
  

  assertEquals("123", node.getProperty(_string).getObject()) ;
  assertEquals("jpg", ((BinaryPropertyValue)node.getProperty(_binary).getObject()).getContentType() ) ;
  assertEquals(12311212121212L, node.getProperty(_long).getObject()) ;
  assertEquals(123.3D, node.getProperty(_double).getObject()) ;
  assertEquals("20060504-141224", DateUtil.date2String((Calendar)node.getProperty(_date).getObject()) ) ;
  assertEquals(Boolean.TRUE, node.getProperty(_boolean).getObject()) ;
  assertEquals("namedd", node.getProperty(_name).getObject()) ;
  assertEquals("path...", node.getProperty(_path).getObject()) ;
  assertEquals(root.getIdentifier(), node.getProperty(_reference).getObject()) ;
  assertEquals("abcdd", node.getProperty(_text).getObject()) ;
  assertEquals(123, node.getProperty(_integer).getObject()) ;
  assertEquals(node.getIdentifier(), node.getProperty(_ftext).getObject()) ;
  
  node.setProperty(_boolean, "TRUE") ;
  assertEquals(true, node.getProperty(_boolean).getValue().getBoolean()) ;

  node.setProperty(_boolean, "true") ;
  assertEquals(true, node.getProperty(_boolean).getValue().getBoolean()) ;

  node.setProperty(_boolean, "1213") ;
  assertEquals(false, node.getProperty(_boolean).getValue().getBoolean()) ;

  try {
   node.setProperty(_long, "123a") ;
   fail() ;
  } catch(NotMatchePropertyTypeException ignore){
  }


< interface Property extends Cloneable, Serializable, IConstant, Publishable >
 public IDString getId();
 public Property newClone() throws RepositoryException;
 public Value getValue();
 public Property.Type getType() ;
 public String getTypeName();
 public String getString();
 public Node getNode() ;
 public void setValue(Value value);
 public boolean equalValue(Property property);
 public Object getObject() throws RepositoryException ;
 public Object getObject(Map param) throws RepositoryException;
 public void setMode(Mode mode) ;
 public Mode getMode() ;





3단계를 거치는 검사 정보를 저장하는 테이블을 설계한다고 하면 많은 프로그래머는

Create Table (processId varchar2(20), phase1 char(1), phase2 char(1), phase3 char(1)) 와 같이 만든다.


Process가 3단계라는건 어디까지나 현재시점이므로

Create Table(processId varchar2(20), phaseNo byte, phase char(1)) 로 하는게 좀더 유연하다.

processId당 1row를 읽었던 것을 3row로 읽기 때문에 성능이 떨어질 것이라고 생각하지만 IO는 byte의 양이며 양은 선이 아니라 넓이이다. 1 * 6 이나 3*2나 6*1이나 모두 양은 같으며 IO가 bit레벨이 아닌 kb의 일정한 단위로 이루어진다는 걸 생각하면 거의 영향이 없다. 오히려 동시성을 고려한 Lock 메커니즘을 생각하면 후자가 빠를때가 많다는 것은 DB에서는 상식에 가깝다. Table의 컬럼은 일종의 틀이며 제약이기 때문에 변할 가능성의 여부는 횡이 아니라 종에서 가능하도록 하면 설계의 유연성이 늘어난다. 대신에 1,2,3단계를 모두 통과한 process를 구하는 Query가 아주 조금 더 복잡해질 뿐이다.

이를 좀 더 극단적으로 생각하면 어떨까?
CREATE TABLE EMP
(
 EMPNO            NUMBER NOT NULL,
 ENAME            VARCHAR2 (10),
 JOB              VARCHAR2 (9),
 MGR              NUMBER (4),
 HIREDATE         DATE,
 SAL              NUMBER (7,2),
 COMM             NUMBER (7,2),
 DEPTNO           NUMBER NOT NULL
)
이라는 테이블은 극단적으로

Create Table property_tblc(
  propertyId varchar2(20) NOT NULL,
  propertyType varchar2(10) NOT NULL,
  propertyValue varchar(200)
)
로 바꿀수도 있다.

1102, bleujin, Dev, MGR, 2000.1.1, 100, 0, 20   이라는 row를

empNo, Number, 1102
ename, String, bleujin
job, String, Dev
mgr, String, MGR
....
와 같이 저장할수도 있다. 후자는 emp뿐 아니라 dept 테이블이라고 해도 모델이 변경될 필요가 없을정도로 극단적이다. 사실 이런 모델링은 이펙티브 오라클의 톰의 말대로 바보같은 모델링이다. 유연할지 모르겠지만 그 반작용은 아주 크다. 라고 죽 생각해 왔다.

그러나 경험상 30초짜리 쿼리를 3초로 줄이다든가 3초짜리 쿼리를 0.3초로 줄이면 클라이언트들은 아주 만족해 한다. 하지만 30ms 쿼리를 3ms 로 줄인다고 해서 기뻐하는 경우는 거의 없다.

다소 아이러니 하지만 ROI에서 본다면 당연한 이야기이다. 하나의 request에는 연산과 NetworkIO 등의 최소 한계 속도가 있다. CPU나 메모리의 연산속도는 ns지만 일반적으로 다루는 모든 I/O는 ms 단위로 이루어지게 된다. 30ms에서 3ms는 배수로는 동일하게 10배차이지만 하나의 request의 한계 속도가 0.3s라고 했을때 하나의 request는 고작해야 10%(27ms)도 안되는 차이가 있을뿐이다. 그리고 10%의 효과를 얻기 위한 비용은 막대하다.

초단위의 튜닝은 상대적으로 난이도가 쉽지만 ms단위의 튜닝은 수만줄의 DB쿼리에 각각에 대해 비싼 비용의 전문가들이 작성해야 하고 이미 그전에 하드웨어 튜닝, OS 튜닝, 그리고 물리적인 설계에서부터 테이블과 인덱스 옵션 하나하나의 제어와 통제, 다중사용자 환경을 고려한 Process와 Latch 경합에 대한 고려도 필요하다. 그럴바에는 차라리 초급 프로그래머가 대충 만들수 있는 이미지 등의 리소스 캐쉬가 비용도 작고 효과도 더욱 좋다. 

CPU 설계같은 일이 아닌 일반 어플리케이션에서 한자리수 ms 단위의 억세스는 ROI상 그렇게 효과적이지 못하다면 극단적으로 통제를 해서 얻는 27ms를 별로 관심도 없는 속도 향상에 투자하는게 아니라 아니라 여기서 얻는 여유를 몽땅 다른곳으로 돌려버리면 어떨까? 라는 시덥잖은 생각에서 출발했다.

'Framework > Another Lore' 카테고리의 다른 글

AL : Workspace  (0) 2009.04.23
AL : Node  (0) 2009.04.23
AL : PropertyDefinition  (0) 2009.04.23
AL : NodeType  (0) 2009.04.23
AL : Lock  (0) 2009.04.23
Posted by bleujin
Framework/Another Lore2009. 4. 23. 11:08

Property Definitions(속성정의) :
A node type contains a set of definitions specifying the properties that nodes of this node type are allowed (or required) to have and the characteristics of those properties.  A property definition has all the attributes of a generic item definition as well as the following property-specific attributes:


PropertyType

A property definition must specify a property type. Type[String, Binary, Long, Double, Boolean, Date, Name, Text, Reference, Path, ViertualText, Undefined] : defines integer constants for the available property types as well as for their standardized type names (used in serialization) and two methods for converting back and forth between name and integer value:

[Example Code]
 GlobalStore globalStore = session.getGlobalStore() ;
 PropertyDefinition explain = globalStore.createPropertyDefinition(IDString.propId("explain"), Property.Type.STRING) ;
 assertEquals(explain.requiredType(), Property.Type.STRING) ;

 NodeType newType = globalStore.createNodeType(objectType, "temporaryType", new PropertyDefinition[]{explain}) ;
  MappedDefinition md = newType.getMappedDefinition(IDString.propId("explain")) ;
  assertEquals(md.requiredType(), Property.Type.STRING) ;
  assertEquals(1, newType.getMappedDefinitions().size()) ;
  assertEquals("explain", md.getId().getString()) ;




<Inteface PropertyDefinition extends Serializable>
public IDString getId();
public boolean isSameProperty(IDString propId);
public Property.Type requiredType();



PropertyDefinition은 Id와 PropertyType으로 구성되어 있다.


DB의 컬럼과 비슷하기는 하지만 컬럼과 달리 constraint와 defaultValue를 가지지 않는다. 이러한 것들은 NodeType과 Mapping되는 과정에서 추가되고 PropertyDefinition은 단지 해당 아이디를 가진 PropertyDefinition은 특정 Property.Type을 가진다고 정의할 뿐이다.


'Framework > Another Lore' 카테고리의 다른 글

AL : Node  (0) 2009.04.23
AL : Property  (0) 2009.04.23
AL : NodeType  (0) 2009.04.23
AL : Lock  (0) 2009.04.23
AL : sequence  (0) 2009.04.23
Posted by bleujin
Framework/Another Lore2009. 4. 23. 04:46


An important feature of many repositories is the ability to distinguish the entities stored in the repository by type.
Node types are used to enforce structural restrictions on the nodes and properties in a workspace by defining for each node, its required and permitted child nodes and properties.

Every node has one declared node type. node types are typically used to defined the core characteristics of
a node. In a writable repository a node's primary type is first assigned upon node creation.

Repository implementations may vary as to how flexible they are in allowing changes to the node types assigned to a node. Each repository has a single, system-wide registry of node types. Typically, a repository will come with some implementation-determined set of built-in node types.

Some of these types may be vendor-specific while others may be standard predefined node types defined. Some repositories may further allow users to register new node types programmatically


A node type definition consists of the following attributes:


NodeTypeId


Every registered node type has a name, unique within the repository. NodeTypeId consists of prefix of AnotherLore URI and short local name in a workspace




Supertypes


A node type has zero or one supertype. Supertypes are specified by name. The supertype relation is transitive: If T1 is a supertype of T2 and T2 is a supertype of T3 then T1 is a supertype of T3.



Property Definitions

A node type may contain a list of property definitions, which specify the properties that nodes of that type are permitted or required to have and the characteristics of those properties. The list of property definitions may be empty.


Orderable Member NodeType

A node type may declare its member type orderable, meaning that for all nodes of that type, the order that the member nodes are iterated over can be programmatically controlled by the user. A node type may contain a list of member node definitions, which specify the permitted or required child nodes and their characteristics. member type definitions may be empty. if not setted, in principle have objectType(objectType have no propertyDefinition and is supertype of all nodetype)


Mapping Property Definition

The default values attribute of a property definition defines the values assigned to property if it is auto-created. If the property is single-valued this attribute will hold a single value.

A property definition may impose constraints on the value that the property may hold. These value constraints are defined by an array of strings, whose format differs depending on the type of the property.


[example Code]

NodeType commentType = globalStore.createNodeType(objectType, "comment", new PropertyDefinition[] { comment, regUserId, regDate });
  commentType.setConstraint(COMMENT, new RangeByteLengthConstraint(10, 4000));
  commentType.setConstraint(REGUSERID, new RequireConstraint());
  commentType.setConstraint(REGDATE, new RequireConstraint());      // set constraint
  commentType.setDefaultValue(REGDATE, new CurrentDateValue()) ;  // set default



<Inteface NodeType extends Serializable, Publishable>

 public NodeType setConstraint(IDString propId, IValueConstraint constraint)  throws RepositoryException ;
 public NodeType setDefaultValue(IDString propId, IDefaultValue defaultValue)  throws RepositoryException ;
 public NodeTypeId getId() ;
 public NodeType getSuperType()  throws RepositoryException ;
 public void setMemberType(NodeType memberType)  throws RepositoryException ; 
 public NodeType getMemberType()  throws RepositoryException ;
 public boolean isAllowedMemberType(NodeType memberType)  throws RepositoryException ;
 public GenericIterator<MappedDefinition> getMappedDefinitions() throws RepositoryException;
 public MappedDefinition getMappedDefinition(IDString propId) throws RepositoryException ;
 public boolean containsPropId(IDString propId)  throws RepositoryException ;
 public boolean isNodeType(NodeType thatType)  throws RepositoryException ;
 public boolean isNodeType(String thatTypeId)  throws RepositoryException ;
 public GenericIterator<ValidMessage> getViolateConstraintMessage(Node node) throws RepositoryException;




'Framework > Another Lore' 카테고리의 다른 글

AL : Property  (0) 2009.04.23
AL : PropertyDefinition  (0) 2009.04.23
AL : Lock  (0) 2009.04.23
AL : sequence  (0) 2009.04.23
자동화 테스트 - 자바스크립트  (0) 2009.04.17
Posted by bleujin
Framework/Another Lore2009. 4. 23. 03:14


A lock is placed on a node by calling node.lock(boolean isDeep). The node on which a lock is placed is called the holding node
of that lock.


Shallow and Deep Locks

A lock can be specified as either shallow or deep. A shallow lock applies only holding node and its properties. A deep lock applies to its holding node and descendants. Consequently, there is a distinction between a lock being held
node and a lock applying to a node. A lock always applies to its holding node. However, if it is a deep lock, it also applies to all nodes in the holding node's subgraph. When a lock applies to a node, that node is said to be locked.

Lock Owner

Initially, the session through which a lock is placed is the owner of that lock. This means the session has the power to alter the locked node and to remove the lock.

Repositories may support client-specified lock owner information.


Placing and Removing a Lock

When a lock is placed on a node, it can be specified to be either a session-scoped lock. A session-scoped lock automatically expires when the session through which the lock owner placed the lock expires. or can is released througt explicitly unlocked


Getting a Lock

node.getLock() returns the Lock object that applies to the node. if session is not same, lock cant called unlock()

void lock.unlock()
Removes the lock


Testing for Lock Holding

boolean node.isLocked()

returns true if the node holds a lock; otherwise returns false. To hold a lock means that the node has actually had a lock placed on it specifically, as opposed to having a lock apply to it due to a deep lock held by a node above.


Lock Object

The Lock object represents a lock on a particular node. It is acquired either on lock creation through node.getLock() 


[Example Code]
  Node parent = session.createNode(root, objectType, "parent");
  Node child = session.createNode(parent, objectType, "child");
  boolean isDeep = true;
  parent.lock(isDeep);
  assertTrue(child.isLocked());
  assertTrue(parent.isLocked());

  Lock lock = parent.getLock() ;
  lock.unlock();
  assertFalse(child.isLocked());
  assertFalse(parent.isLocked());



<Inteface Lock>
 public Node getNode() throws RepositoryException; // Getting the Lock Holding Node
 public boolean isDeep(); // Testing Lock Depth
 public void unlock() throws RepositoryException, InterruptedException;
 public boolean isOwner(UserBean user); // Testing Lock Owning Session


LockException

When a method fails due to the presence or absence of a lock on a particular node a LockException is thrown.
LockException extends RepositoryException,


종종 내가 특정 노드를 수정하고 있을때 (즉 edit Mode일때) 다른 누군가가 수정을 할 수 없게 하고 싶을 경우가 있다.
그러나 잘 알다시피 웹은 디스커넥트 환경이기 때문에 이러한 Lock의 구현이 조금 다르다.

첫번째로 했던 방법은 editMode로 갈때 userId와 node.UUID를 특정 테이블에 표시를 해두는 방식이다. 그리고 editMode로 들어가기 전에 이런 표시가 있는지 살펴보는 뻔한 방식을 생각해 볼 수 있다. 이 방법의 단점은 첫번째 사용자가 editMode에서 다른 사이트로 가버리거나 브라우저를 끄고 놀러가버렸을 때이다.

적절한 process를 거쳐 취소나 저장을 누르지 않은 상태에서 다른 곳으로 가버렸기 때문에 먼저 마크했던 정보가 여전히 남아있어서 다른 사용자는 여전히 접근할 수 었다는 단점이 있다. 관리자가 수동으로 풀어주는 것도 한두번이다.

두번째의 문제는 editMode로 갈때와 viewMode일때의 구분이 어렵다는 것이다. 모두 select이며 이걸 viewMode로 쓸지 editMode로 쓸지는 전적으로 프로그램에게 달려 있다.

또 다른 문제는 만약 setProperty시 Lock을 건다고 해도 commit까지의 시간이 아주 짧아(ns 레벨) 효과가 아주 미비하고 추가적으로 관리해야 할  Lock정보가 너무 많아지는 문제가 있다.



그래서 AL에서는 DB에서 사용하는 Lock 개념과 좀 달리 접근할 필요가 있었다.

첫번째 DB등의 persitence에 마킹하는 방법은 사용자가 다른 곳으로 가버렸을때나 AL의 restart등에도 남아있기 때문에 Lock 정보는 메모리에서 관리한다.

두번째 최소한의 정보를 관리하기 위해 Lock은 별도로 호출하기 전까지는 자동으로 걸리지 않는다.

세번째 Lock은 얼마의 시간이 지나면 자동으로 release 된다.



첫번째와 세번째 조건을 만족하기 위한 가장 좋은 장소는 Session이다. http Session이 아니라 AL은 웹과 상관없이 자체적으로 Session을 관리한다. session은 일정 시간이 지나도록 행동이 없으면 자동으로 invalidate 되기 때문에 위 조건을 만족할 수 있다.

두번째로는 node.lock() 을 호출하는 순간에만 lock이 걸린다. lock이 걸린다고 setProperty를 호출 할 수 없는 것은 아니다. 다만 node.isLocked에서 반환되는 값이 달라질 뿐이다.

명시적으로 해제하고 싶으면 해당 Owner가 node.getLock().unlock()를 호출해야 한다. 추가적으로 lock escalation으로 관리해야 할 락을 줄인다.


'Framework > Another Lore' 카테고리의 다른 글

AL : PropertyDefinition  (0) 2009.04.23
AL : NodeType  (0) 2009.04.23
AL : sequence  (0) 2009.04.23
자동화 테스트 - 자바스크립트  (0) 2009.04.17
와~~~  (0) 2009.04.09
Posted by bleujin
Framework/Another Lore2009. 4. 23. 02:05


[Example Code]
  DatabaseSequence dseq = new DatabaseSequence(session.getGlobalStore().getNamespace(), DBController.getTestInstance()) ;
  Sequence seq = new CacheSequence(dseq) ;
  
  IDString seqId = IDString.seqId("seq_name") ;
  seq.reset(seqId) ;
  
  long i = seq.nextVal(seqId) ;
  assertEquals(1, i) ;

  assertEquals(1L, seq.currVal(seqId)) ;
  assertEquals(2L, seq.nextVal(seqId)) ;
  assertEquals(3L, seq.nextVal(seqId)) ;
  
  seq.reset(seqId) ;
  assertEquals(0, seq.currVal(seqId)) ;
  for (int j = 1; j < 111; j++) {
   assertEquals(1L *j, seq.nextVal(seqId)) ;
   assertEquals(1L *j, seq.currVal(seqId)) ;
  }

<interface Sequence>
 public void reset(IDString seqName);
 public long currVal(IDString seqName);
 public long nextVal(IDString seqName);


적합한 PK의 역할을 할 수 있는 컬럼(들)이 없거나 그 컬럼(들)의 length가 길때 Sequecne는 꽤 유용하다.

다만 DB의 sequence는 몇가지 불편한 점이 있다.
이를테면 parent - child 구조의 data를 하나의 트랜잭션으로 입력하려고 할때 parent의 PK가 sequence 라면

parent가 입력되기 전까지 sequence를 모른다.
child는 parent의 PK를 알아야 입력이 가능하다. 라는 명제가 충돌한다.

그나마 오라클 같은 경우 sequence는 별도의 세그먼트 이기 때문에

현재 사용하는 sequence의 next value를 알아와서 그 값으로 parent와 child의 값을 설정한후 입력한다.
가 가능하지만..

다른 DB의 경우 이를테면 MSSQL의 경우 seq는 table에 IDENTITY형으로 종속되어 있기 때문에
하나의 트랜잭션으로 유지하기 위해 꽤 번거로운 짓을 많이 해야 한다.


AL에서는 그래서 별도의 Sequence 객체를 제공하는데.
원리는 간단하다. key, value 의 하나의 테이블을 만들고 key에는 sequnceName을 value에는 currValue를 저장하고 nextValue를 요청했을 경우 value+1로 업데이트 하고 해당 value를 던져주면 된다. 이렇게 구현할 경우 필요할 경우 sequece를 아주 간단하게 만들수 있다. 

예컨데 이런 SQL 이다. 

    function reset(v_seqName varchar2)
    return number
    is
    begin
        update sequence_tblc set seqValue = 0 where seqName = v_seqName ;
   
        return SQL%ROWCOUNT ;
    end ;

    function currVal(v_seqName varchar2)
    return number
    is
        v_result number ;
    begin
        select NVL(max(seqValue), -1) into v_result from sequence_tblc where seqName = v_seqName ;
       
        If v_result = -1 then
            insert into sequence_tblc(seqName, seqValue) values(v_seqName, 0) ;
            return 0 ;
       
        end if ;
        return v_result ;
    end ;
   
    function nextVal(v_seqName varchar2)
    return number
    is
        v_result number ;
    begin
        update sequence_tblc set seqValue = seqValue+1 where seqName = v_seqName returning seqValue into v_result;
       
        IF SQL%ROWCOUNT = 0 Then
            insert into sequence_tblc(seqName, seqValue) values(v_seqName, 1) ;
            return 1 ;
        End if ;
       
        return v_result ;
    end ;

다만 이렇게 사용하면 Sequence를 사용하기는 매우 편하지만 nextVal을 호출할때마다 DB Call이 일어난다. 그리고 Table update 방식은 DB sequence 방식보다 무겁다.

AL에서는 그래서 DB의 sequnce의 아이디어를 그대로 사용한다. AL.Sequnece의 nextVal을 호출하면 실제로는 +20으로 업데이트하고 20개의 값을 queue에 저장해둔다. queue가 비어 있지 않으면 queue값을 poll 해주고 20개를 모두 소모해 비어 있으면 다시 +20을 해주고 반복한다.

'Framework > Another Lore' 카테고리의 다른 글

AL : NodeType  (0) 2009.04.23
AL : Lock  (0) 2009.04.23
자동화 테스트 - 자바스크립트  (0) 2009.04.17
와~~~  (0) 2009.04.09
AL - Extreme  (0) 2009.04.04
Posted by bleujin
Framework/Another Lore2009. 4. 17. 00:23

AL을 혼자서 꾸역꾸역 만들다보니 단점은

온갖 여러가지 일을 해야 한다는 점이다. 이전에 회사에 소속되어 무슨 일을 할때는 다른 누군가가 해주던 백업및 배포용 build script도 만들어야 하고 호출하는 테스트 프로그램도 직접 만들고 나중에 고생하기 싫으면 유닛 테스트도 훨씬 더 정교하게 만들어야 한다.

이렇게 혼자서 이것저것 하는 방식은 최근 5년간에는 거의 하지 않았기 때문에 이것 저것 번거럽고 귀챃지만
덕분에 훨썬 더 많은 자동화 기술을 강제적으로 사용해야 되는 동기가 되고 있다.

웬만한건 다 자동화로 처리하는데 예외중의 하나가 테스트 프로그램으로 만든 WebApplication의 테스트이다. 가능한 Jsp에 code를 최소화하긴 했지만 그럼에도 수동적인 테스트를 할때마다 10분씩을 잡아먹고 있다.

그래서 이전에 만든적이 있는 스파이더에 몇시간 동안 사이트의 링크를 따라가면서 테스트 하는 툴을 만들다가
자바 스크립트 링크를 사용하는 것에서 막혔는데..구글한테 물어보니 링크로 스크립트를 사용하는 것이 크롤링 솔류션에서도 꽤 난감한 문제이긴 한가보다.

DOM을 스크립트 엔진에 로드해서 하는 방식은 배꼽이 더 커지기 때문에 어렵고 단순히 귀찮음을 떠나서 성능 테스트를 할때도 가상 브라우저로 써야 하기때문에 더 고민하다가 정 방법이 없으면
시나리오를 저장해서 동작하는 방식으로라도 써야. -ㅅ-;


AL은 세가지 모드가 있는데.
첫번째는 프레임워크 라이브러리 형태로 호출해서 쓰는 방식이고
두번째는 StandAlone 형태로 별도의 프로세스 서비스로 동작하면 WebDav나 WS로 네트워크 콜해서 쓰는 방식이 있고
세번째는 지들끼리 서로 통신하는 분산 방식으로 구상하고 있다.

첫번째는 머 원래 만들기가 어렵지 않으니 곧 대충 대충 될것 같고.. 문제는 2번째와 3번째..

'Framework > Another Lore' 카테고리의 다른 글

AL : Lock  (0) 2009.04.23
AL : sequence  (0) 2009.04.23
와~~~  (0) 2009.04.09
AL - Extreme  (0) 2009.04.04
AL - Code Example  (0) 2009.03.20
Posted by bleujin



객체 지향의 여섯번째 계는 숨기는 것이다. 흔히 Encapsulation이라고 불리며 인터페이스등을 이용하여 메소드의 상세 구현여부를 숨기는 것으로 알려져 있다.

인터페이스를 통한 메소드의 구현 말고도 숨겨할 예는 몇가지 더 있다.
예컨데 Type이다.

User 객체를 만든다고 할때 new User(String userId) 와 같이 생성하는 건 좋지 못한 습관이다. 아마도 User.create(String userId) 같은 Factory Pattern 얘기를 떠올렸을지 모르지만 그 보다 좀더 근본적인 문제이다. 위 코드의 문제는 String userId 그 자체에 있다. DB repository를 생각한다면 아마도 userId는 varchar 타입에 저장될테고 varchar와 String은 별 문제가 없어보인다.


코드를 좀더 구현해서 getUser(String userId)를 구현해야 할때가 되면 슬슬 걱정이 되기 시작한다. 과연 인자로 넘어온 userId는 소문자일까?

getUser(String userId) {
     String _userId = userId.toLowerCase() ;l
      ......
}
와 같이 코드를 수정한다. 다시 두번째 걱정이 든다. 과연 인자로 넘어온 userId는 null이 아닐까?

getUser(String userId) {
     if (userId == null) throw new IllegalArgumentException("userId must be nullable") ;
     String _userId = userId.toLowerCase() ;l
      ......
}
와 같이 코드를 수정한다. 다시 세번째 걱정이 든다. 앞의 User를 생성하는 new User(String userId)의 인자는 소문자가 넘어갔을까?

// in User Class
public boolean equals(Object obj){
     if (obj instanceof User) return false ;
     User that = (User)obj ;
     this.getId().equalsIgnoreCase(that.getId()) ;
}
와 hashCode 메소드를 추가한다. 다시 네번째 걱정이 든다. 과연 DB에는 소문자가 저장이 될까?

// in User Class
public String getId(){
     return id.toLowerCase()  ;
}
와 같이 코드를 수정한다. 다시 다섯번째 걱정이 든다. 어라? 다른 메소드들은 모두 괜찮은건가?? .. .이하 반복



이러한 걱정이 드는 이유는 간단하다. 조금 더 생각해 보면 userId는 단순한 String 이상의 의미가 있다. 이를테면 length는 4byte - 20byte, 허용되는 문자는 AlpahNumUnderbar이며 '_'로 시작해서는 안된다 등의 세부 규칙이 있으며 User 객체를 구분하는 Key의 role을 가지고 있다. 그리고 이러한 정보들은 String에 담기지 않기 때문에 걱정이 들고 이것저것 잔뜩 체크하는 코드들을 중복해서 심어놓게 된다.


이 문제를 해결하기 위해서는 두가지 방법이 있다.

첫번째 방법은 지금보다 훨씬 더 단순하게 만들어서 길을 단 하나만 만들어놓고 입구에서만 체크하는 방법이다. 구조를 단순한게 만드는 작업이 쉽지 않지만 프로그램이 CRUD만을 하는 것이라면 그렇게까지 어렵지는 않다. 프리젠트 레이어에서 모델 레이어까지 거의 원방향 직통 터널을 뚫는 것과 같다. 이 방법을 사용한다면 Entity객체를 만들지 않는게 좋다. 단순한 게시판에 이방법을 사용할때 Board 객체니 Article 객체니 하는것은 단순하게 만드는데 방해가 되기 때문에 그래서 좋지 않다.

음 그러나 만들기는 어렵지 않으나 지속적인 유지보수하기에는 어려운 방법이다. 왜냐하면 엔트로피는 증가하기 마련이고 프로그램은 복잡해지기 마련이다. 일명 세컨드 시스템 이펙트로 프로젝트가 성장 할수록 누군가가 터널에 구멍을 뚫어 저것과 인터페이싱하면 그닥기능과 저닥기능도 될수 있다고 누군가가 주장하기 시작할테고 이것저것 통합하다보면 얼마 지나지 않아 뚫린 곳으로 검증되지 못한 데이타가 들어오고 그것을 체크하기 위한 코드로 프로그램은 누더기가 될것이다. (성장하지 않는 프로그램이라면 - 이를테면 파일럿 예제라든가 - 첫번째 방법은 좋은 방법이 된다.)



두번째 방법은 구조를 단순하게 만들기가 힘들다면 더 복잡하게 만들어서 String을 사용하지 않고 userId가 가지는 규칙과 role을 가지는 새로운 타입을 만드는 방법이 있다. 예컨대 

 private IDString(String id) {
     this.id = id ;
 }

 private static IDString create(String id, int minLength, int maxLength){
        if (id == null) throw new IllegalArgumentException("....") ;

       if (StringUtil.isAlphaNumUnderbar(lowerId, minLength, maxLength)) {
            return new IDString(lowerId) ;
       }
        throw new IllegalArgumentException("......") ;
 }

 public static IDString userId(String userId){
    return create(userId, 2, 12) ;
 }

IDString class를 만들어서 

public User(IDString userId){ // 생성자
   ...
}

public User getUser(IDString userId) {
   .....
}

와 같이 인터페이스를 하는 방법을 사용할 수 있다. 만약 이런 식으로 사용하는 ID 형식의 Type이 많다면 공통점을 묶을 수도 있고 UserId Class 로 별도의 클래스로 만들수도 있다. (균형을 찾는 것은 언제나 어렵다.)

위와 같이 별도의 클래스를 사용하면 인자로 넘어오는게 소문자인지 허용할 수 없는 문자가 들어오는지 일일이 체크하지 않아도 된다.(이를 확실히 보장하려면 해당 클래스를 final로 만들수도 있다.) IDString이 null 로 넘어오는 것을 막을 수는 없지만 불필요한 toLowerCase() 같은 함수를 쓰지 않음으로서 가능성을 대폭 줄여준다. (null의 문제는 설계로 줄여줄수는 있어서 이론적으로 완벽히 막을 수는 없다. )


그래서 인캡슐은 단순히 숨기기만 하는게 아니다. 코드는 숨기는 대신 의도는 드러내야 한다. int, Sring 자체는 아무 의미없는 스칼라 값이기 때문에 의미를 부여하기 위해 첫번째 변수명을 잘 지어야 하고 규칙과 롤이 있어서 충분하지 않다면 그 의도를 반영해주는 포장을 해야 한다. 

어쩌면 이런 질문이 떠오를지도 모른다. 이봐~ 그래봐야 결국에는 String을 사용할거잖아? 언제까지 포장 객체를 쓸수는 없는 노릇이니까 말야. 물론 맨 마지막에는 이런 포장을 뜯어야 한다. 포장은 객체 매개 변수로 전달이 되는 운송과정에서 의미가 있다. String 객체와 달리 IDString으로 포장된 String은 컴파일러와 다른 누군가 혹은 자신에게 그 값이 어떤 값이며 왜 쓰이고 있는 지를 전달해준다. 그리고 마지막 포장을 풀고 String이 필요해질때 getter 메소드가 있는 Bean의 위치를 통해 그 값의 의미를 알게 된다.


Encapsulation에는 단순히 다형성만을 뜻하는 것이 아닌 하나의 패러다임에 가깝다. 아마도 varchar로 저장되겠지만 varchar에 대응되는 String형을 사용하지 말아야 한다는 것은 몰라도 되는 것을 숨겨야 할 뿐 아니라 이미 알고 있는 것도 숨기는게 좋다는 의미이다. (@TODO : 브룩스-맨먼쓰미신-나중에 추가)



'Framework > 아키텍쳐 일반' 카테고리의 다른 글

AOP  (0) 2009.06.08
Self-Signed Java Applets  (0) 2009.06.01
중복을 볼 수 있는 눈  (0) 2009.03.13
Here is Dragon  (0) 2009.03.12
프로그래밍 패러다임  (0) 2009.03.12
Posted by bleujin
Framework/Another Lore2009. 4. 9. 15:45

계획된 2주 예정보다 하루 일찍 드디어 AL Project - DBPersistence[Oracle]의 테스트 케이스 95개가 모두 성공하였다.


예전에 보던 룸펜스타의 별돌이의 기분과 비슷하다.

'Framework > Another Lore' 카테고리의 다른 글

AL : sequence  (0) 2009.04.23
자동화 테스트 - 자바스크립트  (0) 2009.04.17
AL - Extreme  (0) 2009.04.04
AL - Code Example  (0) 2009.03.20
AL - Abstraction & Model  (0) 2009.03.13
Posted by bleujin
Framework/Another Lore2009. 4. 4. 16:42

숫자 0과 1의 차이는 무한한 차이이다. 존재와 무존재의 차이니까 말이다.

그럼 1과 2의 차이는? 1과 10의 차이는? 그 차이는 앞의 0과 1의 차이보다 크지 않다. 0과 1의 차이가 질적인 차이라면 1과 10의 차이는 그냥 10의 차이일 뿐이다. 그렇다면 1과 10000의 차이는 그냥 10000의 차이일까? 그럴수도 있고 아닐수도 있다. 흥미로운건 어느 양을 초과하게 되면 단지 양의 차이가 다시 질적인 차이를 일으킨다.

갈매기의 꿈의 조나단은 얼마의 속도를 돌파하게 되자 단지 하나의 갈매기가 아니라 자유로운 갈매기가 되었다. 물은 100도에 이르는 순간 더이상 액체가 아니라 기체로 형질 변환을 일으킨다. 자동차도 시속 1000km로 달리는 순간 날수 있다. 이런 양적인 변화가 질적인 변화를 일으키는 얼마든지 있다.

AL은 유연성을 극도로 올린 프레임워크이다. 항상 시소의 양끝처럼 하나가 올라가면 하나가 내려가는 관계는 아니지만 일반적으로 하나의 특성을 올리면 다른 하나의 특성은 떨어진다. 대표적인 특성 하나가 성능이다. 유연성이 높아질수록 일반적으로 성능은 떨어진다. 그러나 성능은 유연성과 달리 극단적으로 추구한다고 해서 양적에서 질적인 변화로 바뀌기 어려운 분야이다. 더군다나 성능은 항상 한계 속도가 있다. 그것에 한없이 가까워지도록 노력할 수는 있지만 결코 돌파할 수는 없는 선(이를테면 하드웨어)이 있다. 반면에 유연성은 질적인 변화가 쉽과 한계점의 제한으로부터 더 자유롭다.

유연성을 갖춘 프레임워크는 많다. 일찌기 RAD툴이라는 말이 나오기 시작한 15년 전부터 유연성은 프레임워크의 핵심중 하나였다. 최근의 루비온 레일즈도 그러한 예이다. 그러함에도 왜 AL을 만드는가? 이유는 간단하다. 이전의 유연함을 강조하는 것들은 모두 프로세싱과 관련되어 있다. 함수가 유연해지고 구조가 유연해진다고 해서 그리 큰 성과를 거두기는 어렵다. 왜냐하면 프로세싱의 밑단계에는 모델이 깔려있기 때문이다.

데이타모델이 정적인 상황에서 프로세싱이 유연해서 달성할 수 있는 효과에는 한계가 있다. DB Table과 OR Mapping하는 프로세싱 프레임워크의 단점은 바로 이것이다. 이는 어디까지나 제한적인 유연성이며 선을 벗어나지 못하는 유연성이다. AL은 유연한 프레임워크를 강조하지만 프로세싱보다 모델에 집중하고 있고 가장 유연하지 못하는 모델을 가장 유연하게 만들려고 노력하는 프레임워크이다.

지난 1주일동안 1년반이 넘게 팽개쳐 뒀던 AL을 다시 뜯어보고 있다. 평소에도 나는 프로그래밍을 잘한다고 생각한 적이 없다. 그리고 DB도 마찬가지이다. 다만 DBA중에서는 프로그래밍을 잘하고 프로그래머 중에서는 DB를 잘 안다고 생각한다. 각기 다른 전문가의 대화를 통해서는 충분한 정보의 전달이 어렵지만 나는 다른 컨피던스 전환시에 엔트로피 손실이 거의 없기 때문에 아마도 나에게 가장 잘맞는 프레임워크라고 생각하고 있고 아마 그래서 가장 완성해 보고 싶은 프레임워크기이기도 하다.

'Framework > Another Lore' 카테고리의 다른 글

자동화 테스트 - 자바스크립트  (0) 2009.04.17
와~~~  (0) 2009.04.09
AL - Code Example  (0) 2009.03.20
AL - Abstraction & Model  (0) 2009.03.13
AL - 배경  (0) 2009.03.07
Posted by bleujin
Framework/예외처리2009. 3. 26. 23:29

이전의 처리를 통해 예외를 한군데로 모을 수 있었으면 이제 다음 단계의 예외 처리 프레임워크로 들어갈 준비가 된 것이다. 프레임워크를 만든다는 것은 어찌보면 왼쪽 그림을 오른쪽 그림으로 만드는 것에 불과하다.



언뜻보기에 클래스의 무작위적인 메시지 교환에서 추상적인 공통점을 찾아내고 이를 단계적인 체인으로 풀어내는 것이다. 이를테면 웹 프레임워크는 브라우저에서 서버 프로그램까지의 과정을 쪼개고 공통점을 묶어서 몇단계의 연결된 과정으로 바꾼다. 이러한 연결된 과정으로 바꾸는 과정에서 중간중간 오른쪽의 세모 그림처럼 request의 접합점을 담당하는 클래스가 생기는데 - 웹프레임워크의 경우 Controller가 이 역활을 한다 - 이 세모 부분의 역할에 따라 프레임워크의 질이 결정된다. 여기서 제 5계를 말할 수 있는데 클래스간의 복잡한 메시지 교환을 횡으로 단계를 가지도록 바꾸면 복잡성이 대폭 감소한다. 이 단계의 과정에서 접합점 역할을 하는 단계에 주의해라.

다시 예외 얘기로 돌아가서 우리는 앞의 글에서 모든 예외가 거쳐가게 되는확장포인트를 만들었다. 중간에 현명한 처리를 할 수 있는 일부 checked exception을 제외하고 마땅한 처리방법이 없는 다른 모든 checked exception 예외와 runtime exception을 하나의 접함점으로 모으면 로그 형식이나 알람 정도 말고도 다양하게 할수 있는게 생긴다.

보통의 프로그램에서는 예외가 생각보다 많이 발생한다. 순간적으로 DB 네트웍이 막혀서 접속이 안될 수도 있고 정기적인 다운타임을 기다리기 귀찮은 누군가 몰래 프로그램을 고칠 수도 있다. "이봐 예외가 발생하지 않는 프로그램을 만들어야지" 라고 주장할 수도 있겠지만 아무리 노력해도 테스트환경에서는 잡히지 않았던 예외들이 사용자의 이상야릇한 PC환경에서는 발생하고 이러한 예외들은 실험실에서 재현하기가 아주 어렵고 사실 사용자는 귀찮기 때문에 웬만한 예외가 발생해도 보고하는 확률은 높지 않다.

보통의 경우 예외 처리 방안은 1)로그에 남긴다. 2) 어드민에게 이메일등을 통해 알린다 정도가 있을 수 있는데 이러한 방법은 모두 후속 처리에 해당한다. 즉 실제 예외가 발생한 후 상당한 시간이 지난후에야 확인이 가능하거나 혹은 지겹게 날라오는 같은 메시지에 질려서 관리자가 무시해 버리기도 해서 대응될때까지 상당한 시간이 흐른다.

두번째로 생각해 봐야 할 점은 수백메가의 예외 로그는 대부분이 중복이라는 사실이다. 넷스케이프 5.0이 처음 나왔을때 버그 리포팅 기능을 생각해보자. 전 세계 1억 2천만명이 매일 날리는 버그 리포팅을 사람이 일일히 확인해서 대응할 수 있을리가 없다.

로그에 남기거나 어드민에게 알리는 방법의 단점은 이렇게 후속처리이며 동시에 대부분의 예외는 아마도 대부분 같은 종류일터인데 사람이 일일이 확인하는 것은 비효율적 이라는 사실이다. 이런 문제를 해결하기 위해 예외 처리 프레임워크는 3가지의 주요 기능을 가져야 한다.

1) 즉시 처리가 되야 한다. 이 말은 수동적인 로그 남기기가 아닌 적극적인 처리를 말한다. 자기 수정같은 거창한 이야기가 아니라 예외가 발생했을때 과거 비슷한 패턴의 예외의 해결방안을 제공하는 시나리오를 제공한다는 말이다. 예외가 발생했을때 "알수 없는 예외가 발생했습니다." 따위는 사용자에게 전혀 도움이 되지 않는다. 사용자가 알고 싶은 것은 첫번째는 "왜 이런게 발생했지"이고 두번째는 "이걸 피하려면 어떻게 해야하나"의 이 두가지이다. 이러기 위해서는 과거 예외 발생 정보와 처리 방안이 어딘가에 저장되어 있어야 한다는 것을 말한다.

2) 예외의 발생 빈도등을 체크하여 예외를 트리아지(Triage) 분류가 가능해야 한다. 보통의 솔류션 제품이 판매되면 1개월의 로그만 모아도 수백메가의 로그 파일이 남는다. 이 로그파일에서 예외 정보는 대부분 중복된 내용일테고 그걸 일일히 살펴보는 것은 시간낭비이기 때문에 사람의 분류의 이전에 프로그램이 예외가 발생한 클래스의 패키지, 예외의 종류, 발생 빈도 등을 고려하여 오류 등급을 미리 분류해 줄 수 있어야 한다.

3) 예외가 발생했을때 자동으로 관련정보를 묶어서 제공해줄 수 있어야 한다. 이는 고객을 위해서가 아니라 이를 수정해야 하는 프로그래머를 위한 정보를 말한다. 해당 예외의 종류가 이전에도 발생한 경우가 있는가? 그에 대한 대처는 어땠는가? 해당 클래스가 이전에도 다른 예외가 발생하지 않았나? 그렇다면 그 클래스의 마지막 수정자는 누구였는가 등의 정보를 같이 제공해 주어야 한다. 또한 현재 수집할수 있는 사용자의 환경과 동작시킨 기능 등도 물론 포함되어야 한다. 사용자의 PC환경은 매우 다양하기 때문에 가능한 많은 정보를 수집해두는게 좋다. 실제로는 어떤 빌어먹을 유틸리티들이 다른 프로세스 영역에 침범함으로 오류를 발생시키기도 하기 때문에 충분하지는 않겠지만 말이다.

이 분야에 대해 조엘의 책에 따르면 자동으로 다음과 같은 자료를 수집하는걸 권장한다.
- 제품의 정확한 버전
- 운영체제 버전과 브라우저 버전
- 예외가 발생한 파일명과 행번호
- 오류 메시지
- 어떤 작업을 하고 있었는지 사용자 설명
- 필요하다면 연락 가능한 사용자 정보


단순한 분류는 사람보다 컴퓨터가 월등히 뛰어나다는 장점을 잘 활용해서 이런 과정은 자동으로 이루어져야 한다. 현재의 대부분의 버그 관리 소프트웨어는 수동으로 이루어지는데 어쩔수 없이 사람이 판단의 개입되어야 하는 일부를 제외하고 자동화 시킬수 있는 부분은 아주 많다. (사실 이 자동 예외 처리기는 AL로 만들 첫번째 소프트웨어로 생각한 것이지만 작업기간이 맞지 않아서 따로 만들고 있다.)

'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
exception framework  (0) 2009.03.10
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin