좋은 API 디자인하기, 왜 그것이 중요한가?

| 0 comments

죠수아 블로치는 구글의 소프트웨어 아키텍쳐로 유명한 사람입니다.

아래 내용은 2007년 1월 24일 Google Tech Talks 에서 발표한 내용입니다.

API 를 개발하면서 한 번쯤 고민했던 내용들이 모두 들어있습니다.

API 를 개발하는 개발자라면 거의 하나도 놓칠 게 없는 경험이라고 생각합니다.

그만큼 많이 번역되고 블로그에 퍼졌던 글들입니다.

좀 길지만, 많은 개발자들이 두고두고 참조했으면 하는 바람이 있습니다.

원제 : How to Design a Good API and Why it Matters(Google Tech Talks, 2007.1.24)

저자 : Joshua Bloch, Principal Software Engineer, Google

  • API 디자인이 왜 중요한가?

    API는 회사가 가진 중요한 자산일 수 있다.

    - 고객들은 무섭게 투자한다 : 구매하고, 사용하고, 배운다.

    - 운영중인 API를 중단시키는 비용은 엄두도 낼 수 없다.

    - 성공적인 public API 는 고객을 잡는다.Public API 는 영원하다. – 그것을 제대로 만들 기회는 오직 딱 한 번 뿐이다.

  • API 가 왜 당신에게 중요한가?

    당신이 만일 프로그래머라면, 당신은 이미 API 디자이너이기 때문이다.

    - 좋은 코드는 모듈화되어 있고, 모든 모듈들은 API 를 가지고 있다.유용한 모듈들은 재사용되어지기 때문이다.

    - 일단 모듈이 사용자를 가지기 시작하면, 임의로 API를 바꿀 수 없다.

    - 좋은 방향으로 재사용되어지는 모듈들은 회사의 자산이다.

    API라는 관점에서 생각하는 것은 Code 의 Quality를 높이기 때문이다.

  • 좋은 API의 특징

    배우기 쉬울 것

    문서가 없어도 사용하기 쉬울 것.

    잘못 사용하기 어려울 것.

    읽기 쉽고, API를 사용하는 코드를 유지보수하기 쉬울 것

    요구사항을 만족시키기에 충분히 강할 것.

    확장하기 쉬울 것

    사용하는 사람들의 수준에 맞을 것

첫째. The Process of API Design

  1. 적당한 수준에서 회의적 태도로 요구사항 수집하기당신은 종종 솔루션들을 제안받을수도 있다.

    - 만일 더 나은 솔루션들이 있을 수 있다면,당신의 일은 진짜 요구사항을 수집하는 것이어야 한다.

    - 유즈케이스 형태로 수집을 해야 한다.

    더 일반적일수록 더 쉬워지거나 더 보상이 될 수 있다.

  2. 짧은 스펙으로 시작하라 – 1페이지는 이상이다.이 단계에서는 Agility 가 완벽함을 이긴다.

    가능한 많은 사람들로부터 반응을 살펴라.

    - 그들의 input 에 대해 귀기울여 듣고, 심각하게 받아들여라.

    스펙을 작게 할수록, 고치기 쉽다.

    자신감을 얻을 때까지 살을 붙여라.

    - 이것은 필연적으로 코딩을 포함한다.

  3. 미리 자주 API 에 글을 적어라.(그냥 function 내에 필요할 기능을 스크립트 형식으로 적어넣는다.)API를 구현하기 전에 시작하라.

    - 구현하는 시간을 절약해준다.가능한 스펙을 잡기 이전부터 시작하라.

    - 스펙을 잡는 시간을 절약해준다.

    살이 붙을 때까지 API 에 글을 적어라.

    - 끔찍한 실패를 예방해준다.

    - 코드는 예제와 unit test 를 먹고 산다.

  4. SPI 에 글을 적는 것은 더 중요하다.Service Provider Interface (SPI)

    - 플러그인 방식의 인터페이스는 여러 개의 구현을 가능하게 한다.

    - 예 : Java Ctyptography Extension (JCE)배포하기 전에 multiple plug-in 들을 만들어라. (윌 트랙은 “세개의 법칙”이라고 부른다.)

    - 만일 플러그인 하나를 만들면, 아마도 API가 다른 것은 지원하지 못하게 될 것이다.

    - 두 개를 만든다면, 어렵게라도 여러개를 지원하기는 할 것이다.

    - 세 개를 만든다면, 그것은 아주 잘 작동할 것이다.

  5. 현실적 기대수준을 받아들여라.대부분의 API 설계는 지나치게 제약되어 있다.

    - 당신은 모든 사람을 만족시켜줄 수 없다.

    - 모든 사람들을 똑같이 불만족하게 만드는 것에 초점을 맞추어라.실수하기를 기대하라

    - 몇 년 간 실사용이 되면, 실수따위는 잊어버리게 된다.

    - 실수를 통해 API를 진화시켜라.

둘째. General Principles (일반 원칙)

  1. API는 하나의 일만 해야 하고, 그것을 아주 잘 해야 한다.기능이 설명하기 쉬워야 한다.

    - 이름짓기 힘들다면, 통상적으로 잘못 만들어진 것이다.

    - 좋은 이름이 자연스럽게 개발로 이어지게 만든다.

    - 모듈을 쪼개거나 합칠 수 있어야 한다.

  2. API는 가능한한 작게 만들어야 하지만, 더 작아져서는 안된다.API는 요구사항을 만족시켜야 한다.

    의심이 들면 그대로 두어라.

    - 기능, 클래스, 메쏘드, 파라미터까지.

    - 뭔가를 더할 수는 있지만, 결코 제거할 수는 없다. (배포되면 회수불가)개념적인 무게가 규모보다 더 중요하다.

    (API의) 힘과 무게(power-to-weight ratio) 간의 균형비를 찾아라.

  3. Implementation 이 API에 영향을 주어서는 안된다.구현이 너무 세세해지면

    - 사용자를 혼란스럽게 한다.

    - implemenation 을 바꿀 자유를 방해한다.세세한 구현이 무엇인지를 정확히 알아라.

    - method behavior 스펙을 너무 과하게 잡지 마라.

    - 예를 들면 : hash function 들은 스펙화하지 마라.

    - 모든 튜닝 파라미터들은 의심해야 한다. (이게 많다고 좋은 API가 아니라는 뜻)

    세세한 구현이 API 로 흘러들어가게 해서는 안된다.

    - 디스크 상이나, 네트워크 상이나, 예외로라도 !!!

  4. 모든 것의 접근을 최소화하라.가능한 private 하게 class 와 member를 만들어라.

    public class 가 (상수에 대한 예외와 함께) public field를 가져서는 안된다.이렇게 해서 정보은폐를 최대화하라. (밖으로 드러나지 않게 Capsulation 하라는 뜻.)

    모듈이 독립적으로 debug 되고, 이해되어지고, 구축되어지고, 테스트 되어지도록 하라.

  5. 이름이 중요하다. – API 는 작은 언어다.이름은 굳이 설명하지 않아도 이해될 수 있어야 한다.

    - 기호나 축약을 사용하지 마라.Consistent 하게 하라 – 똑같은 단어는 같은 것이어야 한다.

    - API 전반에 걸쳐서 (API 플랫폼 전반에 걸쳐서!!!)

    규칙적이어야 한다. – 대칭과 균형을 갈구하라.

    코드는 산문처럼 읽힐 수 있어야 한다. (얼마나 읽기 쉬운가?)

    if (car.speed() > 2 * SPEED_LIMIT)

    generateAlert(“Watch out for cops!”);

  6. 문서화가 중요하다.

    재사용성은 “재사용한다”는 Action 보다 “재사용하겠다.”고 쉽게 말할 수 있는 어떤 것이어야 한다. Action 은 좋은 디자인과 좋은 문서를 필요로 한다. 비록 아주 가끔 우리가 좋은 디자인(설계, 아키텍쳐)을 보게 되었다고 하더라도, 우리가 문서 없이 재사용되어질 정도로 좋은 컴포넌트를 볼 수는 없다. – D.L.Parnas, Software Aging, 1994

  7. 종교처럼 문서화하라.”모든” class, interface, method, constructor, parameter, and exception 을 문서화하라.

    - class : instance 화 되는 것들

    - method : method 와 그 client 들간의 계약들이다.

    - parameter : units, form, ownership 등을 지칭한다.state-space(상태공간, 전체적인 것에 대해)을 주의깊게 문서화하라.

  8. API 디자인에 대한 성능결과에 대해 고려하라.

    나쁜 의사결정은 성능 한계를 만들기도 한다.

    - type 을 상호 교환가능하게 만들어라.

    - static factory 대신 constructor 를 제공하라.

    - interface 대신에 implementation type 을 사용하라.성능을 얻기 위해 API를 뒤틀지 마라. (변형하지 마라)

    - 기본적인 성능 이슈는 고쳐질 것이다. 하지만 두통이 그대와 함께 영원할 것이다.

    - 좋은 디자인은 일반적으로 좋은 성능과 일치한다.

  9. 성능을 고려한 API 설계는 실질적인 효과로 나타날 뿐 아니라 영원하다.

    - Component.getSize()는 Dimension을 return 한다.

    - Dimension 은 mutual 하다.

    - 모든 getSize call 은 Dimension을 할당해야만 한다.

    - 수많은 불필요한 object allocation 이 발생한다.

    - 대체안이 1.2 버전에 추가된다 : old client code 는 여전히 느리다.

  10. API 는 플랫폼과 평화적으로 공존해야만 한다.관습적인 것을 따라라.

    - 표준 naming rule 을 따라라.

    - 독자적인 파라미터나 Return Type을 쓰지마라.

    - 코어 API나 언어에 있는 패턴을 흉내내어 써라.API 친화적인 특징을 이용하라.

    - Generics, varargs, enums, default arguments

    API 의 위험과 단점들을 잘 알고 회피하라.

    - Finalizers, public static final arrays

셋째. Class Design

  1. 변경을 최소화하라.만일 다른 일을 해야 할 충분한 이유가 없다면, class 는 불변의 것이어야 한다.

    - 장점 : simple, thread-safe, reusable

    - 단점 : 각 value 별로 분리된 object

    만일 변경해야 한다면, 상태공간(state-space)를 작게 유지하고, 정의를 잘 유지하라.

    - 언제, 어떤 method를 부르는게 합리적인지를 명확히 하라.

    Bad: Date, Calendar

    Good: TimerTask

  2. subclass는 substitutability (대체성)를 의미한다.

    - (..은..이다)라는 관계가 존재할 때만 subclass 를 써라.

    - 그렇지 않으면, composition 을 사용하라.Bad: Properties extends Hashtable, Stack extends Vector

    Good: Set extends Collection

  3. 상속을 위해 설계하고 문서를 남겨라.

    설계가 그렇게 되어 있지 않고 문서가 없다면, 상속을 못하게 만들어라.상속이란 캡슐화와 상충한다.

    - subclass 는 superclass 의 세세한 구현에 민감하다.

    만일 subclass 를 허락한다면, self-use를 문서화하라.

    - method가 어떻게 다른 method 를 사용하는지?

    보수적인 정책 : 모든 구체적인 class 는 final 이다.

    Bad: Many concrete classes in J2SE libraries

    Good: AbstractSet, AbstractMap

넷째. Method Design

  1. 모듈이 할 수 있는 것을 client 가 하게 하지 마라.boilerplate code 에 대한 필요를 줄여라.

    - boilerplate code 란 일반적으로 cut-and-paste 로 행해지는 코드를 말한다.

    - (모듈을 cut&paste해서 client 내에 넣어버리면) 아주 추하고 번거롭고, 오류 발생이 잦다.

    import org.w3c.dom.*;

    import java.io.*;

    import javax.xml.transform.*;

    import javax.xml.transform.dom.*;

    import javax.xml.transform.stream.*;

    // DOM code to write an XML document to a specified output stream.

    private static final void writeDoc(Document doc, OutputStream out)throws IOException{

    try {

    Transformer t = TransformerFactory.newInstance().newTransformer();

    t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());

    t.transform(new DOMSource(doc), new StreamResult(out));

    } catch(TransformerException e) {

    throw new AssertionError(e); // Can’t happen!

    }

    }

  2. “사용자를 놀라게 하지 않기” 원칙을 준수하라.

    API 사용자는 behavior 에 의해 놀라서는 안된다.

    - 그렇게 하기 위해 구현에 추가적인 노력을 기울일만한 가치가 있다.

    - 그리고, 조금 성능을 깎여도 될만한 가치가 있다.

    public class Thread implements Runnable {

    // Tests whether current thread has been interrupted.

    // Clears the interrupted status of current thread.

    public static boolean interrupted();

    }

  3. 빨리 실패하라 – 장애가 발생되면 가능한한 빨리 에러를 알려라.컴파일 시간이 제일 좋다. – static typing, generics

    런타임에, 첫 잘못된 method invocation 이 제일 좋다.

    - method 는 반드시 failure-atomic 해야 한다.

    // A Properties instance maps strings to strings

    public class Properties extends Hashtable {

    public Object put(Object key, Object value);

    // Throws ClassCastException if this properties

    // contains any keys or values that are not strings

    public void save(OutputStream out, String comments);

    }

  4. String 포맷으로 있는 모든 데이터를 프로그램 구조로 바꾸어라.그렇게 하지 않으면, client 는 string 을 parse 해야 한다.

    - client 는 괴롭다.

    - 더 나쁜 건, string 이 사실상의 API 로 변질되어 버린다.

    public class Throwable {

    public void printStackTrace(PrintStream s);

    public StackTraceElement[] getStackTrace(); // Since 1.4

    }

    public final class StackTraceElement {

    public String getFileName();

    public int getLineNumber();

    public String getClassName();

    public String getMethodName();

    public boolean isNativeMethod();

    }

  5. 주의를 가지고 오버로딩 하라.모호한 오버로드는 피하라.

    - 여러 개의 오버로딩이 동시에 적용될 수 있다.

    - 보수적이 되어라 : 동일한 argument 숫자가 없도록 하라.당신이 모호한 오버로딩을 제공해야 한다면, 동일한 arguments에 대해서는 동일한 behavior 가 일어나게 하라.

    public TreeSet(Collection c); // Ignores order

    public TreeSet(SortedSet s); // Respects order

  6. 적절한 파라미터와 리턴타입을 사용하라.Input 을 위해 class 전반에 interface type 을 장려하라.

    - 유연성과 성능을 제공하라.가장 구체적이면서 가능한 input parameter type 들을 사용하라.

    - 런타임 시간으로부터 컴파일 시간까지 에러를 옮겨라.

    더 좋은 type 이 있으면, string 을 사용하지 마라.

    - string 은 무겁고, 에러가 나기 쉽고, 느리기까지 하다.

    통화를 표현하는데 floating point 를 사용하지 마라

    - binary floating point 는 부정확한 결과를 야기시킨다.

    float(32 bits) 보다 double(64 bits)를 사용하라.

    - 정확성 손실은 현실이고, 성능 손실은 무시할만 하다.

  7. method 전반에 걸쳐 일관적인 parameter odering 을 사용하라.특히 parameter type 들이 동일할 때 더욱 중요하다.

    #include

    char *strcpy (char *dest, char *src);

    void bcopy (void *src, void *dst, int n);

    java.util.Collections first parameter always collection to be modified or queried

    java.util.concurrent – time always specified as long delay, TimeUnit unit

  8. Avoid Long Parameter Lists세 개, 또는 더 적은 수의 파라미터들이 이상적이다.

    - 더 많다면, 사용자들은 문서를 참조하려 할 것이다.똑같이 타이핑된 파라미터들의 긴 리스트는 위험하다.

    - 프로그래머들이 parameter 순서를 실수로 바꾸어버릴 수도 있다.

    - 프로그래머들이 여전히 컴파일하고, 실행한다. 그러나 그것이 잘못일 수 있다.

    파라미터 리스트를 짧게할 수 있는 두가지 기법이 있다.

    - method 를 두개로 나누어라.

    - 파라미터를 유지할 수 있도록 helper class 를 만들어라.

    // Eleven parameters including four consecutive ints

    HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName,

    DWORD dwStyle, int x, int y, int nWidth, int nHeight,

    HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,

    LPVOID lpParam);

  9. 예외처리를 요구하는 return value를 만들지 마라zero-length array를 리털하거나 빈 collection 을 리턴하라. null 을 리턴하지 마라.

    package java.awt.image;

    public interface BufferedImageOp {

    // Returns the rendering hints for this operation,

    // or null if no hints have been set.

    public RenderingHints getRenderingHints();

    }

다섯째. Exception Design

  1. 예외 상황을 표시하기 위해 exeption 을 던져라.client 가 control flow 를 위해 exception을 사용하도록 해서는 안된다.

    private byte[] a = new byte[BUF_SIZE];

    void processBuffer (ByteBuffer buf) {

    try {

    while (true) {

    buf.get(a);

    processBytes(tmp, BUF_SIZE);

    }

    } catch (BufferUnderflowException e) {

    int remaining = buf.remaining();

    buf.get(a, 0, remaining);

    processBytes(bufArray, remaining);

    }

    }

    정반대로, 조용하게 fail 이 나서는 안된다.

    ThreadGroup.enumerate(Thread[] list)

  2. Unchecked Exceptions 을 사용하라.. Checked – client 가 recovery action 을 해야 한다

    . Unchecked – programming error

    . Checked exceptions 의 과한 사용은 boilerplate 를 야기시킨다.

    try {

    Foo f = (Foo) super.clone();

    ….

    } catch (CloneNotSupportedException e) {

    // This can’t happen, since we’re Cloneable

    throw new AssertionError();

    }

  3. 예외 안에 에러정보(failure-capture information)를 포함시켜라.진단을 허용하고 repair 하거나 recovery 하라.

    unchecked exception에 대해서는 메시지로 충분하다.

    checked exception에 대해서는 accessor 를 제공하라.

여섯째. Refactoring API Designs

  1. Sublist Operations in Vector

    public class Vector {

    public int indexOf(Object elem, int index);

    public int lastIndexOf(Object elem, int index);

    }

    매우 파워풀하지 않다. – 단지 검색만을 지원한다.

    문서없이 사용하기 힘들다.

    * Sublist Operations Refactored

    public interface List {

    List subList(int fromIndex, int toIndex);

    }

    매우 파워풀하다. 모든 동작을 지원한다.

    인터페이스 사용이 개념적 무게를 작게한다.

    - power-to-weight ratio 가 올라간다.

    문서 없이도 사용할 수 있다.

  2. Thread-Local Variables

    // Broken – inappropriate use of String as capability.

    // Keys constitute a shared global namespace.

    public class ThreadLocal {

    private ThreadLocal() { } // Non-instantiable

    // Sets current thread’s value for named variable.

    public static void set(String key, Object value);

    // Returns current thread’s value for named variable.

    public static Object get(String key);

    }

    * Thread-Local Variables Refactored (1)

    public class ThreadLocal {

    private ThreadLocal() { } // Noninstantiable

    public static class Key { Key() { } }

    // Generates a unique, unforgeable key

    public static Key getKey() { return new Key(); }

    public static void set(Key key, Object value);

    public static Object get(Key key);

    }

    동작하기는 한다. 그러나 사용하려면 boilerplate code 가 필요하다.

    static ThreadLocal.Key serialNumberKey = ThreadLocal.getKey();

    ThreadLocal.set(serialNumberKey, nextSerialNumber());

    System.out.println(ThreadLocal.get(serialNumberKey));

    * Thread-Local Variables Refactored (2)

    public class ThreadLocal {

    public ThreadLocal() { }

    public void set(Object value);

    public Object get();

    }

    API나 client code 로부터 잡동사니를 없애라.

    static ThreadLocal serialNumber = new ThreadLocal();

    serialNumber.set(nextSerialNumber());

    System.out.println(serialNumber.get());

일곱째. 결론

  1. API 디자인은 우아하면서도 돈을 벌 수 있는 행위이다.

    - 많은 프로그래머와 사용자들과 회사들을 이롭게 한다.

  2. 이 이야기는 어떤 의미에서 휴리스틱한 기법들을 덮어버린다.

    - 노예같이 휴리스틱한 기술들에 달라붙지 마라.

    - 충분한 이유없이 그들을 침범하지도 마라.

  3. API 디자인은 힘들다.

    - 혼자하는 작업이 아니다.

    - 완벽할 수 없다. 그러나 완벽해지려고 시도하라.

  4. 뻔뻔하게 스스로 Promotion 하라.

About the author

김수보 팀장/로컬플랫폼팀 플랫폼개발실 KTH

  • More at
  • logo image
  • logo image

Leave a Reply

Your email address will not be published. Required fields are marked *

*

HTML tags are not allowed.

Proudly powered by WordPress | Theme: Yoko by Elmastudio

Top