Spring Security - OID, SID, ACL 2

Posted at 2010.07.23 09:39 // in Principle // by MOOVA 무바㏇

Spring Security의 Domain ACL[각주:1]에선 개별의 Entity객체에 CRUD권한을 걸어둘 수 있다. Role,URL,Resource등 여러가지 자원에 자물쇠를 걸어두는 것은 일반적인 권한 상식이나 실행중인 클래스에 권한을 걸어두는 것은 지극히 드문일일 것이다.
(Windchill의 ACL 편집기에선 Domain Class기준으로 ACL을 설정할 수 있다. JPA도 그렇고, 기타 시큐리티 오픈 소스도 그렇고 좋은 사상은 언제나 배껴가고 복사하고 따라가게 되어있다. 모방은 창조의 어머니라고 하지 않았나)


그렇다면 구지 왜? 실행중인 클래스에 자물쇠를 걸어놓고 사용자에 따라서 차등을 두어야 하는 것일까? Role과 URL권한, 메소드 권한까지 모자라서 클래스에 왜? 이와같은 번거로운 작업을 해야하는 것일까?

한가지 시나리오를 상상해 보자.

2010:07:23 08:41:59
Figure 3. 성인과 청소년에 대한 도메인 클래스의 인가처리


ROLE_ADMIN과 ROLE_USER는 성인과 청소년으로 구별되어 있다. index.task 자원과 Document.word자원 또한 모두 통과 가능할 것이다. 하지만, 도메인클래스영역을 보자. 성인은 Adult 클래스를 조작 가능 하지만, 청소년은 Adult 클래스를 조작하지 못하도록 해야한다.
만약에 청소년이 Adult클래스를 조작가능하다면 이 시스템에 접속하는 대부분의 유저들이 나이를 불문하고 성인물을 감상할 수 있을테니까 말이다. 결과적으로 Domain Class에 권한에 따른 설정을 해 두면 별도의 프로그래밍없이 다음과 같은 뷰를 볼 수 있을 것이다.


성인
---------------------------------------------------------------------------------------------------
제목                               분류                        등급                          대여자
---------------------------------------------------------------------------------------------------
구르물섯달버서난..           액션                        일반                         오승택
라스트사무라이                액션                   성인(19)                         오승택
---------------------------------------------------------------------------------------------------


청소년
---------------------------------------------------------------------------------------------------
제목                               분류                        등급                          대여자
---------------------------------------------------------------------------------------------------
구르물섯달버서난..           액션                        일반                         오승택
---------------------------------------------------------------------------------------------------


이처럼 Domain Class에 자물쇠를 걸어두면 세부적인 사항까지 구현할 수 있게 도와줄 뿐만 아니라, 객체분할과 같은 논리적인 영역까지 그 범위를 넓히게 한다. 또한 클래스당 권한을 강제할 수 있기때문에 앞으로 객체지향언어에서 추천되는 인가 방법이라고 할 수 있다.


다음장은 Spring Security의 Domain ACL에 대해서 알아보도록 하자. (언제가 될지 모르겠다.:)

  1. ACL[에이씨엘]은 개개의 사용자들이 디렉토리나 파일과 같은 특정 시스템 개체에 접근할 수 있는 권한을 컴퓨터의 운영체계에 알리기 위해 설정해 놓은 표라고 할 수 있다. 각 개체는 접근제어목록을 식별할 수 있는 보안 속성을 가지며, 그 목록은 접근권한을 가진 각 시스템 사용자들을 위한 엔트리를 가진다. 가장 일반적인 권한은 1개의 파일이나 또는 한 개의 디렉토리 안에 있는 모든 파일들을 읽을 수 있고(Read), 기록할 수 있으며(Write), 그리고 만약 그것이 실행가능한 파일이나 프로그램인 경우라면 실행시킬 수 있는(Execute) 권한 등을 포함한다. [본문으로]
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
블로그코리아에 블UP하기
  1. Favicon of http://minimonk.net BlogIcon 구차니

    2010.07.23 21:05 신고 [수정/삭제] [답글]

    음.. 리눅스에도 ACL이 있는데 그걸 웹으로 확장한건가요? 아니면 이름만 같은 별개의 유래를 가진 녀석일려나요?

댓글을 남겨주세요.

Spring Security - OID, SID, ACL

Posted at 2010.07.23 09:37 // in Principle // by MOOVA 무바㏇
2010:07:23 04:04:12
Figure 1은 OODBMS에 포함된 개념에 대해서 열거한 그림이다.


Spring Security의 Domain ACL의 개념은 Security Identity(SID)라고 하는 객체 보안 식별자에 의해서 인가작업을 하기 때문에, SID의 부모격인 OID(Object Identity)가 무엇인지 알아볼 필요가 있다.


OID를 사용하기 시작한것은 OODBMS에 객체를 식별하기 위한 방법으로 종종 사용하곤 했다. 최근엔 ORM Framework에서 종종 볼 수 있는 Object의 Uniq한 식별자라고 말하기도 하는데, Domain Driven Design의 Eric Evans는 Entities를 정의하면서 Object Identity를 다음과 같이 설명한다.


객체 지향 언어의 Object identity조작은, ENTITY의 identity를 정의함에 있어서 다소 미흡한 면이 많다.
데이터베이스로부터 얻은 Object의 id는 java와 같은 랭귀지의 identity와 는 다르다. 데이터베이스로부터 얻은 객체는 영속성이 있는 반면 프로그래밍언어의 identity는 비영속성이라는 점이 다르기 때문이다. 하지만 데이터베이스와 프로그래밍 랭귀지를 굳이 동기화하자면 특별한 key로 랭귀지 객체에 아이디를 부여하는 것이 일반적이다. 그 identity는 불변하며, 변화될 수 없다. ID는 시스템에 의해서 자동으로 작성할 수 있고 다른 Object와 구별할 수 있는 유일한 식별자가 바로 Object Identity이다.


보통 데이터베이스와 프로그래밍 언어의 동기화를 위해 식별자로 사용된 Object Identity는 Evans가 Entities를 설명하기 훨씬 전부터 정의되어 왔고 변화됐다는 것을 다음의 목록에서 확인하자. (1978년도 이전부터 Object Identity라는 용어가 사용되어왔으니, 필자가 태어나기 이전부터 그 가치가 논의되고 있었다는 소리다. 참.. 놀랍고도 위대하다.)


[각주:1]Khashafian, Copeland (1986) - identity는 Object 내부에 있다. 그것의 목적은 독립적인 Object의 특성을 표현하는데 있다.
Atkinson et al. (1990) - 두 Object가 동일한 제 삼의 Object를 참조할 수 있다. 이것은 공유 Object의 개념으로 Object를 업데이트하거나 조작하거나 카피할 때 Object identity로 그 식별을 대신한다.
Kent (1991) - 다양한 관점으로 Object identity에 대해서 논의했다. Object Identity를 비교하는 것이 아니라 그들을 참조하는 Object에 관점을 두었다. 즉 Object의 실제주소값으로 Object Identity를 설명했다.
Beeri (1993) - 그가 논의했던 OID는 현재 사용되고 있는 Object Identity의 원류가 되었다. identity의 경우에는 그것을 검색하는 쿼리에 따라서 식별된다



발전된 MDA를 채택한 벤더 솔루션중 하나인 Windchill이 사용하는 OID를 잠깐 살펴보자. Windchill내부의 ORM 프레임웍으로 새로운 Entity Object를 생성해 데이터베이스에 객체 저장을 했다. ( Windchill의 영속 객체는 JPA나 HIbernate의 생명주기와 비슷한 그것을 가지고 있다. ) 실제 데이터베이스에 접속하여 OID를 추출해 보면 다음과 같은 형태의 Object Identity를 발견할 수 있다.

-----------------------------------------------------------------------------------------------
Oid
-----------------------------------------------------------------------------------------------
com.tistory.moova.Moovie:001
com.tistory.moova.Moovie:002
com.tistory.moova.Moovie:003
com.tistory.moova.Moovie:004
-----------------------------------------------------------------------------------------------


Evans가 논의한 것처럼 데이터베이스의 식별자와 OOP 언어(자바와 같은)의 식별자는 개념상 다르기 때문에, 도메인에 적합한 OID생성규칙을 사용해서 동기화 하고 있다. 실제로 이것은 Object Identity가 아닌 Object identifier라고 하는 직렬화된 식별자이다.

2010:07:23 07:51:21
Figure2. ObjectIdentity와 ObjectIdentifier의 관계도

ObjectIdentity도 Object의 일종으로써 실제 구분할 값을 찾기 위해 ObjectIdentifier라는 직렬화된 식별자를 참조의 형태로 포함하고 있다.(OCP원리를 따르면서 SRP원리도 포함하고 있다.). 실제로 "com.tistory.moova.Moovie:004"와 같은 값을 얻기 위해선 다음의 코드가 필요할지 모르겠다.

..
PersistentHelper.store(moovie);
moovie.getObjectIdentifier() : Serializable

JPA는 @Id, @IdClass, @EmbeddedId로 OID를 활용하고 있으며, Windchill은 Persistent를 상속하는 방법으로 OID를 활용하고 있다.

JPA 예
@Entity
public class Inventory implements Serializable {

        @Id[각주:2]
        @GeneratedValue(generator="InvSeq")
        @SequenceGenerator(name="InvSeq",sequenceName="INV_SEQ", allocationSize=5)
        private long id;

Wnidchill 예
public class Inventory extends Persistent {...}

언어와 플렛폼의 차이를 그대로 보여준다. 하지만, 근본적인 사상과 배경은 같다는 것을 알 수 있다.

이 설명으로 Object Identity와 Object Identifier에 대한 개념은 개략적으로 이해할 수 있으리라 본다. Secure Identity(SID)를 설명하기에 앞서 OID를 설명한 이유는 SID와 OID는 바늘과 실과 같은 수족과 같은 관계이기 때문이다.

보안작업을 하려하는 사람을 예로 들어보자.

예를들어 오승택이라는 사람이 주민등록증이 없으면 보안작업을 할 때 필요한 보안인가증도 발급받을 수 없다. 주민등록은 그 사람을 대표하는 ID이기 때문에, 말 그대로 객체 ID가 없으면 보안인가(SID)에서 필요한 신원을 확인할 수 없다.

충분히 이해가 되었으리라 믿는다. 지금까지의 설명은 Spring Security의 Domain ACL을 설명하기 위한 재료물들이다.




참조 문서

In “The Art of the Interpreter or, the Modularity Complex (Parts Zero, One, and Two)” [75] (1978),
• In “Values and Objects in Programming Languages” [56] (1982), Bruce J. MacLennan discusses the distinction between values and objects.
• The paper “Object Identity” [43] (1986) by Setrag N. Khoshafian and George P. Copeland is the most cited one with regard to the topic of object identity, and gives a thorough presentation of the concept and its implementation in programming languages and database systems.
• In “The Object-Oriented Database System Manifesto” [4] (1990), Mal-colm Atkinson et al. list requirements that are to be fulfilled by object-oriented databases. Of course, object identity plays an important role here.
• “A Rigorous Model of Object References, Identity and Existence” [42] (1991) by William Kent describes a model for object identity that, among other things, aims at a facility for merging objects and their identities after the fact.
• Catriel Beeri criticizes a too strong focus on object-oriented features in the context of databases in “Some Thoughts on the Future Evolution of Object-Oriented Database Concepts” [8] (1993), and sketches a re- duction of the concept of object identity to pure value-based properties in relational databases.
• Roel Wieringa and Wiebren de Jonge present a detailed formal model in “Object Identifiers, Keys, and Surrogates – Object Identifiers Re-visited” [85] (1995) that captures the essential properties of object identity and can be interpreted as the common denominator of previ-ous approaches.



 

  1. http://deposit.ddb.de/cgi-bin/dokserv?idn=972868291&dok_var=d1&dok_ext=pdf&filename=972868291.pdf [본문으로]
  2. http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html [본문으로]
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
블로그코리아에 블UP하기

댓글을 남겨주세요.

Spring Security - URL 권한을 DB로 관리하기 2.

Posted at 2010.07.18 10:10 // in OpenSource // by MOOVA 무바㏇
필자는 그동안 경험했던 정보나 팁들, 또는 지식을 언제부터인가 온라인보다 오프라인으로 공유하는 것을 즐겨했습니다.
상당수가 프로젝트 보안이 걸려있는 문제이기도 하겠지요, 그 모든 부분을 온라인에 공유할 수 없는 심정, 이루 말할 수 없겠죠?
오픈소스와 관련된 사항도 프로젝트와 연관이 있으면 보안이라는 타이틀이 걸려있습니다. 하지만 공개할 것은 공개하자라는 근본 원칙은 항상 마음속에 간직하고 있습니다.
"버려야 새로운 것을 얻는다." 바로 이 정신. 참 아릅답습니다.~~


그림1. 커스터마이징 할 filterSecurityInterceptor의 인터페이스와 클래스 사용도입니다.


그림2. FilterSecurityInterceptor가 의존하는 두개의 매니저 클래스 ( Authentication Manager, AccessDecision Manager )

FilterSecurityInterceptor는 Spring Security의 중요 필터중 하나입니다. FilterSecurityInterceptor는 AuthenticationManager를 의존하며 User와 관련된 (인증 Authentication) 프로세스를 수행하고, 동시에 수행될 자원에 인가여부를 따지는 AccessDecisionManager를 의존하여 투표를 실시합니다.

<beans:bean id="filterSecurityInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor"
autowire="byType">
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" />
<beans:property name="authenticationManager" ref="authenticationManager"></beans:property>
<beans:property name="accessDecisionManager" ref="accessDecisionManager" />
<beans:property name="objectDefinitionSource" >
                    <security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
                    <security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
             </beans:property>
</beans:bean>

FilterSecurityInterceptor의 objectDefinitionSource필드를 커스터마이징 해야합니다. 바로 이 부분이 앞에서 모델링한 테이블과 연동을 해야 할 부분입니다. 위의 objectDefinitionSource를 다음과 같이 바꾸어 줍니다.

<beans:property name="objectDefinitionSource"  ref="coolInvocationDefinitionSource"/>

objectDefinitionsource에 reference될 bean을 등록해야합니다.

<beans:bean id="coolInvocationDefinitionSource"
class="com.moova.secure.filterInvocation.CoolObjectDefinitionSourceFactoryBean">
<beans:property name="dataSource" ref="springSecurityDataSource" />
<beans:property name="resourceQuery"
value="
                                 SELECT URL.URL_SPRING, R.NAME
FROM ROLE ROLE JOIN URL_ROLE UR ON ROLE.ID = UR.ROLE_ID JOIN PROGRAM ON PROGRAM.ID = UR.PROGRAM_ID JOIN URL_REPOSITORY REPO ON PROGRAM.ID = REPO.ID
        " />
</beans:bean>

FilterSecurityInterceptor의 objectDefinitionSource의 API를 살펴보면 다음과 같이 setter injection으로 되어 있는 것을 볼 수 있습니다.

Spring Security 2.0

Spring Security 3.0
 SecurityMetadataSource obtainSecurityMetadataSource() 
           
 void setObjectDefinitionSource(FilterInvocationSecurityMetadataSource newSource) 
          Deprecated. use setSecurityMetadataSource instead
 void setObserveOncePerRequest(boolean observeOncePerRequest) 
           
 void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) 

Spring Security 2.0 과 3.0 API를 살펴보면 크게 변화된 부분이 바로 이 objectDefinitionSource입니다. 3.0에선 objectDefinitionSource가 deplecated되었습니다. 이것이 securitymetadataSource로 변경이 되어 있네요. 2.0의 objectDefinitionSource의 Type은 FilterInvocationDefinitionSource 이고 3.0의 objectDefinitionSource의 Type은 FilterInvocationSecurityMetadataSourcen 인터페이스입니다.

이를 확인하고 각 버전에 따라 작성해야하는 클래스가 다를 수 있다는 것에 주목해 주십시오. 여기선 그대로 objectDefinitionSource를 사용합니다.

새로 제작할 CoolObjectDefinitionSourceFactoryBean는 FactoryBean을 implements합니다. FactoryBean을 implements 하면 어떤 객체라도 Spring lifecycle안에서 DI할 수 있게 해줍니다. 이는 단순 객체가 아닌 Factory 클래스로써 getObject()와 getObjectType()를 구현해 주기만 하면 새롭게 사용할 클래스를 반환시켜줍니다. 추가로 데이터베이스와 연계가 필요하니  "org.springframework.jdbc.core.support.JdbcDaoSupport"를 상속합니다.

사용할 구조

public class CoolObjectDefinitionSourceFactoryBean extends JdbcDaoSupport implements FactoryBean {
private String resourceQuery;

public boolean isSingleton() {
return true;
}

public Class getObjectType() {
return FilterInvocationDefinitionSource.class;
}

public Object getObject() {
return new DefaultFilterInvocationDefinitionSource(
this.getUrlMatcher(), this.buildRequestMap());
}
}

데이터베이스 연계부분은 MappingSqlQuery을 사용합니다. MappingSqlQuery 사용법은 여기를 참조해 주세요.

private class UrlRepository {
private String url;
private String role;

public UrlRepository(String url, String role) {
this.url = url;
this.role = role;
}

public String getUrl() {
return url;
}

public String getRole() {
return role;
}
}

private class UrlRepositoryMapping extends MappingSqlQuery {
protected UrlRepositoryMapping(DataSource dataSource, String resourceQuery) {
super(dataSource, resourceQuery);
compile();
}

protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
String url = rs.getString(1);
String role = rs.getString(2);
UrlRepository resource = new UrlRepository(url, role);

return resource;
}
}

"com.moova.secure.filterInvocation.CoolFilterInvocationDefinitionSourceFactoryBean"의 원본 파일

더보기




executeResourceMap()메소드에서 URL과 ROLE 관련된 매핑부분을 key와 Value로 조합해야 합니다. 
DefaultFilterInvocationDefinitionSource의 두 번째 인자는 LinkedHashMap입니다. 
당연히 HampMap값을 넘겨주어야 하기 때문에 buildRequestMap에서는 LinkedHashMap를 new해서 반환해 주고 있습니다. 
DefaultFilterInvocationDefinitionSource의 첫 번째 인자는 AntUrlPathMatcher를 new해서 주입하고 있습니다.

스키마를 새로 개정하거나 생성해서 사용해도 무방합니다.
하지만 URL과 Role에 대한 모델링 연관은 그다지 바뀔게 없다고 봅니다.


저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
블로그코리아에 블UP하기

댓글을 남겨주세요.

Spring Security - URL 권한을 DB로 관리하기 1.

Posted at 2010.07.18 10:08 // in OpenSource // by MOOVA 무바㏇
필자는 오픈소스나 자/타 제품을 접할 때 먼저 해당 제품의 생명주기(LifeCycle)을 먼저 파악합니다. 서로 연결된 인터페이스나 외부에 공개된 컴포넌트의 연관관계를 파악하고, 자주 사용하는 기능이나 중요하다고 생각되는 기능이 생명주기에 의존되어 있는지를 파악합니다. 전체 생명주기나 구조적 관점을 늘 중요시 하다보면, 치명적인 이슈를 제외한 세부적인 기술이슈까지도  그때 그때 처리할 수 있으리라 봅니다.
<http auto-config='true'>
    <intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" /> 
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/**" access="ROLE_USER" />
    <form-login/>
    <logout logout-success-url="/pages/logout-redirect.jsp"
invalidate-session="true" />
    <remember-me key="moovaSecureRMKey" user-service-ref="userDetailsService" />
</http>


Spring Security에서 url path에 대한 권한 부여는 보통 위와 같이 설정을 해야합니다. (굵게 표기한 부분)
( 권한을 가지고 있는 사용자가 요청한 url 패턴을 기준으로, 해당 자원을 접속할 수 있는지를 따지게 하여 주는 설정입니다. ) 하지만 안타깝게도 보통 실무에서는, url path나 문서 조회권한에 관련된 설정 파일은 따로 관리하지 않습니다. 

User,Member,Group등 중요 인증 및 인가에 관련된 테이블을 데이터베이스로 관리한다면 URL 권한도 데이터베이스로 관리되어야 차 후 유지보수나 확장에도 이점을 얻을 수 있습니다. Spring Security에선 위의 intercept-url설정이 기본기능으로 구성되어 있습니다만, 이것을 데이터베이스로의 관리방법으로 변경해 보도록 할까 합니다.

하고자 하는것은 매우 단순합니다. Spring Security의 중요 Filter중 하나인 FilterSecurityInterceptor의 objectDefinitionSource를 커스터마이징 할 objectDefinitionSource로 바꿔치기하는 것입니다.



1. 테이블 구축


그림1. 실제 실무에서 사용된 인증/인가와 관련된 테이블 ( 축소공개형 ) 



실제 Magpie 오픈소스로 구축중인 테이블 스키마의 축소 일부입니다. 
User와 Role N:N관계, Role과 Program도 N:N관계, Program과 Url_Repository는 1:1관계로 되어 있습니다.
Role과 Url_Repository를 N:N관계로 모델링 해도 무방하지만 보통 URL은 동적인 속성이 강한 부분이니 카테고리로 분류할 수 있는 Program이란 Entity를 따로 생성하여 url port같은 기능을 하도록 설정해 두었습니다.

사용해둘 데이터를 미리 뽑아 보고, 기존 설정에서 사용했던 포멧으로 데이터가 축출 가능한지 확인합니다.


그림 2. 기존 Spring Definition 설정 위치와 비교한 데이터의 위치.

2. 데이터 확인 및 데이터 추출
SELECT URL.URL_SPRING, R.NAME FROM ROLE ROLE JOIN URL_ROLE UR ON ROLE.ID = UR.ROLE_ID JOIN PROGRAM ON PROGRAM.ID = UR.PROGRAM_ID JOIN URL_REPOSITORY REPO ON PROGRAM.ID = REPO.ID RESULT : ROLE_USER | /** ROLE_ANONYMOUSLY | /** ROLE_ADMIN | /admin.jsp ROLE_USER | /login.jsp ROLE_ANONYMOUSLY | /login.jsp ROLE_MANAGER | /manager/index.jsp
"Spring Security URL 권한을 DB로 관리하기"에서 사전 준비해야 할 재료는 모두 준비된 상태입니다. 이제 Spring Security의 일부분을 커스터마이징해야 합니다. ( "org.springframework.security.intercept.web.FilterSecurityInterceptor" )

참고 문서 :  
http://static.springsource.org/spring-security/site/docs/3.0.x/reference/springsecurity.pdf

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

'OpenSource' 카테고리의 다른 글

근무환경  (2) 2010.08.10
Spring Security - URL 권한을 DB로 관리하기 2.  (0) 2010.07.18
Spring Security - URL 권한을 DB로 관리하기 1.  (0) 2010.07.18
심심풀이 납품용  (2) 2010.06.05
[Guide] Vaadin Korean Guide (ver01)  (3) 2010.05.11
[Share Document Note] Evernote  (0) 2010.01.24
블로그코리아에 블UP하기

댓글을 남겨주세요.

[세탁소 이야기] 아키텍처 패턴 지향 2 (사례)

Posted at 2010.06.14 17:05 // in 분류없음 // by MOOVA 무바㏇

아키텍처 패턴 적용사례.
1. Windchill의 Service Helper Pattern을 보면 PLM 영역안에서 Remote Call과 관련된 일종의 변형된 서비스 패턴이란 것을 알 수 있다. 이는 Core J2EE Patterns의 ServiceLocator Pattern과 Service Activator과 흡사하고 동일한 원류를 가진다.

2. Windchill의 workflow workflow pattern을 따른다.

3. JCA 의 FormProssesor Pattern이나 Eclipse의 GEF EditPart Pattern을 보면 Architect Pattern중 Command Processor에 근본을 두고 있다. (Command Pattern과 Command Processor의 차이점은 차 후 설명할 것이다.)

4. BOM영역에서도 부품과 부품,제품군들을 유기적으로 묶을 수 있게 Architect Pattern중 일부인 Whole-Part Pattern을 응용했음을 알 수 있다.

5. Hivemind와 Eclipse PDEPlug-in pattern을 응용했다.

6. 모 Batch 솔루션은 Partitioning Pattern Parallel Processing을 일부 응용했다.

7. Struts2는 Proxy, Pipe and Filter, Interceptors , Invocation 등 다양한 패턴을 응용했다.

이러한 사례들은 열거하자면 끝이 없다.

이미 검증된 패턴들은, 우리가 그동안 쉽게 접할 수 있는 제품들 속에 중심으로서 자리 잡고 있다. 위와 같이 정리 없이 나열된 패턴들, 이
렇게 따로따로 사용된 패턴들은 모두 공통의 카테고리 범주 내에서 정리될 수 있다. 그리고 모두 연결되어 있다는 것에 다시 한번 놀라지 않을 수 없다. (그림1참조)
"특화된 도메인에서 사용된 변형된 패턴일지라도 원류와 근본(혹은 사상)은 모두 같은 사상에 연결되어 있다"


Notice.

※. 패턴모방 - 세상에 패턴 모방은 수없이 많다. 하지만, 음악계가 사용하는 저작권 침해와 같은 엄격한 제한이 걸려 있는 것은 아니다. 패턴을 모방함으로써 더 좋은 패턴을 발굴해 낼 수 있고, 역으로 그 원류를 찾을 수 있기도 하다.

※. 특정 솔루션만을 다룬 분들 또는 특화 도메인에 오랫동안 일한 엔지니어들은 대게 이러한 사실(연결되어 있다는 사실)을 간과시한다. 변형되었고 특화되었다는 이유때문에? 아니면 국지적인 경험만 고집해서? 
어떤 이유때문인지는 몰라도 특별하다고 생각하여 범용성을 외면하는 것이다. (한 번쯤 자신이 속한 도메인에서 활용한 아키텍처 패턴이 무엇이 있는지 분석해 볼 필요가 있다.) 위에서도 말했듯이 이미 모든 패턴의 범주는 범용적인 패턴 안에 녹아 있다. 각 도메인을 넘나들면서 까지도 범용의 패턴은 여전히 살아있는 것이다. 
- Super Consultant분들은 예외이니 오해 마세요~~

아키텍처 패턴 세부 적용 사례

이해를 돕고자 주변에서 쉽게 찾을 수 있는 제품 안에 아키텍처 패턴을 적용한 사례를 몇 가지 알아보고, 그 패턴을 사용함으로써 얻는 이점에 대해서 살펴보자.


1. Eclipse GEF의 Command (Command Processor)

자신의 회사가 편집기 솔루션을 개발하고 있다고 가정하자. 대게 편집기는 기본적인 기능(copy,paste,cut,undo,redo 등)이 녹아 있어야 하고 사용자 편의성에 중심을 두어야 한다. windows 시스템을 쓰는 사용자라면 Ctrl키와 Y,Z,C,X등의 단축키를 이용해서 편집할 수 있어야 한다. 어떻게 그것을 우리의 편집기안에서 가능하게 만들까? 여기에 쉬운 아이디어 하나가 있다.

그림2와 같이 스택자료구조를 두 개 만들어서 현재 진행한 Task를 Before Stack에 Push를 이용하여 계속 적재를 한다.
사용자가 Undo 버튼을 사용하면 Before Stack에 있는 Task를 Pop하여 After Stack에 Push한다. 그 후 사용자가 Redo 버튼을 사용하면 역으로 After Stack에 있는 Task를 Pop하여 Before Stack에 다시 적재한다.

그림2. 두개의 Stack 자료구조를 이용한 편집기 기본기능 구현


이러한 단순한 아이디어와 자료구조를 이용하여 편집기의 기본기능은 모두 구현할 수 있다. 

하지만 어떻게? 생각나는 데로 구현했다고 가정하자. 물론 기능은 쉽게 돌아갈 것이다. 뛰어난 아이디어를 사용했으니 사용자에게도 더 가까워질 수 있을 것 같다. 하지만, 생각나는데로 구현한 코드를 들여다 보면 재사용에 취약할뿐더러 심한 반복의 냄새를 코드에서 찾을 수 있다. 더욱이 다른 확장 모듈에 공통으로 배포해야 하는 기본기능이 정리되지 않았기 때문에 ( 이 경우 편집기의 기본기능은 프로젝트에서 공통기능과 관련이 있다.) 팀 단위 의사소통에도 분명히 마찰이 생긴다.

이러한 문제점들을 해결하기 위해서 우리는 패턴을 참조할 수 있다. 아키텍트 패턴中 일부인 Command Processor패턴을 잠깐 살펴보자.

Command Processor

context(정황) : 스케쥴링이나 작업취소 등의 사용자 함수의 실행과 관련된 서비스를 제공해야 한다.

problem(문제) : 사용자의 요청을 실행하기 위해 시스템의 핵심 기능 범위를 넘는 서비스를 구현해야 하는 경우. 예를 들어 작업취소(undo), 재작업 (redo),여러 요청을 묶은 매크로, 동적로깅, 요청 스케쥴링, 요청 일시정지 등을 들 수 있다.

solution(해법) :
 Command Processor 패턴은 Command 패턴에 기초를 두고 있다. 사용자가 다른 모드에서 요청한 Command를 Command Processor가 중점적으로 그것을 관리한다. Command Processor는 명령(command)의 실행을 스케쥴링하고 추후에 있을지 모를 작업취소에 대비해 명령을 저장하는 것이 기본적인 기능이다.

※. 이처럼 패턴·랭귀지를 참조하여 응용하는 방법은 "자기의 상황에 따라서 패턴을 선택하고, 거기에 기술되고 있는 추상적인 해결 방법을 나름대로 구체화해서 실천하는 것"이다. Command Processor에서 우리가 원하는 편집기(기본 기능)에 대한 해법을 발견할 수 있다. 

Eclipse GEF의 일부 기능인 Command에 대해서 잠깐 살펴보기로 하자. 
Eclipse의 GEF는 Graphical Editing Framework의 약어로써 말 그대로 비쥬얼하게 편집을 하는 프레임웍이다.
이 프레임웍의 내부를 들여다보면 기본적으로 제공해 주는 기능에 undo , redo, cancel, execute와 같은 기본적인 Command 개체가 숨어 있다. GEF의 모든 커맨드는 Command-Stack에 의해 관리되고 Command-Stack은 Command가 실행되었을 때 execute()메소드를 호출하고 나서 그림2와 같은 자료연산을 해 준다. 이해를 돕기 위해 GEF의 요청 처리를 보자.(그림3,그림4)

그림3, GEF의 요청처리 흐름을 신체기관에 비유한 그림

그림4. GEF의 요청 처리 흐름을 쉽게 표현한 그림

요청이 들어오면 Controller인 EditPart에서 실행할 EditPolicy을 선택한다. EditPolicy가 실행되면 모델을 변경하기 위한 Command 개체를 작성하고 역할과 필요에 따라서, 사용자가 조작한 내용을 시각적으로 표현하기 위한 피드백을 표시하는 역할을 한다. 그림 5는 Command Processor의 요청 흐름을 표현한 시퀀스 다이어 그램이다.


그림 5. Command Processor의 요청 흐름을 표현

2. PDM의 WorkFlow Pattern

PDM 솔루션들에서는 데이터를 처리하는 단위를 프로세스(Process)라고 하는데, PDM 솔루션 환경에서는 다양한 프로세스가 존재한다. 데이터(도면,보고서,부품,설계변경)의 작성, 데이터의 수정, 데이터의 검토나 도면의 검토 및 결재, 승인 거부, 데이터와 복사, 전송, 변화들의 프로세스이다. 이러한 프로세스 단위를 일련의 일의 흐름과 데이터를 처리하는 역할까지 수용한 체제가 WorkFlow란 업무이다. 말 그대로 이 영역에서는 WorkFlow Pattern에 근간을 두고 있다.


그림6. WorkFlow Pattern 시퀀스 다이어그램


WorkFlow Engine : JBoss jBPM, OSWorkflow , Bonita , Open for Business Workflow Engine , XFlow
참고 URL : http://www.workflowpatterns.com/patterns/ (매우 정리가 잘 되어 있다.)


3. PDM의 BOM(Bills of Materials) - Whole-Part

부품에 대한 version과 revision및 체크인/체크아웃기능은 BOM영역의 일부에 해당한다.

BOM관리는 BOM 구조 편집기를 사용하여 간편하게 제품구조를 편집, 비교, 전개 등을 할 수 있으며, BOM
관리 영역에서는 사양관리 대안설계 / 유효성 관리 등의 업무 목적에 맞는 다양한 BOM View (2D,3D,Cad View)를 생성할 수 있는 기능도 포함한다.

제품구조는 제품에 대한 사용자의 추상적인 개념을 시각화 한 것이다.
때에 따라서 사용자의 영역에 따라 다양한 시각을 가지게 된다.
마우스를 예로 들어보자 마우스를 마우스 휠, 그리고 레이저, 몸통에 대한 3개의 개념 개체를 가진다.(Building Block) 개념적으로 하위의 부품은 상위의 부품에 계층화된 관계를 지니고 각 부품의 제품에 변경되었을 때 version이 올라가면서 (인터페이스는 같다.) revision이 된다.
이러한 것들은 모두 사용자의 시각에 기초를 두게 된다. 사용자관점이라는 중요한 성질이 포함되어 있다.

BOMPart List, assembly, product configuration 또는 BOM strucrues를 생성관리하고, 이런 구조에 PDM 시스템에 의해 관리되는 설계정보를 연계시킨다.

Whole-Part Pattern

Whole-Part 디자인 패턴은 의미적 단위로 컴포넌트를 모으는(aggregate)데 도움을 준다. 집한 컴포넌트(aggregate component), 즉 Whole 객체는 그것을 구성하는 컴포넌트(constituent component)들, 즉 Part 객체들을 캡슐화한다.

context(정황) : 집합 객체(aggregate object)를 구현해야 한다.

problem(문제) : 거의 모든 소프트웨어 시스템에서 객체들은 다른 객체들의 조합으로 이루어져 있다.

solution(해법) : 더 작은 객체들을 캡슐화하는 컴포넌트를 도입해서, 클라이언트가 컴포넌트의 구성 부분들에 직접 액세스할 수 없도록 막는다. 이 객체들의 기능에 액세스하기 위해 집합 객체의 인터페이스를 통해야 한다. assembly-parts,container-contents,collection-members 의 관계를 통하여 관계를 정의한다.

※. 이와 같이 패턴·랭귀지를 참조하여 응용하는 방법은 "자기의 상황에 따라서 패턴을 선택하고, 거기에 기술되고 있는 추상적인 해결 방법을 나름대로 구체화해서 실천하는 것"이다.

                                                                          그림7. 자동차 CAD 도면

                                                                     그림8. Large Assembly View



4. Struts2의 Proxy, Action Invocation, Interceptor Pattern, Pipe and Filter


Struts2의 아키텍처 개념을 살펴보면 수많은 Intercepto가 액션 인보케이션과 맞물려 있다.
먼저 ActionProxy(Proxy Pattern)가 Action Invocation(Invocation Pattern)을 호출하면 Action Invocation의 invoke()메소드를 호출하고, invoke 메소드는 인터셉터를 찾고 그것의 interceptor()(Interceptor Pattern)메소드를 호출한다. 이 인터셉터의 interceptor() 메소드는 파라미터로 넘어온 액션 인보케이션의 invoke() 메소드를 다시 호출하며 체인이 형성된다.(Pipe and Filter Pattern) , 또한 Struts2는 수많은 Plug-in을 제공하여 Pluggable Pattern을 학습할 수 있는 좋은 오픈프레임웍이다.

                                                                       그림9. Struts2 Architecture

5. MVC 웹프레임웍의 Client-Dispatcher-Server 디자인 패턴

Client-Dispatcher-Server 디자인 패턴은 클라이언트와 서버 간에 디스패처 컴포넌트를 중간에 도입한다. 디스패처는 네임 서비스를 통해 위치 투명성을 제공하며 클라이언트와 서버 간의 통신을 위한 세부 구현을 숨긴다.
웹 MVC로 유명한 Spring MVC, Struts1,2등은 모두 변형된 Client-Dispatch-Server 패턴을 사용한다. 사용자의 요청을 Dispatcher 클래스에 이관하여 적절한 컨트롤러를 찾는다.


이와 같이 패턴이 녹아 있는 프레임웍을 적절하게 선택하거나 패턴을 직접 응용하면 얻게 되는 가치가 상당하다. 이미 검증된 아키텍처 패턴까지 동시에 사용할 수 있으므로, 차 후 발생하게 될 비용의 문제(유지보수,의사소통,인력이동)까지 해결할 수 있다. 또한, 패턴사용의 경험은 꾸준히 재발견해야 하는 시간적인 노력의 인내도 포함하기 때문에 실무에서 패턴의 경험이란 정말 소중한 연구 재산이 된다.



저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
블로그코리아에 블UP하기
  1. Favicon of http://www.arload.net BlogIcon PatternLoader

    2010.06.16 08:59 신고 [수정/삭제] [답글]

    와우 좋은 글이군요.

    다양한 분야의 패턴을 익히고, 실사례를 찾아보고, 회고하고
    다시 나만의 것으로 다듬고, 만들어 나가는 것이 힘이 된다고 느낀 one of them입니다.

    앞으로 갈길이 정말 먼거 같애요. 휴~~ 그래도 동지를 발견하게 되서 반갑네요!! :)

    • Favicon of http://moova.tistory.com BlogIcon MOOVA 무바㏇

      2010.06.17 12:07 신고 [수정/삭제]

      사실 PatternLoader님의 글들을 보고 공감이 되어 매우 반가웠습니다. 실무에 적용하고 다듬고 하는것이 그리 쉽지 않았던것 같아요.

      실무 또는 프로젝트를 진행하면서 이 분야에 관심있어 하는 사람은 별로 없었던 것 같아요.
      그래서 저도 요즘 Eva활동이나 영수님의 활동을 보고 동지애가 많이 느껴집니당.

      랭귀지에 한계를 떠나서 사상의 만남이라고 느껴집니다.ㅎㅎ

      나중에 한국에 Plop같은 학회 하나 만드시면 적극 지원해드리겠습니다.~

  2. Favicon of http://9933.oo.ag BlogIcon 길동무

    2010.10.28 09:51 신고 [수정/삭제] [답글]

    건강㎬정보 <좋은 글 정보 감사합니다.<늘! 건강하시고 행복하시기를 기원합니다. 날마다 좋은날 되세요.

댓글을 남겨주세요.

[세탁소 이야기] 뇌주름 아키텍처 패턴 지향

Posted at 2010.06.14 13:42 // in Architecture // by MOOVA 무바㏇
패턴에 대한 인식을 재정립하고, 패턴 발굴의 확산을 기리며 쉬엄쉬엄 작성해 정리한 글입니다. 

엔지니어들이 쉽게 사용하는 IDE나 벤더 솔루션에서 가끔 
아키텍처 패턴을 응용한 제품을 접하곤 한다.
(필자는 실무에서 객체를 사용한 제품 분석을 통해 패턴감각을 익혔고, 차 후에 아키텍처 패턴을 학습한 경우다. 그 후 특화된 도메인에서 사용된 패턴들이 수없이 많다는 것을 확인했고, 차 후 범용적인 패턴까지도 서로 연결되어 있다고 믿었다. 비 정형화된 패턴과 정형화된 패턴의 만남이었다.- GOF의 디자인패턴과는 다른 이야기.) 이런 것을 발견할 때마다 신기하게 느낀 이유는 대부분 활용한 패턴들이 마치 끈처럼 연결된 듯한 느낌이 들었다. 

한마디로 "연결되어 있다" 였다. 

각 제품에서 활용한 패턴들은 대부분 똑같은데 보이는 결과물은 같은 것도 있었고, 다른 것도 있었고, 비슷한 것도 있었다. 모방은 창조의 어머니라고 하지 않았나?! 그 후 여러 가지 패턴을 조합하여 실무에 적용하려 매우 골머리를 썩힌 기억이 난다.
그 후 계속되는 연쇄작용으로 각 패턴이 연결되기 시작했고 서로 연관을 지을 수 있게 되었다. ( 결국 내 뇌는 뇌 주름만 더해간 고통의 인내과정을 겪었다.)

양보다 질을 따져서 지식적으로 제한되어 있던 패턴을 실무에 접목하고자 노력을 하는 사람들과 연구단체들이 많다. 또한, 실제로 실무에 접목하고 또 발굴해 내려는 분들 또한 많아졌다. ( 매우 반가운 소식이다 ) 그런 노력은 새로운 패턴 발굴에 도움을 줄 것이고 기존 패턴 연구에도 피드백을 줌으로써 패턴의 인식을 변화시킬 것이라 장담한다. 

패턴 대부분은 대부분 업무에 응용할 수 있고 확장할 수 있다. 또한, 정형화된 패턴 이외에 특화된 도메인에서 응용했던 비 정형화된 패턴까지도 한마디로 연결되어 있다는 사실의 발견에 매우 놀라지 않을 수 없다. 나중에 알게 된 사실이지만 이런 패턴의 연결고리를 정리한 것이 Plop에서 제안된 Pattern Map이란 지도였다. 역시 배움의 길은 멀고도 험하다.

- 알고 넘어가기
  1. 무분별한 패턴사용은 사용하지 않음만 못하다.
  2. 패턴의 범주는 이디엄 < 디자인 패턴 < 아키텍처 패턴으로 분류할 수 있다.

    위에 그림은 뇌주름이란 제목의 그림이다. 이처럼 뇌 주름에서도 쉽게 패턴을 찾을 수 있다. 하지만, 본고와는 무관한 그림이다.:)


애피소드. (세탁소 아저씨 이야기)


때문에 자주 지방에 내려갈 때, 전용으로 양복을 맡겼던 세탁소가 하나 있다.
그 세탁소 주인은 30년 동안 HH양복회사에서 일을 했었고 퇴사 후 자신만의 가게를 차렸다고 했다.
그분은 소일거리로 백화점에서 의뢰받은 수공품을 만들고 계셨는데 그 솜씨가 기가 막혔다.
백화점 브랜드이긴 하지만 어디서 설계도면을 가져왔는지
설계도면[각주:1]만 있으면 완전히 똑같은 모조품을 만들 수 있다고 했다. (물론 단순한 그림 쪼가리는 아니다. 3D 캐드 도면이었다.)

이때 그분에게서 들었던 것이
설계패턴이라는 단어였다. 설계패턴은 어딜 가도 동일하니 패턴에 맞게 하면 뛰어난 수공품이 된다는 것이었다. - 난 이것에 놀라지 않을 수 없었다.
의류업계에서도 패턴이 있었다니, 심지어는 패턴을 외워서 모조품까지 만드는 난공불락의 신념도 들어 있었다.( 어찌 됐든 모조 제품은 좋지 않다.:) 


같은 패턴 , 다른 패턴 , 비슷한 패턴.

소프트웨어는 이와 비슷하지만 조금은 대조적이다.

환경이 항상 달라서 똑같은 패턴을 쓰더라도 그 용도와 효용성은 조금씩 차이가 있게 마련이다. 
하지만, 조금씩 차이를 가진 패턴이라도 그 속을 들여다보면 비슷한 카테고리를 찾을 수 있다.  
이것을 정리한 것이 바로 Pattern Map이다.
상황과 환경이 매번 다르지만, Pattern Map을 참고 해서 원류를 찾을 수 있기도 하고, 또 그 속에 포함되어 얽혀 있는 패턴까지도 정리할 수 있다. 더불어 
패턴의 패턴 즉, 패턴의 모태도 찾을 수 있다. 자신과 자신이 속한 기업에 특화된 패턴을 발굴하여 자체 Pattern Map을 개발해 낼 수도 있을 것이다. (현재까지 정리된 Pattern Map의 Pattern들의 개수는 수 없이 많다.)

- 알고 넘어가자! 패턴 랭귀지

패턴·랭귀지는, 건축가의 Alexander가 제창한 지식 기술의 방법이다. Alexander는, 건물이나 거리의 형태에 되풀이되어 드러나는 법칙성을 '패턴'이라고 부르고, 그것을 '언어'로서 공유하는 방법을 고안했다. 그가 목표한 것은, 거리나 건물의 디자인에 관한 공통 언어를 만들고, 누구라도 디자인의 프로세스에 참가할 수 있도록 하는 것이었다.

패턴·랭귀지에서는, 다양한 경험을 패턴이라고 하는 단위에 정리한다. 패턴에는, 디자인에의 '문제'와, 그 '해결'의 발상이 한 벌이 되어서 기술되고, 거기에 이름을 써넣을 수 있다. 패턴·랭귀지를 사용하는 사람에게는, 자기의 상황에 따라서 패턴을 선택하고, 거기에 기술되고 있는 추상적인 해결 방법을, 나름대로 구체화해서 실천하는 것이 요구된다.

이러한 패턴·랭귀지의 사고방식은, 건축의 분야 이외에, 소프트웨어 개발을 넘어서, interaction·디자인이나 조직 디자인, 교육의 디자인 등에 응용되어 사용되고 있다. 패턴·랭귀지의 사고방식은, 실무 경험 지식을 공통 언어로 승화하는 방법으로서, 앞으로도 여러 가지 분야에 응용되면 좋을 것이다.




< 다빈치의 설계 패턴 中 >

보통 엔지니어들에게 확장성이 좋기로 소문이 나 있는 제품에는 고 수준의 패턴이 숨어 있으므로 쉽게 참고 해서 분석해 낼 수 있다. 패턴의 적절한 활용에 대해 어떻게 어디서 참고해야 하는지 당장 궁금하다면 널리 알려진 오픈소스를 한번 분석해 보면 된다. 또한, 정형화된 패턴에 벗어난 범주 즉, 비정형적인 패턴일지라 하더라도 결국 원류와 근본이 있기 마련이고 결국 패턴의 모태와 연결되어 있다. ( 특화된 도메인에서 지역패턴을 만들어서 국지적으로 사용했더라도 이미 원류는 범용적인 패턴범주에 속한다. )

그림1에서 볼 수 있듯이 특화된 도메인에서 개발한 패턴은 범용적인 패턴의 모태에서 파생되어 있다.



  1. PLM영역에서 설계도면이란 그렇게 간단하지가 않다. 단순히 종이 설계도면이나 UML로 만든 2D모양의 그림이 아니다. 대부분 3D툴을 이용해(캐드,마야등) 상위레벨과 하위레벨을 연결지을 수 있으며 평행선상의 구조적 부분도 연쇄 가능하다. 구조적 부분에서도 무척이나 건축분야와 닮아 있다.일반적인 종이설계문서와는 다르니 참고하자. [본문으로]
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
블로그코리아에 블UP하기
  1. Favicon of http://minimonk.net BlogIcon 구차니

    2010.06.14 17:52 신고 [수정/삭제] [답글]

    악! 어려워요!
    음.. 디자인 패턴이라는 용어는 잘 모르겠지만 return to source 라고 하면 조금 비슷하려나요?
    핵심을 뚫는 원리를 알고 있으면 좋을텐데 그걸 시원하게 알기가 어려우니 안타까워요

    • Favicon of http://moova.tistory.com BlogIcon MOOVA 무바㏇

      2010.06.15 11:00 신고 [수정/삭제]

      흠..글쎄요 '원시로의 복귀'
      그것도 비슷한 감이 오지만 패턴과는 좀 다른 이야기일껍니다. 패턴은 원시의 소스를 중시함에 덧붙여 전체적인 구조를 바라보는 영역에 더 가깝거든요.

      디자인패턴은 'GOF의 디자인 패턴'이 유명합니다. 특정 언어에 국한되지 않고 볼 수 있는 소트프웨어 필독서죠.

      위에 서술된 아키텍트 패턴은 좀 더 상위의 개념입니다. 보통 개발하시는 분들이 GOF의 23가지 패턴이 전부라고 생각하는 분들이 많습니다. 하지만 패턴은 수천 수만가지죠.(비공식적인것 까지 합하면)

      패턴은 학습도 중요하지만 학습만 가지고는 실제로 감이 잘 오지 않습니다. 실무에 적용해보고 감을 찾고 발전해 나갈 때 핵심을 뚫은 자신만의 비법을 발견할 수 있을 것 같군요. 차근 차근 알아나가는 것이 중요한것 같아요

      We have to saw the forest!

  2. Favicon of http://9933.goofy.kr BlogIcon 지극정성

    2010.10.28 09:52 신고 [수정/삭제] [답글]

    건강㎴정보▒ <좋은 글 정보 감사합니다.<늘! 건강하시고 행복하시기를 기원합니다. 날마다 좋은날 되세요.

댓글을 남겨주세요.

티스토리 툴바