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


Archive»


 
 

대충보는 Storm #5-Apache Storm 병렬 분산 처리 이해하기

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

 

Storm에 있는 Spout Bolt들은 여러개의 머신에서 어떻게 나눠서 처리될까? Storm 클러스터는 여러대의 분산된 서버에서 운용되기 때문에, 당연히 Spout Bolt도 나눠서 처리된다 그렇다면 이런 Storm의 병렬 처리 구조는 어떻게 되는 것일까?

이 글에서는 Spout Bolt를 병렬로 처리하는 Storm의 구조에 대해서 알아보도록 한다.

Storm의 병렬 처리를 이해하기 위한 개념

Storm의 병렬 처리를 이해하기 위해서는 몇가지 개념을 정리해야 한다. Node,Worker,Exectutor,Task 이 네 가지 개념을 이해해야 한다.


Node

Node는 물리적인 서버이다. Nimbus Supervisor 프로세스가 기동되는 물리적인 서버이다.

Nimbus는 전체 노드에 하나의 프로세스만 기동하며, Supervisor는 일반적으로 하나의 노드에 하나만 기동한다. 여러대를 기동시킬 수 도 있지만, Supervisor의 역할 자체가 해당 노드를 관리하는 역할이기 때문에 하나의 노드에 여러개의 Supervisor를 기동할 필요는 없다.


Worker

Worker Supervisor가 기동되어 있는 노드에서 기동되는 자바 프로세스로 실제로 Spout Bolt를 실행하는 역할을 한다.


Executor

Executor Worker내에서 수행되는 하나의 자바 쓰레드를 지칭한다.


Task

Task Bolt Spout의 객체를 지칭한다. Task Executor (쓰레드)에 의해서 수행된다.

이 개념을 다시 정리해보면 다음과 같은 그림이 된다.



<그림. Node,Worker,Executor,Task 의 개념>

각 슬레이브 노드에는 Supervisor 프로세스가 하나씩 떠있고, conf/storm.yaml에 정의된 설정에 따라서 worker 프로세스를 띄운다.supervisor.slots.ports에 각 Worker가 사용할 TCP 포트를 정해주면 된다. 아래는 5개의 Worker 프로세스를 사용하도록 한 설정이다.



<그림. Storm 설정에서 Supervisor 5개 띄우도록한 설정>

 

그리고 난후에, Topology를 생성할때, Topology에 상세 Worker,Executor,Task의 수를 정의한다. 앞에서 예제로 사용했던 HelloTopology 클래스 코드를 다시 살펴보자. 아래 코드는 Worker,Executor,Task등을 설정한 예이다.

package com.terry.storm.hellostorm;

 

import backtype.storm.Config;

import backtype.storm.StormSubmitter;

import backtype.storm.generated.AlreadyAliveException;

import backtype.storm.generated.InvalidTopologyException;

import backtype.storm.topology.TopologyBuilder;

 

public class HelloTopology {

        public static void main(String args[]){

               TopologyBuilder builder = new TopologyBuilder();

               builder.setSpout("HelloSpout", new HelloSpout(),2);

               builder.setBolt("HelloBolt", new HelloBolt(),2)

                       .setNumTasks(4)

                       .shuffleGrouping("HelloSpout");

              

              

               Config conf = new Config();

               conf.setNumWorkers(5);

               // Submit topology to cluster

               try{

                       StormSubmitter.submitTopology(args[0], conf, builder.createTopology());

               }catch(AlreadyAliveException ae){

                       System.out.println(ae);

               }catch(InvalidTopologyException ie){

                       System.out.println(ie);

               }

              

        }

 

}

<코드. Worker,Executor,Task 수를 설정한 HelloTopology 예제>

     Topology가 사용할 Worker 프로세스의 수 설정
Config
에서 setNumWorkers(5)를 이용해서 이 토폴로지에서 사용한 Worker 프로세스 수를 5개로 지정했다.

     Spout Executor(쓰레드 수) 설정
다음으로 setSpout에서 3번째 인자로 “2”라는 숫자를 넘겼는데, setSpout에 마지막 인자는 Executor의 수이다. 이를 Parallelism 힌트라고 하는데, Spout 컴포넌트가 수행될 쓰레드의 수이다. 여기서는 Spout Task (객체의 수)를 정의하지 않았는데, 정의하지 않은 경우 디폴트로 Executor의 수와 같이 설정된다.

     Bolt Executor(쓰레드 수)Task(객체)수 설정
Bolt
도 마찬가지로 setBolt 3번째 마지막 인자가 Parallelism 힌트인데, 역시 2개로 지정하였다. 여기서는 Task수를 별도로 지정하였는데, setTaskNum(4)을 이용해서 지정한다. 이렇게 설정하면 HelloBolt 객체는 총 4개가 생기고 2개의 Thread에서 번갈아 가면서 실행하게 된다.

자아 그러면 실제로 설정하는데로 동작하는 지 몇가지 확인을 해보자. 자바의 jps 명령을 이용하면 현재 동작중인 자바 프로세스 수를 볼 수 있다.



<그림 Worker 프로세스 수의 확인>

위의 테스트는 하나의 환경에서 nimbus,zookeeper,supervisor,worker를 모두 띄워놓은 형태인데,worker가 설정대로 5개의 프로세스가 떠있고, nimbus,supervisor가 떠 있는 것이 확인되고, QuorumPeerMainzookeeper 프로세스이다.

실제로 Executor가 지정한데로 Thread가 뜨는지 확인을 해보자. 여러개의 Worker 프로세스에 나눠서 뜨면 모니터링하기가 복잡하니 편의상 conf.setNumer(1)로 해서, 하나의 Worker 프로세스에서 모든 Executor가 뜨도록 Topology를 변경한후, Worker 프로세스의 쓰레드를 모니터링 하니 다음과 같은 결과를 얻었다.

코드상에서 HelloSpout에 대한 Parallelism 힌트를 2로 설정하고, HelloBolt에 대한 Parallelism 힌트도 2로 설정하였다.



<그림. Worker 프로세스의 쓰레드 덤프>

실제로 Worker 프로세스내의 쓰레드를 보면 HelloSpout용 쓰레드가 2, HelloBolt용 쓰레드가 2개가 기동됨을 확인할 수 있다.


리밸런싱

Storm 운영중에 노드를 추가 삭제 하거나 또는 성능 튜닝을 위해서 운영중인 환경에 Worker, Executor의 수를 재 조정이 가능하다. 이를 rebalance라고 하는데, 다음과 같은 명령어를 이용해서 가능하다.

% bin/storm rebalance [TopologyName] -n [NumberOfWorkers] -e [Spout]=[NumberOfExecutos] -e [Bolt1]=[NumberOfExecutos] [Bolt2]=[NumberOfExecutos]

미들웨어 엔지니어로써 본 Storm 튜닝

본인의 경우 경력이 톰캣이나 오라클社의 웹로직에 대해 장애진단과 성능 튜닝을 한 경력을 가지고 있어서 JVM이나 미들웨어 튜닝에 많은 관심을 가지고 있는데, 이 미들웨어 튜닝이라는 것이 대부분 JVM과 쓰레드 수등의 튜닝에 맞춰 있다보니, Storm의 병렬성 부분을 공부하다 보니, Executor Worker,Task의 수에 따라서 성능이 많이 차이가 나겠다는 생각이 든다.

특히나 하나의 토폴리지만 기동하는 것이 아니라, 여러개의 토폴로지를 하나의 클러스터에서 구동 할 경우 더 많은 변수가 작용할 수 있는데, 쓰레드란 것의 특성이 동시에 하나의 코어를 차지하고 돌기 때문에, 쓰레드수가 많다고 시스템의 성능이 좋아지지 않으며 반대로 적으면 성능을 극대화할 수 없기 때문에, 이 쓰레드의 수와 이 쓰레드에서 돌아가는 객체(Task)의 수에 따라서 성능 차이가 많이 날것으로 생각된다. 아마도 주요 튜닝 포인트가 되지 않을까 싶은데, 예전에는 보통 JVM당 적정 쓰레드 수는 50~100개 정도로 책정했는데 (톰캣과 같은 WAS 미들웨어 기준). 요즘은 코어수도 많아져서 조금 더 많은 쓰레드를 책정해도 되지 않을까 싶다. 쓰레드 수 뿐 아니라, 프로세스수도 영향을 미치는데, JVM 프로세스의 컨텐스트 스위칭은 쓰레드의 컨텐스트 스위칭보다 길기 때문에, 프로세스를 적게 띄우는 것이 좋을것으로 예상 되지만, JVM 프로세스는 메모리 GC에 의한 pausing 시간이 발생하기 때문에 이 GC 시간을 적절하게 나눠주기 위해서 적절 수 의 프로세스를 찾는 것도 숙제가 아닐까 싶다. 디폴트 worker의 옵션을 보니 768M의 힙 메모리를 가지고 기동하게 되어 있는데, 메모리를 많이 사용하는 연산는 다소 부족하지 않을까 하는 느낌이 든다.

Bolt가 데이타 베이스, 파일 또는 네트워크를 통해서 데이타를 주고 받는 연산을 얼마나 하느냐에 따라서도 CPU 사용률이 차이가 날것이기 때문에 (IO작업중에는 쓰레드가 idle 상태로 빠지고 CPU가 노는 상태가 되기 때문에) IO 작업이 많은 경우에는 쓰레드의 수를 늘리는 것이 어떨까 한다.

Bolt Spout와 같은 통신은 내부적으로 ZeroMQ를 사용하는 것으로 알고 있는데, 아직 내부 구조는 제대로 살펴보지는 않았지만, 같은 프로세스내에서는 네트워크 호출 없이 call-by-reference를 이용해서 통신 효율을 높이고, 통신이 잦은 컴포넌트는 같은 프로세스에 배치 하는 affinity와 같은 속성(?)이 있지 않을까 예측을 해본다.

결과적으로 튜닝 포인트는, Worker,Executor,Task 수의 적절한 산정과, 만약에 옵션이 있다면 리모트 호출을 줄이기 위한 Bolt Spout 컴포넌트의 배치 전략에 있지 않을까 한다.


다음 글에서는 이런 병렬 처리를 기반으로 각 컴포넌트간에 메세지를 보낼때, 여러 Task간에 어떻게 메세지를 라우팅을 하는지에 대한 정리한 그룹핑(Grouping)에 대한 개념에 대해서 알아보도록한다.


대충보는 Storm #2-Storm 설치와 HelloStorm 작성하기

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


Apache Storm Spark

앞서 데이타 스트리밍 처리에 대해서 설명했다. 스트리밍 처리에 대표적인 오픈소스 프레임웍으로는 Apache Storm Apache Spark이 있는데ㅔ, Spark은 최근에 나온 것으로 스트리밍 처리뿐 만 아니라 조금 더 보편적인 분산 컴퓨팅을 지원하는데, Storm의 경우 나온지도 오래되었고 무엇보다 안정성 부분에서 아직까지는 Spark보다 우위에 있기 때문에, Storm을 중심으로 설명하고자 한다

HelloStorm

Storm의 내부 구조 개념등을 설명하기에 앞서, 일단 깔아서 코드부터 돌려보고 개념을 잡아보자


HelloStorm 구조

HelloWorld 처럼 간단한 HelloStorm을 만들어보자. 만들어보려고 하는 Storm 프로그램은 다음과 같다.



<그림. HelloStorm 개념 구조>


HelloSpout 이라는 클래스는, Storm에 데이타를 읽어오는 클래스로 이 예제에서는 자체적으로 데이타를 생성해낸다. Storm으로 들어오는 데이타는 Tuple이라는 형식을 따르는데, Key/Value 형식의 데이타 형을 따른다. 여기서는 키(필드명)“say”, 데이타는 “Hello” 라는 문자열을 가지고 있는 데이타 tuple을 생성한다.

HelloSpout에서 생성된 데이타는 HelloBolt라는 곳으로 전달이 되는데, HelloBolt 클래스는 데이타를 받아서 처리하는 부분으로 간단하게 들어온 데이타에서 “say” 라는 필드의 데이타 값을 System.out으로 출력해주는 역할만을 한다.


개발하기

이클립스를 사용하여, maven project를 생성한다.



다음으로 pom.xml을 작성한다.


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

 

  <groupId>com.terry.storm</groupId>

  <artifactId>hellostorm</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>jar</packaging>

 

  <name>hellostorm</name>

  <url>http://maven.apache.org</url>

 

  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

  </properties>

 

<dependencies>

  <dependency>

    <groupId>junit</groupId>

    <artifactId>junit</artifactId>

    <version>3.8.1</version>

    <scope>test</scope>

  </dependency>

  <dependency>

        <groupId>org.apache.storm</groupId>

        <artifactId>storm-core</artifactId>

        <version>0.9.3</version>

  </dependency>

 

</dependencies>

 

<build>

  <plugins>

    <plugin>

      <artifactId>maven-assembly-plugin</artifactId>

      <version>2.2.1</version>

      <configuration>

        <descriptorRefs>

          <descriptorRef>jar-with-dependencies</descriptorRef>

        </descriptorRefs>

        <archive>

          <manifest>

            <mainClass />

          </manifest>

        </archive>

      </configuration>

      <executions>

        <execution>

          <id>make-assembly</id>

          <phase>package</phase>

          <goals>

            <goal>single</goal>

          </goals>

        </execution>

      </executions>

    </plugin>

  </plugins>

</build>

 

</project>

<그림. pom.xml>


이 예제에서는 storm 0.9.3을 사용했기 때문에 위와 같이 storm-core 0.9.3 dependency 부분에 정의하였다.

다음으로 데이타를 생성하는 HelloSpout을 구현하자


package com.terry.storm.hellostorm;

 

import java.util.Map;

 

import backtype.storm.spout.SpoutOutputCollector;

import backtype.storm.task.TopologyContext;

import backtype.storm.topology.OutputFieldsDeclarer;

import backtype.storm.topology.base.BaseRichSpout;

import backtype.storm.tuple.Fields;

import backtype.storm.tuple.Values;

 

public class HelloSpout extends BaseRichSpout {

          private static final long serialVersionUID = 1L;

          private SpoutOutputCollector collector;

         

          public void open(Map conf,TopologyContext context,SpoutOutputCollector collector){

               this.collector = collector; 

          }

         

          public void nextTuple(){

                 this.collector.emit(new Values("hello world"));

          }

         

          public void declareOutputFields(OutputFieldsDeclarer declarer){

                 declarer.declare(new Fields("say"));

          }

         

}

<그림. HelloSpout.java>


HelloSpout 실행이 되면, 필드 “say” 값이 “hello world” 데이타를 생성해서 다음 워크 플로우로 보낸다.

nextTuple() 이라는 함수에서 외부에서 데이타를 받아들여서 다음 워크 플로우로 보내는 일을 하는데, 여기서는 외부에서 데이타를 받아들이지 않고 자체적으로 데이타를 생성하도록 한다. 데이타를 뒤에 워크플로우에 보내는 함수는 emit인데, emmit부분에 “hello world”라는 value 넣어서 보내도록 하였다. 그렇다면 필드의 값은 어떻게 정의 하느냐? 필드값은 declareOutputField라는 함수에 정의하는데, 데이타의 필드는 “say” 정의하였다.


다음으로 이 HelloSpout에서 생성할 데이타를 처리한 HelloBolt를 구현해보자


package com.terry.storm.hellostorm;

 

import backtype.storm.topology.BasicOutputCollector;

import backtype.storm.topology.OutputFieldsDeclarer;

import backtype.storm.topology.base.BaseBasicBolt;

import backtype.storm.tuple.Tuple;

 

public class HelloBolt extends BaseBasicBolt{

 

        public void execute(Tuple tuple, BasicOutputCollector collector) {

               // TODO Auto-generated method stub

               String value = tuple.getStringByField("say");

               System.out.println("Tuple value is"+value);

        }

 

        public void declareOutputFields(OutputFieldsDeclarer declarer) {

               // TODO Auto-generated method stub

              

        }

 

}

<그림. HelloBolt.java>


HelloSpout에서 생성된 데이타는 HelloBolt 들어오는데, 데이타가 들어오면 execute라는 메서드가 자동으로 수행된다. 이때, Tuple 통해서 데이타가 전달된다. 여기서는 tuple에서 필드이름이 “say” 값을 tuple.getStringByField(“say”) 이용해서 꺼내서 System.out으로 출력했다.

눈치가 빠른 사람이라면 벌써 알아차렸겠지만, 데이타를 다음 플로우로 보내고자 할때는 앞의 HelloSpout에서 한것처럼, execute 메서드내에서 데이타 처리가 끝난후에, collector.emit 이용해서 다음 플로우로 보내고, delcareOutputField에서 데이타에 대한 필드를 정의하면 된다.

데이타를 생성하는 Spout 데이타를 처리 하는 Bolt 구현했으면 둘을 연결 시켜줘야 한다. 이를 연결시켜주는 것이 Topology인데, HelloTopologyLocal 클래스를 구현해 보자


package com.terry.storm.hellostorm;

 

import backtype.storm.Config;

import backtype.storm.LocalCluster;

import backtype.storm.topology.TopologyBuilder;

import backtype.storm.utils.Utils;

 

public class HelloTopologyLocal {

        public static void main(String args[]){

               TopologyBuilder builder = new TopologyBuilder();

               builder.setSpout("HelloSpout", new HelloSpout(),2);

               builder.setBolt("HelloBolt", new HelloBolt(),4).shuffleGrouping("HelloSpout");

              

               Config conf = new Config();

               conf.setDebug(true);

               LocalCluster cluster = new LocalCluster();

              

               cluster.submitTopology("HelloTopologyLocal", conf,builder.createTopology());

               Utils.sleep(10000);

               // kill the LearningStormTopology

               cluster.killTopology("HelloTopologyLocal");

               // shutdown the storm test cluster

               cluster.shutdown();          

        }

 

}

<그림. HelloTolologyLocal.java>


나중에 개념에서 자세하 설명하겠지만, Topology 데이타를 생성하는 Spout 처리하는 Bolt간에 토폴로지 데이타 흐름을 정의하는 부분이다. Spout Bolt들을 묶어 주는 부분이다.

먼저 TopologyBuilder 이용해서 Topology 생성하고, setSpout 이용해서 앞에서 구현한 HelloSpout 연결한다.

다음으로, setBolt 이용해서 Bolt Topology 연결한다. 후에, HelloSpout HelloBolt 연결해야 하는데, setBolt시에, SuffleGrouping 메서드를 이용하여, HelloBolt HelloSpout으로 부터 생성되는 데이타를 읽어들임을 명시한다.

builder.setBolt("HelloBolt", new HelloBolt(),4).shuffleGrouping("HelloSpout");

이렇게 Topology 구성되었으면이 Topology 실제로 실행해야 하는데, Topology 어떤 서버에서 어떤 포트등을 이용해서 실행될지는 Config 정의할 있지만, 여기서는 간단한 테스트이기  때문에 별도의 복잡한 Config 정보는 기술하지 않았다.

다음으로 이렇게 만들어진 Topology Storm 클러스터에 배포해야 하는데, Storm 개발의 편의를 위해서 두가지 형태의 클러스터를 제공한다. 개발용 클러스터와 실운영 환경용 클러스터를 제공하는데, 여기서는 LocalCluster cluster = new LocalCluster();

라는  것을 사용하였다.

LocalCluster 개발환경용 클러스터로, 개발자의 환경에서 최소한의 서버들만을 기동하여 개발한 토폴로지를 테스트할 있게 해준다. 이렇게 Cluster 생성했으면 cluster.submitTopology 이용하여 개발한 토폴로지를 배포한다. 토폴로지가 배포되면 자동으로 토폴로지가 실행이 된다. HelloSpout 계속해서 데이타를 생성하고, HelloBolt 생성된 데이타를 받아서 System.out.println으로 출력하게 되는데, 10초후에 멈추게 하기 위해서, Sleep 10초를 준다. 토폴로지 코드를 실행하는 쓰레드는 Sleep으로 빠질지 모르지만 토폴로지에서 생성된 HelloSpout HelloBolt 쓰레드는 백그라운드에서 작업을 계속 진행한다.

10초후에는 killTopology 이용해서 해당 토폴로지를 제거하고 shutdown 이용해서 Storm 클러스터를 종료시킨다.

실행하기

여기까지 구현했으면 첫번째 Storm 프로그램을 기동해보자. 다음과 같이 maven 명령어를 이용하면 실행이 가능하다.

C:\dev\ws\java_workspace\com.terry.storm>mvn exec:java -Dexec.mainClass=com.terry.storm.hellostorm.HelloTopologyLocal -Dexec.classpath.Scope=compile

실행을 해보면, HelloSpout 데이타를 생성하고, HelloBolt 이를 받아서 화면에 출력하는 것을 있다.


6292 [Thread-16-HelloSpout] INFO  backtype.storm.daemon.task - Emitting: HelloSpout default [hello world]

6292 [Thread-22-HelloBolt] INFO  backtype.storm.daemon.executor - Processing received message source: HelloSpout:5, stream: default, id: {}, [hello world]

Tuple value ishello world

6292 [Thread-10-HelloBolt] INFO  backtype.storm.daemon.executor - Processing received message source: HelloSpout:6, stream: default, id: {}, [hello world]

Tuple value ishello world

ZooKeeper 에러 대응하기

종종 환경에 따라서 실행이 안되면서 다음과 같은 에러가 출력되는 경우가 있는데


3629 [main] INFO  org.apache.storm.zookeeper.ZooKeeper - Initiating client connection, connectString=localhost:2000 sessionTimeout=20000 watcher=org.apache.storm.curator.ConnectionState@7bfd25ce

3649 [main-SendThread(0:0:0:0:0:0:0:1:2000)] INFO  org.apache.storm.zookeeper.ClientCnxn - Opening socket connection to server 0:0:0:0:0:0:0:1/0:0:0:0:0:0:0:1:2000. Will not attempt to authenticate using SASL (java.lang.SecurityException: 로그인 구성을 찾을 없습니다.)

3650 [main-SendThread(0:0:0:0:0:0:0:1:2000)] ERROR org.apache.storm.zookeeper.ClientCnxnSocketNIO - Unable to open socket to 0:0:0:0:0:0:0:1/0:0:0:0:0:0:0:1:2000

3655 [main-SendThread(0:0:0:0:0:0:0:1:2000)] WARN  org.apache.storm.zookeeper.ClientCnxn - Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect

java.net.SocketException: Address family not supported by protocol family: connect

        at sun.nio.ch.Net.connect(Native Method) ~[na:1.6.0_37]

        at sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:532) ~[na:1.6.0_37]

        at org.apache.storm.zookeeper.ClientCnxnSocketNIO.registerAndConnect(ClientCnxnSocketNIO.java:277) ~[storm-core-0.9.3.jar:0.9.3]

        at org.apache.storm.zookeeper.ClientCnxnSocketNIO.connect(ClientCnxnSocketNIO.java:287) ~[storm-core-0.9.3.jar:0.9.3]

        at org.apache.storm.zookeeper.ClientCnxn$SendThread.startConnect(ClientCnxn.java:967) ~[storm-core-0.9.3.jar:0.9.3]

        at org.apache.storm.zookeeper.ClientCnxn$SendThread.run(ClientCnxn.java:1003) ~[storm-core-0.9.3.jar:0.9.3]


이 에러는 Storm Zookeeper와 연결을 할 수 없어서 나는 에러인데, LocalCluster 모드로 기동할 경우,Storm embedded Zookeeper를 기동해서 이 Zookeeper와 연결되어야 하나. IPV6로 연결을 시도하기 때문에, (ZK IPV4 Listen하는데) 발생하는 문제로

java로 실행할때 "-Djava.net.preferIPv4Stack=true" 옵션을 주면, JVM IPV6를 사용하지 않고, V4를 사용하기 때문에, ZooKeeper IPV4로 뜨고, Storm IPV4로 연결을 시도하기 때문에 문제가 없어진다.

지금까지 간단하게나마 첫번째 Storm 프로그램을 작성해서 실행해보았다.

다음에는 Storm 이루는 컴포넌트 구조와 아키텍쳐에 대해서 설명하도록 한다

 

REST API의 이해와 설계


#3 API 보안


REST API 보안 

API 보안에 대해서는 백번,천번을 강조해도 과함이 없다. 근래에 대부분의 서비스 시스템들은 API를 기반으로 통신을 한다.

앱과 서버간의 통신 또는 자바스크립트 웹 클라이언트 와 서버간의 통신등 대부분의 통신이 이 API들을 이용해서 이루어지기 때문에, 한번 보안이 뚫려 버리면 개인 정보가 탈취되는 것 뿐만 아니라 많은 큰 문제를 야기할 수 있다.


REST API 보안 관점

API는 보안 포인트에 따라서 여러가지 보안 관점이 존재하는데, 크게 아래와 같이 5가지 정도로 볼 수 있다.


인증 (Authentication)

인증은 누가 서비스를 사용하는지를 확인하는 절차이다.

쉽게 생각하면 웹 사이트에 사용자 아이디와 비밀 번호를 넣어서, 사용자를 확인하는 과정이 인증이다.

API도 마찬가지로 API를 호출하는 대상 (단말이 되었건, 다른 서버가 되었건, 사용자가 되었건)을 확인하는 절차가 필요하고 이를 API 인증이라고 한다. 


인가 (Authorization)

인가는 해당 리소스에 대해서, 사용자가 그 리소스를 사용할 권한이 있는지 체크하는 권한 체크 과정이다.

예를 들어 /users라는 리소스가 있을 때, 일반 사용자 권한으로는 내 사용자 정보만 볼 수 있지만, 관리자 권한으로는 다른 사용자 정보를 볼 수 있는 것과 같은 권한의 차이를 의미한다.


네트워크 레벨 암호화

인증과 인가 과정이 끝나서 API를 호출하게 되면, 네트워크를 통해서 데이터가 왔다갔다 하는데, 해커등이 중간에 이 네트워크 통신을 낚아 채서(감청) 데이터를 볼 수 없게 할 필요가 있다.

이를 네트워크 프로토콜단에서 처리하는 것을 네트워크 레벨의 암호화라고 하는데, HTTP에서의 네트워크 레벨 암호화는 일반적으로 HTTPS 기반의 보안 프로토콜을 사용한다.


메시지 무결성 보장

메시지 무결성이란, 메시지가 중간에 해커와 같은 외부 요인에 의해서 변조가 되지 않게 방지하는 것을 이야기 한다.

무결성을 보장하기 위해서 많이 사용하는 방식은 메시지에 대한 Signature를 생성해서 메시지와 같이 보낸 후에 검증하는 방식으로, 예를 들어 메시지 문자열이 있을 때, 이 문자열에 대한 해쉬코드를 생성해서, 문자열과 함께 보낸 후, 수신쪽에서 받은 문자열과 이 받은 문자열로 생성한 해쉬 코드를 문자열과 함께 온 해쉬코드와 비교하는 방법이 있다. 만약에 문자열이 중간에 변조되었으면, 원래 문자열과 함께 전송된 해쉬코드와 맞지 않기 때문에 메시지가 중간에 변조가되었는지 확인할 수 있다.

메시지의 무결성의 경우, 앞에서 언급한 네트워크 레벨의 암호화를 완벽하게 사용한다면 외부적인 요인(해커)등에 의해서 메시지를 해석 당할 염려가 없기 때문에 사용할 필요가 없다.


메시지 본문 암호화

네트워크 레벨의 암호화를 사용할 수 없거나, 또는 네트워크 레벨의 암호화를 신뢰할 수 없는 상황의 경우 추가적으로 메시지 자체를 암호화 하는 방법을 사용한다. 이는 애플리케이션 단에서 구현하는데, 전체 메시지를 암호화 하는 방법과 특정 필드만 암호화 하는 방법 두가지로 접근할 수 있다.

전체 메시지를 암호화할 경우, 암호화에 소요되는 비용이 클 뿐더라 중간에 API Gateway등을 통해서 메시지를 열어보고 메시지 기반으로 라우팅 변환하는 작업등이 어렵기 때문에 일반적으로 전체를 암호화 하기 보다는 보안이 필요한 특정 필드만 암호화 하는 방법을 사용한다.


그러면 지금부터 각 보안 관점에 대해서 조금 더 구체적으로 살펴보도록 하자. 


인증 (Authentication)

API 에 대한 인증은 여러가지 방법이 있으며 각 방식에 따라 보안 수준과 구현 난이도가 다르기 때문에, 각 방식의 장단점을 잘 이해하여 서비스 수준에 맞는 적절한 API 인증 방식을 선택하도록 할 필요가 있다.


API Key 방식

가장 기초적인 방법은 API Key를 이용하는 방법이다. API Key란 특정 사용자만 알 수 있는 일종의 문자열이다. API를 사용하고자 할 때, 개발자는 API 제공사의 포탈 페이등 등에서 API Key를 발급 받고, API를 호출할 때 API Key를 메시지 안에 넣어 호출한다. 서버는 메시지 안에서 API Key를 읽어 이 API가 누가 호출한 API인지를 인증하는 흐름이다.

모든 클라이언트들이 같은 API Key를 공유하기 때문에 한번 API Key가 노출이 되면 전체 API가 뚫려 버리는 문제가 있기 때문에 높은 보안 인증을 요구 하는 경우에는 권장하지 않는다.


API Token 방식

다른 방식으로는 API Token을 발급하는 방식이 있는데, 사용자 ID,PASSWD등으로 사용자를 인증한 후에, 그 사용자가 API 호출에 사용할 기간이 유효한 API Token을 발급해서 API Token으로 사용자를 인증하는 방식이다.

매번 API 호출시 사용자 ID,PASSWD를 보내지 않고, API Token을 사용하는 이유는 사용자 PASSWD는 주기적으로 바뀔 수 있기 때문이고, 매번 네트워크를 통해서 사용자 ID와 PASSWD를 보내는 것은 보안적으로 사용자 계정 정보를 탈취 당할 가능성이 높기 때문에 API Token을 별도로 발급해서 사용하는 것이다.

API Token을 탈취 당하면 API를 호출할 수 는 있지만, 반대로 사용자 ID와 PASSWD는 탈취 당하지 않는다. 사용자PASSWD를 탈취당하면 일반적으로 사용자들은 다른 서비스에도 같은 PASSWD를 사용하는 경우가 많기 때문에 연쇄적으로 다른 서비스에 대해서도 공격을 당할 수 있는 가능성이 높아지기 때문이다. 예를 들어서 매번 호출시마다 사용자 ID,PASSWD를 보내서 페이스북의 계정과 비밀번호를 탈취 당한 경우, 해커가 이 계정과 비밀 번호를 이용해서 GMail이나 트위터와 같은 다른 서비스까지 해킹 할 수 있기 때문에, 이러한 가능성을 최소화하기 위함이다.

 


흐름을 설명하면 위의 그림과 같다.

1. API Client가 사용자 ID,PASSWD를 보내서 API호출을 위한 API Token을 요청한다.

2. API 인증 서버는 사용자 ID,PASSWD를 가지고, 사용자 정보를 바탕으로 사용자를 인증한다.

3. 인증된 사용자에 대해서 API Token을 발급한다. (유효 기간을 가지고 있다.)

4. API Client는 이 API Token으로 API를 호출한다. API Server는 API Token이 유효한지를 API Token 관리 서버에 문의하고, API Token이 유효하면 API 호출을 받아 들인다.

이 인증 방식에는 여러가지 다양한 변종이 존재하는데

먼저 1단계의 사용자 인증 단계에서는 보안 수준에 따라서 여러가지 방식을 사용할 수 있다.


HTTP Basic Auth 

※ 상세 : http://en.wikipedia.org/wiki/Basic_access_authentication

가장 기본적이고 단순한 형태의 인증 방식으로 사용자 ID와 PASSWD를 HTTP Header에 Base64 인코딩 형태로 넣어서 인증을 요청한다.

예를 들어 사용자 ID가 terry이고 PASSWD가 hello world일 때, 다음과 같이 HTTP 헤더에 “terry:hello world”라는 문자열을 Base64 인코딩을해서 “Authorization”이라는 이름의 헤더로 서버에 전송하여 인증을 요청한다.

Authorization: Basic VGVycnk6aGVsbG8gd29ybGQ=

중간에 패킷을 가로채서 이 헤더를 Base64로 디코딩하면 사용자 ID와 PASSWD가 그대로 노출되기 때문에 반드시 HTTPS 프로토콜을 사용해야 한다.


Digest access Authentication

상세 : http://en.wikipedia.org/wiki/Digest_access_authentication

HTTP Basic Auth가 Base64 형태로 PASSWD를 실어서 보내는 단점을 보강하여 나온 인증 프로토콜이 Digest access Authentication 이라는 방법으로, 기본 원리는 클라이언트가 인증을 요청할 때, 클라이언트가 서버로부터 nonce 라는 일종의 난수값을 받은 후에, (서버와 클라이언트는 이 난수 값을 서로 알고 있음), 사용자 ID와 PASSWD를 이 난수값을 이용해서 HASH화하여 서버로 전송하는 방식이다.

이 경우에는 직접 ID와 PASSWD가 평문 형태로 날아가지 않기 때문에, 해커가 중간에 PASSWD를 탈취할 수 없고, 설령 HASH 알고리즘을 알고 있다고 하더라도, HASH된 값에서 반대로 PASSWD를 추출하기가 어렵기 때문에, Basic Auth 방식에 비해서 향상된 보안을 제공한다. 전체적인 흐름을 보자


 

1. 클라이언트가 서버에 특정리소스 /car/index.html 을 요청한다.


2. 서버는 해당 세션에 대한 nonce값을 생성하여 저장한 후에, 클라이언트에 리턴한다. 이때 realm을 같이 리턴하는데, realm은 인증의 범위로, 예를 들어 하나의 웹 서버에 car.war, market.war가 각각 http://myweb/car , http://myweb/market 이라는 URL로 배포가 되었다고 하면, 이 웹사이트는 각각 애플리케이션 car.war와 market.war에 대해서 서로 다른 인증realm을 갖는다. 

※ 해당 session에 대해서 nonce 값을 유지 저장해야 하기 때문에, 서버 쪽에서는 상태 유지에 대한 부담이 생긴다. HTTP Session을 사용하거나 또는 서버간에 공유 메모리(memcached나 redis등)을 넣어서 서버간에 상태 정보를 유지할 수 있는 설계가 필요하다.


3. 클라이언트는 앞에서 서버로부터 받은 realm과 nonce값으로 Hash 값을 생성하는데, 

HA1 = MD5(사용자이름:realm:비밀번호)

HA2 = MD5(HTTP method:HTTP URL)

response hash = MD5(HA1:nonce:HA2)

를 통해서 response hash 값을 생성한다.

예를 들어서 /car/index.html 페이지를 접근하려고 했다고 하자, 서버에서 nonce값을 dcd98b7102dd2f0e8b11d0f600bfb0c093를 리턴하였고, realm은 car_realm@myweb.com 이라고 하자. 그리고 사용자 이름이 terry, 비밀 번호가 hello world하면

HA1 = MD5(terry:car_realm@myweb.com:hello world)로 7f052c45acf53fa508741fcf68b5c860 값이 생성되고

HA2 = MD5(GET:/car/index.html) 으로 0c9f8cf299f5fc5c38d5a68198f27247 값이 생성된다.

Response Hash는MD5(7f052c45acf53fa508741fcf68b5c860: dcd98b7102dd2f0e8b11d0f600bfb0c093:0c9f8cf299f5fc5c38d5a68198f27247) 로 결과는 95b0497f435dcc9019c335253791762f 된다.

클라이언트는 사용자 이름인 “terry”와 앞서 받은 nonce값인 dcd98b7102dd2f0e8b11d0f600bfb0c093와 계산된 hash값인 95b0497f435dcc9019c335253791762f 값을 서버에게 전송한다.


4. 서버는 먼저 3에서 전달된 nonce값이 이 세션을 위해서 서버에 저장된 nonce 값과 같은지 비교를 한후, 전달된 사용자 이름인 terry와nonce값 그리고 서버에 저장된 사용자 비밀 번호를 이용해서 3번과 같은 방식으로 response hash 값을 계산하여 클라이언트에서 전달된 hash값과 같은지 비교를 하고 같으면 해당 리소스를 (/car/index.html 파일)을 리턴한다. 


간단한 기본 메커니즘만 설명한것이며, 사실 digest access authentication은 qop (quality of protection)이라는 레벨에 따라서 여러가지 변종(추가적인 보안)을 지원한다. 언뜻 보면 복잡해서 보안 레벨이 높아보이지만 사실 Hash 알고리즘으로 MD5를 사용하는데, 이 MD5는 보안 레벨이 낮기 때문에 미정부 보안 인증 규격인 FIPS인증 (http://csrc.nist.gov/publications/fips/fips140-2/fips1402annexa.pdf) 에서 인증하고 있지 않다. FIPS 인증에서는 최소한 SHA-1,SHA1-244,SHA1-256 이상의 해쉬 알고리즘을 사용하도록 권장하고 있다.

MD5 해쉬의 경우에는 특히나 Dictionary Attack에 취약한데, Dictionary Attack이란, Hash된 값과 원래 값을 Dictionary (사전) 데이터 베이스로 유지해놓고, Hash 값으로 원본 메시지를 검색하는 방식인데, 실제로 http://en.wikipedia.org/wiki/Digest_access_authentication 설명에서 예제로 든

  • HA1 = MD5( "Mufasa:testrealm@host.com:Circle Of Life" )  = 939e7578ed9e3c518a452acee763bce9

의 MD5 해쉬 값인 939e7578ed9e3c518a452acee763bce9 값을 가지고, MD5 Dictionary 사이트인 http://md5.gromweb.com/?md5=939e7578ed9e3c518a452acee763bce9 에서 검색해보면, Hash 값으로 원본 메시지인 Mufasa:testrealm@host.com:Circle Of Life 값이 Decrypt 되는 것을 확인할 수 있다.

 


그래서 반드시 추가적인 보안 (HTTPS) 로직등을 겸비해서 사용하기를 바라며, 더 높은 보안 레벨이 필요한 경우 다른 인증 메커니즘을 사용하는 것이 좋다. 

FIPS 인증 수준의 보안 인증 프로토콜로는 SHA-1 알고리즘을 사용하는 SRP6a 등이 있다. 높은 수준의 보안이 필요할 경우에는 아래 링크를 참고하기 바란다. http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol


클라이언트 인증 추가

추가적인 보안 강화를 위해서 사용자 인증 뿐만 아니라, 클라이언트 인증 방식을 추가할 수 있다. 페이스북의 경우 API Token을 발급 받기 위해서, 사용자 ID,PASSWD 뿐만 아니라 client Id와 Client Secret이라는 것을 같이 입력 받도록 하는데,

Client Id는 특정 앱에 대한 등록 Id이고, Client Secret은 특정 앱에 대한 비밀 번호로 페이스북 개바자 포털에서 앱을 등록하면 앱 별로 발급 되는 일종의 비밀 번호이다.

 


그림. 페이스북 개발자 포탈에서 등록된 client Id(appId)와 client secret(App Secret)을 확인하는 화면

API Token을 발급 받을 때, Client Id와 Client Secret 을 이용하여, 클라이언트 앱을 인증하고 사용자 ID와 PASSWD를 추가로 받아서 사용자를 인증하여 API access Token을 발급한다.


제3자 인증 방식

3자 인증 방식은 페이스북이나 트위터와 같은 API 서비스 제공자들이 파트너 애플리케이션에 많이 적용하는 방법으로 만약 내가 My Server Application라는 서비를 Facebook 계정을 이용하여 인증을 하는 경우이다.

이때 중요한 점은 서비스 My Server Application에 대해서 해당 사용자가 페이스북 사용자임을 인증을 해주지만, 서비스 My Server Application는 사용자의 비밀번호를 받지 않고, 페이스북이 사용자를 인증하고 서비스 My Server Application에게 알려주는 방식이다. 즉 파트너 서비스에 페이스북 사용자의 비밀번호가 노출되지 않는 방식이다.

전체적인 흐름을 보면 다음과 같다.

 


1. 먼저 페이스북의 Developer Portal에 접속을 하여, 페이스북 인증을 사용하고자 하는 애플리케이션 정보를 등록한다. (서비스 명, 서비스 URL,그리고 인증이 성공했을 때 인증 성공 정보를 받을 CallBack URL)

2. 페이스북 Developer Portal은 등록된 정보를 기준으로 해당 애플리케이션에 대한 client_id와 client_secret을 발급한다. 이 값은 앞에서 설명한 클라이언트 인증에 사용된다.

3. 다음으로, 개발하고자 하는 애플리케이션에, 이 client_id와 client_secret등을 넣고, 페이스북 인증 페이지 정보등을 넣어서 애플리케이션을 개발한다.

애플리케이션이 개발되서 실행이 되면, 아래와 같은 흐름에 따라서 사용자 인증을 수행하게 된다.

 


1. 웹브라우져에서 사용자가 My Server Application 서비스를 접근하려고 요청한다.

2. My Server Application은 사용자가 인증이되어 있지 않기 때문에, 페이스북 로그인 페이지 URL을 HTTP Redirection으로 URL을 브라우져에게 보낸다. 이때 이 URL에 페이스북에 이 로그인 요청이 My Server Application에 대한 사용자 인증 요청임을 알려주기 위해서, client_id등의 추가 정보와 함께, 페이스북의 정보 접근 권한 (사용자 정보, 그룹 정보등)을 scope라는 필드를 통해서 요청한다.

3. 브라우져는 페이스북 로그인 페이지로 이동하여, 2단계에서 받은 추가적인 정보와 함께 로그인을 요청한다.

4. 페이스북은 사용자에게 로그인 창을 보낸다.

5. 사용자는 로그인창에 ID/PASSWD를 입력한다.

6. 페이스북은 사용자를 인증하고, 인증 관련 정보과 함께 브라우져로 전달하면서, My Server Application의 로그인 완료 페이지로 Redirection을 요청한다.

7. My Server Application을 6에서 온 인증 관련 정보를 받는다.

8. My Server Application은 이 정보를 가지고, 페이스북에, 이 사용자가 제대로 인증을 받은 사용자인지를 문의한다.

9. 페이스북은 해당 정보를 보고 사용자가 제대로 인증된 사용자임을 확인해주고, API Access Token을 발급한다.

10.  My Server Application은 9에서 받은 API Access Token으로 페이스북 API 서비스에 접근한다.


앞에서 설명했듯이, 이러한 방식은 자사가 아닌 파트너 서비스에게 자사 서비스 사용자의 인증을 거쳐서 API의 접근 권한을 전달하는 방식이다.

이러한 인증 방식의 대표적인 구현체는 OAuth 2.0으로, 이와 같은 제3자 인증뿐만 아니라, 직접 자사의 애플리케이션을 인증하기 위해서, 클라이언트로부터 직접 ID/PASSWD를 입력 받는 등.

클라이언트 타입(웹,서버,모바일 애플리케이션)에 대한 다양한 시나리오를 제공한다.

※ OAuth 2.0에 대한 자세한 설명은 PACKT 출판사의 OAuth 2.0 Identity and Access Management Patterns (by Martin Spasovski) 책을 참고하기를 추천한다.

이러한 3자 인증 방식은 일반적인 서비스에서는 필요하지 않지만, 자사의 API를 파트너등 외부 시스템에 제공하면서 사용자의 ID/PASSWD를 보호하는데는 필요한 서비스이기 때문에, API 를 외부에 적용하는 경우에는 고려를 해야 한다.


IP White List을 이용한 터널링

만약에 API를 호출하는 클라이언트의 API가 일정하다면 사용할 수 있는 손쉬운 방법인데, 서버간의 통신이나 타사 서버와 자사 서버간의 통신 같은 경우에, API 서버는 특정 API URL에 대해서 들어오는 IP 주소를 White List로 유지하는 방법이 있다.

API 서버 앞단에, HAProxy나 Apache와 같은 웹서버를 배치하여서 특정 URL로 들어올 수 있는 IP List를 제한 하거나, 아니면 전체 API가 특정 서버와의 통신에만 사용된다면 아예, 하드웨어 방화벽 자체에 들어올 수 있는 IP List를 제한할 수 있다.

설정만으로 가능한 방법이기 때문에, 서버간의 통신이 있는 경우에는 적용하는 것을 권장한다.


Bi-diretional Certification (Mutual SSL)

가장 높은 수준의 인증 방식을 제공할 수 있는 개념으로, 보통 HTTPS 통신을 사용할 때 서버에 공인 인증서를 놓고 단방향으로 SSL을 제공한다.

반면 Bi-Directional Certification (양방향 인증서 방식) 방식은 클라이언트에도 인증서를 놓고 양방향으로 SSL을 제공하면서, API 호출에 대한 인증을 클라이언트의 인증서를 이용 하는 방식이다. 

구현 방법이 가장 복잡한 방식이기는 하지만, 공인 기관에서 발행된 인증서를 사용한다면 API를 호출하는 쪽의 신원을 확실하게 할 수 있고, 메시지까지 암호화되기 때문에, 가장 높은 수준의 인증을 제공한다. 이런 인증 방식은 일반 서비스에서는 사용되지 않으며, 높은 인증 수준을 제공하는 몇몇 서비스나 특정 서버 간 통신에 사용하는 것이 좋다.


권한 인가 (Authorization)

인증이 끝나면 다음 단계는 권한에 대한 인증, 즉 인가 (Authorization) 과정이 필요하다.

사용자가 인증을 받고 로그인을 했다해더라도 해당 API를 호출 할 수 있는 권한이 있는 가를 체크해야 한다.

예를 들어 “일반 사용자 A가 로그인 했을 때, 다른 사용자를 삭제하는 것은 사용자 A가 관리자 권한을 가지고 있고, 이 요청이 웹 관리 콘솔을 통해서 들어온 경우에만 허용한다.” 와 같은 경우이다. 사용자가 인증(Authentication)을 통해서 시스템 내의 사용자 임을 확인 받았지만, API 호출을 하기 위해서 적절한 권한이 있는지를 검증해야 한다.


API 인가 방식

권한 인가(Authorization)방식에는 여러가지 방식이 있는데, 대표적인 방식 몇가지만 보면

가장 일반적인 권한 인증 방식은 사용자의 역할(ROLE)을 기반으로 하는 RBAC (Role Based Access Control)이라는 방식이 있다.

이 방식은 정해진 ROLE에 권한을 연결해놓고, 이 ROLE을 가지고 있는 사용자게 해당 권한을 부여하는 것이다. 

예를 들어

  • 일반 관리자 - 사용자 관리, 게시물 관리, 회원 가입 승인
  • 마스터 관리자 - 까페 게시판 게시판 관리, 메뉴 관리, 사용자 관리, 게시물 관리, 회원 가입 승인

와 같은 권한을 만든후, 

Terry에 "마스터 관리자"라는 ROLE을 부여하면, 사용자 Terry는 "까페 게시판 게시판 관리, 메뉴 관리, 사용자 관리, 게시물 관리, 회원 가입 승인" 등의 권한을 가지게 된다.

이렇게 권한 부여의 대상이 되는 사용자나 그룹을 Object라고 하고, 각 개별 권한을 Permission이라고 정의하며, 사용자의 역할을 Role이라고 정의한다. RBAC는 이 Role에 권한을 맵핑 한 다음 Object에 이 Role을 부여 하는 방식으로 많은 권한 인가는 사용자 역할을 기반으로 하기 때문에, 사용하기가 용이하다.

 


다른 권한 인증 모델로는 ACL (Access Control List)라는 방식이 있다.

RBAC 방식이 권한을 ROLE이라는 중간 매개체를 통해서 사용자에게 부여하는데 반해서, ACL 방식은 사용자(또는 그룹과 같은 권한의 부여 대상) 에게 직접 권한을 부여하는 방식이다.

사용자 Terry 에 직접 "까페 게시판 게시판 관리, 메뉴 관리, 사용자 관리, 게시물 관리, 회원 가입 승인" 권한을 부여 하는 방식이 ACL의 대표적인 예이다.



 

이러한 API 권한 인가 체크는 인증 (Authentication)이 끝나 후에, 인가에 사용된 api accesstoken을 이용하여 사용자 정보를 조회하고, 사용자 정보에 연관된 권한 정보 (Permission이나 Role정보)를 받아서 이 권한 정보를 기반으로 API 사용 권한을 인가하는 방법을 사용한다.  사용자 정보 조회 "HTTP GET /users/{id}"라는 API 가 있다고 가정하자.

이 API의 권한은 일반 사용자의 경우 자신의 id에 대해서만 사용자 정보 조회가 가능하고, (자신의 정보만), 만약에 관리자의 경우에는 다른 사용자의 id도 조회가 가능하도록 차등하여 권한을 부여할 수 있다.

이러한 권한 검증은 API access token으로 사용자를 찾은 후, 사용자에게 assign 된 ROLE이나 Access Control을 받아서 API 인증을 처리할 수 있다.


API 권한 인가 처리 위치

API에 대한 권한 인가 처리는 여러가지 계층에서 처리할 수 있다.

권한 인가는 API를 호출 하는 쪽인 클라이언트, API를 실행하는 API 서버쪽, 그리고 API 에 대한 중간 길목 역할을 하는 gateway 3군데서 처리할 수 있으며 근래에는 API 서버쪽에서 처리하는 것이 가장 일반적이다.


클라이언트에 의한 API 권한 인가 처리

API를 호출 하는 클라이언트 쪽에서 사용자의 권한에 따라서 API를 호출하는 방식인데, 이 방식의 경우 클라이언트가 신뢰할 수 있는 경우에만 사용할 수 있다.

이 방식은 기존에, 웹 UX 로직이 서버에 배치되어 있는 형태 (Struts나 Spring MVC와 같은 웹 레이어가 있는 경우)에 주로 사용했다.

위의 사용자 API를 예를 들어보면 웹 애플리케이션에서, 사용자 로그인 정보(세션 정보와 같은)를 보고 사용자 권한을 조회한 후에, API를 호출 하는 방식이다.

사용자 세션에, 사용자 ID와 ROLE을 본 후에, ROLE이 일반 사용자일 경우, 세션내의 사용자 ID와 조회하고자 하는 사용자 ID가 일치하는 경우에만 API를 호출 하는 방식이다.

이러한 구조를 사용할 경우 모바일 디바이스 등에 제공하는 API는 사용자 ROLE을 갖는 API와 같이 별도의 권한 인가가 필요 없는 API를 호출 하는 구조를 갖는다.

이 구조를 그림으로 표현해보면 다음과 같다.

Mobile Client는 일반 사용자만 사용한다고 가정하고, 웹 애플리케이션은 일반 사용자와 관리자 모두 사용한다고 했을 때, 일반 사용자의 Mobile Client를 위한 API Server를 별도로 배치하고, 사용자 인증(Authentication)만 되면 모든 API 호출을 허용하도록 한다. Mobile Client에 대한 API는 권한 인증에 대한 개념이 없기 때문에, 인증 처리만 하면 되고, 웹 애플리케이션의 경우에는 일반 사용자냐, 관리자냐에 따라서 권한 인가가 필요하기 때문에 아래 그림과 같이 Web Application에서, API를 호출하기 전에 사용자의 id와 권한에 따라서 API 호출 여부를 결정하는 API 권한 인가(Authorization) 처리를 하게 한다.

 


Gateway에 의한 권한 인가 처리

이러한 권한 인가는 모바일 클라이언트, 자바스크립트 기반의 웹 클라이언트등 다양한 클라이언트가 지원됨에 따라 점차 서버쪽으로 이동하기 시작했는데, 특히 자바 스크립트 클라이언트의 경우 클라이언트에서 권한에 대한 인가는 의미가 없기 때문에 어쩔 수 없이 서버 쪽에서 권한 인가 처리를 할 수 밖에 없게 된다. 만약에 자바 스크립트에 권한 인가 로직을 넣을 경우, 자바 스크립트의 경우 브라우져의 디버거등으로 코드 수정이 가능하기 때문에 권한 처리 로직을 위회할 수 도 있고 또한 API 포맷만 안다면 직접 API를 서버로 호출해서 권한 인가 없이 API를 사용할 수 있다.

서버에서 권한을 처리하는 방법은 API 호출의 길목이 되는 gateway나 API 비지니스 로직 두군데서 처리가 가능한데, API gateway에 의한 권한 처리는 구현이 쉽지 않기 때문에,API 서버에서 권한 처리를 하는 것이 일반적이다.

아래 그림은 API gateway에서 권한 인가를 처리하는 방법인데, API 호출이 들어오면, API access Token을 사용자 정보와 권한 정보로 API token management 정보를 이용해서 변환 한 후에, 접근하고자 하는 API에 대해서 권한 인가 처리를 한다.

이는 API 별로 API를 접근하고자 하는데 필요한 권한을 체크해야 하는데, HTTP GET /users/{id}의 API를 예로 들어보면, 이 URL에 대한 API를 호출하기 위해서는 일반 사용자 권한을 가지고 있는 사용자의 경우에는 호출하는 사용자 id와 URL상의 {id}가 일치할 때 호출을 허용하고, 같지 않을 때는 호출을 불허해야 한다.

만약 사용자가 관리자 권한을 가지고 있을 경우에는 호출하는 사용자 id와 URL상의 {id}가 일치하지 않더라도 호출을 허용해야 한다.

 


그러나 이러한 api gateway에서의 권한 인가는 쉽지가 않은데, 위의 /users/{id} API의 경우에는 사용자 id가 URL에 들어가 있기 때문에, API access token과 맵핑되는 사용자 ID와 그에 대한 권한을 통해서 API 접근 권한을 통제할 수 있지만, API에 따라서 사용자 id나 권한 인증에 필요한 정보가 HTTP Body에 json 형태나 HTTP Header 등에 들어가 있는 경우, 일일이 메세지 포맷에 따라서 별도의 권한 통제 로직을 gateway 단에서 구현해야 하는 부담이 있고, 권한 통제를 위해서 HTTP 메세지 전체를 일일이 파싱해야 하는 오버로드가 발생하기 때문에, 공통 필드등으로 API 권한 처리를 하지 않는 경우에는 사용하기가 어려운 부분이다.


서버에 의한 API 권한 인가 처리

그래서 가장 일반적이고 보편적인 방법은 API 요청을 처리하는 API 서버의 비지니스 로직단에서 권한 처리를 하는 방식이다.

이 방식은 앞에서 언급한 api gateway 방식과 비교했을때, 각 비지니스 로직에서 API 메세지를 각각 파싱하기 때문에, API 별로 권한 인가 로직을 구현하기가 용이 하다.

이 경우에는 권한 인가에 필요한 필드들을 api gateway에서 변환해서 API 서버로 전달해줌으로써 구현을 간략하게 할 수 있는데,

아래 그림과 같이 API 클라이언트가 api access token을 이용해서 API를 호출했을 경우, api gateway가 이 access token을 권한 인가에 필요한 사용자 id, role등으로 변환해서 API 서버에 전달해주게 되면, 각 비지니스 로직은 API 권한 인가에 필요한 사용자 정보등을 별도로 데이타 베이스를 뒤지지 않고 이 헤더의 내용만을 이용해서 API 권한 인가 처리를 할 수 있게 된다.

 


네트워크 (전송) 레벨 암호화

가장 기본적이고 필수적인 REST API 보안 방법은 네트워크 전송 프로토콜에서 HTTPS 보안 프로토콜을 사용하는 방법이다. HTTPS 프로토콜만 사용한다 하더라도, 메시지 자체를 암호화 해서 전송하기 때문에, 해킹으로 인한 메시지 누출 위협을 해소화 할 수 있다.

그런데, HTTPS를 사용하더라도, 이러한 메시지를 낚아채거나 변조하는 방법이 있는데, 이러한 해킹 방법을 Man-in-The-Middle-Attack 이라고 한다.

정상적인 HTTPS 통신의 경우, 다음과 같이 서버에서 제공하는 인증서 A를 이용하여 API 클라이언트와 서버 상화간에 암호화된 신뢰된 네트워크 연결을 만든다.

 


Man in the middle attack의 경우에는 신뢰된 연결을 만들려고 할 때, 해커가 API 클라이언트와 서버 사이에 끼어 들어온다.

 


다음은 그림과 같이 신뢰된 연결을 만들기 위해서 서버가 인증서 A를 클라이언트에게 내릴 때 해커가 이 인증서가 아닌 인증서 B를 클라이언트에 내리고, 인증서 B를 이용해서 API Client와 Hacker간에 HTTPS SSL 연결을 만든다. 그리고는 서버에게서 받은 인증서 A를 이용해서 해커와 API 서버간의 HTTP SSL 서버를 만든다.

이렇게 되면, 해커는 중간에서 API 클라이언트와 서버 사이에 메시지를 모두 열어 보고 변조도 가능하게 된다.

종종 대기업이나 공공 기관에서 웹브라우져를 사용하다 보면,인증서가 바뀌었다는 메시지를 볼 수 가 있는데 (특히 파이어폭스 브라우져를 사용하면 인증서 변경을 잘 잡아낸다.) 이는 회사의 보안 정책상 HTTPS 프로토콜의 내용을 보고, 이를 감사 하기 위한 목적으로 사용된다.


이런 Man in middle attack을 방지 하는 방법은 여러가지 방식이 있지만, 가장 손쉬운 방법은 공인된 인증서를 사용하고 인증서를 체크하는 것이다. 

해커가 인증서를 바꿔 치려면, 인증서를 발급해야 하는데, 공인 인증서는 Verisign과 같은 기간에서 인증서에 대한 공인 인증을 해준다. 즉, 이 인증서를 발급한 사람이 누구이고 이에 대한 신원 정보를 가지고 있다. 이를 공인 인증서라고 하는데, 공인 인증서는 인증 기관의 Signature로 싸인이 되어 있다. (공인 인증기관이 인증했다는 정보가 암호화 되서 들어간다.)

만약 해커가 공인 인증서를 사용하려면 인증 기관에 가서 개인 정보를 등록해야 하기 때문에, 공인 인증서를 사용하기 어렵고 보통 자체 발급한 비공인 인증서를 사용하기 때문에, 이를 이용해서 체크가 가능하고, 특히 인증서안에는 인증서를 발급한 기관의 정보와 인증서에 대한 고유 Serial 번호가 들어가 있기 때문에, 클라이언트에서 이 값을 체크해서 내가 발급하고 인증 받은 공인 인증서인지를 체크하도록 하면 된다.

아래는 자바의 keytool 유틸리티를 이용해서 자체발급한 인증서의 정보를 프린트해본 내용이다.

(※ keytool -printcert -file CERT.RSA)

Owner: CN=Android Debug, O=Android, C=US

Issuer: CN=Android Debug, O=Android, C=US

Serial number: 6b14b6db

Valid from: Mon Nov 19 09:58:00 KST 2012 until: Wed Nov 12 09:58:00 KST 2042

Certificate fingerprints:

         MD5:  78:69:7F:D5:BD:D7:B7:47:AD:11:6A:D2:F6:83:D7:CB

         SHA1: 44:14:35:A5:C5:28:77:A4:C4:DD:CA:80:26:02:68:A1:84:2E:BD:15

Issuer가 인증서를 발급한 기관의 이름이며, Serial number는 이 인증서에 대한 고유 번호이다. 맨 아래에 있는 fingerprints는 인증서에 대한 해쉬 값으로, 만약에 인증서가 변조되면 이 해쉬값이 달라지기 때문에, 인증서 변조를 확인할 수 있다.


메시지 본문 암호화

다음으로는 간단하게 암호화가 필요한 특정 필드만 애플리케이션단에서 암호화하여 보내는 방법이 있다. 

메시지를 암호화 하여 통신하기 위해서는 클라이언트와 서버가 암호화 키를 가져야 하는데, 암호화 키는 크게, 대칭키와, 비대칭키 알고리즘 두가지가 있다.

비대칭키 알고리즘은, 암호화를 시키는 키와, 암호를 푸는 복호화 키가 다른 경우로, 암호화 시키는 키를 보통 Public Key (공개키)라고 하고, 암호화를 푸는 키를 Private Key(비밀키)라고 한다. 이 공개키는 암호화는 할 수 있지만 반대로 암호화된 메시지를 풀 수 가 없기 때문에 누출이 되더라도 안전하다. (해커가 중간에서 공개키를 낚아 챈다고 하더라도, 이 키로는 암호화된 메시지를 복호화 할 수 없다.)

그래서, 처음에 클라이언트가 서버에 인증이 된 경우, 클라이언트에게 이 공개키를 내린 후에, 향후 메시지를 이 공개키를 통해서 암호화를 하게 하면, 이 암호화된 메시지는 비밀키를 가지고 있는 서버만이 풀 수 있어서 안전하게 서버로 메세지를 암호화 해서 보낼 수 있다.

대표적인 비대칭키 알고리즘으로는 RSA등이 있으며, 우리가 익숙한 HTTPS의 경우에도 이 RSA 알고리즘을 사용한다. RSA 알고리즘을 사용하는 비대칭키 암호화 로직과 라이브러리들은 공개된 것이 많으니 참고해서 사용하도록 한다.

비대칭키 알고리즘의 경우 클라이언트에서 서버로 보내는 단방향 메시지에 대해서는 암호화하여 사용할 수 있지만, 반대로 서버에서 클라이언트로 내려오는 응답 메시지등에는 적용하기가 어렵다. 아니면 클라이언트가 서버에 등록될 때, 위와 반대 방법으로 클라이언트에서 비공개키와 공개키 쌍을 생성한 후에, 서버로 공개키를 보내서 향후 서버에서 클라이언트로의 통신을 그 공개키를 사용하도록 해도 된다. 이경우, 클라이언트 ? 서버, 그리고 서버? 클라이언트간의 키 쌍 두개를 관리해야 하기 때문에 복잡할 수 있는데, 이런 경우에는 대칭키 알고리즘을 고려해볼 수 있다.

대칭키 알고리즘은 암호화와 복호화키가 같은 알고리즘이다.

이 경우 API 클라이언트와 서버가 같은 키를 알고 있어야 하는데, 키를 네트워크를 통해서 보낼 경우 중간에 해커에 의해서 낚아채질 염려가 있기 때문에, 양쪽에 안전하게 키를 전송하는 방법이 필요한데, 다음과 같은 방법을 사용할 수 있다.

1. 서버에서 공개키KA1와 비공개키KA2 쌍을 생성한다.

2. 클라이언트에게 공개키 KA1을 네트워크를 통해서 내려보낸다.

3. 클라이언트는 새로운 비공개 대칭키 KB를 생성한후 KA1을 이용해서 암호화하여 서버로 전송한다.

4. 서버는 전송된 암호화 메시지를 KA2로 복화화 하여, 그 안에 있는 비공개키 KB를 꺼낸다.

5. 향후 클라이언트와 서버는 상호 API통신시 비공개 대칭키 KB를 이용하여 암호화와 복호화를 진행한다.

대칭 키도 여러가지 종류가 있는데, 보안과 성능 측면에서 차이가 있다.

http://www.javamex.com/tutorials/cryptography/ciphers.shtml를 참고하면, 대칭키 기반의 암호화 알고리즘 속도를 비교해놓은 것이 있다. 일반적으로 AES256을 사용하면 빠른 암호화 속도와 높은 보안성을 보장받을 수 있다.(아래, 대칭키 기반의 암호화 알고리즘 속도 비교)

 


메시지 무결성 보장

무결성이란 서버 입장에서 API 호출을 받았을 때 이 호출이 신뢰할 수 있는 호출인지 아닌지를 구별하는 방법이다. 즉 해커가 중간에서 메시지를 가로챈 후, 내용을 변조하여 서버에 보냈을 때, 내용이 변조되었는지 여부를 판단하는 방법인데, 일반적으로 HMAC을 이용한 방식이 널리 사용된다. 어떤 원리로 작동하는지 살펴보도록 하자.

먼저 Rest API를 호출하는 클라이언트와 서버 간에는 대칭키 기반의 암호화 키 ‘Key’를 가지고 있다고 전제하자. 이 키는 클라이언트와 서버 양쪽이 알고 있는 대칭키로, API access Token을 사용할 수 도 있고, 앞의 메시지 본문 암호화에서 나온 방법을 이용해서 서로 대칭키를 교환하여 사용할 수 도 있다..

 


1. 먼저 클라이언트는 호출하고자 하는 REST API의 URL을 앞에서 정의한 Key를 이용하여 HMAC 알고리즘을 사용하여 Hash 값을 추출한다.

중요: 여기서는 편의상 URL을 가지고 HMAC 해시를 생성하였했는데, 전체 메시지에 대한 무결성을 보장하려면 URL이 아니라 메시지 전문 자체에 대해서 대해 Hash를 추출해야 한다.

2. API를 호출할 때, 메시지(또는 URL)에 추출한 HMAC을 포함해서 호출한다.

3. 서버는 호출된 URL을 보고 HMAC을 제외한 나머지 URL을 미리 정의된 Key를 이용해서, HMAC 알고리즘으로 Hash 값을 추출한다.

4. 서버는 3에서 생성된 HMAC 값과 API 호출 시 같이 넘어온 HMAC 값을 비교해서, 값이 같으면 이 호출을 유효한 호출이라고 판단한다.

만약에 만약 해커가 메시지를 중간에서 가로채어 변조하였했을 경우, 서버에서 Hash를 생성하면 변조된 메시지에 대한 Hash가 생성되기 때문에 클라이언트에서 변조 전에 보낸 Hash 값과 다르게 된다. 이를 통해서 통해 메시지가 변조되었는지 여부를 판단할 수 있다.

그런데, 만약에 만약 메시지를 변경하지 않고 Hacker가 동일한 요청을 계속 보낸다면? 메시지를 변조하지 않았기 때문에 서버는 이를 유효한 호출로 인식할 수 있다. 이를 replay attack이라고 하는데 이를 방지하기 위해서는위해서는 time stamp를 사용하는 방법이 있다.

이 방법은 HMAC을 생성할 때, 메시지를 이용해서만 Hash 값을 생성하는 것이 아니라 timestamp를 포함하여 메시지를 생성하는 것이다.

  • HMAC (Key, (메시지 데이터+timestamp) )

그리고 API를 호출할 때, timestamp 값을 같이 실어 보낸다.

http://service.myapi.com/restapiservice?xxxxx&hmac={hashvalue}&timestamp={호출시간}

이렇게 하면 서버는 메시지가 호출된 시간을 알 수 있고, 호출된 시간 +-10분(아니면 개발자가 정한 시간폭)만큼의 호출만 정상적인 호출로 인식하고 시간이 지난 호출의 메시지는 비정상적인 호출로 무시하면 된다.


* 참고 : : Hacker가 timestamp URL등 등을 통해서 통해 볼 수 있다고 하더라도, Key 값을 모르기 때문에 timestamp를 변조할 수 없다. timestamp를 변조할 경우에는 원본 Hash가 원본 timestamp로 생성되었기 때문에, timestamp가 변조된 경우 hash 값이 맞지 않게 된다.

HMAC을 구현하는 해시 알고리즘에는 MD5, SHA1등 등이 있는데, 보통 SHA-1 256bit 알고리즘을 널리 사용한다. 

HMAC 기반의 REST Hash 구현 방법은

http://www.thebuzzmedia.com/designing-a-secure-rest-api-without-oauth-authentication/

에 설명이 있으니 참고하기 바란다.

또한 HMAC 알고리즘 구현에 대해서는 위키(http://en.wikipedia.org/wiki/HMAC)를 보면 각 프로그래밍 언어별로 예제 링크가 있으므로 참고하기 바란다.

지금까지 간단하게나마 API 보안 방식에 대해서 살펴보았다.보안에 대해서 이야기 하자면 한도 끝도 없겠지만, API보안에서는 최소한 HTTPS를 이용한 네트워크 보안과 함께, API Token등의 인증 방식을 반드시 사용하기를 권장한다.

 보안 처리는 하지 않아도, API의 작동이나 사용에는 문제가 없다. 그러나 보안이라는 것은 한번 뚫려버리면 많은 정보가 누출이 되는 것은 물론이고 시스템이 심각한 손상까지 입을 수 있기 때문에,  시간이 걸리더라도 반드시 신경써서 설계 및 구현하는 것을 권장한다.


자바스크립트 클라이언트 지원


근래에 들어서 자바스크립트 기술이 발전하면서 SPA (Sigle Page Application)이 유행하기 시작했는데, SPA란 브라우져에서 페이지간의 이동없이 자바스크립트를 이용해서 동적으로 페이지를 변경할 수 있는 방식이다.

페이지 reloading이 없기 때문에 반응성이 좋아서 많이 사용되는데, SPA 의 경우 서버와의 통신을 자바스크립가 직접 XMLHTTPRequest 객체를 이용해서 API 호출을 바로 하는 형태이다.

이러한 변화는 API 보안 부분에도 새로운 요구사항을 가지고 왔는데, 자바스크립트 클라이언트는 기존의 모바일이나 웹 애플리케이션, 서버등과 다른 기술적인 특성을 가지고 있기 때문이다.

자바스크립트 클라이언트는 코드 자체가 노출된다. 자바스크립트 코드는 브라우져로 로딩되서 수행되기 때문에 사용자 또는 해커가 클라이언트 코드를 볼 수 있다. 그래서 보안 로직등이 들어가 있다고 하더라도 로직 자체는 탈취 당할 수 있다.

아울러 자바스크립트는 실행중에 브라우져의 디버거를 이용해서 변수 값을 보거나 또는 변수값을 변경하거나 비즈니스 로직을 변경하는 등의 행위가 가능하다.

그래서 일반적인 API 보안과는 다른 접근이 필요하다.


Same Origin Policy에 대한 처리

먼저 자바스크립트의 API에 대한 호출은 same origin policy(동일 출처 정책)의 제약을 받는다. Same origin policy란, 자바스크립트와 같이 웹 브라우져에서 동작하는 프로그래밍 언어에서, 웹 브라우져에서 동작하는 프로그램은 해당 프로그램이 로딩된 위치에 있는 리소스만 접근이 가능하다. (http냐 https냐 와 같은 프로토콜과 호출 포트도 정확하게 일치해야 한다.)

아래 그림과 같이 웹사이트 sitea.com에서 자바스크립트를 로딩한 후에, 이 스크립트에서 api.my.com에 있는 API를 XMLHTTPRequest를 통해서 호출하고자 하면, Sane origin policy에 의해서 호출 에러가 난다.

 


이를 해결하는 방법으로는 인프라 측면에서 Proxy를 사용하는 방법이나 또는 JSONP와 CORS (Cross Origin Resource Sharing)이라는 방법이 있는데, 여기서는 많이 사용되는 CORS에 대해서 소개하고자 한다.


Proxy를 이용하는 방식

Proxy를 이용하는 방식은 간단하다. Same origin policy의 문제는 API 서버와 Javascript가 호스팅 되는 서버의 URL이 다르기 때문에 발생하는 문제인데, 이를 앞단에 Reverse Proxy등을 넣어서, 전체 URL을 같게 만들어 주면 된다.

앞의 상황과 같은 일들이 있다고 가정할 때, sitea.com과 api.my.com 앞에 reverse proxy를 넣고, reverse proxy의 주소를 http://mysite.com 으로 세팅한다.

그리고 mysite.com/javascript로 들어오는 요청은 sitea.com으로 라우팅 하고, mysite.com/의 다른 URL로 들어오는 요청은 api.my.com으로 라우팅한다.

 


이러한 구조가 되면, javascript 가 로딩된 사이트도 mysite.com이 되고, javascript에서 호출하고자 하는 api URL도 mysite.com이 되기 때문에, Same Origin Policy에 위배되지 않는다.

이 방식은 단순하지만, 자사의 웹사이트를 서비스 하는 경우에만 가능하다. (타사의 사이트를 Reverse Proxy뒤에 놓기는 쉽지 않다.) 그래서 자사의 서비스용 API를 만드는데는 괜찮지만, 파트너사나 일반 개발자에게 자바스크립트용 REST API를 오픈하는 경우에는 적절하지 않다.


특정 사이트에 대한 접근 허용 방식

CORS 방식중,이 방식은 가장 간단하고 쉬운 방식으로 API 서버의 설정에서 모든 소스에서 들어오는 API 호출을 허용하도록 하는 것이다. api.my.com 이라는 API 서비스를 제공할 때, 이 API를 어느 사이트에서라도 로딩된 자바스크립트라도 호출이 가능하게 하는 것이다.

이는 HTTP로 API를 호출하였을 경우 HTTP Response에 응답을 주면서 HTTP Header에 Request Origin (요청을 처리해줄 수 있는 출처)를 명시 하는 방식이다.

api.my.com에서 응답 헤더에 

  • Access-Control-Allow-Origin: sitea.com

와 같이 명시해주면 sitea.com에 의해서 로딩된 자바스크립트 클라이언트 요청에 대해서만 api.my.com가 요청을 처리해준다.

만약에 다음과 * 로 해주면, request origin에 상관 없이 사이트에서 로딩된 자바스크립트 요청에 대해서 처리를 해준다.

  • Access-Control-Allow-Origin: *

Pre-flight를 이용한 세세한 CORS 통제

REST 리소스 (URL)당 섬세한 CORS 통제가 필요한 경우에는 Pre-flight 호출이라는 것을 이용할 수 있다. 이 방식은 REST 리소스를 호출하기 전에, 웹 브라우져가 HTTP OPTIONS 요청을 보내면 해당 REST Resource에 대해서 가능한 CORS 정보를 보내준다. (접근이 허용된 사이트, 접근이 허용된 메서드 등)

웹브라우져에서는 XMLHTTPRequest를 특정 URL로 요청하기 전에 먼저 HTTP Options를 호출한다.

그러면 서버는 해당 URL을 접근할 수 있는 Origin URL과 HTTP Method를 리턴해준다. 이를 pre-flight 호출이라고 하는데, 이 정보를 기반으로 브라우져는 해당 URL에 XMLHTTPRequest를 보낼 수 있다.

아래 그림을 보자, 브라우져는 http://javascriptclient.com에서 로딩된 자바스크립트로 REST 호출을 하려고 한다.

이를 위해서 HTTP OPTION 메서드로 아래 첫번째 화살표와 같이 /myresource URL에 대해서 pre-flight 호출을 보낸다. 여기에는 Origin Site URL과 허가를 요청하는 HTTP 메서드등을 명시한다. 

서버는 이 URL에 대한 접근 권한을 리턴하는데, 두번째 화살표와 같이 CORS접근이 가능한 Origin 사이트를 http://javascriptclient.com으로 리턴하고 사용할 수 있는 메서드는 POST,GET,OPTIONS 3개로 정의해서 리턴한다. 그리고 이 pre-flight 호출은 Access-Control-Max-Age에 정의된 1728000초 동안 유효하다.  (한번 pre-flight 호출을 하고 나면 이 시간 동안은 다시 pre-flight 호출을 할 필요가 없다.)

 


이러한 CORS 설정은 API 호출 코드에서 직접 구현할 수 도 있지만, 그 보다는 앞단에서 로드 밸런서 역할을 하는 HA Proxy나 nginx와 같은 reverse proxy에서 설정을 통해서 간단하게 처리가 가능하다. 만약에 API단에서 구현이 필요하다하더라도 HTTP Header를 직접 건드리지 말고, Spring 등의 프레임웍에서 이미 CORS 구현을 지원하고 있으니 프레임웍을 통해서 간단하게 구현하는 것을 권장한다.


API access Token에 대한 인증 처리

앞서서 언급하였듯이 자바스크립트 클라이언트는 모바일 앱이나, 서버와 같은 다른 API 클라이언트와 비교해서 api access token을 안전하게 저장할 수 있는 방법이 없기 때문에, 이 API access token에 대해서 다른 관리 방식이 필요하다.

몇가지 추가적인 방식을 사용하는데, 내용은 다음과 같다.


api access token을 Secure Cookie를 통해서 주고 받는다.

api access token을 서버에서 발급하여 자바스크립트 클라이언트로 리턴할 때, HTTP body에 리턴하는 것이 아니라 Secure Cookie에 넣어서 리턴한다. 

※ Secure Cookie : https://www.owasp.org/index.php/SecureFlag

Secure Cookie는 일반 HTTP 프로토콜을 통해서는 전송이 불가능하고 항상 HTTPS를 통해서만 전송이 가능하다. 같은 API 서버로도 일반 HTTP 호출을 할 경우 api access token이 Cookie를 통해서 전달되지 않기 때문에, 네트워크를 통해서 access token을 탈취하는 것은 불가능하다.

여기에 HTTP_ONLY라는 옵션을 쿠키에 추가하는데, 이 옵션을 적용하게 되면, Cookie를 자바스크립트를 통해서 읽거나 조작할 수 없다. 단지 브라우져가 서버로 요청을 보낼 때, 브라우져에 의해서 자동으로 Cookie가 전송된다. 

※ HTTP ONLY 옵션 https://www.owasp.org/index.php/HttpOnly#What_is_HttpOnly.3F

이 두 가지 방법을 쓰면 최소한 자바스크립트 소스코드 분석이나 네트워크 프로토콜 감청을 통한 api access token을 방어할 수 있다


api access token은 해당 세션에서만 유효하도록 한다.

여기에 몇 가지 추가적인 방어 기재를 추가하도록 하는데, 마치 HTTP Session과 같이 특정 IP와 시간내에서만 api access token이 유효하도록 하는 방식이다.

Access token을 발급할 때, access token을 요청한 클라이언트의 IP와 클라이언트의 Origin 을 같이 저장해놓고, 발급할때 유효시간(Expire time)을 정해놓는다. (20 분 등으로).

다음 access token을 이용해서 API가 호출 될 때 마다 IP와 Origin을 확인하고, acess token이 유효시간 (Expire time)시간 내면 이 유효시간을 다시 연장해준다.(+20분을 다시 추가해준다.) 만약에 브라우져에서 일정 시간동안 (20분) API를 호출하지 않았으면 API access token은 폐기되고 다시 access token을 발급 받도록 한다.

이 두 가지 흐름을 도식화해 보면 다음 그림과 같다.

 


모든 통신을 HTTPS를 이용한다.

1. 자바스크립트 클라이언트가 user id와 password를 보내서 사용자 인증과 함께, API access token을 요청한다. HTTPS를 사용한다하더라도 Man in middle attack에 의해서 password가 노출 될 수 있기 때문에, 앞에서 언급한 Digest access Authentication 등의 인증 메커니즘을 활용하여 가급적이면 password를 직접 보내지 않고 인증을 하는 것이 좋다.

2. 서버에서 사용자 인증이 끝나면 api access token을 발급하고 이를 내부 token store에 저장한다. (앞에서 설명한 origin url, ip, expire time등을 저장한다.). 이 필드들은 웹 자바스크립트를 위한 필드로 설명을 위해서 이 필들만 그림에 정의했지만, 실제 시스템 디자인은 웹 클라이언트용과 일반 서버/모바일 앱등을 위한 api access token 정보도 같이 저장해서 두가지 타입을 access token에 대해서 지원하도록 하는 것이 좋다.

3. 생성된 토큰은 Secure Cookie와 HTTP Only 옵션을 통해서 브라우져에게로 전달된다.

4. 브라우져의 자바스크립트 클라이언트에서는 API를 호출할 때 이 api access token이 secure cookie를 통해서 자동으로 서버에 전송되고, 서버는 이 api access token을 통해서 접근 인증 처리를 하고 api server로 요청을 전달하여 처리하도록 한다.


지금까지 간략하게 나마 REST의 개념에서부터 설계 방식 그리고 보안 측면에 대해서 알아보았다.

적절한 표준이나 가이드가 적은 사항이기 때문에 무엇이 딱 맞다고 말할 수 는 없기 때문에 더더욱 설계등이 어려울 수 있으니, 많은 자료들을 참고해보기 바란다.

특히 아무리 좋은 표준이 있다하더라도 팀이 이를 이해하고 사용하지 못하면 표준은 그냥 문서 덩어리일 뿐이다. 표준과 가이드 정립뿐만 아니라 팀에 대한 교육과 표준 준수에 대한 감사 활동등도 고려 해야 한다.

그리고 항상 강조하지만 보안에 대한 부분은 귀찮더라도 항상 빼먹지 말고 구현하도록 하자.


  • REST API 이해와 설계 - #1 개념 잡기 http://bcho.tistory.com/953
  • REST API 이해와 설계 - #2 디자인 가이드  http://bcho.tistory.com/954
  • REST API 이해와 설계 - #3 보안 가이드  http://bcho.tistory.com/955


REST API 이해와 설계 

#2 API 설계 가이드


REST API 디자인 가이드

그러면 REST의 특성을 이해하고 나쁜 안티패턴을 회피해서 REST API 디자인은 어떻게 해야 할까? 짧지만 여기에 몇가지 디자인 방식에 대해서 소개 한다.


REST URI는 심플하고 직관적으로 만들자

REST API를 URI만 보고도, 직관적으로 이해할 수 있어야 한다 URL을 길게 만드는것 보다, 최대 2 depth 정도로 간단하게 만드는 것이 이해하기 편하다.

  • /dogs
  • /dogs/1234

URI에 리소스명은 동사보다는 명사를 사용한다.

REST API는 리소스에 대해서 행동을 정의하는 형태를 사용한다. 예를 들어서

  • POST /dogs

는 /dogs라는 리소스를 생성하라는 의미로, URL은 HTTP Method에 의해 CRUD (생성,읽기,수정,삭제)의 대상이 되는 개체(명사)라야 한다.

잘못된 예들을 보면

  • HTTP Post : /getDogs
  • HTTP Post : /setDogsOwner

위의 예제는 행위를 HTTP Post로 정의하지 않고, get/set 등의 행위를 URL에 붙인 경우인데, 좋지 않은 예 이다. 이보다는

  • HTTP Get : /dogs
  • HTTP Post : /dogs/{puppy}/owner/{terry}

를 사용하는 것이 좋다. 그리고 가급적이면 의미상 단수형 명사(/dog)보다는 복수형 명사(/dogs)를 사용하는 것이 의미상 표현하기가 더 좋다.

일반적으로 권고되는 디자인은 다음과 같다.

리소스

POST

GET

PUT

DELETE

create

read

update

delete

/dogs

새로운 dogs 등록

dogs 목록을 리턴

Bulk 여러 dogs 정보를 업데이트

모든 dogs 정보를 삭제

/dogs/baduk

에러

baduk 이라는 이름의 dogs 정보를 리턴

baduk이라는 이름의 dogs 정보를 업데이트

baduk 이라는 이름의 dogs 정보를 삭제


리소스간의 관계를 표현하는 방법

REST 리소스간에는 서로 연관관계가 있을 수 있다. 예를 들어 사용자가 소유하고 있는 디바이스 목록이나 사용자가 가지고 있는 강아지들 등이 예가 될 수 가 있는데, 사용자-디바이스 또는 사용자-강아지등과 같은 각각의 리소스간의 관계를 표현하는 방법은 여러가지가 있다.


Option 1. 서브 리소스로 표현하는 방법

예를 들어 사용자가 가지고 있는 핸드폰 디바이스 목록을 표현해보면

  • /”리소스명”/”리소스 id”/”관계가 있는 다른 리소스명” 형태 
  • HTTP Get : /users/{userid}/devices
    예) /users/terry/devices

과 같이 /terry라는 사용자가 가지고 있는 디바이스 목록을 리턴하는 방법이 있고


Option 2. 서브 리소스에 관계를 명시 하는 방법

만약에 관계의 명이 복잡하다면 관계명을 명시적으로 표현하는 방법이 있다. 예를 들어 사용자가 “좋아하는” 디바이스 목록을 표현해보면

  • HTTP Get : /users/{userid}/likes/devices
    예) /users/terry/likes/devices

는 terry라는 사용자가 좋아하는 디바이스 목록을 리턴하는 방식이다.

Option 1,2 어떤 형태를 사용하더라도 문제는 없지만, Option 1의 경우 일반적으로 소유 “has”의 관계를 묵시적으로 표현할 때 좋으며, Option 2의 경우에는 관계의 명이 애매하거나 구체적인 표현이 필요할 때 사용한다. 


에러처리

에러처리의 기본은 HTTP Response Code를 사용한 후, Response body에 error detail을 서술하는 것이 좋다.

대표적인 API 서비스들이 어떤 HTTP Response Code를 사용하는지를 살펴보면 다음과 같다.

구글의 gdata 서비스의 경우 10개, 넷플릭스의 경우 9개, Digg의 경우 8개의 Response Code를 사용한다.  (정보 출처:  http://info.apigee.com/Portals/62317/docs/web%20api.pdf)

Google GData

200 201 304 400 401 403 404 409 410 500


Netflix

200 201 304 400 401 403 404 412 500


Digg

200 400 401 403 404 410 500 503

여러 개의 response code를 사용하면 명시적이긴 하지만, 코드 관리가 어렵기 때문에 아래와 같이 몇가지 response code만을 사용하는 것을 권장한다.

  • 200 성공
  • 400 Bad Request - field validation 실패시
  • 401 Unauthorized - API 인증,인가 실패
  • 404 Not found ? 해당 리소스가 없음
  • 500 Internal Server Error - 서버 에러

추가적인 HTTP response code에 대한 사용이 필요하면 http response code 정의 http://en.wikipedia.org/wiki/Http_error_codes 문서를 참고하기 바란다.

다음으로 에러에는 에러 내용에 대한 디테일 내용을 http body에 정의해서, 상세한 에러의 원인을 전달하는 것이 디버깅에 유리하다.

Twillo의 Error Message 형식의 경우

  • HTTP Status Code : 401
  • {“status”:”401”,”message”:”Authenticate”,”code”:200003,”more info”:”http://www.twillo.com/docs/errors/20003"}

와 같이 표현하는데, 에러 코드 번호와 해당 에러 코드 번호에 대한 Error dictionary link를 제공한다.

비단 API 뿐 아니라, 잘 정의된 소프트웨어 제품의 경우에는 별도의 Error 번호 에 대한 Dictionary 를 제공하는데, Oracle의 WebLogic의 경우에도http://docs.oracle.com/cd/E24329_01/doc.1211/e26117/chapter_bea_messages.htm#sthref7 와 같이 Error 번호, 이에 대한 자세한 설명과, 조치 방법등을 설명한다. 이는 개발자나 Trouble Shooting하는 사람에게 많은 정보를 제공해서, 조금 더 디버깅을 손쉽게 한다. (가급적이면 Error Code 번호를 제공하는 것이 좋다.)

다음으로 에러 발생시에, 선택적으로 에러에 대한 스택 정보를 포함 시킬 수 있다.

에러메세지에서 Error Stack 정보를 출력하는 것은 대단히 위험한 일이다. 내부적인 코드 구조와 프레임웍 구조를 외부에 노출함으로써, 해커들에게, 해킹을 할 수 있는 정보를 제공하기 때문이다. 일반적인 서비스 구조에서는 아래와 같은 에러 스택정보를 API 에러 메세지에 포함 시키지 않는 것이 바람직 하다.

log4j:ERROR setFile(null,true) call failed.

java.io.FileNotFoundException: stacktrace.log (Permission denied)

at java.io.FileOutputStream.openAppend(Native Method)

at java.io.FileOutputStream.(FileOutputStream.java:177)

at java.io.FileOutputStream.(FileOutputStream.java:102)

at org.apache.log4j.FileAppender.setFile(FileAppender.java:290)

at org.apache.log4j.FileAppender.activateOptions(FileAppender.java:164)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

그렇지만, 내부 개발중이거나 디버깅 시에는 매우 유용한데, API 서비스를 개발시, 서버의 모드를 production과 dev 모드로 분리해서, 옵션에 따라 dev 모드등으로 기동시, REST API의 에러 응답 메세지에 에러 스택 정보를 포함해서 리턴하도록 하면, 디버깅에 매우 유용하게 사용할 수 있다.


API 버전 관리

API 정의에서 중요한 것중의 하나는 버전 관리이다. 이미 배포된 API 의 경우에는 계속해서 서비스를 제공하면서,새로운 기능이 들어간 새로운 API를 배포할때는 하위 호환성을 보장하면서 서비스를 제공해야 하기 때문에, 같은 API라도 버전에 따라서 다른 기능을 제공하도록 하는 것이 필요하다.

API의 버전을 정의하는 방법에는 여러가지가 있는데,

  • Facebook ?v=2.0
  • salesforce.com /services/data/v20.0/sobjects/Account

필자의 경우에는

  • {servicename}/{version}/{REST URL}
  • example) api.server.com/account/v2.0/groups

형태로 정의 하는 것을 권장한다.

이는 서비스의 배포 모델과 관계가 있는데, 자바 애플리케이션의 경우, account.v1.0.war, account.v2.0.war와 같이 다른 war로 각각 배포하여 버전별로 배포 바이너리를 관리할 수 있고, 앞단에 서비스 명을 별도의 URL로 떼어 놓는 것은 향후 서비스가 확장되었을 경우에, account 서비스만 별도의 서버로 분리해서 배포하는 경우를 생각할 수 있다.

외부로 제공되는 URL은 api.server.com/account/v2.0/groups로 하나의 서버를 가르키지만, 내부적으로, HAProxy등의 reverse proxy를 이용해서 이런 URL을 맵핑할 수 있는데, api.server.com/account/v2.0/groups를 내부적으로 account.server.com/v2.0/groups 로 맵핑 하도록 하면, 외부에 노출되는 URL 변경이 없이 향후 확장되었을때 서버를 물리적으로 분리해내기가 편리하다.


페이징

큰 사이즈의 리스트 형태의 응답을 처리하기 위해서는 페이징 처리와 partial response 처리가 필요하다.

리턴되는 리스트 내용이 1,000,000개인데, 이를 하나의 HTTP Response로 처리하는 것은 서버 성능, 네트워크 비용도 문제지만 무엇보다 비현실적이다. 그래서, 페이징을 고려하는 것이 중요하다.

페이징을 처리하기 위해서는 여러가지 디자인이 있다.

예를 들어 100번째 레코드부터 125번째 레코드까지 받는 API를 정의하면

Facebook API 스타일 : /record?offset=100&limit=25

  • Twitter API 스타일 : /record?page=5&rpp=25 (RPP는 Record per page로 페이지당 레코드수로 RPP=25이면 페이지 5는 100~125 레코드가 된다.)
  • LikedIn API 스타일 : /record?start=50&count=25
  • 구현 관점에서 보면 , 페이스북 API가 조금 더 직관적이기 때문에, 페이스북 스타일을 사용하는 것을 권장한다.
  • record?offset=100&limit=25

100번째 레코드에서부터 25개의 레코드를 출력한다.


Partial Response 처리

리소스에 대한 응답 메세지에 대해서 굳이 모든 필드를 포함할 필요가 없는 케이스가 있다.

 예를 들어 페이스북 FEED의 경우에는 사용자 ID, 이름, 글 내용, 날짜, 좋아요 카운트, 댓글, 사용자 사진등등 여러가지 정보를 갖는데, API를 요청하는 Client의 용도에 따라 선별적으로 몇가지 필드만이 필요한 경우가 있다. 필드를 제한하는 것은 전체 응답의 양을 줄여서 네트워크 대역폭(특히 모바일에서) 절약할 수 있고, 응답 메세지를 간소화하여 파싱등을 간략화할 수 있다.

그래서 몇몇 잘 디자인된, REST API의 경우 이러한 Partial Response 기능을 제공하는데, 주요 서비스들을 비교해보면 다음과 같다.

  • Linked in : /people:(id,first-name,last-name,industry)
  • Facebook : /terry/friends?fields=id,name
  • Google : ?fields=title,media:group(media:thumnail)

Linked in 스타일의 경우 가독성은 높지만 :()로 구별하기 때문에, HTTP 프레임웍으로 파싱하기가 어렵다. 전체를 하나의 URL로 인식하고, :( 부분을 별도의 Parameter로 구별하지 않기 때문이다.

Facebook과 Google은 비슷한 접근 방법을 사용하는데, 특히 Google의 스타일은 더 재미있는데, group(media:thumnail) 와 같이 JSON의 Sub-Object 개념을 지원한다.

Partial Response는 Facebook 스타일이 구현하기가 간단하기 때문에, 필자의 경우는 Facebook 스타일의 partial response를 사용하는 것을 권장한다.


검색 (전역검색과 지역검색)

검색은 일반적으로 HTTP GET에서 Query String에 검색 조건을 정의하는 경우가 일반적인데, 이 경우 검색조건이 다른 Query String과 섞여 버릴 수 있다. 예를 들어 name=cho이고, region=seoul인 사용자를 검색하는 검색을 Query String만 사용하게 되면 다음과 같이 표현할 수 있다.

  • /users?name=cho&region=seoul

그런데, 여기에 페이징 처리를 추가하게 되면

  • /users?name=cho&region=seoul&offset=20&limit=10

페이징 처리에 정의된 offset과 limit가 검색 조건인지 아니면 페이징 조건인지 분간이 안간다. 그래서, 쿼리 조건은 하나의 Query String으로 정의하는 것이 좋은데

  • /user?q=name%3Dcho,region%3Dseoul&offset=20&limit=10

이런식으로 검색 조건을 URLEncode를 써서 “q=name%3Dcho,region%3D=seoul” 처럼 (실제로는 q= name=cho,region=seoul )표현하고 Deleminator를 , 등을 사용하게 되면 검색 조건은 다른 Query 스트링과 분리된다.

물론 이 검색 조건은 서버에 의해서 토큰 단위로 파싱되어야 한다.

다음으로는 검색의 범위에 대해서 고려할 필요가 있는데, 전역 검색은 전체 리소스에 대한 검색을, 리소스에 대한 검색은 특정 리소스에 대한 검색을 정의한다.

예를 들어 시스템에 user,dogs,cars와 같은 리소스가 정의되어 있을때,id=’terry’인 리소스에 대한 전역 검색은

  • /search?q=id%3Dterry

와 같은 식으로 정의할 수 있다. /search와 같은 전역 검색 URI를 사용하는 것이다.

반대로 특정 리소스안에서만의 검색은

  • /users?q=id%3Dterry

와 같이 리소스명에 쿼리 조건을 붙이는 식으로 표현이 가능하다.


HATEOS를 이용한 링크 처리

HATEOS는 Hypermedia as the engine of application state의 약어로, 하이퍼미디어의 특징을 이용하여 HTTP Response에 다음 Action이나 관계되는 리소스에 대한 HTTP Link를 함께 리턴하는 것이다.

예를 들어 앞서 설명한 페이징 처리의 경우, 리턴시, 전후페이지에 대한 링크를 제공한다거나

{  

   [  

      {  

         "id":"user1",

         "name":"terry"

      },

      {  

         "id":"user2",

         "name":"carry"

      }

   ],

   "links":[  

      {  

         "rel":"pre_page",

         "href":"http://xxx/users?offset=6&limit=5"

      },

      {  

         "rel":"next_page",

         "href":"http://xxx/users?offset=11&limit=5"

      }

   ]

}

와 같이 표현하거나

연관된 리소스에 대한 디테일한 링크를 표시 하는 것등에 이용할 수 있다.

{  

   "id":"terry",

   "links":[  

      {  

         "rel":"friends",

         "href":"http://xxx/users/terry/friends"

      }

   ]

}

HATEOAS를 API에 적용하게 되면, Self-Descriptive 특성이 증대되어 API에 대한 가독성이 증가되는 장점을 가지고 있기는 하지만, 응답 메세지가 다른 리소스 URI에 대한 의존성을 가지기 때문에, 구현이 다소 까다롭다는 단점이 있다.

요즘은 Spring과 같은 프레임웍에서 프레임웍 차원에서 HATEOAS를 지원하고 있으니 참고하기 바란다.http://spring.io/understanding/HATEOAS


단일 API 엔드포인트 활용

API 서버가 물리적으로 분리된 여러개의 서버에서 동작하고 있을때, user.apiserver.com, car.apiserver.com과 같이 API 서비스마다 URL이 분리되어 있으면 개발자가 사용하기 불편하다. 매번 다른 서버로 연결을 해야하거니와 중간에 방화벽이라도 있으면, 일일이 방화벽을 해제해야 한다.

API 서비스는 물리적으로 서버가 분리되어 있더라도 단일 URL을 사용하는 것이 좋은데, 방법은 HAProxy나 nginx와 같은 reverse proxy를 사용하는 방법이 있다. HAProxy를 앞에 새우고 api.apiserver.com이라는 단일 URL을 구축한후에

HAProxy 설정에서

  • api.apiserver.com/user는 user.apiserver.com 로 라우팅하게 하고
  • api.apiserver.com/car 는 car.apiserver.com으로 라우팅 하도록 구현하면 된다.

이렇게 할 경우 향후 뒷단에 API 서버들이 확장이 되더라도 API를 사용하는 클라이언트 입장에서는 단일 엔드포인트를 보면 되고, 관리 관점에서도 단일 엔드포인트를 통해서 부하 분산 및 로그를 통한 Audit(감사)등을 할 수 있기 때문에 편리하며, API 에 대한 라우팅을 reverse proxy를 이용해서 함으로써 조금 더 유연한 운용이 가능하다.


REST에 문제점


그렇다면 이렇게 많은 장점이 있는 REST는 만능인가? REST역시 몇가지 취약점과 단점을 가지고 있다.


JSON+HTTP 를 쓰면 REST인가?

REST에 대한 잘못된 이해중의 하나가, HTTP + JSON만 쓰면 REST라고 부르는 경우인데, 앞의 안티패턴에서도 언급하였듯이, REST 아키텍쳐를 제대로 사용하는 것은, 리소스를 제대로 정의하고 이에대한 CRUD를 HTTP 메서드인 POST/PUT/GET/DELETE에 대해서 맞춰 사용하며, 에러코드에 대해서 HTTP Response code를 사용하는 등, REST에 대한 속성을 제대로 이해하고 디자인해야 제대로된 REST 스타일이라고 볼 수 있다.

수년전 뿐만 아니라 지금에도 이러한 안티패턴이 적용된 REST API 형태가 많이 있기 때문에, 제대로된 REST 사상의 이해 후에, REST를 사용하도록 해야 한다.


표준 규약이 없다

REST는 표준이 없다. 그래서 관리가 어렵다. 

SOAP 기반의 웹 서비스와 같이 메시지 구조를 정의하는 WSDL도 없고, UDDI와 같은 서비스 관리체계도 없다. WS-I이나 WS-*와 같은 메시지 규약도 없다. 

REST가 최근 부각되는 이유 자체가 WebService의 복잡성과 표준의 난이도 때문에 Non Enterprise 진영(Google, Yahoo, Amazone)을 중심으로 집중적으로 소개된 것이다. 데이터에 대한 의미 자체가 어떤 비즈니스 요건처럼 Mission Critical한 요건이 아니기 때문에, 서로 데이터를 전송할 수 있는 정도의 상호 이해 수준의 표준만이 필요했지 Enterprise 수준의 표준이 필요하지도 않았고, 벤더들처럼 이를 주도하는 회사도 없었다.

단순히 많이 사용하고 암묵적으로 암암리에 생겨난 표준 비슷한 것이 있을 뿐이다(이런 것을 Defactor 표준이라고 부른다). 

그런데 문제는 정확한 표준이 없다 보니, 개발에 있어 이를 관리하기가 어렵다는 것이다. 표준을 따르면   몇 가지 스펙에 맞춰서  맞춰 개발 프로세스나 패턴을 만들 수 있는데, REST에는 표준이 없으니 REST 기반으로 시스템을 설계하자면 사용할 REST에 대한 자체 표준을 정해야 하고, 어떤 경우에는 REST에 대한 잘못된 이해로 잘못된 REST 아키텍처에 ‘이건 REST다’는 딱지를 붙이기도 한다. 실제로 WEB 2.0의 대표 주자격인 Flickr.com도 REST의 특성을 살리지 못하면서 RPC 스타일로 디자인한 API를 HTTP + XML을 사용했다는 이유로 Hybrid REST라는 이름을 붙여 REST 아키텍쳐아키텍처에 대한 혼란을 초래했다.

근래에 들어서 YAML등과 같이 REST 에 대한 표준을 만들고자 하는 움직임은 있으나, JSON의 자유도를 제약하는 방향이고 Learning curve가 다소 높기 때문에, 그다지 확산이 되지 않고 있다.

이런 비표준에서 오는 관리의 문제점은, 제대로된 REST API 표준 가이드와, API 개발 전후로 API 문서(Spec)을 제대로 만들어서 리뷰하는 프로세스를 갖추는 방법으로 해결하는 방법이 좋다.


기존의 전통적인 RDBMS에 적용 시키기에 쉽지 않다.

예를 들어 리소스를 표현할 때,  리소스는 DB의 하나의 Row가 되는 경우가 많은데, DB의 경우는 Primary Key가 복합 Key 형태로 존재하는 경우가 많다. (여러 개의 컬럼이 묶여서 하나의 PK가 되는 경우) DB에서는 유효한 설계일지 몰라도, HTTP URI는 / 에 따라서 계층 구조를 가지기 때문에, 이에 대한 표현이 매우 부자연 스러워진다.

예를 들어 DB의 PK가 “세대주의 주민번호”+”사는 지역”+”본인 이름일 때” DB에서는 이렇게 표현하는 것이 하나 이상할 것이 없으나, REST에서 이를 userinfo/{세대주 주민번호}/{사는 지역}/{본인 이름} 식으로 표현하게 되면 다소 이상한 의미가 부여될 수 있다.

이외에도 resource에 대한 Unique한 Key를 부여하는 것에 여러가지 애로점이 있는데, 이를 해결하는 대안으로는 Alternative Key (AK)를 사용하는 방법이 있다. 의미를 가지지 않은 Unique Value를 Key로 잡아서 DB Table에 AK라는 필드로 잡아서 사용 하는 방법인데. 이미 Google 의 REST도 이러한 AK를 사용하는 아키텍쳐를 채택하고 있다.

그러나 DB에 AK 필드를 추가하는 것은 전체적인 DB설계에 대한 변경을 의미하고 이는 즉 REST를 위해서 전체 시스템의 아키텍쳐에 변화를 준다는 점에서 REST 사용시 아키텍쳐적인 접근의 필요성을 의미한다. 

그래서 근래에 나온 mongoDB나 CouchDB,Riak등의 Document based NoSQL의 경우 JSON Document를 그대로 넣을 수 있는 구조를 갖추는데,하나의 도큐먼트를 하나의 REST 리소스로 취급하면 되기 때문에 REST의 리소스 구조에 맵핑 하기가 수월하다.



  • REST API 이해와 설계 - #1 개념 잡기 http://bcho.tistory.com/953
  • REST API 이해와 설계 - #2 디자인 가이드  http://bcho.tistory.com/954
  • REST API 이해와 설계 - #3 보안 가이드  http://bcho.tistory.com/955


REST API 디자인 가이드

아키텍쳐 /WEB 2.0 | 2014.06.12 21:54 | Posted by 조대협

REST API 디자인 가이드

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

REST API 디자인을 보면, REST 사상에 맞춰서 제대로 디자인 (CRUD를 HTTP method에 맞춘)하기도 어렵고, URI Convention등이나 보안, 버전 관리등 고려할 사항이 많다. 이번 글에서는 REST API를 디자인에 대한 가이드를 소개하고자 한다.

동사보다는 명사를 사용하자

URL을 심플하고 직관적으로 만들자

REST API를 URL만 보고도, 직관적으로 이해할 수 있어야 한다
URL을 길게 만드는것 보다, 최대 2 depth 정도로 간단하게 만드는 것이 이해하기 편하다.

/dogs
/dogs/1234

URL에 동사보다는 명사를 사용한다.

REST API는 리소스에 대해서 행동을 정의하는 형태를 사용한다. 예를 들어서

POST /dogs

는 /dogs라는 리소스를 생성하라는 의미로, URL은 HTTP Method에 의해 CRUD (생성,읽기,수정,삭제)의 대상이 되는 개체(명사)라야 한다.
잘못된 예들을 보면

HTTP Post : /getDogs
HTTP Post : /setDogsOwner

위의 예제는 행위를 HTTP Post로 정의하지 않고, get/set 등의 행위를 URL에 붙인 경우인데, 좋지 않은 예 이다. 이보다는

HTTP Get : /dogs
HTTP Post : /dogs/{puppy}/owner/{terry}

를 사용하는 것이 좋다.
일반적으로 권고되는 디자인은 다음과 같다.

리소스POSTGETPUTDELETE
createreadupdatedelete
/dogs새로운 dogs 등록dogs 목록을 리턴Bulk로 여러 dogs 정보를 업데이트모든 dogs 정보를 삭제
/dogs/baduk에러baduk 이라는 이름의 dogs 정보를 리턴baduk이라는 이름의 dogs 정보를 업데이트baduk 이라는 이름의 dogs 정보를 삭제

단수(Singular) 보다는 복수(Plural)형 명상를 사용한다.

되도록이면 추상적인 이름보다 구체적인 이름을 사용하자

리소스간의 관계를 표현하는 방법

Option A.

다른 리소스와의 관계를 표현. 예를 들어 owner가 가지고 있는 개(dogs) 목록

GET /owner/{terry}/dogs

와 같이 /resource명/identifier/other-related-resource 형태로, 해당 리소스에 대한 경로를 /resource명/{그 리소스에 대한 identifier}/{연관되는 다른 리소스 other-related-resource} 형태로 표현한다.

Option B.

https://usergrid.incubator.apache.org/docs/relationships/ 에 보면 다른 형태의 관계 정의 방법에 대해서 나와 있는데, 조금 더 구체적인 API 관계 정의 방법은 다음과 같다.

/resource/identifier/relation/other-related-resource
GET /owner/terry/likes/dogs

이 방식은 리소스간의 관계(relationship)을 URL 내에 정의하는 방법으로,훨씬 더 명시적일 수 있다. (세련되어 보이지는 않지만)
리소스간의 관계가 복잡하지 않은 서비스의 경우에는 Option A를, 리소스간의 관계가 다소 복잡한 경우에는 Option B를 사용하도록 한다.

에러 처리

에러 처리의 기본은 HTTP Response Code를 사용한후, Response body에 error detail을 사용해주는 것이 좋다.

Use HTTP Status Code

HTTP Status Code는 대략 70개의 코드가 있다. 일반적인 개발자들이 모든 코드를 기억할리는 없고, 에러 코드에서는 자주 사용되는 몇개의 코드만 의미에 맞춰서 사용하는 것이 좋다.
Google의 GData의 경우에는 10개, Neflix의 경우에는 9개, Digg의 경우에는 8개를 사용한다.
(※ http://info.apigee.com/Portals/62317/docs/web%20api.pdf)

  • Google GData
    200 201 304 400 401 403 404 409 410 500
  • Netflix
    200 201 304 400 401 403 404 412 500
  • Digg
    200 400 401 403 404 410 500 503

필자의 경우, 아래와 같은 정도의 HTTP Code를 사용하기를 권장한다.

  • 200 성공
  • 400 Bad Request - field validation 실패시
  • 401 Unauthorized - API 인증,인가 실패
  • 404 Not found
  • 500 Internal Server Error - 서버 에러

자세한 HTTP Status Code는 http://en.wikipedia.org/wiki/Http_error_codes 를 참고하기 바란다.

Error Message

HTTP Status Code 이외에, Response body에 detail한 에러 정보를 표현하는 것이 좋은데,
Twillo의 Error Message 형식의 경우

HTTP Status Code : 401
{“status”:”401”,”message”:”Authenticate”,”code”:200003,”more info”:”http://www.twillo.com/docs/errors/20003"}

와 같이 표현하는데, 에러 코드 #와 해당 에러 코드 #에 대한 Error dictionary link를 제공한다.
비단 API 뿐 아니라, 잘 정의된 소프트웨어 제품의 경우에는 별도의 Error # 에 대한 Dictionary 를 제공하는데, Oracle의 WebLogic의 경우에도http://docs.oracle.com/cd/E24329_01/doc.1211/e26117/chapter_bea_messages.htm#sthref7 와 같이 Error #와, 이에 대한 자세한 설명과, 조치 방법등을 설명한다. 이는 개발자나 Trouble Shooting하는 사람에게 많은 정보를 제공해서, 조금 더 디버깅을 손쉽게 한다. (가급적이면 Error Code #를 제공하는 것이 좋다.)

Error Stack

에러메세지에서 Error Stack 정보를 출력하는 것은 대단히 위험한 일이다. 내부적인 코드 구조와 프레임웍 구조를 외부에 노출함으로써, 해커들에게, 해킹을 할 수 있는 정보를 제공하기 때문이다. 일반적인 서비스 구조에서는 아래와 같은 에러 스택정보를 API 에러 메세지에 포함 시키지 않는 것이 바람직 하다.

log4j:ERROR setFile(null,true) call failed.
java.io.FileNotFoundException: stacktrace.log (Permission denied)
at java.io.FileOutputStream.openAppend(Native Method)
at java.io.FileOutputStream.(FileOutputStream.java:177)
at java.io.FileOutputStream.(FileOutputStream.java:102)
at org.apache.log4j.FileAppender.setFile(FileAppender.java:290)
at org.apache.log4j.FileAppender.activateOptions(FileAppender.java:164)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

그렇지만, 내부 개발중이거나 디버깅 시에는 매우 유용한데, API 서비스를 개발시, 서버의 모드를 production과 dev 모드로 분리해서, 옵션에 따라 dev 모드등으로 기동시, REST API의 에러 응답 메세지에 에러 스택 정보를 포함해서 리턴하도록 하면, 디버깅에 매우 유용하게 사용할 수 있다.

버전 관리

API 정의에서 중요한 것중의 하나는 버전 관리이다. 이미 배포된 API 의 경우에는 계속해서 서비스를 제공하면서,새로운 기능이 들어간 새로운 API를 배포할때는 하위 호환성을 보장하면서 서비스를 제공해야 하기 때문에, 같은 API라도 버전에 따라서 다른 기능을 제공하도록 하는 것이 필요하다.
API의 버전을 정의하는 방법에는 여러가지가 있는데,

  • Facebook ?v=2.0
  • salesforce.com /services/data/v20.0/sobjects/Account
    필자의 경우에는

    {servicename}/{version}/{REST URL}
    example) api.server.com/account/v2.0/groups

형태로 정의 하는 것을 권장한다.
이는 서비스의 배포 모델과 관계가 있는데, 자바 애플리케이션의 경우, account.v1.0.war, account.v2.0.war와 같이 다른 war로 각각 배포하여 버전별로 배포 바이너리를 관리할 수 있고, 앞단에 서비스 명을 별도의 URL로 떼어 놓는 것은 향후 서비스가 확장되었을 경우에, account 서비스만 별도의 서버로 분리해서 배포하는 경우를 생각할 수 있다.
외부로 제공되는 URL은 api.server.com/account/v2.0/groups로 하나의 서버를 가르키지만, 내부적으로, HAProxy등의 reverse proxy를 이용해서 이런 URL을 맵핑할 수 있는데, api.server.com/account/v2.0/groups를 내부적으로 account.server.com/v2.0/groups 로 맵핑 하도록 하면, 외부에 노출되는 URL 변경이 없이 향후 확장되었을때 서버를 물리적으로 분리해내기가 편리하다.

페이징 처리와 Partial response

페이징

큰 사이즈의 리스트 형태의 응답을 처리하기 위해서 필요한 것은 페이징 처리와 partial response 처리이다. 리스트 내용이 1000,000개인데, 이를 하나의 HTTP Response로 처리하는 것은 서버 성능, 네트워크 비용도 문제지만 무엇보다 비현실적이다. 그래서, 페이징을 고려하는 것이 중요하다.
페이징을 처리하기 위해서는 여러가지 디자인이 있다.

예를 들어 100번째 레코드부터 125번째 레코드까지 받는 API를 정의하면

  • Facebook API 스타일 : /record?offset=100&limit=25
  • Twitter API 스타일 : /record?page=5&rpp=25 (RPP는 Record per page로 페이지당 레코드수로 RPP=25이면 페이지 5는 100~125 레코드가 된다.)
  • LikedIn API 스타일 : /record?start=50&count=25

apigee의 API가이드를 보면 좀더 직관적이라는 이유로 페이스북 스타일을 권장하고 있다.
record?offset=100&limit=25

Partial Response (Optional)

리소스에 대한 응답 메세지에 대해서 굳이 모든 필드를 포함할 필요가 없는 케이스가 있다. 예를 들어 페이스북 FEED의 경우에는 사용자 ID, 이름, 글 내용, 날짜, 좋아요 카운트, 댓글, 사용자 사진등등 여러가지 정보를 갖는데, API를 요청하는 Client의 용도에 따라 선별적으로 몇가지 필드만이 필요한 경우가 있다. 필드를 제한하는 것은 전체 응답의 양을 줄여서 네트워크 대역폭(특히 모바일에서) 절약할 수 있고, 응답 메세지를 간소화하여 파싱등을 간략화할 수 있다.
그래서 몇몇 잘 디자인된, REST API의 경우 이러한 Partial Response 기능을 제공하는데, 주요 서비스들을 비교해보면 다음과 같다.

  • Linked in : /people:(id,first-name,last-name,industry)
  • Facebook : /terry/friends?fields=id,name
  • Google : ?fields=title,media:group(media:thumnail)
    Linked in 스타일의 경우 가독성은 높지만 :()로 구별하기 때문에, HTTP 프레임웍으로 파싱하기가 어렵다. 전체를 하나의 URL로 인식하고, :( 부분을 별도의 Parameter로 구별하지 않기 때문이다.
    Facebook과 Google은 비슷한 접근 방법을 사용하는데, 특히 Google의 스타일은 더 재미있는데, group(media:thumnail) 와 같이 JSON의 Sub-Object 개념을 지원한다.
    Partial Response는 Google 스타일을 이용하는 것을 권장한다.

검색

검색은 일반적으로 HTTP GET에서 Query String에 검색 조건을 정의하는 경우가 일반적인데, 이 경우 검색조건이 다른 Query String과 섞여 버릴 수 있다. 예를 들어 name=cho이고, region=seoul인 사용자를 검색하는 검색을 Query String만 사용하게 되면 다음과 같이 표현할 수 있다.
/users?name=cho&region=seoul
그런데, 여기에 페이징 처리를 추가하게 되면

/users?name=cho&region=seoul&offset=20&limit=10

페이징 처리에 정의된 offset과 limit가 검색 조건인지 아니면 페이징 조건인지 분간이 안간다. 그래서, 쿼리 조건은 하나의 Query String으로 정의하는 것이 좋은데

/user?q=name%3Dcho,region%3Dseoul&offset=20&limit=10

이런식으로 검색 조건을 URLEncode를 써서 “q=name%3Dcho,region%3D=seoul” 처럼 (실제로는 q= name=cho,region=seoul )표현하고 Deleminator를 , 등을 사용하게 되면 검색 조건은 다른 Query 스트링과 분리된다.
물론 이 검색 조건은 서버에 의해서 토큰 단위로 파싱되어야 하낟.

전역 검색과 리소스 검색

다음으로는 검색의 범위에 대해서 고려할 필요가 있는데, 전역 검색은 전체 리소스에 대한 검색을, 리소스에 대한 검색은 특정 리소스에 대한 검색을 정의한다.
예를 들어 시스템에 user,dogs,cars와 같은 리소스가 정의되어 있을때,id=’terry’인 리소스에 대한 전역 검색은

/search?q=id%3Dterry

와 같은 식으로 정의할 수 있다. /search와 같은 전역 검색 URI를 사용하는 것이다.
반대로 특정 리소스안에서만의 검색은

/users?q=id%3Dterry

와 같이 리소스명에 쿼리 조건을 붙이는 식으로 표현이 가능하다.

HATEOAS (Optional)

HATEOS는 Hypermedia as the engine of application state의 약어로, 디자인의 요지는 하이퍼미디어의 특징을 이용하여 HTTP Response에 다음 Action에 대한 HTTP Link를 함께 리턴하는 것이다.
예를 들어 앞서 설명한 페이징 처리의 경우, 리턴시, 전후페이지에 대한 링크를 제공한다거나

HTTP GET users?offset=10&limit=5
{
[
{‘id’:’user1’,’name’:’terry’}
,{‘id’:’user2’,’name’:’carry’}
]
,’links’ :[
{
‘rel’:’pre_page’,
‘href’:’http://xxx/users?offset=6&limit=5
}
,
{
‘rel’:’next_page’,
‘href’:’http://xxx/users?offset=11&limit=5
}
]
}
와 같이 표현하거나
연관된 리소스에 대한 디테일한 링크를 표시 하는 것등에 이용할 수 있다.
HTTP GET users/terry
{
‘id’:’terry’
‘links’:[{
‘rel’:’friends’,
‘href’:’http://xxx/users/terry/friends
}]
}

HATEOAS를 API에 적용하게 되면, Self-Descriptive 특성이 증대되어 API에 대한 가독성이 증가하는 장점을 가지고 있기는 하지만, 응답 메세지가 다른 리소스 URI에 대한 의존성을 가지기 때문에, 구현이 다소 까다롭다는 단점이 있다.
요즘은 Spring과 같은 프레임웍에서 프레임웍 차원에서 HATEOAS를 지원하고 있으니 참고하기 바란다.http://spring.io/understanding/HATEOAS

단일 API URL

API 서버가 물리적으로 분리된 여러개의 서버에서 동작하고 있을때, user.apiserver.com, car.apiserver.com과 같이 API 서비스마다 URL이 분리되어 있으면 개발자가 사용하기 불편하다. 매번 다른 서버로 연결을 해야하거니와 중간에 방화벽이라도 있으면, 일일이 방화벽을 해제해야 한다.
API 서비스는 물리적으로 서버가 분리되어 있더라도 단일 URL을 사용하는 것이 좋은데, 방법은 HAProxy나 nginx와 같은 reverse proxy를 사용하는 방법이 있다.
HAProxy를 앞에 새우고 api.apiserver.com이라는 단일 URL을 구축한후에
HAProxy 설정에서

api.apiserver.com/user는 user.apiserver.com 로 라우팅하게 하고
api.apiserver.com/car 는 car.apiserver.com으로 라우팅 하도록 구현하면 된다.

보안

API 서비스에 있어서 보안은 매우 중요한 요소이다. API 에 대한 보안은 다음과 같이 보안 대상에 따라서 몇가지로 나눠진다.

인증

인증은, API를 호출하는 클라이언트가 VALID한 사용자인지, 불법적인 사용자인지를 구별하는 방식이다.

HTTP Basic Auth

가장 쉬운 방식으로는 사용자 id,password를 표준 HTTP Basic Auth에 넣어서 전송하는 방식으로 사용자 단위의 인증과 권한 컨트롤이 가능하다는 장점을 가지고 있다. 그러나 이 경우, 매번 사용자 id와 password가 네트워크를 통해서 전송되기 때문에, 해커에 의해서 사용자 id,password가 누출될 수 있다. 사용자 id,password가 누출되면 API 호출 권한뿐만 아니라 웹에 로그인해서 다른 서비스를 사용하는 등 치명적이기 때문에 그리 권장되지 않는 방법이다.
SSL을 사용해서 암호화할 수 는 있겠지만 기본적으로 SSL은 Man in the middle attack (중간에 인증서를 가로체서 SSL 패킷을 열어보는 방법) 에 취약하기 때문에 완벽하다고 볼 수 없다.

Access Token

매번 네트워크를 통해서 사용자 id,password를 보내는 것이 위험하다면, 처음 인증시에만 id,password를 보내고, 인증이 성공하면 서버에서 access_token을 발급하여 API 호출시 access_token으로만 호출하는 방식이다. (OAuth2.0이 유사한 메커니즘을 사용한다.) 이 경우 API를 호출하는 클라이언트가 access_token을 저장하는 메카니즘을 가져야 한다. 또한 access_token이 누출될때를 대비하여, 서버쪽에서 compromised된 (노출된/오염된) token의 경우 revoke(사용금지 처리)를 하고, 클라이언트에게 다시 access_token을 발급하도록 하는 메커니즘과, Expire time을 둬서 주기적으로 token을 교체하도록 하는 방식이 좋다.

API Key

API Key 시나리오는 일반적으로 API 제공자가 API 포탈등을 통해서, 개발자를 인증하고 개발자에게 API Key를 발급한후, 개발자가 API Key를 애플리케이션 코드내에 탑재해서 사용하는 방법을 사용한다. API를 외부 파트너에게 공개하는 경우에 손쉽게 사용할 수 있으며, 꽤 많이 사용되던 방식이다. 단 애플리케이션 (특히 모바일 애플리케이션)이 디컴파일 될 경우 API Key가 누출될 수 있는 위험성을 가지고 있기 때문에, API Key를 잘 관리 하는 것이 중요하다. (난독화를 한다던가)
이 시나리오의 경우 애플리케이션 단위의 인증을 하기 때문에 앞서 설명한 두 방식처럼 사용자 단위의 인증은 불가능 하다.
(사용자 단위의 인증이 필요한 경우 API Key로 애플리케이션을 인증한 후에, 클라이언트 마다 새로운 access_token을 발급하는 방식을 사용할 수 있다. 사실 이게 OAuth 2.0의 client_secret과 access_token 시나리오와 유사하다.)

OAuth 2.0 (Recommended)

OAuth는 근래에 가장 많이 사용되는 API 인가/인증 기술이다. 특징중의 하나는 Authentication(인증)만이 아니라 권한에 대한 통제(Authorization)이 가능하다는 특징을 가지고 있으며, 3 legged 인증을 통해서, 파트너사가 API를 사용할 경우, 인증시에 사용자 ID와 비밀번호를 파트너에게 노출하지 않을 수 있는 장점이 있다. (페이스북 계정을 이용한 웹 애플리케이션들을 보면 가끔, 페이스북 로그인 화면으로 리다이렉트되어 “XX 애플리케이션이 XX에 대한 권한을 요청합니다. 수락하시겠습니까?”와 같은 창이 뜨는 것을 볼 수 있는데, 페이스북 로그인 화면에, 사용자 ID와 비밀 번호를 넣고 페이스북은 인증이 되었다는 정보를 인증을 요청한 웹애플리케이션으로 보내서, 해당 사용자가 인증되었음을 알려준다. 이경우, 웹 애플리케이션은 사용자의 비밀번호를 알 수 없다. )
기본적인 OAuth의 원리는, 사용자 ID/PASSWD로 인증을 한 후에, access_token을 받아서, access_token을 이용해서 추후 커뮤니케이션을 하는 방식이다.

OAuth는 크게 용도에 따라 4가지 타입의 인증 방식을 제공한다.

  • Authorization Code 방식 - 주로 웹 애플리케이션 인증에 유리하며, 위에서 설명한 케이스와 같이 웹을 통해서 Redirect 하는 방식이다.
  • Implicit 방식 - 자바스크립트 기반의 애플리케이션이나 모바일 애플리케이션 처럼 서버 백엔드가 없는 경우 사용한다.
  • Resource Owner password credential 방식 - 인증을 요청하는 클라이언트에서 직접 ID와 PASSWD를 보내는 방식으로, (이 경우 위의 방식들과 다르게 서비스 제공자의 로그인창으로 리다이렉션이 필요 없다.) 클라이언트가 직접 ID,PASSWD를 받기 때문에, 클라이언트에 사용자의 비밀번호가 노출될 수 있어서 서버와 클라이언트를 같은 회사에서 제작한 경우나, 사용자의 정보를 공유해도 되는 1’st party 파트너등과 같은 경우에 사용한다.
  • Client Credential 방식 - 일반적인 애플리케이션 Access에 사용한다.

일반적으로 API를 3’rd party에 제공할 경우에는 Authorization Code 방식을, 자사의 API를 자사나 1’st party 파트너만 사용할 경우에는 Resource Owner password credential 방식이 좋다.

Mutual SSL

가장 강력한 인증 방법으로,클라이언트와 서버가 각자 인증서를 가지고 상호 인증하는 방식이다. 양방향(2-way)SSL 이라고도 한다. 이 경우에는 클라이언트의 인증서(Certificate)를 서버에게 안전하게 전송할 수 있는 메커니즘이 필요하다. 클라이언트가 접속했을때, Certificate를 네트워크를 통해서 전송하고, 서버는 이 인증서가 공인된 인증서인지를 확인하는 방법도 있고, 내지는 서버의 Admin Console등을 통해서 클라이언트가 사용하는 인증서 자체를 업로드 해놓는 방법등 다양한 방법이 있다.
Mutual SSL은 양쪽에 인증서를 사용하기 때문에, Man in the middle attack이 불가능하고, Packet을 snipping해서 보는 것 조차도 불가능 하다. (대신 구현이 다소 까다롭다.)

WhiteList 방식

서버간의 통신에서는 가장 간단하게 할 수 있는 방식이 서버가 API 호출을 허용할 수 있는 IP 목록을 유지하는 방법이다. (WhiteList 방식). 다른 IP에서 들어오는 API 호출의 경우 받지 않는 방법으로, 가장 구현이 간단하다. 방화벽이나 Reverse proxy 설정등으로도 가능하고, 필요하다면, VPN (Virtual Private Network)등을 이용할 수 도 있다.

프로토콜 레벨 암호화

HTTP 통신 프로토콜 자체를 암호화 하는 방식인데, SSL을 이용한 HTTPS가 대표적인 경우이다. API 디자인에서 HTTPS는 반드시 적용하는 것을 권장한다.
HTTPS는 앞에서도 잠깐 언급했듯이 Man in the middle attack에 취약한데 Man in the middle attack의 기본적인 메커니즘은 서버에서 보낸 인증서를 바꿔치기 해서, 클라이언트로 보내는 방식을 이용한다. (http://en.wikipedia.org/wiki/Man-in-the-middle_attack)
가능하면, 인증서 체크 로직을 클라이언트에 두는 것이 좋다. 인증서가 공인된 인증서인지, (또는 그 서버의 인증서가 맞는지를 Issuer등을 통해서 확인할 수 있다. 인증서에 있는 내용들은 기본적으로 중간에 해커가 바꿀 수 다 없다. Signing이 되어있기 때문에, 내용을 바꾸면 Singing된 Signature가 맞지 않는다.) attack

메세지 레벨 암호화

다음으로 JSON과 같은 메세지 자체를 암호화할 수 있는데, 앞서 설명해듯이 SSL을 사용하더라도, 중간에 인증서를 바꿔 치는 등의 행위를 통해서 패킷을 열어볼 경우, 메세지 내용을 노출될 가능성이 있기 때문에 이를 방지 하기 위해서, 중요한 메세지는 암호화하는 것을 권장한다.
이때 전체 메세지를 암호화 하는 것은 비효율적이며 특정 필드의 값만 필요에 따라서 암호화를 하는 것이 좋다.

무결성 관리 (HMAC)

메세지의 무결성 보장이란, 중간에 해커에 의해서 메세지가 변경할 수 없도록 하는 것이다. 기본적인 원리는 메세지에 대한 해쉬값을 계산해서, 보내서, 받는 쪽에서 받은 메세지를 가지고 똑같은 알고리즘으로 해쉬를 생성한후, 보내온 해쉬값과 비교하는 방법을 사용한다. 만약에 메세지가 변조가 되었다면,해쉬값이 일치하지 않기 때문에 메세지 변조 여부를 파악할 수 있다. 자세한 설명과 구현 방법은 http://bcho.tistory.com/807를 참고하기 바란다.

지금까지 간략하게 나마 REST API 설계 방식에 대해서 알아보았다.
자세한 자료들은 아래 참고 자료들은 참고하기 바란다.

참고 : http://info.apigee.com/Portals/62317/docs/web%20api.pdf
참고 : APICract Google groups https://groups.google.com/forum/?fromgroups#!topic/api-craft/
참고 : 마틴파울러의 ‘Glory of REST 향한 단계들’ http://jinson.tistory.com/190


새로운 문서가 업데이트 되었습니다.

REST API 이해와 설계 - #1 개념 잡기 http://bcho.tistory.com/953

REST API 이해와 설계 - #2 디자인 가이드  http://bcho.tistory.com/954

REST API 이해와 설계 - #3 보안 가이드  http://bcho.tistory.com/955



Selenium 테스트 메모

ALM/Test Automation | 2013.12.24 00:11 | Posted by 조대협

Firefox selenium IDE를 이용하여 Record 가능. 간단하게 IDE내에서 Command 추가등도 가능

아래는 네이버에서 "조대협" 으로 검색하여, 검색 결과에 "조대협의 블로그" 문자열이 나오면 성공하는 테스트 케이스 




작성 완료후 Export하면

Java/JUnit 3,4 , Test NG

Ruby,Python,C# 등으로 TG Export 가능


아래는 JUnit4로 Export한 소스 코드

package com.example.tests;


import com.thoughtworks.selenium.*;

import org.junit.After;

import org.junit.Before;

import org.junit.Test;

import static org.junit.Assert.*;

import java.util.regex.Pattern;


public class selenium_TC_naver {

private Selenium selenium;


@Before

public void setUp() throws Exception {

selenium = new DefaultSelenium("localhost", 4444, "*chrome", "http://www.naver.com/");

selenium.start();

}


@Test

public void testSelenium_TC_naver() throws Exception {

selenium.open("/");

selenium.click("id=query");

selenium.type("id=query", "조대협");

selenium.click("id=search_btn");

selenium.waitForPageToLoad("30000");

assertTrue(selenium.isTextPresent("조대협의 블로그"));

}


@After

public void tearDown() throws Exception {

selenium.stop();

}

}

다음은 Junit 4/Web Driver용으로 Export한 소스
package com.example.tests;

import java.util.regex.Pattern;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.openqa.selenium.*;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.Select;

public class SeleniumTCNaverWebdriver {
  private WebDriver driver;
  private String baseUrl;
  private boolean acceptNextAlert = true;
  private StringBuffer verificationErrors = new StringBuffer();

  @Before
  public void setUp() throws Exception {
    driver = new FirefoxDriver();
    baseUrl = "http://www.naver.com/";
    driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
  }

  @Test
  public void testSeleniumTCNaverWebdriver() throws Exception {
    driver.get(baseUrl + "/");
    driver.findElement(By.id("query")).click();
    driver.findElement(By.id("query")).clear();
    driver.findElement(By.id("query")).sendKeys("조대협");
    driver.findElement(By.id("search_btn")).click();
    // Warning: assertTextPresent may require manual changes
    assertTrue(driver.findElement(By.cssSelector("BODY")).getText().matches("^[\\s\\S]*조대협의 블로그[\\s\\S]*$"));
  }

  @After
  public void tearDown() throws Exception {
    driver.quit();
    String verificationErrorString = verificationErrors.toString();
    if (!"".equals(verificationErrorString)) {
      fail(verificationErrorString);
    }
  }

  private boolean isElementPresent(By by) {
    try {
      driver.findElement(by);
      return true;
    } catch (NoSuchElementException e) {
      return false;
    }
  }

  private boolean isAlertPresent() {
    try {
      driver.switchTo().alert();
      return true;
    } catch (NoAlertPresentException e) {
      return false;
    }
  }

  private String closeAlertAndGetItsText() {
    try {
      Alert alert = driver.switchTo().alert();
      String alertText = alert.getText();
      if (acceptNextAlert) {
        alert.accept();
      } else {
        alert.dismiss();
      }
      return alertText;
    } finally {
      acceptNextAlert = true;
    }
  }
}


'ALM > Test Automation' 카테고리의 다른 글

Selenium Test Suite 수행  (0) 2013.12.29
Selenium WebDriver와 RC 차이  (0) 2013.12.24
Selenium 테스트 메모  (0) 2013.12.24
테스트 팀의 조직 구조  (1) 2012.08.21
JUnit Max  (1) 2009.05.06
Software Testing Proces  (0) 2009.04.09

What is SOA? How to SOA?

아키텍쳐 /SOA | 2007.09.04 10:34 | Posted by 조대협

컴퓨터 시스템이 사용되면서부터, 각 시대의 기업 전략에 맞는 소프트웨어 아키텍쳐 존재하여 왔다. 초기 시대의 메인프레임에서는 기업의 업무를 전산화 하는데 목적이 맞춰졌고, 소프트웨어는 구조적 프로그래밍 (Structured Programming)으로 개발되었다.

그 후 개인 PC가 도입되면서 클라이언트 서버 시대 아키텍쳐가 도입되었고, 근래의 인터넷과 e비지니스 시대에서는EJB COM기반으로하는 컴포넌트 기반의 개발이 중심이 되었다.

그리고 지금의 IT 시스템들은 비즈니스의 급격한 변화를 수용할 수 있는 민첩성이 요구 되게 되었고, 이 요구를 충족시키기 위한 아키텍쳐가 서비스 지향 아키텍쳐 SOA 이다.

 이번 강좌에서는 SOA가 무엇이고 어떻게 SOA를 진행할지에 대해서 간략히 살펴보도록 한다.

 

1.    SOA 기본 개념

SOA, 기존의 애플리케이션들의 기능들을 비즈니스적인 의미를 가지는 기능 단위로 묶어서 표준화된 호출 인터페이스를 통해서 서비스라는 소프트웨어 컴포넌트 단위로 재 조합한후, 이 서비스들을 서로 조합(Orchestration)하여 업무 기능을 구현한 애플리케이션을 만들어내는 소프트웨어 아키텍쳐이다.

 또한 기존의 시스템이 각각의 독립된 업무 시스템으로 개발되어왔던 반면 SOA는 기업의 전체 업무가 하나의 거대한 SOA시스템으로 구성이 된다.

 

이해를 돕기 위해 예를 들어보자, 자바 기술 기반으로 개발된 고객 정보 시스템, 룰 엔진으로 구현된 룰 엔진, 메인프레임을 구현된 계좌이체 업무 등이 있다고 가정하자.

이 각각의 업무들은 각각의 목적에 따라서 따로 개발된것이고, 서로 연계가 불가능했다.

이 각각의 시스템의 기능들을 업무를 기준으로 주요 기능들로 묶어서 플랫폼에 독립적인 인터페이스 (예를 들어 XML/HTTP, CORBA,SOAP)를 구현하여 외부로 서비스를 제공한다.

사용자 삽입 이미지
<그림 1 SOA의 개념 >

이렇게 제공된 서비스 인터페이스를 이용하여 신용 대출 이라는 새로운 업무를 구현할 때 새롭게 시스템을 신규 개발하는것등이 아니라 기존의 이미 제공되어 있는 서비스들을 조합하여, 하나의 업무를 구현할 수 있다. 이것이 SOA의 기본적인 개념이다.

 

SOA 자체는 새로운 개념은 아니다. 소프트웨어 개발에 있어서 계속해서 이야기 되어 왔던 소프트웨어의 재사용성레고웨어 [. 소프트웨어 모듈을 레고블럭처럼사용성이 높은 단위로 구성한후, 애플리케이션을 마치 레고 블록을 조립해서 하나의 조립물을 만들듯이 제작하는 개념] 의 연장성에 있는 것이다.

 

이런 SOA 개념과 구현된 프로젝트는 이미 1990년대부터 구현되어 왔고 존재되어 왔다. 그럼 지금에 와서야 SOA가 주목 받는 이유는 무었인가? 예전에 서비스 구축을 위한 표준 인터페이스에 대한 방안으로 제시되었던 CORBA등의 기술의 난이도가 높은 점이 문제가 되어 왔으나 현재에는 XML/HTTP SOAP 기반의 웹서비스 기술등의 등장으로 서비스의 구현의 기술 난이도 문제가 해결되었고,

e비즈니스 환경에서 기존 업무 환경을 전산화 하는데에만 목적이 맞춰져 있어서 각각의 업무별로 독립된 시스템의 형태로 개발이 되어, 이에 대한 통합이 필요하게 되었으며 [예를 들면 고객 정보를 매출 내용과 연계하여 판매 전략을 수립하는 경우 등] 급격한 비즈니스 환경의 변화에 따라 비즈니스의 요구를 민첩하게 IT시스템에 반영되어야할 필요성이 대두됨에 따라 이에 대한 대안으로 SOA가 대두 되었다.

 

앞에서도 설명하였지만 SOA는 크게 서비스와 이를 조합하여 애플리케이션을 구성하는 으로 구성된다. 지금부터 각 서비스서비스를 구성하는 방법 에 대해서 알아보도록 하자

 

2.    서비스란?

서비스란 플랫폼에 종속되지 않는 표준 인터페이스(CORBA웹서비스) 통해서 기업의 업무를 표현한 Loosely coupled하고 상호 조합 가능한 소프트웨어 이다.

현대의 SOA에서 서비스의 플랫폼 종속성은 SOAP기반의 웹서비스 또는 XML을 통해서 구현된다. 서비스를 표현하는데 있어서 가장 중요한 특징은 기업의 업무를 표현한다는 것이다. 임직원 정보 서비스,계좌 이체 서비스와 같이 기업의 업무는 서비스로 정의할 수 있지만  JNDI Lookup, SMTP 이메일 클라이언트와 같은 비업무성 서비스는 존재할 수 없다.

 

(1) 서비스의 구성

서비스는 크게 3가지 요소로 구성된다.

 - 서비스 인터페이스

 - 서비스 규약

 - 서비스의 구현체

사용자 삽입 이미지

< 그림 2서비스의 구조 >

서비스 인터페이스는 서비스내의 하나의 업무 기능을 이야기 한다.

즉 주문서비스라는 서비스가 있을 때, 이 서비스는 상품 주문 주문 내용 조회라는 인터페이스를 가진다. (EJB나 자바 오브젝트의 비즈니스 메서드를 생각하면 되지 않을까?)

그리고 이 서비스를 사용하기 위한 여러가지 규약들 데이터 포맷이나 수형 서비스를 호출하기 위한 인자나 인터페이스 이름등이 정의되는 곳이 서비스 Contract이다.

이에 대한 실제 구현체가 Implementation.

현대의 SOA에서는 대부분이 웹서비스를 표준 인터페이스로 사용하기 때문에 서비스 Contract WSDL로 정의된다.

 

(2)서비스의 특징

서비스는 몇가지 특성을 가지고 있는데, 그중 몇 가지 중요한 특성을 뽑아보면 다음과 같다.

Ÿ          수직적 분할 (Vertical Slicing)

수직적 분할이란 애플리케이션을 개발할 때 전체 애플리케이션을 여러 개의 서비스로 나누고 각각의 서비스를 독립적으로 개발하는 것을 이야기 한다.

이전의 소프트웨어 개발은 애플리케이션을 각 Data Layer, Business Logic,View Layer와 같이 수평적으로 분리하였다. 그러나 SOA에서는 각각의 서비스가 Data Layer,Business Logic,View에 대한 모듈을 모두 가지고 있고 그래서 각 서비스간의 의존성이 최소화 된다.

사용자 삽입 이미지
<그림 3 Vertical slicing의 개념 >

Ÿ          Has standard interface

서비스가 제공하는 인터페이스는 표준 기술로 구현이 되어야 한다. 서비스를 사용하고자 하는 사람이 서비스 규약만을 가지고도 해당 서비스를 호출할 수 있어야 하며, 이는 해당 SOA시스템 내에서 플랫폼이나 기술에 종속되지 않아야 한다.

Ÿ          Loosely Coupled

Vertical Slicing에서도 설명하였듯이 각 서비스 컴포넌트들은 다른 서비스에 대해서 의존성이 최소화 되어 있어서 서비스의 구현내용등을 변경하였을 때 다른 서비스가 이에 의해서 영향을 거의 받지 않는다.

Ÿ          Composable

서비스 컴포넌트들은 서로 연결되어 조합된 형태의 하나의 애플리케이션을 구성해야 하기 때문에, 서비스간에 연결 및 조합이 가능해야 한다.

Ÿ          Coarse grainned

서비스의 구성단위나 인테페이스의 단위는 업무 단위를 기본으로 한다. IT 개발 조직이 아니라 현업의 비즈니스 조직이라도 해당 서비스가 무엇이고 무슨 기능을 하는지 이해할 수 있어야 한다.

Ÿ          Dicoverable

서비스에 대한 정보가 검색 가능해야 한다. SOA시스템의 규모가 증가함에 따라 서비스의 중복이 발생할 수 있고, 이를 방지하기 위해서 이미 구현된 서비스가 있는지를 검색할 수 있어야 하며, 검색 내용에는 서비스의 내용과 서비스에 대한 사용방법,권한,보안에 대한 정보들이 포함되어야 한다.

 

(3) 서비스의 종류

서비스를 그 기능에 따라서 5가지 정도로 나눠볼 수 있다.

일반적으로 우리가 지금까지 이야기 했던 서비스는 비즈니스 서비스 이다.

이 비즈니스 서비스는 말 그대로 비즈니스 적인 의미를 가지는 서비스로 SOA의 최소 단위가 되며, 비즈니스 로직을 구현한 Task centric service와 비즈니스 데이터를 대표하는 Data centric service로 분리된다. [. EJB Session Bean Entity Bean 정도로 생각하면 된다.]

 

다음은 Intermediary 서비스 인데, 업무적인 기능을 가지는 것이 아니라 서비스들을 연결하는 데서 발생하는 차이점을 보안해주는 서비스 이다.

몇가지 예를 들어보자

기존의 백화점의 구매 프로세스가 존재하였다고 가정하자. 백화점의 업무 요건이 바뀌어서 일반고객과 VIP고객에 대한 구매 프로세스를 차별화 하였을 때, 고객의 요건에 따라 구매 프로세스를 서로 다르게 호출해야 한다. 이런 유형을 Routing 서비스라고 한다.

서비스에 들어오는 데이터 타입이 구매자의 이름,구매액과 물품 목록 이었는데, 서비스가 수정되면서 데이터 타입이 변화가 되었을 때 기존에 이 서비스를 호출하던 모든 서비스 소비자는 데이터 타입의 변경으로 모두 변경이 되어야 하며 이런 이유로 업무 변화에 유연하게 대처를 하지 못하는데, 이런 경우 구 데이터 타입을 새로운 데이터 타입으로 변화시켜주는 서비스가 있으면 유연하게 변화에 대처할 수 있다. 이런 형태의 서비스를 Transform 서비스라고 한다.

사용자 삽입 이미지

<그림 4 Routing 서비스 >

 

사용자 삽입 이미지

< 그림 5 Transform 서비스>

그 외에도 기존 서비스에 새로운 기능을 추가하는 Functional Adding서비스, Façade 기능을 구현한 서비스등을 그 예로 들 수 있다.

 Process centric 서비스 는 비즈니스 서비스들을 조합하여 하나의 업무 프로세스를 구현해내는 서비스로, 주로 상태가 있는 (Stateful)를 구현하는데 이용이 된다.

 Application 서비스 Technical한 서비스 트렌젝션 서비스, 로깅 서비스가 예가 된다. 지금까지 설명하면서 서비스는 비즈니스적인 의미를 가진 컴포넌트 이며 트렌젝션 등은 서비스가 될 수 없다.. 라고 이야기를 했지만, 현실세계에서 이런 서비스가 나오지 않을 수 있는 보장이 있을까? 언제나 예외는 존재한다고 SOA에서 지극히 예외적인 서비스이다. 잘 설계된 SOA에서는 Application 서비스가 존재하지 않는다.

 Public enterprise 서비스는 다른 회사나 다른 SOA시스템으로 제공되는 서비스이다. [은행의 대외계 업무 정도가 이에 해당한다.] 다른 서비스에 비해서 외부로 제공되는 서비스인 만큼 성능, 트렌젝션, 보안에 대한 깊은 고려가 필요하다. 

3.    SOA 아키텍쳐 모델

지금까지 서비스가 무엇인가? 에 대해서 알아보았다. 지금부터는 이 서비스들을 어떻게 조합하여 소프트웨어 시스템을 구성하는지, 구성 방법에 대해서 알아보도록 하자.

서비스의 구성 방법의 기업의 SOA 성숙도와 발전 정도에 따라 단계적으로 적용되어야 한다.

단계적인 발전 모델을 설명하기에 앞서서 application frontend 대해서 설명하면, 서비스들이 사용되어 최종 사용자에게 보이는 곳이 application front end이다. 일반적인 웹사이트나 기업 포탈, X 인터넷 클라이언트, 4GL 클라이언트등이 될 수 있다. 

(1)Fundamental SOA [통합]

가장 기본적인 형태의 SOA, 비즈니스 서비스와 애플리케이션 서비스만 존재하며 이 서비스들의 조합들은 application front end에서 이루어 진다.

Fundamental SOA의 가장 큰 목적은 기존의 시스템을 각각 서비스화하는 것과 독립되었던 시스템들을 통합하여 하나의 시스템으로 운영한다는데 목적이 있다. 

(2)Networked SOA [ 유연성과 통제 추가 ]

서비스화하여 통합된 SOA시스템은 시간이 갈 수 그 크기가 커지게 되고 서비스간의 호출은 관계는 날이 갈 수 복잡해진다. 그리고 서비스의 내용이 변경 또는 보강 되어 가면서 의존성에 의해서 서비스간에 수정이 필요한 경우가 발생한다. 이는 서비스의 변화를 어렵게 만들고 결과적으로 기업 업무에 대한 경직성 유발하는데, 이를 해결하기 위해서 모든 서비스들을 중앙에 하나의 버스를 통해서 관리하여 서비스간 연결의 복잡도를 해소하고 Intermediary 서비스를 추가 함으로써, 서비스의 내용이 변경되었을 때 그 차이를 보강해줄 수 있어야 한다.

 

사용자 삽입 이미지

<그림 6 Networked SOA 개념 >

이렇게 중앙에 버스 역할을 하는 것이 Enterprise Service Bus (ESB)이고, 여기에는 서비스에 대한 모니터링과, Intermediary 서비스 기능의 제공, 위치에 대한 투명성 제공을 통해서 기본적인 통제유연성 제공을 특징을 한다. 

(3) Process Oriented SOA [ 민첩성의 추가 ]

기업의 업무프로세스가 자주 변화가 되고, IT 시스템이 이에 민첩하게 반응해야 하거나, SOA로 구현해야 하는 기업 업무들에 복잡한 업무 플로우가 존재할 경우 이 업무 프로세스들을 BPM기반으로 구현하는 SOA를 고려해볼 수 있다.

 각 서비스를 조합하는 것을 BPM으로 구현함으로써, 업무의 조합이 별도의 코딩 없이 BPM툴로 이루어 지게 되고, 업무프로세스가 바뀌었을 때 BPM툴에서 업무프로세스를 조정하는 것만으로도 빠르게 비즈니스 조직의 요구에 대응하여 IT 시스템이 비즈니스 업무에 대한 민첩한 대응력을 확보할 수 있다.

 또한 업무 조직과 기술 조직간의 의사 소통에 있어서도, 기존에는 업무조직은 EJB JAVA와 같은 기술적인 내용을 이해하지 못하였고, 기술 조직의 경우 업무에 대한 이해도가 낮았기 때문에 의사소통에 있어서 많은 문제를 유발하였는데, BPM을 도입할 경우, BPM에서 사용되는 모든 서비스는 이미 업무적인 의미를 가지는 컴포넌트이기 때문에 IT조직과 업무 조직간에 의사 소통을 하는데 문제가 없으며, 특히 업무 프로세스의 경우 직접 업무 조직이 큰 흐름을 정의하고 IT조직이 구현화에 있어서 보강만 함으로써 빠르고 정확한 의사 소통을 이룰 수 있다.

 BPM과 함께 생각할 수 있는 것이 BPA(Business Process Analysis) BAM(Business Activity Monitoring) 이다. BPA는 실제 업무플로우를 BPM으로 구현하기 전에 업무팀에서 해당 업무 플로우에 대한 설계를 하고 이에 대한 시뮬레이션을 할 수 있는 Business Process 분석 설계 도구이다.

BPA를 통해서 현 업무팀에서 해당 업무프로세스를 정의하고, 작동 내용을 시뮬레이션 함으로써 보다 완성된 업무 프로세스를 얻을 수 있으며, 이렇게 설계된 업무는 IT 개발팀에 의해서 BPM으로 변환이 된다.

BPM으로 변환된 업무는 SOA 시스템에 반영되어 실제 운영이 되게 되고,

BAM이라는 비즈니스 프로세스 모니터링 도구를 이용하여 반영된 BPM에 대한 평가가 이루어진다. 이 평가를 기반으로 업무팀에서는 다시 BPA를 이용하여 해당 업무의 최적화를 수행하고 다시 이는 BPM으로 구현이되고.. 이러한 반복을 통해서 업무의 개선과 SOA시스템이 최적화를 이룰 수 있다.

사용자 삽입 이미지
<그림 7. BPA,BPM,BAM 통한 업무 프로세스의 구현과 업무의 최적화 >

4.    SOA 아키텍쳐 모델의 구현

앞에서 설명한 3단계 아키텍쳐를 실제로 구축하는데 있어서 고려할 사항이 있다. 

(1) 서비스화

먼저 Fundamental SOA에서 가장 중요한 것은 기존의 시스템을 서비스화 시키는 것이다. 현재의 서비스 인터페이스는 대부분 SOAP 기반의 웹서비스를 사용하는데, 솔루션 중에는 이미 Legacy System에 대해서 웹서비스를 제공하는 서비스 아답터 제공된다. (IWay社의 SAP, Siebel 아답터, BEA Tuxedo 웹서비스 아답터 SALT etc)

대부분의 서비스 아답터를 통해서 서비스화된 기존 메서드들은 비즈니스 적인 의미를 가지지 않는 Application 서비스가 될 가능성이 많다. 서비스화를 하기전에 기존 시스템에 기능등을 업무 단위의 Coarse grainned된 컴포넌트로 묶은후에 서비스화 하는 것이 좋다.

 

(2) 스펙과 범위

고려 사항중 하나가 트렌젝션이나 Reliable 메세징과 같은 웹서비스 스펙에 해당하는 문제이다. 현재 웹서비스의 확장 규약인 WS* (Webservice Extenstion)의 경우에는 트렌젝션이나 Reliable Messaging과 같은 기능들을 제공하고 있지만 문제는 서비스화를 시켜주는 솔루션에는 이에 대한 지원이 의무사항이 아니라는 것이다. 트렌젝션 기능들을 웹서비스에서 지원하는 기능을 사용하려고 했다가 막상 해당 시스템의 웹서비스에서 트렌젝션 기능들을 지원하지 않아서 문제가 될 수 있다. 서비스를 정의할 때 어떻게 이런 기능들을 구현할지에 대한 고려가 앞서야 한다.

 이런 서비스 구현에 있어서 유용하게 사용할 수 있는 것이 EAI이다. EAI는 시스템간의 통합을 목적으로 하는 솔루션으로, SOA는 다르게 Tightly Coupled 되고 비즈니스적인 통합이 아니라 IT 시스템간의 통합을 지원함으로써,  SOA 통합에서 다루기 힘든 트렌젝션이나 보안에 대한 내용을 해결해준다.

[ EAI 솔루션에도 IT 시스템간의 연계 흐름을 정의 하기 위해서 BPM이 사용되는데, 시스템간의 흐름과 업무 흐름을 분리하기 위해서 이를 각각 IC-BPM HC-BPM으로 구분하여 부른다.]

 

(3) 보안

보안에 대해서는 따로 길게 설명하지 않겠다. 암호화 선택할때는 전체 메시지를 암호화할지 메시지 내용중 일부만 암호화 할지가 결정해야 하며, 중앙에서 각 서비스에 대한 사용 권한과 계정에 대한 관리를 할 수 있는 솔루션을 구성할 필요가 있다.

 

(4) 모니터링

여러 시스템을 하나의 SOA 시스템에 구축한 만큼 각각의 서비스에 대한 모니터링과 성능 측정은 매우 중요한 요소로 작용한다. SOA 시스템을 구축할 때 미리 로깅과 모니터링 성능 측정에 대한 기능을 미리 구현할 필요가 있다.

 

(5) 서비스 검색

SOA 시스템에서 이미 개발된 서비스를 재사용하기 위해서 서비스를 검색할 수 있어야 하며, 서비스에 대한 메타 정보 (보안,과금체계 등에 대한 규약등)들도 검색할 수 있어야 한다. 웹서비스에서는 이를 UDDI로 구현한다.

 

(6) Application frontend

Application frontend는 최종 사용자에게 업무 기능을 제공하는 부분으로, 일반적인 웹 인터페이스를 이용할 수 도 있으며, Rich client의 개념을 웹에 도입한 AJAX 기반이나 Flex  같은 X-Internet 솔루션을 사용할 수 도 있다.

SOA 시스템은 여러 시스템을 통합한것으로 여러 메뉴와 UI가 통합 되기 때문에 권한과 메뉴등에 대한 관리 기능이 필요하게 되며, 이는 Enterprise Portal (EP)솔루션등을 이용해서 쉽게 구현할 수 있다. 지금까지 설명한 SOA 아키텍쳐 구성을 하나의 그림으로 표현해보면 다음과 같다.

 

사용자 삽입 이미지

<그림 8 SOA 레퍼런스 아키텍쳐 >

지금까지 SOA가 무엇인지, 그를 구성하는 서비스의 개념과 서비스를 조합하여 SOA시스템을 구축하는 방법에 대해서 알아보았다.

그렇다면 실제로 SOA 프로젝트를 수행하는데 있어서 어떻게 SOA 시스템을 구축하며 프로젝트를 진행해야 할까? 아래부터는 SOA의 수행 방법론에 대해서 알아보도록 하자.

 

5.    SOA 수행 방법론

SOA 시스템은 기존의 시스템과는 다르게 기업에 업무별로 개개별 시스템이 존재하는 것이 아니라, 이러한 개개별 시스템을 통합하여 하나의 거대한 IT 운영 플랫폼을 구축하는데 있다. 이에 따라 SOA 시스템을 수행 하는 방법도 역시 차이가 나는데, 기업의 전략, 비용의 집행방법, 팀의 통제, 프로젝트 관리 방법을 기준으로 하나씩 알아보도록 하자

 

(1) 기업의 전략

IT 시스템은 기본적으로 기업 업무에 대한 반영이다. 그래서 SOA시스템을 통해서 제공하고자 하는 기능은 기업의 전략에서부터 파생된다.예를 들어 기업 경영 전략이

2004년 매출 증대

2005년 고객 만족 실현

2006년 브랜드 이미지 관리

와 같다면, 이를 지원하기 위해서 IT시스템의 구현 전략을 다음과 같은 것들이 될 수 있다.

2004년 매출 내용 전산화

2005 CRM 도입을 통한 고객 정보 수집과 매출 내용을 기반으로 고객 패턴 추출

2006년 수집된 고객 정보를 토대로 마케팅 집중

SOA의 수행은 기업의 경영 전략에 따라서 수행되며, 성공적인 SOA를 위해서는 해당 단계에서 기업의 핵심 업무를 SOA화 하는 것이 필요하다.

또한 기업의 IT전략은 예전처럼 각각의 단위 시스템을 개발하는 것이 아니라 기업의 전략을 IT화하여 구현한 SOA시스템을 기업의 발전에 따라서 계속 반영 및 변화 시켜나가기 때문에 기존 IT전략에 비해서 좀더 장기적인 전략을 필요로 한다.

(2) 비용의 집행

SOA 시스템은 비용 집행에 대한 고려가 필요하다. 기존 IT시스템은 단위 시스템에 대한 개발 프로젝트에 소요되는 비용만 예상하여 집행하면 되었지만, SOA 계속해서 발전해 나가는 시스템이고 한 시스템에 기능이 추가 되기 때문에 비용 집행 방식이 다르다.

 SOA 시스템의 경우 초기에 SOA에 필요한 인프라의 구축 (ESB,BPM,모니터링,보안 인증,UDDI )에 비용이 많이 소요되기 때문에 초기 투자 비용이 기존의 IT 시스템 보다는 높아진다.

사용자 삽입 이미지
< 그림 9 SOA의 비용 집행 방법 >

그러나 위의 그림에서와 같이 계속해서 업무를 추가해나감에 따라 새로운 업무가 완전히 새롭게 구성이 되는 것이 아니라 앞단계에서 개발하였던 서비스들을 다시 재 사용되기 때문에 개발 비용은 지속적으로 감소하게 되고, 기능이 부족한 서비스들은 계속해서 대체 되면서 안정적으로 유지 되기 때문에 시간이 갈 수 소요 비용은 감소하게 된다.

 

(3) 제어와 통제

SOA 시스템이 거대화해짐에 따라 SOA 시스템에 대한 중앙 관리 및 통제 제어가 필요하게 된다. 어느 서비스부터 개발해야 할것인지, 이번 기간내에 진행해야할 SOA범위, 각 개발에 대한 표준화, 자금 조달 계획등을 수행해야 하며 이를 통제하는 조직이 필요하다. 이러한 통제를 Goverance라고 하는데,

이 통제를 실시하는 Governace 조직에서 하는 일은 다음과 같다.

-         SOA 시스템에 대한 정책 수립 및 표준화 - Standard

-         SOA 관련 기술 전파 및 가이드 - Evangelist

-         SOA 구축 계획 수립 및 실행 (로드맵)- Strategy

-         자금 조달 및 집행 계획

-         업무 분석 및 설계

-         문화 변화 IT조직과 비즈니스 협업 조직의 협업문화 개발

-         모범사례 수집과 배포

여기에는 여러가지 통제 모델을 도입할 수 있는데, 운영 환경상에 서비스의 개발 배포 모니터링을 위한 통제 모델이다.

 

사용자 삽입 이미지

<그림 10 SOA 제어 통제 모델 예시 >

중앙 통제 조직은 실 개발 조직인 IT 조직과 비즈니스 조직간의 중계 역할을 하게 되며, 비즈니스 조직으로부터 전략과 요구 사항을 받아 실제 구현 조직에 전달을 하며

각 개발 조직에서 개발된 내용들에 대한 검증 및 배포 작업과 SOA 플랫폼에 대한 운영 및 모니터링 통제를 실시 한다.

 이외에도 CVS등을 이용한 소스의 통제, apache maven과 같은 도구를 이용한 빌드의 관리, 일일 빌드 정책, 개발 방법론등도제어와 통제에 해당한다. 

(4) 프로젝트 관리

SOA의 프로젝트 진행에 있어서 특이한 점은 반복적인 개발(Iterative Development)점진적인 개발(Incremental Development)이다. 한번에 SOA 전체 시스템을 개발하는 것이 아니라 중요 업무를 먼저 개발한후에 반복적으로 업무에 대한 개선 작업을 병행하고, 점차적을 필요한 기능을 추가해나가는 모델이다.

 또한 SOA의 프로젝트 개발 관리는 앞에서 설명 했던 Thin Thread Model의 개발 방식을 준수 하면 좀 더 좋은 효과를 볼 수 있다. 

6.    결론

지금까지 SOA가 무엇인지, SOA를 구성하는 서비스의 형태와 특징은 어떠한지 그리고 SOA 프로젝트를 수행하는 몇가지 권고 방안에 대해서 살펴보았다.

 SOA ESB BPM같은 솔루션을 제외하고라도 이미 서비스의 개념과 서비스 인터페이스의 개념을 도입하여 개발되는 사례가 많다. 특히 AJAX X 인터넷의 등장으로 SOA 기반의 시스템 도입은 가속화 되어갈 전망이다.

 여기서 중요한점은 SOA의 개념이 무엇인지를 명확하게 이해하고, 현재 기업에서 필요한 SOA의 단계를 파악하여 적절한 수준의 SOA를 도입하고, SOA 시스템을 장기적으로 발전 시켜나갈 수 있는 장기적인 전략과 이를 수행할 수 있는 제어와 통제 모델 (Governance)를 구축하는 것이 중요하다.

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

2008년 SOA 전망  (0) 2008.01.10
Next Enterprise  (0) 2007.12.21
What is SOA? How to SOA?  (1) 2007.09.04
SOA & Agile  (0) 2007.09.04
About SOA  (2) 2007.08.20
SOA에 대한 기술적 접근  (2) 2007.08.20