2012.02.07 17:35
본 포스트는 이터너티님의 DDD(Domain-Driven Design) 해설을 학습한 과정을 정리한 것입니다.

PART 1. VALUE OBJECT와 REFERENCE OBJECT

어플리케이션을 구성하는 객체는 Value Object와 Reference Object로 나뉜다. VO와 RO를 구분하는 기준에 대해서 이터니티 님은 아래와 같은 가이드를 제시한다.

시스템 내에서 해당 객체를 계속 추적해야 하는가객체가 표현하는 개념이 유일하게 하나만 존재해야 하는가? 그렇다면 REFERENCE OBJECT로 만든다단지 객체가 추적할 필요가 없는 단순한 값인가속성값이 동일하면 동일한 객체로 간주해도 무방한가고민할 필요 없다그냥 VALUE OBJECT로 만든다.

Reference Object는 유일하게 존재해야 하는 것이라고 말한다. 어떤 것들이 해당할까?

- 유일하게 존재한다는 것은 객체들이 같거다 혹은 다르다는 점을 명확히 구분할 수 있어야 한다. 달리 말해, ID를 가지는 객체들을 말한다.
- 물리적인 장치(DB, file system)에 저장되거나, 네트워크를 통해 전송되는 객체들이다. 잠시 만들었다가 사라지는 데이터에 추적할 필요가 없기 때문이다.

이러한 두가지 특징을 가지는 객체들은 무엇이 있을까? 가장 좋은 사례는 DB에 저장되며, 기본 키(primary key)를 가지는데이터(혹은 엔티티)일 것이다. Reference Object로 정의해야 것들은 최종적으로 DB에 저장되는 객체들이라고 간주하는 것이 무방하다.
 
동일함의 의미 

앞서, Reference Object들은 유일하게 존재해야 한다고 정의했다. 그렇다면, Reference Object를 만들어 쓴다는 것은  유일한 객체임을 보장할 수 있는 수단이 제공되어야 한다는 말이다. 이너니티님의 글을 다시 인용한다.

모든 객체 지향 시스템은 생성된 객체에게 고유한 식별자(identity)를 부여한다. 대부분의 객체 지향 언어는 객체가 위치하고 있는 메모리 상의 주소를 객체의 식별자로 할당하고 이 주소 값을 사용하여 객체를 구별한다.
 
그렇다면 자바에서 객체의 고유 식별자를 알아내는 방법이 있을까? 그리고, 고유 식별자를 이용해 유일성(uniqueness)을 보장받을 수 있을까?

In java there is no any specific method that provides us the object's ID. But each object has its own unique hash value which can be treated as unique Id for that object.
- Rose India 인용

자바에서는 해시 코드(hash code)를 객체의 고유 식별자로 사용할 수 있다고 한다. 그런데, 유일성을 확실히 보장해주지 않는다고 말한다.이게 대체 무슨 말인가? 아래와 같이 설명할 수 있다.

- 동일한 객체는 항상 동일한 해시 코드를 반환한다.
- 하지만, 서로 다른 객체가 동일한 해시 코드를 반환 할 수도 있다.

달리 말해서, 동명이인(同名異人)이 존재할 수도 있다는 말이다. 즉, 해시 코드는 우리가 특정 사람을 지칭할 때 쓰는 이름과 같은 것이다. 그래서, 이너니티님은 아래와 같이 가이드하고 있다.

각 언어는 객체의 식별자를 비교할 수 있는 연산자를 제공하는데 Java의 경우 “==”와 “!=” 연산자를 사용한다. 두 참조가 가리키는 객체가 동일한 식별자를 가지는 경우, 즉 동일한 주소에 위치하는 경우 “==” 연산자는 true를 반환한다.

“==” 연산자를 사용하여 동일성을 판단하기 보다는 equals() 메소드를 오버라이딩하여 금액의 동등성을 테스트해야 한다.

한 발 더 나아가, 동일성(identity)와 동등성(equality)의 차이에 대해서 얘기해 보자.

- 동일성(identity)은 비교하려는 두 개의 객체가 동일한 식별자를 반환할 경우 같다고 판단하는 것이다.
- 동등성(equality)는 두 객체의 내용 (혹은 데이터)이 일치할 경우, 같다고 판단하는 것이다.

대부분의 상황에서는 '동일성 비교'를 기본으로 적용한다. 그러나, DDD(Domain Driven Design)에서는 동등 비교를 원칙으로 한다. (DDD에서는 데이터를 안전하고, 정확하게 사용하는 것이 주된 관심사이다.) 그러니 개발자들은 Reference Object 및 Value Object를 정의함에 있어서 어떻게 동등 비교를 실현할 수 있는지 그 방법론을 정확히 이해해야 한다.

동일 비교 시에 발생하는 문제

일반적인 동일 비교 방식을 적용했을 때, 발생하는 문제에 대해서 간단한 코드를 작성해서 테스트해 보도록 하자. 먼저, 금액을 나타내는 클래스를 작성해 보았다.

 
package org.eternity.customer;

public class Money {
	private long amount;
	
	public Money(long amount) {
		this.amount = amount;
	}
	
	public Money(int amount) {
		this.amount = amount;
	}

	public String toString() {
		return String.valueOf(amount);
	}
}
같은 데이터를 가지고 있지만, 다른 객체로 인식되는 경우를 확인해 보자. 단위 테스트 코드를 아래와 같이 작성했다. 서로 다른 객체이지만, 분명 동일한 금액을 가지고 있다.

package test.eternity.customer;

import static org.junit.Assert.*;

import org.eternity.customer.Money;
import org.junit.Test;

public class MoneyTest {

	@Test
	public void testAmount() {
		Money money1 = new Money(1000);
		Money money2 = new Money(1000);
	
		System.out.println( "money1's hash code = " + money1.hashCode() );
		System.out.println( "money2's hash code = " + money2.hashCode() );

		// money1 == money2 (?)
		assertEquals(money1, money2);
	}

}
테스트는 실패하고 만다. 서로 다른 객체로 판정하는 것이다. 왜 이런 결과가 나오는지 확인하기 위해, 해시 코드의 출력 값을 확인해 보면 아래와 같다. (해시 코드 값은 실행할 때마다 달라질 수 있다.)

money1's hash code = 4565111
money2's hash code = 20392474
이제 동등 비교를 하기 위해 Money 클래스를 아래와 같이 수정했다.

package org.eternity.customer;

public class Money {
	private long amount;
	
	public Money(long amount) {
		this.amount = amount;
	}
	
	public Money(int amount) {
		this.amount = amount;
	}

	public String toString() {
		return String.valueOf(amount);
	}
	
	public boolean equals(Object other) {
		if(this == other) 
			return true;
		
		if(!(other instanceof Money))
			return false;
		
		if( this.amount == ((Money)other).amount )
			return true;
		
		return false;
		
	}
}
이제 단위 테스트를 통과했다. 그런데, 뭔가 문제가 남아 있다. 테스트를 통과했는데 뭐가 문제냐고? 두 개의 객체가 동일하다는 결과가 나오는데 해시 코드는 서로 다르게 출력되는 것이다. 자바의 기본 원칙을 위배한 것이다.

Equal objects must produce the same hash code as long as they are equal, however unequal objects need not produce distinct hash codes. - Equals and Hash Code 참조 

자바의 원칙을 준수하면서, 동등 비교를 구현하기 위해서는 equals() 메소드와 hashCode() 메소드를 함께 오버라이드(override) 해야만 한다.


package org.eternity.customer;

public class Money {
	private long amount;
	
	public Money(long amount) {
		this.amount = amount;
	}
	
	public Money(int amount) {
		this.amount = amount;
	}

	public String toString() {
		return String.valueOf(amount);
	}
	
	public boolean equals(Object other) {
		if(this == other) 
			return true;
		
		if(!(other instanceof Money))
			return false;
		
		if( this.amount == ((Money)other).amount )
			return true;
		
		return false;
		
	}
	
	public int hashCode()
	{
		return (int)(amount ^ (amount >>> 32));
	}
}
이제 테스트를 수행하면 두 객체가 동일하다고 판정되고, 해시 코드 역시 동일한 값을 출력한다.
money1's hash code = 1000
money2's hash code = 1000
hashCode() 메소드는 자바의 컬렉션 클래스들(List, Map 등) 에서 보이지 않게(?) 호출되며, 다양한 API, 라이브러리에서 객체를 관리하기 위한 목적으로 해시 코드 반환 값을 사용하기 때문에 equals() 함수를 오버라이드(override)할 경우에는 잊지 말고, 함께 구현해야 한다. 혹여 잊어 버린다고 해서 컴파일 오류는 나지 않는다. 하지만, 예기치 않은 문제가 발생할 경우에는 원인을 추적하기 매우 어렵게 된다. 어떤 문제가 발생할 수 있는지, hashCode() 함수를 오버라이드 하지 않고 아래와 같은 테스트 코드를 작성했더니, 역시나 문제가 발생했다. 길지 않은 코드이니 직접 컴파일 한 후 확인해 보시는 것을 권장한다.
package test.eternity.customer;

import static org.junit.Assert.*;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eternity.customer.Money;
import org.junit.Test;

public class MoneyTest {

	@Test
	public void testAmount() {
		Money money1 = new Money(1000);
		Money money2 = new Money(1000);
		
		System.out.println( "money1's hash code = " + money1.hashCode() );
		System.out.println( "money2's hash code = " + money2.hashCode() );
	
		// money1 == money2
		assertEquals(money1, money2);
	}
	
	@Test
	public void setSet() {
		Money money1 = new Money(1000);
		Money money2 = new Money(1000);
		
		Set moneyPocket = new HashSet();
		moneyPocket.add(money1);
		assertEquals(1, moneyPocket.size());
		moneyPocket.add(money2);
		assertEquals(2, moneyPocket.size());
		
		Map moneyMap = new HashMap();
		moneyMap.put(money1, 1);
		moneyMap.put(money2, 2);
		
		assertEquals(1, moneyMap.size());
	}
}
Posted by 善 곽중선
2012.01.26 16:06

문서 객체 모델(DOM : Document Object Model)은 객체 지향 모델로써 구조화된 문서를 표현하는 형식이다. DOM은 플랫폼/언어 중립적으로 구조화된 문서를 표현하는 W3C의 공식 표준이다. DOM은 또한 W3C가 표준화한 여러 개의 API의 기반이 된다.

DOM은 HTML 문서의 요소를 제어하기 위해 웹 브라우저에서 처음 지원되었다. DOM은 동적으로 문서의 내용, 구조, 스타일에 접근하고 변경하는 수단이었다. 브라우저 사이에 DOM 구현이 호환되지 않음에 따라, W3C에서 DOM 표준 규격을 작성하게 되었다.
- 위키백과 "문서 객체 모델" 인용 

문서 객체 모델을 어떻게 설명해야 하는가? 기반이 되는 개념들을 설명한 후에 종합하는 것이 유리할 것 같다. 어떠한 개념들이 있는가 나열하기 보다는 위키 백과의 요약된 설명을 인용했다. 위키 백과는 많은 기술과 용어들을 소개하고 있지만 불친절한(?) 해설들이 꽤 많다. 백과사전은 매뉴얼과 다르다 라고 이해하는게 정신 건강에 좋을 것 같고, 어쨋든 잘 정리된 정보인 것은 확실하다.

DOM에 포함된 다양한 개념들을 하나씩 파헤쳐 보자.

1. 문서 혹은 구조화된 문서

일반적으로 문서는 특정 주제를 다룬 문장 혹은 글의 집합이라고 설명할 수 있다. "구조화된 문서(structured document)"는 계층적(hierarchical) 형태 혹은 트리(tree) 형태로 만들어진 문서를 말한다. 달리 말하자면, 문장들이 순서대로 읽을 수도 있지만, 문장, 단락(paragraph) 혹은 단어 간에 부모/자식/형제 관계가 존재한다는 말이다. 구조화된 문서들은 사용자에게 보여지는 방식에 따라서 부모/자식 관계가 눈에 보이기도 하고, 보이지 않는 경우도 있다. 또한, 문서 내부에 삽입되어 있지만 시각화 되지 않는 정보들도 있다.

2. 객체 지향 모델

객체지향 문서라는 표현이 이상하게 여겨질 수 있다. 객체는 "행위"와 "자료"를 함께 묶어둔 프로그램 부품을 말하는데, 문서에도 "행위"가 존재할까? DOM으로 표현된 문서에 표현된 "행위"는 문서 자체가 동작하는 것이 아니라, 문서에 포함된 다양한 요소(element)들을 편집하는 방법을 규정한 것이다.

3. 플랫폼/언어 중립적

DOM 문서는 유닉스, 리눅스, 윈도우 등 거의 모든 운영체제에서 읽고 쓸 수 있다. 또한 DOM 문서는 자바, 자바스크립트, C, Perl 등 다양한 프로그래밍 언어를 이용해 작성하거나, 조회할 수 있다. 이를 위해, W3C는 DOM 문서를 조작할 수 있는 API들을 정의했다.

4. DOM API

W3C는 DOM 문서를 편집할 수 있도록 다양한 API들을 제안(엄격히 지켜야 하는 규정은 아니다) 했다. W3C를 지지하는 다양한 프로그램 개발 조직 및 개발자들은 W3C에서 제안한 API를 다양한 언어와 라이브러리로 구현하였다.

함께 읽으면 좋은 글 : 
DOM 이해하기 (한글) - IBM
Posted by 善 곽중선
2012.01.26 14:22
스프링 시큐리티를 왜 도입해야 하는가? 라는 질문을 던져놓고 필요한 답을 찾아봤습니다. 아래 웹 페이지의 일부 내용을 번역한 것입니다.


1. 스프링 시큐리티는 어플리케이션의 모든 보안 요구사항을 충족할 수 있는가?

스프링 시큐리티는 사용자 인증(authentication) 및 권한 제어(authorization) 요구에 대해 매우 유연한 프레임워크를 제공하지만, 보안 어플리케이션을 구축하기 위해서는 스프링 시큐리티의 범위 밖에 있는 다양한 고려사항이 있다. 웹 어플리케이션은 이미 잘 알려져 있는 모든 종류의 공격에 취약하기 때문에, 개발을 시작하기 전에 발생할 수 있는 문제들을 잘 이해한 후에 코딩하고 설계해야만 한다. 웹 어플리케이션 개발자가 알아야 하는 주요 이슈와 대응책을 얻기 위해서는 OWASP 웹 사이트를 참조하기를 바란다.
 
2. web.xml 보안으로 충분하지 않는가?

자, 당신이 스프링 기반으로 엔터프라이즈 응용 프로그램을 개발하고 있다 가정하자. 

일반적으로 언급되고, 보안 시스템 구축 시 요구되는 4가지 보안 문제가 있다. 인증(authentication), 웹 요청 보안(web request security), 서비스 계층 보안 (비즈니스 로직을 구현한 메소드들), 도메인 객체 인스턴스 보안 (서로 다른 도메인 객체는 다른 접근 제한 등급을 가진다) 등이다. 

1) 인증(authentication) : 서블릿 사양(servlet specification)에는 기본적인 인증 방식이 정의되어 있다. 그러나, 일반적으로 서블릿 인증을 수행하기 위해서는 웹 컨테이너(web container)의 고유 영역(realm)을 설정해야 한다. (예를 들자면, 톰캣의 설정 방식과 JBoss의 설정 방식이 다르다.) 이러한 설정 방식은 이식성(portability)이 없는데다, 컨테이너의 인증 인터페이스를 구현한 자바 클래스를 작성해야 하는 경우 더욱 더 이식성이 없어지게 된다. 스프링 시큐리티를 사용하게 되면 WAR 수준에서 완벽한 호환성을 달성할 수 있다. 게다가, 스프링 시큐리티는 검증된 사용자 인증 제공자 및 메커니즘들을 선택할 수 있으므로, 개발 진행 중에 인증 방식을 선택하거나 교체할 수 있다. 이는 확정되지 않은 운영 환경을 대상으로 개발되는 소프트웨어를 개발하는 업체에게 유용하다.

2) 웹 요청 보안(web request security) : 서블릿 사양(servlet specification)에서는 요청 URI의 보안성을 유지하는 방법을 제공한다. 그러나, 서블릿 사양에 기초해 보안 가능한 URI들은 사양에서 지정한 제한된 URI 포맷을 따라야만 한다. 스프링 시큐리티는 훨씬 더 포괄적인 방식을 제공하고 있다. 예를 들어, Ant 경로 혹은 정규식 표현을 사용할 수 있다. 단순히 요청된 페이지 경로가 아니라, URI의 일부 요소 (HTTP Get 인자 등)를 고려할 수도 있다. 이것은 웹 어플리케이션이 실행되는 동안에 웹 요청 보안이 동적으로 변경될 수 있음을 의미한다.

3) 서비스 레이어와 도메인 객체 보안(Service layer and domain object security) : 서블릿 사양 내에 서비스 계층 보안 및 도메인 객체 인스턴스 보안에 대한 지원(혹은 표준)이 없다는 것은 다계층(multi-tier) 어플리케이션을 개발함에 있어 심각한 한계를 보여준다. 일반적으로 개발자들은 이러한 요구사항을 애써 무시하거나, MVC 컨트롤러 코드 (더 나쁜 경우는 뷰 계층에서...) 내에서 보안 로직을 구현한다. 이러한 대응 방식은 심각한 단점들을 내포하고 있다.

a. 관심사의 분리(Separation of concerns) : 인증은 횡단 관심사(crosscutting concern)로 간주되어야 하고, 그렇게 구현되어야 한다. 반면에 MVC 코드나 뷰에서 인증 코드를 구현할 경우, 제어 로직과 인증 로직을 함께 테스트 하기 어렵고, 디버깅하기 어려우며, 코드 중복으로 이어지게 된다. 

b. 리치 클라이언트와 웹 서비스에 대한 지원(rich clients and web services) : 새로운(혹은 추가된) 유형의 클라이언트를 필수적으로 지원해야 하는 상황이 닥친다면, 웹 계층에 내장된 인증 코드는 재사용할 수 없게 된다. 이런 상황에서는 스프링 리모팅 익스포터(remoting expoters)만이 서비스 계층 빈(MVC 컨트롤러가 아닌 객체)들을 외부로 노출할 수 있다. 다양한 클라이언트 유형을 지원하기 위해서는 인증 로직인 서비스 계층에 존재해야 한다. 

c. 계층 문제(layering issues): MVC 컨트롤러나 뷰(view) 계층에 인증 로직을 담는 것은 매우 잘못된 계층 설계이다. 반면에, 서비스 계층에서 인증하게끔 결정할 경우 모든 서비스 계층 메소드에 추가 인자(argument)를 전달해야 하는 문제가 발생한다. 좀 더 우아한 방식은 ThreadLocal 에 인증 정보를 담는 방식이나, 이는 개발 기간의 증가를 의미한다. 가장 경제적인 방법(비용 절감 차원)은 전용(dedicated) 보안 프레임워크를 사용하는 것이다. 

d. 인증 코드 품질(authorization code quality) :  웹 프레임워크들은 종종 "올바른 것들은 더욱 쉽게 할 수 있고, 잘못된 방식들은 더욱 하기 어렵게 만드는 것(make it easier to do the right things, and harder to do the wrong things)"이라고 불리운다. 보안 프레임워크는 보다 다양한 목적과 추상적 방식으로 설계되기 때문에 웹 프레임워크와 동일한 특징을 가지고 있다. 자신만의 인증 코드를 작성할 경우, "검증(design check)"이 이루어지지 않는다. 반면에 보안 프레임워크들은 객관적으로 검증된 것이다. 또한, 자체 제작(in-house) 된 인증 코드는 광범위한 개발을 통한 개선, 외부 혹은 동료 개발의 리뷰, 버전업 등이 이루어지지 않는다.
 
간단한 어플리케이션인 경우, 서블릿 스펙 보안으로 충분할 수 있다. 그러나, 웹 컨테이너 간의 낮은 이식성, 설정 방식의 비호환성, 웹 요청 보안의 유연성 부족, 서비스 계층과 도메인 객체 인스턴스 보안의 부재 등을 고려해 본다면, 또 다른 솔루션을 고려해야 하는 것이 분명하다. 
Posted by 善 곽중선