죠수아 블로치는 구글의 소프트웨어 아키텍쳐로 유명한 사람입니다.
아래 내용은 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페이지는 이상이다.이 단계에서는 Agility 가 완벽함을 이긴다.
가능한 많은 사람들로부터 반응을 살펴라.
- 그들의 input 에 대해 귀기울여 듣고, 심각하게 받아들여라.
스펙을 작게 할수록, 고치기 쉽다.
자신감을 얻을 때까지 살을 붙여라.
- 이것은 필연적으로 코딩을 포함한다.
- 미리 자주 API 에 글을 적어라.(그냥 function 내에 필요할 기능을 스크립트 형식으로 적어넣는다.)API를 구현하기 전에 시작하라.
- 구현하는 시간을 절약해준다.가능한 스펙을 잡기 이전부터 시작하라.
- 스펙을 잡는 시간을 절약해준다.
살이 붙을 때까지 API 에 글을 적어라.
- 끔찍한 실패를 예방해준다.
- 코드는 예제와 unit test 를 먹고 산다.
- SPI 에 글을 적는 것은 더 중요하다.Service Provider Interface (SPI)
- 플러그인 방식의 인터페이스는 여러 개의 구현을 가능하게 한다.
- 예 : Java Ctyptography Extension (JCE)배포하기 전에 multiple plug-in 들을 만들어라. (윌 트랙은 “세개의 법칙”이라고 부른다.)
- 만일 플러그인 하나를 만들면, 아마도 API가 다른 것은 지원하지 못하게 될 것이다.
- 두 개를 만든다면, 어렵게라도 여러개를 지원하기는 할 것이다.
- 세 개를 만든다면, 그것은 아주 잘 작동할 것이다.
- 현실적 기대수준을 받아들여라.대부분의 API 설계는 지나치게 제약되어 있다.
- 당신은 모든 사람을 만족시켜줄 수 없다.
- 모든 사람들을 똑같이 불만족하게 만드는 것에 초점을 맞추어라.실수하기를 기대하라
- 몇 년 간 실사용이 되면, 실수따위는 잊어버리게 된다.
- 실수를 통해 API를 진화시켜라.
둘째. General Principles (일반 원칙)
- API는 하나의 일만 해야 하고, 그것을 아주 잘 해야 한다.기능이 설명하기 쉬워야 한다.
- 이름짓기 힘들다면, 통상적으로 잘못 만들어진 것이다.
- 좋은 이름이 자연스럽게 개발로 이어지게 만든다.
- 모듈을 쪼개거나 합칠 수 있어야 한다.
- API는 가능한한 작게 만들어야 하지만, 더 작아져서는 안된다.API는 요구사항을 만족시켜야 한다.
의심이 들면 그대로 두어라.
- 기능, 클래스, 메쏘드, 파라미터까지.
- 뭔가를 더할 수는 있지만, 결코 제거할 수는 없다. (배포되면 회수불가)개념적인 무게가 규모보다 더 중요하다.
(API의) 힘과 무게(power-to-weight ratio) 간의 균형비를 찾아라.
- Implementation 이 API에 영향을 주어서는 안된다.구현이 너무 세세해지면
- 사용자를 혼란스럽게 한다.
- implemenation 을 바꿀 자유를 방해한다.세세한 구현이 무엇인지를 정확히 알아라.
- method behavior 스펙을 너무 과하게 잡지 마라.
- 예를 들면 : hash function 들은 스펙화하지 마라.
- 모든 튜닝 파라미터들은 의심해야 한다. (이게 많다고 좋은 API가 아니라는 뜻)
세세한 구현이 API 로 흘러들어가게 해서는 안된다.
- 디스크 상이나, 네트워크 상이나, 예외로라도 !!!
- 모든 것의 접근을 최소화하라.가능한 private 하게 class 와 member를 만들어라.
public class 가 (상수에 대한 예외와 함께) public field를 가져서는 안된다.이렇게 해서 정보은폐를 최대화하라. (밖으로 드러나지 않게 Capsulation 하라는 뜻.)
모듈이 독립적으로 debug 되고, 이해되어지고, 구축되어지고, 테스트 되어지도록 하라.
- 이름이 중요하다. – API 는 작은 언어다.이름은 굳이 설명하지 않아도 이해될 수 있어야 한다.
- 기호나 축약을 사용하지 마라.Consistent 하게 하라 – 똑같은 단어는 같은 것이어야 한다.
- API 전반에 걸쳐서 (API 플랫폼 전반에 걸쳐서!!!)
규칙적이어야 한다. – 대칭과 균형을 갈구하라.
코드는 산문처럼 읽힐 수 있어야 한다. (얼마나 읽기 쉬운가?)
if (car.speed() > 2 * SPEED_LIMIT)
generateAlert(“Watch out for cops!”);
- 문서화가 중요하다.
재사용성은 “재사용한다”는 Action 보다 “재사용하겠다.”고 쉽게 말할 수 있는 어떤 것이어야 한다. Action 은 좋은 디자인과 좋은 문서를 필요로 한다. 비록 아주 가끔 우리가 좋은 디자인(설계, 아키텍쳐)을 보게 되었다고 하더라도, 우리가 문서 없이 재사용되어질 정도로 좋은 컴포넌트를 볼 수는 없다. – D.L.Parnas, Software Aging, 1994
- 종교처럼 문서화하라.”모든” class, interface, method, constructor, parameter, and exception 을 문서화하라.
- class : instance 화 되는 것들
- method : method 와 그 client 들간의 계약들이다.
- parameter : units, form, ownership 등을 지칭한다.state-space(상태공간, 전체적인 것에 대해)을 주의깊게 문서화하라.
- API 디자인에 대한 성능결과에 대해 고려하라.
나쁜 의사결정은 성능 한계를 만들기도 한다.
- type 을 상호 교환가능하게 만들어라.
- static factory 대신 constructor 를 제공하라.
- interface 대신에 implementation type 을 사용하라.성능을 얻기 위해 API를 뒤틀지 마라. (변형하지 마라)
- 기본적인 성능 이슈는 고쳐질 것이다. 하지만 두통이 그대와 함께 영원할 것이다.
- 좋은 디자인은 일반적으로 좋은 성능과 일치한다.
- 성능을 고려한 API 설계는 실질적인 효과로 나타날 뿐 아니라 영원하다.
- Component.getSize()는 Dimension을 return 한다.
- Dimension 은 mutual 하다.
- 모든 getSize call 은 Dimension을 할당해야만 한다.
- 수많은 불필요한 object allocation 이 발생한다.
- 대체안이 1.2 버전에 추가된다 : old client code 는 여전히 느리다.
- API 는 플랫폼과 평화적으로 공존해야만 한다.관습적인 것을 따라라.
- 표준 naming rule 을 따라라.
- 독자적인 파라미터나 Return Type을 쓰지마라.
- 코어 API나 언어에 있는 패턴을 흉내내어 써라.API 친화적인 특징을 이용하라.
- Generics, varargs, enums, default arguments
API 의 위험과 단점들을 잘 알고 회피하라.
- Finalizers, public static final arrays
셋째. Class Design
- 변경을 최소화하라.만일 다른 일을 해야 할 충분한 이유가 없다면, class 는 불변의 것이어야 한다.
- 장점 : simple, thread-safe, reusable
- 단점 : 각 value 별로 분리된 object
만일 변경해야 한다면, 상태공간(state-space)를 작게 유지하고, 정의를 잘 유지하라.
- 언제, 어떤 method를 부르는게 합리적인지를 명확히 하라.
Bad: Date, Calendar
Good: TimerTask
- subclass는 substitutability (대체성)를 의미한다.
- (..은..이다)라는 관계가 존재할 때만 subclass 를 써라.
- 그렇지 않으면, composition 을 사용하라.Bad: Properties extends Hashtable, Stack extends Vector
Good: Set extends Collection
- 상속을 위해 설계하고 문서를 남겨라.
설계가 그렇게 되어 있지 않고 문서가 없다면, 상속을 못하게 만들어라.상속이란 캡슐화와 상충한다.
- subclass 는 superclass 의 세세한 구현에 민감하다.
만일 subclass 를 허락한다면, self-use를 문서화하라.
- method가 어떻게 다른 method 를 사용하는지?
보수적인 정책 : 모든 구체적인 class 는 final 이다.
Bad: Many concrete classes in J2SE libraries
Good: AbstractSet, AbstractMap
넷째. Method Design
- 모듈이 할 수 있는 것을 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!
}
}
- “사용자를 놀라게 하지 않기” 원칙을 준수하라.
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();
}
- 빨리 실패하라 – 장애가 발생되면 가능한한 빨리 에러를 알려라.컴파일 시간이 제일 좋다. – 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);
}
- 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();
}
- 주의를 가지고 오버로딩 하라.모호한 오버로드는 피하라.
- 여러 개의 오버로딩이 동시에 적용될 수 있다.
- 보수적이 되어라 : 동일한 argument 숫자가 없도록 하라.당신이 모호한 오버로딩을 제공해야 한다면, 동일한 arguments에 대해서는 동일한 behavior 가 일어나게 하라.
public TreeSet(Collection c); // Ignores order
public TreeSet(SortedSet s); // Respects order
- 적절한 파라미터와 리턴타입을 사용하라.Input 을 위해 class 전반에 interface type 을 장려하라.
- 유연성과 성능을 제공하라.가장 구체적이면서 가능한 input parameter type 들을 사용하라.
- 런타임 시간으로부터 컴파일 시간까지 에러를 옮겨라.
더 좋은 type 이 있으면, string 을 사용하지 마라.
- string 은 무겁고, 에러가 나기 쉽고, 느리기까지 하다.
통화를 표현하는데 floating point 를 사용하지 마라
- binary floating point 는 부정확한 결과를 야기시킨다.
float(32 bits) 보다 double(64 bits)를 사용하라.
- 정확성 손실은 현실이고, 성능 손실은 무시할만 하다.
- 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
- 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);
- 예외처리를 요구하는 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
- 예외 상황을 표시하기 위해 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)
- 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();
}
- 예외 안에 에러정보(failure-capture information)를 포함시켜라.진단을 허용하고 repair 하거나 recovery 하라.
unchecked exception에 대해서는 메시지로 충분하다.
checked exception에 대해서는 accessor 를 제공하라.
여섯째. Refactoring API Designs
- 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 가 올라간다.
문서 없이도 사용할 수 있다.
- 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());
일곱째. 결론
- API 디자인은 우아하면서도 돈을 벌 수 있는 행위이다.
- 많은 프로그래머와 사용자들과 회사들을 이롭게 한다.
- 이 이야기는 어떤 의미에서 휴리스틱한 기법들을 덮어버린다.
- 노예같이 휴리스틱한 기술들에 달라붙지 마라.
- 충분한 이유없이 그들을 침범하지도 마라.
- API 디자인은 힘들다.
- 혼자하는 작업이 아니다.
- 완벽할 수 없다. 그러나 완벽해지려고 시도하라.
- 뻔뻔하게 스스로 Promotion 하라.
최근 답글