블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

License Key Management

아키텍쳐 | 2013. 8. 1. 23:34 | Posted by 조대협

License Key Authentication에 대한 간단한 아키텍쳐



Anonymous client에 대한 라이센스 발급 방식


1. 클라이언트에서, CPU ID나 MAC 주소와 같은 signature를 이용하여 hash 값을 만들어서 서버에 라이센스 키를 요청

2. 서버에서는 이 내용을 바탕으로 새로운 키를 생성하고, 이 키를 private key로 encrypt하여 클라이언트에 보냄

3.4. 클라이언트는 이 메세지를 public key로 풀어서 text 내용은 미리 상호 약속한 내용 (예를 들어 CPU ID 등)


* request에 클라이언트를 인증하기 위한 user id,password등을 같이 실어서 보낼 수 있음

* 라이센스 키 체크는 클라이언트에서 저장된 public key로 풀어서 일치 하지 않으면 다음 코드 부분으로 진행을 못하도록 애플리케이션 처리를 해야 하는데, decompile등을 통하여 뚤릴 수 있기 때문에, 코드 난독화 솔루션이 필수로 필요한다.

* 매번 키 인증을 받을 수 있지만 처음 Activation이 된 후에는 local에 저장된 라이센스를 가지고 비교해도 된다. 이 라이센스는 client의 cpu id등과 binding되기 때문에, 어짜피 옮기더라도 사용이 불가능하다.


Serial #를 이용한 방식

Serial #를 이용한 방식은 MS Windows와 같은 방식이다.

Serial #로 인증을 하고 라이센스를 내려주는 방식이고, 이미 사용된 Serial #는 disable시킬 수 있다.



참고할만한 오픈소스 : http://truelicense.java.net/

상용 : safenet (시나리오가 좋음)



'아키텍쳐' 카테고리의 다른 글

소프트웨어 개발팀의 구조  (0) 2013.11.01
Technical Debt  (2) 2013.10.30
License Key Management  (0) 2013.08.01
암호화 알고리즘 속도 비교 (대칭키)  (0) 2013.07.17
API platform  (0) 2013.07.17
API design for client which support limited HTTP method  (0) 2013.07.10
본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

Maven을 이용한 Jersey + Spring + MyBatis 기반의 REST 애플리케이션 개발


조대협(http://bcho.tistory.com)

빌드 스크립트 설정

개발 환경에서 가장 중요한 빌드에서 부터 알아보자, 우리가 정의하는 빌드는 다음과 같다.

"빌드란, 실행 환경에 맞춰서 소스 코드를 실행 가능한 형태의 바이너리로 변경 및 패키징 하는  일련의 과정을 정의한다."

단순하게 소스코드를 바이너리로 바꾸는 컴파일이 될 수 도 있고, 실행에 필요한 각종 리소스 (이미지, 각종 설정 파일등)을 실행 환경(서버 주소등)에 맞춰서 같이 패키징 하는 과정을 이야기 한다.

이 빌드 여러개의 연속된 작업을 포함하기 때문에, 보통 스크립트를 기반으로 수행이 되는데, C/C++의 경우에는 make 기반의 빌드 스크립트가, 자바의 경우에는 ant maven이 널리 사용된다.

 

ant vs maven

자바 기반에서 현재 가장 인기 있는 빌드 스크립트 툴은 ant maven이다. 요즘은 상당 부분이 maven으로 넘어갔다. 그렇다면 이 각각 빌드 스크립트 툴의 장점은 무엇일까?

 

유연성과 관리

ant의 경우에는 자유도가 상당히 높다. 파일을 복사하거나, 쉘 명령을 실행할 수 도 있다. 스크립트내에서 빌드, 패키징 은 물론이고 배포,테스트, 미들웨어에 대한 기동이나 정지까지 모든 것이 가능하다. 자유도가 높다는 이야기는 반대로 이야기 하면 표준화가 어렵다는 이야기가 된다.

잘 관리 하지 않으면 프로젝트 마다 또는 팀마다 빌드 스크립트가 제각각이다. 표준화가 되지 않은 빌드 스크립트는 새롭게 합류하는 개발자들에게 별도의 learning curve를 요구하게 되고, 실수를 유발한다.

또한 복잡한 형태의 빌드 프로세스를 요구 하는 개발의 경우에는 빌드 스크립트 자체를 만드는 것 자체가 복잡한 일이 된다.

 

maven은 이런 단점을 보완해서 개발되었다.

maven은 템플릿 기반으로 빌드 스크립트를 구성한다. 템플릿 기반이란, 특정 애플리케이션 타입에 대해서, 디렉토리 구조, 빌드 프로세스등이 모두 정해져있다. 그래서 애플리케이션 타입에 따라서 템플릿만 골라서 사용하게 되면 누구나 같은 디렉토리 구조에서 같은 빌드 프로세스에서 개발하게 된다. 그래서 learning curve가 상대적으로 낮고, 누구나 표준화된 환경에서 빌드가 가능하다.

반대로, 템플릿 이외의 기능에 대해서는 유연성이 떨어져서, 마음대로 무엇인가를 추가 하는 것이 어렵다. (예를 들어서 파일을 특정 클라우드에 복사한다던지). 물론 maven도 플러그인이라는 기능을 통해서 템플릿의 기능을 확장할 수 있는 기능을 제공하지만, 이 플러그인이라는 것 자체의 개발이 쉽지가 않기 때문에 템플릿의 기능을 벗어나는 순간 learning curve가 급속하게 올라간다. 다행이도 근래에는 maven에서 사용할 수 있는 플러그인들이 많이 있기 때문에, 이런 문제들은 상대적으로 줄어들고 있다.

 

의존성 관리

다음으로 라이브러리에 대한 의존성에 대해서 고민해볼 필요가 있다.

ant의 경우에는 소스코드와 라이브러리 그리고 기타 의존된 리소스 파일 (설정 파일, 스크립트, 이미지)등이 디렉토리에 있는 것을 가정하고 빌드를 진행한다. 전적으로 이러한 파일들을 챙기는 것은 개발자와 빌드 메니져의 역할인데, 이러다 보니 특히 라이브러리 관련해서 문제가 발생한다.

예를 들어 원 소스는 spring 3.0 라이브러리를 바탕으로 개발이 되었는데, 어떤 개발자는 spring 2.0을 사용해서 컴파일하고 어떤 개발자는 spring 3.1을 사용해서 컴파일 하는, 이런 일들이 발생할 수 있다는 것이다. (실제로 종종 발생하는 일)

maven의 경우 재미있는 것은 이렇게 컴파일을 하는 데 필요한 라이브러리에 대한 의존성을 정의하고, 정확한 버전을 정의하면, 컴파일 타임에 원격에 있는 repository로 부터, 명시된 버전의 라이브러리를 다운 받아서 컴파일과 패키징을 진행하기 때문에, 라이브러리의 버전 불일치가 발생될 염려가 없다. 또한 오픈소스등에서 작성한 well-known 라이브러리가 아니더라도, 자체적으로 repository 시스템을 구축하여, 팀내에서 개발한 라이브러리를 배포해놓고 사용할 수 있다. 이런 시나리오는 여러개의 모듈을 동시에 개발하는 프로젝트 팀의 경우, 모듈간의 의존 관계에서 오는 문제를 해결할 수 있는 좋은 방안이 된다.

 

근래에는 이런 장점 때문에, 유연성이 다소 적더라도 maven이 많이 사용되는 추세이다.

이제 부터 간단하게 maven 기반의 빌드 방법에 대해서 설명해보도록 한다.

여기서 소개하는 빌드 스크립트는 web application 기반의 빌드 스크립트로, 다음과 같은 시나리오를 구성할것이다.

 

-       Jersey 기반의 JSON/HTTP REST API 지원, Spring DI 기반으로 비지니스 로직 구현, MyBatis를 이용한 데이타 베이스 접근

-       빌드 환경은 개발자 PC local, 개발 환경인 dev, 스테이징 환경 stage,검증 환경인 qa 그리고 실 운영 환경이 production 환경으로 구성된다.

-       빌드 스크립트를 통해서 개발자는 빌드,배포 및 Tomcat상에서 실행 및 테스트를 진행할 수 있어야 한다.

 

maven을 이용한 Jersey + Mybatis + Spring 기반의 개발 환경 구축하기

먼저 maven 이용해서 개발을 하려면 프로젝트를 만들어야 한다. 프로젝트는 특정한 애플리케이션 타입에 맞는 템플릿을 이야기 한다. 디렉토리 구조나 빌드 프로세스들이 미리 정해져 있는 개발을 위한 하나의 비어 있는 틀이다.

 

mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false  -DgroupId=spring-tutorial -DartifactId=terry -Dversion=1.0-SNAPSHOT -Dpackage=terry.spring.tutorial.ch1

 

이렇게 하면 하나의 비어 있는 프로젝트가 생성이 된다.

프로젝트가 생성이 되면, 해당 애플리케이션을 개발하는데 필요한 디렉토리 구조나 필요한 라이브러리 들이 자동으로 다운로드 되서 설치 된다. 프로젝트는 java web application 지원하는 war형태의 프로젝트이다.

maven 앞서도 설명하였듯이, 프로젝트를 생성할때, 프로젝트의 타입을 정할 있다. 여기서는 war 기반의 개발을 하기 위한 web-app 형태의 프로젝트를 생성하였는데, 프로젝트 타입은 -DarchetypeArtificatId 지정할 있다. (여기서는 가장 기본적은 maven-archetype-webapp 사용하였다.)

 

그럼 생성된 프로젝트의 모양을 살펴보자. 생성된 프로젝트의 디렉토리 구조는 다음과 같다.

 


pom.xml

ant ant.xml이나 make makefile처럼 build 대한 모든 configuration 지정한다.

 

다음으로 생성된 디렉토리를 살펴보자 src에는 *.java 소스 파일과 webapp (war root)디렉토리에 들어가는 내용들과 각종 설정 파일들이 들어간다. 상세한 내용을 살펴보자.

 

src/main/resource

디렉토리는 각종 설정 파일이 위치하는 곳이다. 디렉토리 안에 있는 파일을 class path 포함된다. war 경우에는 디렉토리에 있는 파일들은 WEB-INF/classes 디렉토리 아래에 그대로 들어간다.

예를 들어 src/main/resources/sqlmap/sqlmapper.xml 파일은 컴파일 후에, war파일내의 WEB-INF/classes/sqlmap/sqlmapper 저장된다.

 

src/main/webapp

Web resource 해당 하는 부분이다. war안에 / 디렉토리에서 부터 들어가는 html 각종 리소스 파일들을 모두 정의하낟.

 

그런데, 정작 보면 java 코드를 넣을 소스 디렉토리가 없다.

src/main/ 디렉토리 밑에 java 디렉토리를 하나 만들자.

아래 화면은 src/main/java 디렉토리를 만들고, terry.restapi.model.ContactVo.java 구현한 디렉토리 구조이다.

 

 

아래 코드는 Contact.java 클래스로, 간단하게 사용자의 이메일,이름과 전화번호를 저장하는 VO 클래스 이다.

package terry.restapi.model;

import javax.xml.bind.annotation.XmlRootElement;

 

@XmlRootElement(name="Contact")

public class ContactVo {

        String email;

        String name;

        String phone;

        public String getEmail() {

               return email;

        }

        public void setEmail(String email) {

               this.email = email;

        }

        public String getName() {

               return name;

        }

        public void setName(String name) {

               this.name = name;

        }

        public String getPhone() {

               return phone;

        }

        public void setPhone(String phone) {

               this.phone = phone;

        }

       

}

 

여기 까지 진행을 했으면, war 파일이 어떻게 만들어지는지 테스트를 해보자

% mvn package 실행하면

${HOME} 디렉토리 아래 /target/terry.war 파일이 생성된 것을 있다.

파일을 풀어보면 앞에서 만든 WEB-INF/classes/terry/restapi/model/Contact.class 패키징 되어 있는 것을 확인할 있다.

 

지금까지 간단하게 maven 프로젝트를 만들고, 소스코드를 넣고, 웹에 관련된 리소스를 정의한후에, 컴파일 war 패키징을 해보았다.

 

maven ant make 처럼 일종의 build target 제공하는데, maven에서는 이를 goal이라고 한다. pom.xml 스크립트에 따라서 다양한 goal 정할 있으나, maven에서 미리 정해져 있는 goal 중요한 goal들은 다음과 같다.

 

mvn compile : 컴파일 수행. (프로젝트내의 java 코드를 컴파일 한다.)

mvn package : jar,war,ear pom.xml에서 정해진 형태로 파일로 패키징. (컴파일을 한후, 프로젝트 내용에 따라서 패키징을 수행한다.)

mvn test : JUnit 테스트 수행.

mvn install : local repository (PC내의 디렉토리)에 패키징된 파일을 저장

mvn deploy  : remote repository (Nexus)에 패키징된 파일 저장

mvn clean : 컴파일 내용 모두 삭제

 

여기서 compile,package,clean 등은 거의 모든 빌드 스크립트에서(ant ) 공통적으로 지원하기 때문에 별도의 설명을 하지 않는다.

install deploy 대해서 살펴보자

 

앞서도 설명했듯이, maven library 대한 dependency 지정하고 스크립트를 수행하면, repository라는 곳에서 해당 라이브러리들을 읽어온다. 그러면 repository 어디일까?

mvn install 컴파일된 패키지들를 local pc 라이브러리 저장소에 배포한다. 다른 프로젝트가 라이브러리를 사용하고자 한다면, local pc 내에서 라이브러리를 찾아서 같이 컴파일 있다. 그러나 local pc에만 배포가 되었기 때문에 다른 사람은 라이브러리를 참조해서 사용할 없다. 그래서 다른 사람이 라이브러리를 참조할 있게 하려면, 네트워크 상의 공용 repository 필요하다. 이러한 공용 repository 라이브러리를 배포하는 명령이 mvn deploy이다.

spring이나 기타 라이브러리등은 각각의 공용 repository 가지고 있고, mvn 역시 이러한 repository list 기반으로 해서 라이브러리를 로딩하는데, 우리 회사나 팀에서만 사용할 있는 repository 별도로 구축하려면 어떻게 해야 할까? nexus라는 제품을 설치하면, 사내에 전용 repository 서버를 구축할 있다. (http://www.sonatype.org/nexus/)

 

maven 프로젝트 생성, 디렉토리 구조의 이해 그리고 maven 통한 컴파일 패키징에 대한 설명이 끝났다. 그러면 이제 부터, Jersey + Spring + Mybatis 개발하기 위한 설정을 해보자, 먼저 pom.xml 위의 세가지 프레임웍을 사용하기 위해서 라이브러리를 정의해야 한다.

라이브러리 정의는 <dependencies>엘리먼트 아래에 라이브러리 <dependency> 라는 엘리먼트로 정의한다.

 

<dependencies>

    <!--  jersey dependency -->

        <dependency>

            <groupId>com.sun.jersey</groupId>

            <artifactId>jersey-server</artifactId>

            <version>1.17</version>

        </dependency>

: 아래 생략

 

위의 3가지 프레임웍을 사용하기 위해서 필요한 dependency 다음과 같다.

정확하게 여기서는 JSON/REST 이용하기 위해서 Jersey 사용하고, Spring 3.1 에서 Dependency Injection만을 이용할것이며, MyBatis MySQL 사용을 위해서 MySQL JDBC Driver 사용할것이다.

아래는 지면 관계상, dependency 테이블로 정리해놨다. 글의 뒷부분의 pom.xml 전문을 따로 첨부했으니, 참고하기 바란다.

 

프레임웍 이름

groupId

artifactId

version

Jersey

com.sun.jersey

jersey-server

1.17

com.sun.jersey

jersey-servlet

1.17

com.sun.jersey

jersey-json

1.17

Spring

org.springframework

spring-core

3.1.1.RELEASE

org.springframework

spring-context

3.1.1.RELEASE

org.springframework

spring-beans

3.1.1.RELEASE

org.springframework

spring-web

3.1.1.RELEASE

org.springframework

spring-webmvc

3.1.1.RELEASE

Spring + Jersey 연결

com.sun.jersey.contribs

jersey-spring

1.17

Spring + Mybatis 연결

org.mybatis

mybatis-spring

1.0.1

MySQL JDBC

mysql

mysql-connector-java

5.1.24

 

여기서 spring+Jersey 연결에 사용되는 jsersy-spring artifact 잠시 살펴볼 필요가 있다. jersey-spring artifact 자체적으로 spring 프레임웍을 포함하고 있기 때문에, 우리가 정의한  Spring framework 중복 가능성이 있다. 그래서, jersey-spring dependency 의해서 spring framework 중복적으로 가지고 오지 않도록 해당 모듈들을 다음과 같이 exclude(제외)하도록 선언한다.

<!--  jersey + spring depdendency  -->

     <dependency>

                       <groupId>com.sun.jersey.contribs</groupId>

                       <artifactId>jersey-spring</artifactId>

                       <version>1.17</version>

                       <exclusions>

                              <exclusion>

                                      <groupId>org.springframework</groupId>

                                      <artifactId>spring</artifactId>

                              </exclusion>

                               <exclusion>

                                      <groupId>org.springframework</groupId>

                                      <artifactId>spring-core</artifactId>

                              </exclusion>

                              <exclusion>

                                      <groupId>org.springframework</groupId>

                                      <artifactId>spring-web</artifactId>

                              </exclusion>

                              <exclusion>

                                      <groupId>org.springframework</groupId>

                                      <artifactId>spring-beans</artifactId>

                              </exclusion>

                              <exclusion>

                                      <groupId>org.springframework</groupId>

                                      <artifactId>spring-context</artifactId>

                              </exclusion>

                       </exclusions>

               </dependency>

자아. 이제 빌드 스크립트는 Jersey + Mybatis + Spring DI 이용한 개발 준비가 되었다. 그러면 실제 코딩에 들어가 보자

java/terry/restapi 아래, MVC 모델에 맞춰서 service,dao,model 디렉토리를 만들고 아래와 같이 클래스들을 구현한다.

 

java/terry/restapi/dao/ContactDao.java

package terry.restapi.dao;

 

import java.util.HashMap;

 

import terry.restapi.model.ContactVo;

 

public interface ContactDao {

       

        public void create(ContactVo contact);

        public ContactVo get(String email);

        public void delete(String email);

        public void update(String email,ContactVo contact);

}

 

java/terry/restapi/dao/ContactImpl.java Mybatis 이용한 contact 테이블에 대한 CRUD 구현한다.

package terry.restapi.dao;

 

import org.mybatis.spring.support.SqlSessionDaoSupport;

 

import terry.restapi.model.ContactVo;

 

public class ContactDaoImpl extends SqlSessionDaoSupport implements ContactDao {

 

        public void create(ContactVo contact) {

               // TODO Auto-generated method stub

 

        }

 

        public ContactVo get(String email) {

               // TODO Auto-generated method stub

               ContactVo contact = (ContactVo)getSqlSession().selectOne("contactdao.getUserByEmail", email);

               return contact;

        }

 

        public void delete(String email) {

               // TODO Auto-generated method stub

 

        }

 

        public void update(String email, ContactVo contact) {

               // TODO Auto-generated method stub

 

        }

 

}

 

java/terry/restapi/model/ContactVo.java 앞에서 이미 구현했기 때문에, 앞의 코드 참고

 

다음은 Jersey 이용한 REST API 구현이다. 아래 코드는 편의상 create,select,update 구현하였다.

package terry.restapi.service;

 

import javax.ws.rs.Consumes;

import javax.ws.rs.GET;

import javax.ws.rs.POST;

import javax.ws.rs.PUT;

import javax.ws.rs.Path;

import javax.ws.rs.PathParam;

import javax.ws.rs.Produces;

import javax.ws.rs.core.MediaType;

import javax.ws.rs.core.Response;

 

import terry.restapi.dao.ContactDao;

import terry.restapi.model.ContactVo;

 

@Path("/contact")

public class ContactService {

        static ContactDao dao = null;

        public void setContactDao(ContactDao dao){this.dao = dao;}

        public ContactService(){

               //if(dao == null)      setContactDao(new ContactDao());

        }

       

        /**

         * Create Contact Record

         * @param contact

         * @return

         */

        @POST

        @Consumes(MediaType.APPLICATION_JSON)

        public Response create(ContactVo contact){

               dao.create(contact);

               return Response.status(200).entity(contact).build();

 

        }

        /**

         * Query Contact record by email id

         * @param email

         * @return

         */

        @GET

        @Produces(MediaType.APPLICATION_JSON)

        @Path("{email}")

        public ContactVo get(@PathParam("email") String email){

               return dao.get(email);

        }

        /**

         * Upadte Contact Record by email

         * @param email

         * @param contact

         * @return

         */

        @PUT

        @Path("{email}")

        @Consumes(MediaType.APPLICATION_JSON)

        @Produces(MediaType.APPLICATION_JSON)

        public Response  update(@PathParam("email") String email, ContactVo contact){

               dao.update(email, contact);

               return Response.status(200).entity(contact).build();

        }

}

 

 

자아 코드 구현이 끝났다.

이제 코드를 실행하기 위해서는 다음과 같은 추가 작업이 필요하다.

/webapp/WEB-INF/config/spring-context.xml Spring Bean 정의하기 위한 spring context 파일 작성

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xmlns:context="http://www.springframework.org/schema/context"

        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

 

        <!--  load configuration file -->

         <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

          <property name="locations">

           <value>/WEB-INF/config/config.properties</value>

          </property>

         </bean>

         

        <!--  create rest service object and inject dao -->

        <bean class="terry.restapi.service.ContactService" id="contactService">

               <property name="contactDao" ref="contactdao" />

        </bean>

        <!-- declare dao object  -->

        <bean class="terry.restapi.dao.ContactDaoImpl" id="contactdao">

               <property name="sqlSessionFactory" ref="sqlSessionFactory" />

        </bean>

       

        <!--

               mybatis configuration

               sqlSessionFactory & sqlSessionTemplate are required

         -->

        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

               <property name="dataSource" ref="dataSource" />

               <property name="configLocation" value="/WEB-INF/config/mybatis-config.xml"/>

        </bean>

        <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">

               <constructor-arg ref="sqlSessionFactory" />

        </bean>

       

        <!-- 

               data source configuration

               for testing purpose , it uses simple jdbc datasource

         -->

        <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">

               <property name="driverClass" value="${mybatis.jdbc.driverclass}"/>

               <property name="url" value="${mybatis.jdbc.url}" />

               <property name="username" value="${mybatis.jdbc.username}" />

               <property name="password" value="${mybatis.jdbc.password}" />

        </bean>

</beans>

 

그리고 위의 파일 내용을 보면, /WEB-INF/config/config.properties라는 파일을 읽게 되어 있는데, 여기에는 MYSQL DBMS 접속에 필요한 URL,PORT,사용자 id,passwd등이 들어간다.

/webapp/WEB-INF/config/config.properties

 

mybatis.jdbc.driverclass=com.mysql.jdbc.Driver

mybatis.jdbc.url=jdbc:mysql://localhost:3306/development

mybatis.jdbc.username=developer

mybatis.jdbc.password=developer

 

/webapp/WEB-INF/config/mybatis-config.xml MyBatis 사용하는 데이타 베이스 연결 정보등의 설정 파일

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "HTTP://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

 <settings>

  <setting name="cacheEnabled" value="false" />

  <setting name="useGeneratedKeys" value="true" />

  <setting name="defaultExecutorType" value="REUSE" />

 </settings>

 

 <mappers>

  <mapper resource="sqlmap/ContactDao_map.xml" />

 </mappers>

</configuration>

 

/webapp /WEB-INF/web.xml 초기에 Spring Context Jersey framework 로딩하기 위한 설정

<!DOCTYPE web-app PUBLIC

 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"

 "http://java.sun.com/dtd/web-app_2_3.dtd" >

 

<web-app>

  <display-name>Archetype Created Web Application</display-name>

 

        <!--  load spring context configuration -->

        <context-param>

               <param-name>contextConfigLocation</param-name>

               <param-value>

                       /WEB-INF/config/spring-context.xml

                       <!-- /WEB-INF/config/mybatis-context.xml -->

               </param-value>

        </context-param>

 

        <!--  load listener  -->

        <listener>

               <listener-class>

                       org.springframework.web.context.ContextLoaderListener

               </listener-class>

        </listener>

       

        <!--  configure jersey/JSON servlet -->

        <servlet>

          <servlet-name>Jersey Web Application</servlet-name>

               <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>

                     <init-param>

                              <param-name>com.sun.jersey.config.property.resourceConfigClass</param-name>

                              <param-value>com.sun.jersey.api.core.PackagesResourceConfig</param-value>

                 </init-param>

                 <init-param>

                  <param-name>com.sun.jersey.config.property.packages</param-name>

                  <param-value>terry.restapi</param-value>

                </init-param>

                <init-param>

                              <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>

                              <param-value>true</param-value>

                       </init-param>

                          <load-on-startup>1</load-on-startup>

            </servlet>

       

            <servlet-mapping>

               <servlet-name>Jersey Web Application</servlet-name>

                <url-pattern>/*</url-pattern>

            </servlet-mapping>

</web-app>

 

/resources/sqlmap/ContactDao_map.xml MyBatis 실행하는 SQL 들어 있는 SQL Mapper 파일 작성

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

 

<mapper namespace="contactdao">

        <!-- 

               select user by email id

        -->

        <resultMap type="terry.restapi.model.ContactVo" id="resultmap.terry.restapi.model.ContactVo">

               <id column="email" property="email"/>

               <result column="name" property="name"/>

               <result column="phone" property="phone"/>

        </resultMap>

        <select id="getUserByEmail" parameterType="String" resultMap="resultmap.terry.restapi.model.ContactVo">

               select email,name,phone

               from contact_table

               where email = #{email}

        </select>

</mapper>

 

여기 까지 구현하였으면 디렉토리는 다음과 같은 형태가 된다..


 

이제 mvn package 명령어를 이용하여 war 파일을 만들고, tomcat 배포한후,

MySQL DB 아래와 같은 scheme contact_table 만들면 실행을 위한 준비가 끝난다.

 


수동으로 Tomcat 설치하고 실행하고 배포하면 까다롭기도 하거니와, 개발자 마다 다른 버전이나 다른 설정을 있기 때문에,  이번에는 mvn 스크립트내에서 빌드 과정내에, Tomcat 자동으로 기동 시키고 배포하는 스크립트를 추가해보자.

 

과정은 maven 플러그인이라는 기능을 이용하면 된다.

pom.xml <project> 엘리먼트 아래 다음과 같은 내용을 추가한다.

<build>

    <finalName>restapi</finalName>

         <plugins>

              <!-- 

                tomcat7 plugin

                caution!!. mvn tomcat:xxx will invoke default plugin

                do. mvn tomcat7:xxx

                -->

              <plugin>

                          <groupId>org.apache.tomcat.maven</groupId>

                  <artifactId>tomcat7-maven-plugin</artifactId>

                  <version>2.1</version>

                          <configuration>

                                 <warSourceDirectory>${basedir}/target/${artifactId}</warSourceDirectory>

                          </configuration>

             </plugin>

         </plugins>

  </build>

위의 내용은 tomcat 7 플러그인을 추가하여, 빌드 후에 tomcat 기동할 있게 해주며, tomcat 실행시 ${basedir} (pom.xml 있는 프로젝트의 루트 디렉토리)아래 /target/terry 디렉토리를 war root 디렉토리로 인식하고 기동하게 한다.

${artifactid} maven 프로젝트 생성시 지정했던 프로젝트의 이름으로, war 빌드 스크립트는 별도의 지정이 없는한, war파일명을 ${artifactid}.war ${basedir}/target/ 아래 생성한다. 또한 컴파일 과정에서 war파일이 풀린 모든 파일들을 위치해놓는다.

 

자아 이제 모든 스크립트가 완성이 되었다.

%mvn package tomcat7:run

실행해보자

package 컴파일 war파일과 war 파일이 풀린 모든 파일들(exploded war) 만들게 하고

tomcat7:run tomcat7 위에서 만든 exploded war파일들을 읽어서 기동하게 한다.

 

이제 로컬 환경에서 배포와 실행이 되었으면 REST API 호출하여 테스트를 해보자. 간단한 테스트를 위해서 Google Chrome Browser Advanced REST Client 사용하였다.

 

 



 

 

본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 강대권 2013.04.19 06:56  댓글주소  수정/삭제  댓글쓰기

    좋은 아티클 감사합니다.
    실습 진행하면서 보니 dependency 테이블에 mybatis에 관한 내용이 누락되어 있습니다
    정정 부탁 드립니다

  2. 이민영 2013.05.15 21:14  댓글주소  수정/삭제  댓글쓰기

    예제를 보고 작업을 해보았는데..
    mybatis-config.xml 에서
    Error occured processing XML 'Connection timed out: connect' See Error log for more detail 이라는 에러가 뜨는데 원인좀 알수 있을까요 ..? ㅠㅠ

    DocType 선언부에 the file cannot be validated as there was a connection problem 이라는 워닝도 같이 뜨고 있습니다. 혹시 뭐가 잘못된지 아시겠으면 댓글좀 부탁드릴게요

Apache Camel Error Handling

아키텍쳐 /EAI | 2013. 2. 20. 00:54 | Posted by 조대협


Camel의 에러처리

먼저 Camel의 에러 타입에 대해서 살펴보자. 이 에러 타입은 Camel 이 아니더라도, 일반적인 integration 시나리오에서는 공통적으로 존재하는 에러 타입으로, 복구 가능한 에러와 복구 불가능한 에러가 있다.

에러의 종류

복구 가능한 에러 (Recoverable error)

복구 가능한 에러는 다시 시도 했을 경우 정상 처리 할 수 있는 에러이다. 예를 들어 순간적으로 네트워크 Connection이 끊어졌을때는 대부분, 다시 시도 하면 재처리가 가능하다.

복구 불가능한 에러 (Irrecoverable error)

복구 불가능한 에러는 내부 로직 문제나 잘못된 데이타 입력으로 인하여 다시 시도했을때에도 에러가 예상되는 에러를 복구 불가능 에러로 분리한다.

Camel에서 수신 컴포넌트에서부터 전달되는 메세지는 org.apache.camel.Exchange라는 클래스를 이용해서 전달이 되는데, 에러가 발생하면, 이 클래스에 에러를 Binding 시킬 수 있다. 에러 처리는 Camel Route 흐름에 따라서 Channel이라는 단계에서 처리된다. (Channel의 개념은 뒤에서 설명)

Recoverable 에러의 경우에는 Throwable이나 Exception 으로 정의하고, Exchange.setException() 메서드를 통해서 바인딩 한다.

Irrecoverable 에러의 경우에는

Message msg = Exchange.getOut();

msg.setFault(true)

로 설정해주면, 이 에러는 irrecoverable 에러로 정의된다이렇게 발생된 에러는 어떻게 처리 될까? 에러 처리 정책을 결정하는 ErrorHandler에 정의된 정책에 따라서 처리 된다.

ErrorHandler

어떻게 Error 가 처리 되는지를 설명하기 위해서 앞서 설명했던 Camel Route 흐름을 조금 더 자세하게 살펴보자




Camel은 앞서도 설명하였지만, 메세지를 수신 받는 Component, 이를 처리하는 Processor 그리고, 수신 시스템으로 송신하는 Component로 구성이 된다. 이 세 요소 사이에 메세지가 흘러가는데, 이 각각의 구간을 Channel이라고 정의한다. Channel은 단순히 논리적인 구간만이 아니라, 메세지가 흘러가는 길목으로 이 부분을 통해서 에러 처리나 모니터링 등의 (injection과 같은) 작업을 수행할 수 있다. 즉 에러 처리가 여기서 발생한다

에러가 발생하면, 에러는 전단계의 Channel로 넘어가게 되고, 정의된 ErrorHandler의 정책에 따라서 에러가 처리 된다. 즉 위의 그림에서 Processor에서 에러가 발생하면, Exception[1] Channel 부분으로 넘어가게 되고, 정의된 ErrorHandler에 의해서 처리된다. 마찬가지로 (Outbound) Component에서 에러가 발생하면 [2] Channel로 넘어가서 여기서 에러가 처리된다.

그러면 실제로 에러를 처리 하는 ErrorHandler를 살펴보자. Camel은 크게 5개의 Error Handler를 가지고 있다.


(1) DefaultErrorHandler

가장 일반적으로 사용되는 ErrorHandler이다. DefaultErrorHandler의 경우 에러가 발생하면, 앞단계로 Error Propagation한다.

, Processor에서 에러가 발생하면 앞단계의 Inbound Component Error propagte하고 Route process를 끝낸다. Inbound Component의 경우 내부적으로 자체 Error 처리 로직을 가지고 있는 경우가 있다. (File,FTP,DB Component의 경우) 이 컴포넌트들은 에러를 받으면 에러 처리를 한 후에 Route를 종료 시킨다.

만약 특정 에러가 발생하였을 때, Route를 정지 시키지 않고 진행을 하고 싶다면, Error 처리를 한 후에 뒤로 진행하도록 할 수 있다.

다음 코드를 보자

from(“src:start)
 .process(new MyProcessor())
 .to(“des:end”);

 

onException(StepBackException.class).log(“error sent back to caller”);

onException(HandledErrorException.class).handled(true).setBody(constant(“error handled”)).to(“des:errorhandle)

onException(IgnoreErrorException.class).continue(true);

 

이 코드는 별도의 ErrorHandler를 지정하지 않았기 때문에, Default Handler로 작동하고,

StepBackException 이 발생하면 앞단계로 Error propagate하고 Route를 종료한다.

HandledErrorException이 발생하면 handled(true)를 세팅하였기 때문에, setBody~ 부터의 흐름을 진행하여, des:errorhandle로 메세지를 전달한다.

IgnoreErrorException 이 발생하면, continue(true)에 의해서 에러를 무시하고 to(des:end)를 그대로 진행한다.

 

(2) DeadLetterChannel

Error가 발생되었을때, DeadLetterChannel로 메세지를 전송한다. 일반적으로 Message Queue 메카니즘을 사용할때 쓰는 일종의 ErrorQueue로 생각하면 된다.

RouterBuilder Class에서

public void configure(){

 errorHandler(deadLetterChannel("jms:queue:error_queue"));

 from(…)…

 to(…)..

}

와 같이 정의 해놓으면, 에러가 발생하였을때, 메세지를 JMS error_queue로 전송한다.

 

(3) LogErrorHandler

에러가 발생하였을 때, 설정된 Log4J등의 Logger를 통해서 에러를 출력한다.

그 외에 TransactionErrorHandler NoErrorHandler 등이 있다.

ErrorHandler들은 CamelContext에 적용하여 Context binding된 전체 Route들에 적용할 수도 있고, 또는 개개별 Route에만 적용할 수 도 있다.

에러의 재처리

DefaultErrorHandler의 경우, 에러가 발생하였을때, 재처리를 하도록 설정할 수 있다.

errorHandler(defaultErrorHandler().maximumRedeliveries(5))

로 지정하면 5 Retry를 하게 된다. 물론, retry 간격등이나, retry logging 등 다양한 옵션을 지정할 수 있다.

http://camel.apache.org/redeliverypolicy.html


참고 : Apache Camel 소개 -http://bcho.tistory.com/715


'아키텍쳐  > EAI' 카테고리의 다른 글

Apache Camel Error Handling  (0) 2013.02.20
Apache Camel Overview  (0) 2013.02.17
EAI (Enterprise Application Integration) 추진 전략  (2) 2009.07.16
ETL vs EAI  (0) 2009.06.16
EAI 도입 전략  (0) 2007.08.21
본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

AWS SQS(Simple Queue Service)

AWS SQS(Simple Queue Service)는 말 그대로, Simple 한 message queue 서비스 이다.
전반적인 기능을 보면 message에 대한 send, receive 기능만 가능하다.
대신 AWS 클라우드 환경에서 메세지의 복제를 통해서 장애 대비 능력에 촛점이 맞춰져 있다.

JMS 처럼 XA 기반 트렌젝션 관리 능력이나, Error Queue에 대한 처리, auto retry와 같은 고급 기능도 없고
RabbitMQ 처럼, routing,pub/sub 등의 다양한 message exchange pattern도 지원하지 않는다.

단순한 enqueue/dequeue 기능의 큐이다.

몇 가지 특성을 살펴보면

1. message 크기는 max 64kb

2. Visible timeout
- message를 read하면, 메세지가 실제로 큐에서 지워지는 것이 아니라 다른 consumer에게 보이지 않는 상태가 된다.
메세지 처리가 끝나면 consumer가 delete 메서드를 사용해서 명시적으로 그 메시지를 지워야 한다. 지우지 않으면  일정 시간(Visible timeout)이 지나면 다른 consumer도 그 메세지를 볼 수 있다. 
즉 receiveMessage를 consumer가 호출한후 visible timeout내에 메세지 처리를 다 끝내고 명시적으로 delete를 해야 queue에서 삭제가 된다.
consumer가 메세지 처리중 장애가 나거나 delete를 하지 않았을 경우에는 visible time out 시간이 지난후에, 다른 consumer가 해당 메세지를 볼 수 있다. 이 메커니즘은 클라우드 환경에서 Q가 transaction등을 지원하지 않기 때문에, 메세지 처리 실패시에 다른 consumer가 fail over방식으로 메세지를 재처리할 수 있는 메커니즘을 제공하기 위함이다.

3. SOAP기반의 웹서비스나 SDK를 이용하여 접근이 가능
- 요즘 추세가 Queue 프로토콜을 JMS에서 AMQP로 옮겨가면서 성능을 우선시 하는데, HTTP를 기반으로 하고 있기 때문에, 성능을 기대하기는 어려워 보인다.

4. Delayed Timeout
메세지를 send한후에, consumer에게 보일때 까지의 시간을 조정할 수 있다.
즉 메세지를 보낸후, consumer가 직접 받아보는것이 아니라, 수초가 지난후에 consumer가 받을 수 있도록 조정할 수 있다.
이는 명시적인 delayed 처리가 필요한 경우 사용할 수 있다.

5. Batch 처리
그나마 괜찮은 기능인데, 메세지를 한건한건 보내는것이 아니라 한꺼번에 여러 메세지를 bulk로 보낼 수 있다.
마찬가지로 bulk로 메세지를 읽는 기능도 존재한다.

6. ACL
Queue에 대한 read/write 에 대한 권한 제어가 가능하다.

7. Message retantion time
Q 내의 메세지는 일정 시간동안이 지나면 자동으로 삭제된다.

AWS위에서 미들웨어의 사용은 상당한 고민 거리이다. DB를 EC2위에 배포하더라도, AWS의 제약 때문에, AZ (Amazon availible zone - 같은 지역에 있는 물리적으로 다른 데이타 센터)간의 HA(장애시 fail over)를 구성하기도 어렵다. 예전 같은 호스팅 환경이면 데이타 센터 장애가 나면 그러려니 했지만, AWS의 가용성은 99.95%이다. (http://aws.amazon.com/ko/ec2-sla/) 약 1년에 4.38시간은 장애가 난다는 것을 가정으로 한다. 즉 AWS는 장애가 날것을 전제하여 시스테을 설계해야 한다.
그래서 RabbitMQ나 ActiveMQ 같은 솔루션을 AWS 환경에서 HA로 구성하는 노력을 해야 하고, 사실 쉽지도 않다.
간단한 비동기 Queue 시나리오가 필요한 경우에는 SQS를 사용할 수 있지만, 성능이나 기능면에서 미치지 못한다.
그래서 기존 아키텍쳐와 다른 형태의 접근 방법이 필요하다.

SQS의 장점은
- AWS 인프라내에서 장애에 대한 대응성이 뛰어나다.
- 쉽다.
- 별도로 관리할 필요가 없다.

단점은
- JMS,Rabbit MQ와 같이 다양한 기능을 기대할 수 없다.
- 다른 Message Queue 대비 느리다.
- Polling 방식이기 때문에, Consumer 설계시 매번 polling하는 식의 로직을 구현해야 한다.


http://www.nsono.net/blog/?p=3 SQS vs RabbitMQ 읽어볼만 합니다.
여기의 내용중의 하나가 SQS가 polling 기반이며. 그리고 polling request때마다도 돈을 받는다는 군요. 그래서 큐가 비어 있어도 polling을 할때 돈을 내야하고, 비용 절감 차원에서 polling 주기를 늘이면 메세지 처리가 느려지고, 빨리 처리하기 위해서 polling 주기를 줄이면 메시지 처리 성능이 느려진다는 이야기입니다.


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 차도남 2015.11.27 16:03  댓글주소  수정/삭제  댓글쓰기

    빨리 처리하기 위해서 polling 주기를 줄이면 메시지 처리 성능이 빨라지는거 아닌가요?^^

NoSQL 데이타 모델링 #2

Facebook Server Side Architecture Group
http://www.facebook.com/groups/serverside
조대협

NoSQL 데이타 모델링 패턴

NoSQL 데이타 모델링 패턴[1]Key/Value 저장 구조에 Put/Get 밖에 없는 단순한 DBMS에 대해서 다양한 형태의 Query를 지원하기 위한 테이블을 디자인하기 위한 가이드 이다.

특히 RDBMS에는 있는

“ Order by를 이용한 Sorting, group by를 이용한 Grouping, Join등을 이용한 개체간의 relationship 정의, 그리고 Index 기능

들을 데이타를 쿼리하는데, 상당히 유용한 기능인데, NoSQL은 이러한 기능들을 가지고 있지 않기 때문에, NoSQL 내에서 데이타 모델링을 통해서 이러한 기능들을 구현하는 방법에 대한 가이드를 제공한다.


1. 기본적인 데이타 모델링 패턴

NoSQL의 데이타 모델링을 할때, 가장 기본적으로 많이 사용되는 패턴들이다.


1)    Denormalization

Denormalization은 같은 데이타를 중복해서 저장하는 방식이다.
Denormalization
을 하면, 테이블간의 JOIN을 없앨 수 있다. NoSQL에서는 JOIN을 지원하지 않기 때문에, 두 테이블을 조인해서 데이타를 가지고 오는 로직을 구현하려면, 각 테이블에서 데이타를 각각 가져와서 애플리케이션에서 합쳐야 하기 때문에, 2번의 IO가 발생한다.

Denormalization을 적용하여, 하나의 테이블에 JOIN될 데이타를 중복 저장하게 되면 1번의 IO로도 데이타를 가지고 올 수 있다.

예를 들어서 사용자 정보를 저장하는 User 테이블과, 도시 이름을 저장하는 City 테이블이 있다고 하자,



이런 테이블 구조에서 사용자별로 이름,나이,성별,우편번호를 출력하는 쿼리는 다음과 같다.

select u.name,u.age,u.sex,c.zipcode from user u,city c where u.city = c.city

이 테이블 구조를 그대로 해서, NoSQL에서 구현하면

select $city,name,age,sex from user where userid=”사용자ID”

select zipcode from city where city=$city

식으로 구현해야 하는데, User 테이블에서 city값을 읽어온 후에, City 테이블에서 앞서 읽어온그래city값을 키로 해서 다시 zip code를 찾아와야 한다. 두번 쿼리를 발생 시킨다. 단순한 예이지만, Join을 해야 하는 테이블 수가 많을때는 NoSQL로의 IO수가 훨씬 더 늘어난다. IO 수를 줄이려면, 아예 처음부터 Join이 되어 있는 테이블을 하나 더 만들어 버리면 된다.



이런식으로 새로운 테이블을 추가하면 된다.

Denormalization을 이용하여 중복을 허용하였을 경우에 장단점은 다음과 같다.

장점은

Ÿ   성능 향상 – Join을 위해서 몇번씩 쿼리를 하지 않아도 되기 때문에, 당연히 쿼리 성능이 올라간다.

Ÿ   쿼리 로직의 복잡도가 낮아짐 – Join에 대한 로직이 필요 없이, 한번에 데이타를 가지고 오기 때문에, 쿼리 로직이 단순해 진다.

   반대로 다음과 같은 단점을 불러온다.

Ÿ   데이타 일관성 문제 발생 가능 – age,sex,zip code등을 insert하거나 update하였을때, User, City 테이블뿐만 아니라 UserZipCode 테이블도 업데이트 해줘야 한다. 만약에 같은 User 테이블을 업데이트 하다가 에러가 났을 경우, UserZipCode 테이블은 업데이트가 안된 상태이면, 양쪽 테이블에 데이타 불 일치성이 발생할 수 있다.

Ÿ   스토리지 용량 증가 : 데이타를 중복해서 저장하는 만큼 스토리지 용량이 증가 된다.

사실 보면 단점도 있지만, NoSQL의 경우 Join이 어렵고, 애플리케이션 적으로 구현을 한다고 해도 성능이 안나오거나 구현이 어려운 경우가 많기 때문에, Denormalization을 통한 중복은 상당히 효과적인 모델링 패턴이 된다.


2)    Aggregation

드롭박스나, 슈가싱크처럼 파일을 저장하는 개인 스토리지 서비스가 있다고 가정하자. 이 서비스는 파일에 대한 정보를 저장하고, 음악,동영상,사진 등의 파일의 종류에 따라서 추가적인 메타 정보를 저장한다. ERD를 기준으로 개체를 표현해보면 다음과 같다.



NoSQL의 재미있는 특성중의 하나가 Scheme-less 또는 Soft Scheme라는 특성인데, RDBMS의 경우 테이블에 데이타를 넣을때, 반드시 테이블의 구조에 맞도록 넣어야 한다. 컬럼수와 이름도 지켜야 하고, 각 컬럼에 해당 하는 데이타 타입도 준수해야 한다 또한 모든 ROW는 같은 컬럼을 가져야 한다반면 NoSQL의 경우에는 Key만 똑같다면, Row는 제멋대로라도 상관없다. 꼭 같은 컬럼을 가질 필요도 없고, 데이타 타입도 각기 틀려야 한다. Value에 저장되는 Row 데이타들은 한줄의 Row 구조를 가지기는 하지만, 전체 테이블에 대해서 그 Row 구조가 일치할 필요가 없다. 이를 데이타가 구조는 가지고 있지만, 구조가 각각 틀리는 것을 허용한다고 해서 Soft-Scheme 또는 Schemeless 라고 한다.

이 특성을 이용하면, 위의 데이타 모델을 하나의 테이블로 합칠 수 있다



1:N과 같은 복잡한 엔터티들의 관계를 손쉽게 하나의 테이블로 바꿀 수 있고, 이는 결과적으로 Join의 수를 줄여서 Query 성능을 높이는 방법이 된다.


3)    Application Side Join

NoSQL Join이 거의 불가능하다. (지원하는 제품도 있지만). 그래서 Denormalization이나 Aggregation 그리고 앞으로 설명할 데이타 모델링 패턴을 사용하는 것인데, 그래도 어쩔 수 없이 Join을 수행해야할 경우가 있다. 이 때는 NoSQL의 쿼리로는 불가능하고, NoSQL을 사용하는 클라이언트 Application단에서 로직으로 처리해줘야 한다.

예를 들어서 TABLE 1,2 두개의 테이블이 있고, TABLE 1의 컬럼중 하나가 Foreign Key로써 TABLE 2의 데이타를 가르키고 있다면, Application Join을 하려면, Table 1에서 Primary Key로 한 Row를 읽어온 후에, Table 2를 가르키는 컬럼의 값을 Key로 해서, 다시 Table 2에서 쿼리를 해온다.



Application Side Join Join이 필요한 테이블 수 만큼 NoSQL로의 Request/Response IO가 발생하는 만큼 다소 부담 스럽기는 하지만, 반대로 Denormalization등에 비해서는 스토리지 사용량을 절약할 수 있다.


참고 : Map & reduce 기능을 이용한 Server Side Join

Application Side Join이 있다면, 반대로 Server Side Join 기능도 있다. NoSQL에는 Join 기능이 없다고 했는데, Server Side Join은 무엇인가?


Riak이나 MongoDB와 같은 일부 NoSQL DB들은 RDBMS stored procedure [2]를 지원한다. Map & Reduce [3]라는 이름으로 이 기능을 지원한다. 즉 위에서 Application에서 수행한 로직을 NoSQL 서버들 내에서 수행해서 리턴하는 방식이다. 이 로직들은 NoSQL에서 지원하는 언어를 통해서 구현된후, NoSQL 엔진위에서 수행된다.




이렇게 하면, Application에서 NoSQL로의 호출이 Map & Reduce function 한번만 호출하면 되기 때문에, 네트워크 IO를 줄여서 성능을 향상 시킬 수 있다. 반면, Join에 대한 로직이 NoSQL 서버 쪽에서 수행되기 때문에, 반대로 NoSQL에 대한 부담이 가중된다.


2. 확장된 데이타 모델링 패턴

1)    Atomic aggregation

NoSQL에서 고민해야할 것 중의 하나의 두 개이상의 테이블을 업데이트 할때, 트렌젝션에 관리에 대한 문제이다. 그림과 같이 두개의 테이블을 업데이트 하는 시나리오에서, 1번 테이블을 업데이트 한 후에, 애플리케이션 로직이나 NoSQL의 장애로 인해서 2번 테이블이 업데이트가 되지 않는 문제가 발생할 수 있다.



이 경우 데이타의 일관성 문제를 야기하는데, 이에 대한 해결 방안으로 생각할 수 있는 것은, TABLE 1,2를 하나의 테이블로 합쳐 버리는 것이다.



하나의 테이블에 대해서는 NoSQL atomic operation(원자성)을 보장하기 때문에, 트렌젝션을 보장 받을 수 있다. 구현 패턴상으로는 Aggregation과 동일한데, Aggregation1:Nrelationship aggregate해서 join을 없애기 위함이고, Atomic Aggregation은 트렌젝션 보장을 통한 데이타 불일치성을 해결하기 위함이다.

예를 들어 사용자 정보가 UserAddress,UserProfile,UserId라는 이름의 3개의 테이블로 분산이 되어 있을때, 사용자를 생성하는 경우, 3개의 테이블에 Insert를 해줘야 한다. 그러나 테이블이 3개로 분산되어 있기 때문에, 앞에서 언급한 장애시 트렌젝션에 대한 보장이 되지 않는다. 이를 해결하기 위해서 atomic aggregation 패턴을 적용하여, 3개의 테이블을 User 테이블의 필드로 집어 넣게 되면, 하나의 테이블이기 때문에, 장애나 에러로 인한 트렌젝션 불일치 문제를 방지할 수 있다.



2)    Index Table

NoSQL RDBMS 처럼 Index가 없기 때문에, Key 이외의 필드를 이용하여 Search를 하면, 전체 Table Full Scan 하거나 아니면 Key이외의 필드에 대해서는 아예 Search가 불가능하다. 이 문제를 해결하기 위해서 Index를 위한 별도의 Index Table을 만들어서 사용할 수 있는데, 상당히 사용 빈도가 많은 방법이다.

예를 들어, 파일 시스템을 NoSQL에 저장한다고 했을때, 파일 테이블은 다음과 같은 구조를 갔는다.



여기서 특정 디렉토리에 있는 파일만을 리스트업 하고 싶다면, 별도의 Index Table을 다음과 같이 만들면 된다.



Cassandra Riak과 같은 일부 NoSQL에는 제품적으로 secondary Index라는 이름으로 Key 이외의 필드를 Index로 지정하는 기능을 가지고 있는데, 이 기능들은 아직까지 성숙되지 못해서, 여기서 설명하는 Index Table을 사용하는 것보다 성능이 나오지 않는다. 만약에 NoSQL에 있는 Secondary Index 기능을 사용할 예정이라면, 반드시 이 Index Table 패턴과 성능을 비교해보기를 추천한다. 당연히 Secondary Index를 이용하면 구현은 조금 편리해질 수 있지만, 성능 차이가 많이 날 수 있다.


3)    Composite Key

이 글을 읽으면서 눈치가 빠른 사람이라면, Key를 정의하는데, “:” deliminator를 이용하여 복합키를 사용하는 것을 눈치 챘을 것이다. NoSQL에서는 이 Key를 어떻게 정의하느냐가 매우 중요하다. 특히 Ordered KV Store 의 경우에는 이를 이용하여 order by와 같은 sorting 기능이나 grouping을 구현할 수 있다. Composite Key는 하나 이상의 필드를 deliminator를 이용하여 구분지어 사용하는 방법으로 RDBMS의 복합키 (Composite primary key)와 같은 개념이라고 생각하면 된다. 단지 RDBMS의 경우에는 여러개의 컬럼을 묶어서 PK로 지정하지만 NoSQL은 한컬럼에 deliminator를 이용하여 여러개의 키를 묶어서 넣는다.

아래의 예제를 보자, PC의 디렉토리를 Ordered KV Store에 저장한다고 하자.
 windows
하위 디렉토리를 가지고 올때, “windows:etc” 부터 쿼리를 해서, 다음 row를 반복적으로 쿼리해서, key windows로 시작하지 않을때 까지 읽어오면, windows 디렉토리의 하위 디렉토리를 모두 가지고 올 수 있다.



노드가 추가,삭제되더라도, 내부적으로 sorting이 되기 때문에, 문제없이 사용이 가능하다.

, Key값을 선택할때 주의해야할 사항은 특정 서버로의 몰림 현상을 들 수 있다. NoSQL은 특성상, N개의 서버로 구성된 클러스터이다. 그리고 데이타는 Key를 기준으로 N개의 서버에 나눠서 저장이 된다. 예를 들어 Key Range A~Z 26개라고 가정하고, 클러스터의 서버 수가 26대라고 하면, 각 서버는 Key의 시작으로 사용되는 알파벳 키들의 데이타를 저장한다. 1번은 A로 시작되는 Key 데이타들, 2번은 B, 3번은 C로 등등.

Key 값을 “City:User Name”으로 했다고 가정하자, 우리나라에서는 5000만 인구중, 1/5 1000만이 서울에 살고 있다. 그래서 데이타중 약 20%의 키는 “Seoul:xxx”로 시작할 것이고 26대의 서버중에서 S로 시작하는 키를 저장하는 한대의 서버는 20%의 부하를 처리하게 될것이다. 서버가 26대이니까는 각 서버는 1/26 ( 3.8%)의 부하를 처리해야 하는데, 이건 예상치 보다 5배 이상 많은 로드를 처리하기 때문에 성능 저하를 유발할 수 있다. 이런 이유로, Key를 선정할 때는 전체 서버에 걸쳐서 부하가 골고루 분산될 수 있는 Key를 선정하는 것이 좋다.


4)    Inverted Search Index

검색엔진에서 많이 사용하는 방법인데, 검색엔진은 사이트의 모든 페이지를 검색 로봇이 검색해서 문서내의 단어들을 색인하여 URL에 맵핑해서 저장해놓는다.



검색은 단어를 키로 검색이 되기 때문에, 위의 테이블 구조에서는 value에 검색 키워드들이 들어가 있기 때문에, 효과적인 검색을 할 수 없다. 이 검색 키워드를 키로 해서 URLvalue로 하는 테이블을 다시 만들어 보면, 아래와 같은 식으로 표현되고, 검색 키워드로 검색을 하면 빠르게, 검색 키워드를 가지고 있는 URL을 찾아낼 수 있다.



이렇게 value의 내용을 key로 하고, key의 내용을 반대로 value로 하는 패턴을 inverted search index라고 한다.


5)    Enumerable Keys

NoSQL 솔루션에 따라서, RDBMS Sequence와 같은 기능을 제공하는 것들이 있다. 이 기능들은, 키에 대해서 자동으로 카운터를 올려주는 기능을 가지고 있다. (예를 들어 첫번째 키는 1, 두번째 키는 2,다음은 3,4,5 식으로 순차적으로 연속된 키를 부여해주는 기능).

이 기능은 데이타에 대한 traverse 기능을 제공한다. , 100번 키를 가지고 왔는데, 이 앞뒤의 값을 알 수 있다. Sequential한 키를 사용했기 때문에, 당연히 앞의 키값은 99, 다음 키값은 101로 해서 값을 가지고 올 수 있다.


3. 계층 데이타 구조에 대한 모델링 패턴

NoSQL들은 데이타 모델이 KV,Ordered KV,Document들 여러가지가 있기는 하지만, 기본적으로 row,column을 가지고 있는 테이블 구조 저장구조를 갖는다. 애플리케이션 개발중에는 이런 테이블 구조뿐만 아니라 Tree와 같은 계층형 데이타 구조를 저장해야 할 경우가 있는데, 테이블 구조의 저장 구조를 갖는 NoSQL의 경우 이러한 계층형 구조를 저장하는 것이 쉬운일은 아니다.

RDBMS의 경우에도 이런 계층형구조를 저장하기 위해서 많은 고민을 했는데, RDBMS 솔루션에서 기능적으로 자체 지원할 수 도 있고, 데이타 모델링을 통해서도 이러한 계층형 구조를 저장할 수 있다.

여기서 소개하는 NoSQL에서 계층형 구조를 저장하는 기법은 RDBMS에서 사용하는 기법들을 많이 참고하였다. 추가적인 기법은 RDBMS Tree구조 저장 기법을 참고하기 바란다.


1)    Tree Aggregation

Tree 구조 자체를 하나의 Value에 저장하는 방식이다. JSON이나 XML 등을 이용하여, 트리 구조를 정의 하고, Value에 저장하는 방식이다.

Tree 자체가 크지 않고, 변경이 많이 없는 경우에는 사용하기 좋다. “계층형 게시판의 답글 Tree 구조등을 저장하기에 용이하다.


2)    Adjacent Lists

Adjacent List 구조는, 전통적인 자료 구조에서 사용하는 Linked List와 같은 자료 구조형을 사용하여, Tree의 노드에 parent node에 대한 포인터와 child node들에 대한 포인터를 저장하는 방식이다.

Tree의 내용을 검색하려면, root 노드에서 부터 child node child node 포인트를 이용하여, 값을 가져오는 방식이다. (이를 Tree traversing 이라고 한다.)

특정 노드만 알면, 해당 노드의 상위, 하위 노드를 자유롭게 traversing할 수 있어서 traversing에는 장점을 가지고 있지만, 반대로, 하나의 노드를 이동할 때마다, 포인터를 이용해서 매번 쿼리를 해와야 하기 때문에, Tree의 크기가 크다면 NoSQL로의 IO가 엄청나게 많이 발생한다. (트리의 노드 수가 N이면, N번 쿼리를 해야 한다)

아래 그림을 보자, 파일 디렉토리를 저장하는 자료 구조를 만든다고 했을때, Directory라는 테이블을 정의하고, 이 테이블에는 directory 명과, 해당 directory의 상하위 디렉토리 이름을 저장하도록 한다.

이를 바탕으로 테이블에 저장된 데이타를 보면 아래와 같다.



구현이 쉬운 편이긴 하지만 Tree 구조 traverse에 많은 IO를 유발하기 때문에, Tree구조를 저장하거나 잦은 read가 있을때는 권장하지 않는다.

     RDBMS 의 경우에는 이런 Tree 구조 traversing을 지원하기 위해서 recursive 쿼리에 대해서 native recursive function을 지원함으로써, tree 구조 저장을 지원하는 경우도 있다.


3)    Materialized Path

Materialized Path Tree 구조를 테이블에 저장할때, root에서 부터 현재 노드까지의 전체 경로를 key로 저장하는 방법이다.

이 방법은 구현에 드는 노력에 비해서 매우 효율적인 저장 방식이다. 특히 Key에 대한 Search를 할때, Regular Expression을 사용할 수 있으면, 특정 노드의 하위 트리등을 쿼리해 오는 기능등 다양한 쿼리가 가능하다. 일반적은 KV Ordered KV에서는 적용하기는 힘들지만, MongoDB와 같은 Document DB Regular Expression을 지원하기 때문에, 효과적으로 사용할 수 있다.



4)    Nested Sets

Netsted Set의 기본원리는 Node가 포함하는 모든 Child Node에 대한 범위 정보를 가지고 있다. 먼저 예를 보고 설명하자. Node는 배열이나 리스트에 Sorting된 형태로 저장되어 있다.

Node는 자신이 포함하는 모든 Sub Tree (Child Node)들이 포함된 start end index를 저장한다.

아래 A는 전체 Sub Tree를 포함하기 때문에 2~9번에는 A Sub Tree의 내용이 된다.
C
노드의 경우 자신이 포함하는 모든 Child Node 4~9에 포함되어 있기 때문에, Index 4~9로 저장한다.



각 노드만 안다면, Sub Tree start,end index만 있으면 쭈욱 읽어오면 되기 때문에 매우 빠른 성능을 보장할 수 있다. 단 이 데이타 구조 역시 update에 취약하다. update가 발생하였을 경우, Index를 다시 재배열해야 하기 때문에 이에 대한 로드가 매우 크다.

위의 예제에서 B 노드 아래에 J 노드를 추가해보자, J 노드의 index 3이 되어야 하고, 3 Index부터는 모두 +1 씩 더해져야 하며, Child Node의 값 역시 모두 변화 되어야 한다.



트리 생성 후에, 변화가 없는 대규모 트리등을 저장하는데 유용하게 사용할 수 있다

.

결론

지금까지 몇가지 NoSQL 적용시 사용할 수 있는 데이타 모델링 패턴에 대해서 살펴보았다. 기본 패턴은 대부분의 NoSQL 솔루션에 적용할 수 있으며, 확장된 모델링 패턴의 경우에는 NoSQL이 지원하는 기능이나 데이타 구조 (KV, Ordered KV,Document etc)에 따라서 적용할 수 있다.

NoSQL을 이용한 시스템을 개발할때는

1.     데이타 모델링이 80% 이상이다. 선정한 NoSQL과 애플리케이션의 특성에 맞는 데이타 모델링에 집중하자.

2.     NoSQL은 어떤 솔루션이 좋다, 나쁘다가 없다. 어떤 솔루션이 특성이 어떻다는 것만 있기 때문에 반드시 데이타 모델과 내부 아키텍쳐 두 가지를 파악한 후에, 애플리케이션의 특성에 맞는 NoSQL을 선정해야 한다.

3.     하나의 NoSQL로 전체 데이타를 저장하려고 하지 마라. NoSQL은 데이타 구조가 매우 다눈하지만, 애플리케이션들은 하나의 단순한 데이타 구조로 저장할 수 없는 데이타가 반드시 존재한다. RDBMS와 혼용하거나, 다른 NoSQL과 혼용하거나 성능면에서는 캐슁 솔루션과 혼용하는 것을 반드시 고려 해야한다.

NoSQL은 놀라온 성능과 확장성을 제공하지만, 많은 연구와 노력이 필요하다. Oracle과 같은 데이타베이스를 사용할때도, 전문 DBA를 두고, 튜닝에 시간을 두고, 데이타 모델을 가꾸고, 튜닝을 하지 않는가? NoSQL이 오픈소스를 중심으로 사용되고 있지만, 오픈 소스라서 쉬운게 아니다. 그만큼 많은 투자와 연구와 노력이 필요한 만큼, 신중하게 검토하고 도입을 결정하기를 바란다.



[2] RDBMS에서 프로그래밍 언어를 이용해서 구현된 사용자 정의 데이타 Query 함수. 예를Oracle 들어 RDBMS의 경우에는 PL/SQL이라는 프로그래밍 언어를 이용하여, 데이타 베이스에 대한 비지니스 로직을 구현해서 저장해놓고, 클라이언트에서 이 함수를 호출해서 사용하게 할 수 있다.

[3] Map & Reduce Input 데이타를 여러 조각으로 쪼갠 후, 여러대의 서버에서 각 데이타 조각을 처리한후(Map), 그 결과를 모아서 (Reduce) 하나의 결과를 내는 분산 처리 아키텍쳐 이다.




링크 

NoSQL 데이타 모델 및 데이타 모델링 절차 : http://bcho.tistory.com/665

참고 자료 http://www.pearltrees.com/#/N-u=1_752336&N-p=53126547&N-play=1&N-fa=5793524&N-s=1_5827086&N-f=1_5827086


본인은 구글 클라우드의 직원이며, 이 블로그에 있는 모든 글은 회사와 관계 없는 개인의 의견임을 알립니다.

댓글을 달아 주세요

  1. 주디아줌마 2012.08.27 09:23 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사합니다.

  2. Wegra 2012.08.30 17:37  댓글주소  수정/삭제  댓글쓰기

    HBase를 사용할 일이 생겼는데, 예전에 제목 봤던 기억이 나서 찾아왔습니다.
    패턴들 염두해두면서 현재 모델 찬찬히 검토해봐야겠네요.ㅎ

  3. 로켓달구지 2013.10.28 19:50 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사드립니다.
    얼마전부터 DB 공부를 하고 있는데요.
    위에 써주신 내용 보면서 모르는 부분 많이 배우고 있습니다.
    그리고 내용 보면서 궁금한 점이 생겨서 질문드려봅니다.

    nosql 에선 테이블 안에 다른 테이블을 embeded 하는 형태로 만드는 쪽이 속도에 있어서 장점이 있다고 봐서 그런 형태로 만들어봤는데요.

    기존에 존재하는 값에 또 다른 값을 새로 추가하는 건 어떤 식으로 이루어지나요?

    지금 블로그 모델을 만들어서 연습중인데, User 테이블 안에 포스트 한 글의 테이블을 넣었는데, 이 값을 불러와서 포스트를 추가하면 기존에 있던 자료에 overwrite 되는 것 처럼 이전 값들이 사라지고 새로 추가한 값이 덮어씌우는 것 처럼 되어서요 ..;

    모델 형태는 아래와 같은 형태로 구현했습니다.

    {
    name : String,
    age : String,
    post : [{
    title : String,
    author : String,
    body : String
    }]
    }



  4. 유탱뿌잉 2020.11.17 17:54 신고  댓글주소  수정/삭제  댓글쓰기

    많은 도움 받았습니다. 포스팅 감사합니다.

  5. 유입 2021.02.01 11:39  댓글주소  수정/삭제  댓글쓰기

    대박이네요 흘러 흘러 찾은 블로그인데 정말 설명 도움 많이 되고 기가막힙니다. 이론 정립에 큰 도움이 되었습니다. 감사하빈다