Android Code Style Guide

기본적으로 Google android 개발자 site에 있는 가이드(http://source.android.com/source/code-style.html) 를 대부분 준수하도록 하자. 다음의 규칙은 단순한 가이드지만 권장사항이므로, 특히 새로 작성 되는 코드에선 반드시 지켜주길 바랍니다.

1. Java Language Rules

기본적으로 java 규칙을 따르며, 안드로이드 팀에서 다음의 몇가지를 추가함.

1.1 Don't Ignore Exceptions (예외를 무시하지 말것)

이런식으로 예외를 무시하지 말자.

void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
        } catch (NumberFormatException e) {

        }
    }
}

NumberFormatException 'value'

절대로 예외를 무시하지 말자.
예외를 처리하는게 중요하지 않다고 생각할 수도 있고 이런 에러가 절대 일어나지 않을 것이라고 장담할 수도 있습니다.
하지만 위와 같이 예외를 무시해버리면 코드에 지뢰를 심어놓는 것과 같습니다.
그리고 누군가는 언젠가 이 지뢰를 밟게되며 반드시 코드 상의 모든 예외에 대해서 원칙대로 처리를 하길 권장합니다.

예외를 caller에게 바로 던지거나.

void setServerPort(String value) throws NumberFormatException {
    serverPort = Integer.parseInt(value);
}

caller 수준에 맞게 추상화한 예외를 새로 생성하여 던진다.

void setServerPort(String value) throws ConfigurationException {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) {
        throw new ConfigurationException("Port " + value + " is not valid.");
    }   
}

에러를 정중히 처리하고, 적절한 값 혹은 루틴을 catch 문에 대신 넣도록한다.

/** Set port. If value is not a valid number, 80 is substituted. */
void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) {
        serverPort = 80; // default port for server
    }
}

예외 처리를 하면서 RuntimeException을 발생시킨다.
(이는 위험한데, 만약 이 에러가 발생할 때 동작을 멈추는게 적절한 처리라고 생각된다면 적용하도록 한다.)

/** Set port. If value is not a valid number, die. */
void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) {
        throw new RuntimeException("port " + value " is invalid, ", e);
    }
}

원래 예외(위 코드에선 NumberFormatException)의 값이 RuntimeException의 생성자로 넘어가는 것을 기억하자. 만약 코드를 자바 1.3 이하 버전에서 컴파일한다면, 발생했던 예외를 생략할 필요가 있다.

모든 규칙에는 다 예외사항이 있다. 만약 정말 예외처리를 무시해도 된다고 자신있게 말할 수 있으면, 무시해도 된다. 단, 타당한 이유를 적은 주석을 남기는 것이 좋다.

/** If value is not a valid number, original port number is used. */
void setServerPort(String value) {
    try {
        serverPort = Integer.parseInt(value);
    } catch (NumberFormatException e) {
        // Method is documented to just ignore invalid user input.
        // serverPort will just be unchanged.
    }
}

1.2 Don't Catch Generic Exception (예외를 일반화 형태로 처리하지 말것)

가끔씩은 귀찮아서 아래와 같이 예외처리를 할 때도 있다.

try {
    someComplicatedIOFunction(); // may throw IOException
    someComplicatedParsingFunction(); // may throw ParsingException
    someComplicatedSecurityFunction(); // may throw SecurityException
    // phew, made it all the way
} catch (Exception e) {
    // I'll just catch all exceptions
    handleError(); // with one generic handler!
}

someIOFunction() , someParsingFunction, someSecurityFunction Exception.

이렇게 하지말자. 이러한 거의 모든 상황에서 한방에 예외를 처리하거나 Throw 처리하는 것은 부적절하다. 오히려 Throw하지 않는 것이 낫다. 왜냐하면 일반적인 예외 뿐만 아니라 오류도 포함될 수 있기 때문이다. 이 경우 굉장히 위험하다.

즉, 이는 ClassCastException과 같은 RuntimeException과 같이 전혀 예상치못한 예외가 발생해서 어플리케이션 레벨의 오류처리 범위를 넘어서서 결국 어플리케이션이 종료될 수 있는 결과가 나타난다.

또 이런 경우가 있을 수 있다. 만약 어떤 사람이 IOException만을 Throw 하는 someIOFunction()에서 NullPointerException도 Throw하도록 예외를 추가했다고 새로운 종류의 예외를 여러분이 호출하는 코드에 추가하고자 할때, 컴파일러는 프로그래머에게 새로운 종류의 예외를 다르게 처리해야 한다는 경고를 주지않는다.

아래 코드를 보자. Context.openFileOutput은 FileNotFoundException을, write는 IOException을 발생시킨다. 이를 Exception으로 묶어서 처리하지 말고 아래와 같이 따로 분리해서 처리해야 한다.

try {
    fos = openFileOutput("Filename.txt",MODE_PRIVATE);
    fos.write(strFileContents.getBytes());
    fos.close();
} catch (FileNotFoundException e) {
    // do nothing
} catch (IOException e) {
    Toast.makeText(this, "", 500).show();
    return;
}

물론 예외사항도 있다. 이런 경우는 거의 없지만, 어떤 테스트코드나 UI에 에러메시지가 나타나는 것을 막거나 일괄처리(batch job)을 처리해야하는 최상위 레벨의 코드에서 사용한다.
이 경우 일반적인 예외를 처리하나 Throw를 사용해서 에러를 적절히 제어한다. 물론, 이렇게 사용하기 전에 아주 신중히 생각해야 하고, 사용할때는 반드시 주석으로 내가 왜 이를 이렇게 사용했는지, 여기에 사용하는게 안전한지에 대한 설명도 적어야 한다.

대안

  • 하나의 try문 뒤에 각각의 예외에 대해서 처리하는 여러개의 catch 블럭으로 예외처리를 해야한다. 물론 귀찮고, 귀찮고, 또 귀찮지만 모든 예외를 처리하는데 있어서 더 좋은 방법이다. 그렇다고 catch 블럭에 너무 많은 코드를 반복하지는 말기를.
  • 예외를 throw 해라. 현재 레벨에서 모든 예외를 처리 할 필요는 없다.
  • 여러개의 try 블럭을 사용해서 코드를 좀 더 에러 처리를 잘 할 수 있도록 리팩토링하라.

1.3 Don't Use Finalizers (Finalize를 사용하지 말것)

VM(Virtual Machine, 가상머신)은 다르지만 자바 문법에 충실한 안드로이드 프레임워크에도 finalize() 메소드가 있다. System.gc(), System.runFinalization(), System.runFinalizersOnExit() 를 호출하면 VM의 가비지 컬렉터가 가비지 컬렉션을 수행한다(물론 객체의 참조가 없다고 확인된 경우). 이때, finalize()는 객체가 소멸되기 직전에 호출되며, 원칙상으로는 소멸되기 직전의 일련의 작업을 수행하고자 할때 finalize()에 코드를 작성하면 된다. 하지만 finalize() 메소드는 자바에서도 오버라이딩을 권장하지 않는 메소드이고, 안드로이드에서도 역시 마찬가지다.

안드로이드팀은 finalize()의 장점과 단점을 아래와 같이 명시하였다.

장점 : 메모리 정리할때 편리하다. 특히 외부 리소스들을 정리할때 더욱 더 그렇다.
단점 : 언제 finalize가 호출될지 보장할 수 없으며 호출될지도 장담할 수 없다.

자바VM처럼 안드로이드의 Dalvik VM 역시, System.gc()를 호출한다고 해서 언제 호출될지 보장할수 없고, 호출될지 말지도 장담을 못한다.
게다가 가비지 컬렉터는 메인쓰레드와는 별개의 쓰레드로 처리되기 때문에 동기화 문제도 발생할 수 있다.

결론은 안드로이드 팀은 finalize를 사용하지 않는다고 정해놓았다.
대부분의 경우, finalizer로부터 필요한 것들은 좋은 예외처리로 할 수 있다. 만약 절대적으로 finalize가 필요하다면, close()메소드나 그비슷한걸 정의하고 그 메소드가 호출되야한다고 정확하게 문서화를 해놓도록 하자.
InputStream같은 파일 I/O, DB 커넥션이 그 대표적인 예가 되겠다.

1.4 Import문은 전체를 다 표시할 것

2. Java Style Rules

2.1 Use Javadoc Standard Comments

copyright는 문서 상단에 기술하고 그 아래 package, import, class 또는 인터페이스 순으로 기술하며
각 블럭은 빈줄로 구분 그 아래 클래스나 인터페이스는 어떤 기능을 하는지 javadoc comment로 기술한다.

/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.foo;

import android.os.Blah;
import android.view.Yada;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
* Does X and Y and provides an abstraction for Z.
*/

public class Foo { ... }

모든 class와 public methods (단순하고 명시적인 것 빼고)는 반드시 한문장 이상의 javadoc comment를 기술한다. 이 문장은 제 3자의 작업을 위해서 기술한다.

Examples:

/** Returns the correctly rounded positive square root of a double value. */
static double sqrt(double a) {
    ...
}

or

/**
* Constructs a new String by converting the specified array of
* bytes using the platform's default character encoding.
*/
public String(byte[] bytes) {
    ...
}

setFoo Javadoc comment. javadoc comment.

API에 속해 있는 public 메소드의 경우에 Javadoc 이 요구된다.
안드로이드팀의 javadoc 스타일 가이드는 따로 없고 요기를 보시라. Sun Javadoc conventions

2.2 Write Short Methods

메소드는 40줄이 넘지않게 짧게 작성하기. 40줄이 넘어간다면 설계가 잘못된 것일 수 있음.

2.3 Define Fields in Standard Places

Fields 선언은 표준위치에 즉, 초기에 선언하기나 사용하기 바로 전에 선언할 것.

2.4 Limit Variable Scope

지역변수의 (사용)범위를 최소화 해라. 읽기도 좋고, 관리도 편해진다.
지역 변수는 브레이스 블럭 내부에서 사용해라. (예외의 경우 try~catch)

// Instantiate class cl, which represents some sort of Set
Set s = null;
try {
    s = (Set) cl.newInstance();
} catch(IllegalAccessException e) {
    throw new IllegalArgumentException(cl + " not accessible");
} catch(InstantiationException e) {
    throw new IllegalArgumentException(cl + " not instantiable");
}

// Exercise the set
s.addAll(Arrays.asList(args));

Method

Set createSet(Class cl) {
    // Instantiate class cl, which represents some sort of Set
    try {
        return (Set) cl.newInstance();
    } catch(IllegalAccessException e) {
        throw new IllegalArgumentException(cl + " not accessible");
    } catch(InstantiationException e) {
        throw new IllegalArgumentException(cl + " not instantiable");
    }
}

...

// Exercise the set
Set s = createSet(cl);
s.addAll(Arrays.asList(args));

Loop for

for (int i = 0; i < n; i++) {
    doSomething(i);
}

그리고,

for (Iterator i = c.iterator(); i.hasNext(); ) {
    doSomethingElse(i.next());
}

2.5 Follow Field Naming Conventions

Non-public, non-static field 는 m으로 시작
Static field 는 s로 시작
나머지는 소문자로 시작.
Public static final fields (상수) 는 모두 대문자로 ALL_CAPS_WITH_UNDERSCORES.

예:

public class MyClass {
    public static final int SOME_CONSTANT = 42;
    public int publicField;
    private static MyClass sSingleton;
    int mPackagePrivate;
    private int mPrivate;
    protected int mProtected;
}

2.6 표준 {} Style 사용

시작 중괄호는 "{" 따로 한줄을 차지하지 않는다.

class MyClass {
    int func() {
        if (something) {
        // ...
        } else if (somethingElse) {
        // ...
        } else {
        // ...
        }
    }
}

:

if (condition) body();

:

if (condition)
    body(); // bad!

2.7 표준 Java Annotations를 사용할 것.

2.8 축약어는 단어처럼 취급할 것.

Good Bad
XmlHttpRequest XMLHTTPRequest
getCustomerId getCustomerID
class Html class HTML
String url String URL
long id long ID

JDK나 Android 기초 코드에서 이 규칙을 어긴 사례가 있지만 너무 깊이 신경 쓰지는 말자.

2.9 TODO 주석문 또는 FIXME 주석문을 사용 할 것.

단기처방이나, 완벽하지 않은 해결책인 경우에는 반드시 TODO or FIXME주석문을 기입할 것.

// TODO: UrlTable2

and

// FIXME: flag

2.10 로깅(Log)은 간결하게.

로깅은 성능에 지대한 영향을 준다. 로깅하기 전에 먼저, 로그 레벨을 이해하자.

  • ERROR: 항상 기록되는 레벨임 . 따라서 조심스럽게 사용해야 한다. 먼가 치명적인 상황을 기록한다. 예를 들어 어떤 데이터를 직접 삭제하거나, 앱을 지우거나, 폰을 싹 밀어버리거나 하기 전에는 복구가 안되는 상황을 사용자가 직면해야 할때만 기록하고, (가능하면)통계수집 서버에 통보한다.
  • WARNING: 항상 기록되는 레벨임. 심각한 상황을 기록한다. 예를 들어 사용자가 인식할 수 있는 오류결과가 나타나고, 사용하던 데이터(다운로드 중인 데이터 등등)가 날아가버리고, 사용자가 앱 혹은 폰을 리부팅하거나, 화면을 죽이거나, 새로운 버전의 앱을 다운 받기 전에는 복구가 안되는 상황. 0 INFORMATIVE: 역시 항상 기록되는 레벨임. 오류는 아니더라고, 뭔가 모두가 알았으면 하는 대단한 상황일 경우 기록한다.
    예를 들어 오류는 아니지만, 동접자 수가 100만명이 넘어갔다거나, 악성코드로 의심되는 입력값이 감지되었다거나 등등 모두가 알면 좋을 만한 사건이 발생한 상황
  • DEBUG: 디버그 모드에서만 기록되는 레벨임. 디버깅 과정과 관련한 추가 정보를 기록할 때 사용한다. 만약 로깅 자체보다는 개발자 본인의 관심사와 관련한 정보라면 verbose level로 로깅해라.
    배포 버전에도 로깅이 가능하며, 로깅과 관련한 소스가 컴파일 되기 때문에 성능을 고려하여 로깅 소스는 반드시 다음과 같이 if (LOCAL_LOG) or if (LOCAL_LOGD) 구문내에 구현해야 한다. 혹시나, 다른 대안으로 리펙토링을 통해 logging 기능을 밖으로 빼는 방법을 생각해보기도 하는데 (re-factoring out), 이는 좋은 생각이 아니다. 왜냐하면, String 파라미터가 전달되면서 불필요한 String 생성이 발생하기 때문임.
  • VERBOSE: 뭐든 로깅해도 되는 level 이지만, debug level 처럼 반드시 if구문내에 구현해야 함. 아니면 배포전에 아예 없애버리던가.

기억할 것:

  • (VERBOSE level 이 아닐경우) 하나의 모듈 내에 있는 하나의 function chain 내에서 오류는 가능한 한번만 보고 되어야 한다.
    무슨 말인고 하니, 이를테면 method overloading 방식으로 methodA(param1) 가 methodA(param1, param2)를 호출하면 error는 methodA(param1, param2) 에서만 기록하란 말이얏. 그래야 issue를 격리시키는 게 쉬워진다.
  • (역시 VERBOSE level 이 아닐경우) 연쇄적으로 작동하는 모듈간에 만약 상위 모듈에서 전달된 data에 문제가 있다고 판단될 경우 로깅을 하려면 debug모드로 해야 한다. 굳이 information 이상 레벨의 로깅이 필요하다면, 상위 레벨 모듈에서 처리해야한다. 특히 lib 나 프레임워크개발시에 이런 규칙이 중요함. lib. 나 framework를 갖다 썼더니 막 지멋대로 로깅을 남기고 그럼 곤란하다.
  • 반복되거나 동일한 작업이 예상되는 것은 일정한 조건을 걸어서 로깅해라. 이를 테면, onTouchEvent 내에 로깅할때
  • 네트웍 단절 상태등은 충분히 예상되는 내용이다. debug verbose 이상의 로깅을 하지 말것.
  • 다른 앱에서 접근가능한 파일시스템 용량 초과 같은 이슈는 INFORMATIVE 이하 레벨로 검증 되지 않은 소스로부터 들어오는 잘못된 데이터 입력등의 이슈는 DEBUG 이하로 작성할 것.
  • Log.v() 는 배포버전에서도 컴파일 된다. 레벨에 따라 로깅이 안될 뿐이지. 따라서 반드시 if 구문안에서 String을 처리해야함.
  • 모든 로깅은 간결하고 누구나 쉽게 이해할 수 있어야 한다. 작성자만 알아볼 수 있게 하지 말 것. (DEBUG level 이상의 로깅이라면 말이지)
  • 가능한 한줄로 표현되게끔 해주세요. 너무 길지도 않게.
  • 작업 성공 여부 같은 것은 VERBOSE 외에선 사용하지 말 것.
  • 검사나 분석을 위한 임시 로깅은 반드시 DEBUG or VERBOSE level 에서만 작성할 것.
  • 로깅으로 개인정보 등 보안 정보가 유출되지 않도록 각별히 신경 쓸것.
  • System.out.println() (or printf() for native code) 는 절대 사용하지 말것. System.out 와 System.err 는 /dev/null 로 연결되므로 아무 기능도 안하게 되는데 괜히 StringBuilder 만 잡아 먹게 된다.
  • 로그가 로그를 만들게 하지 말자.

2.11 일관성을 준수하자

로마에가면 로마법을 따르자, 기존 코드나 레거시 규칙이 있고 거기에 추가로 개발을 하게 된다면 기존 룰을 준수하여 일관성을 지키자.
표준 개발 양식(style guidelines)를 만드는 이유는 공통의 어휘로 의사 소통하는데에 있다. 로깅에서 중요한 것은 이야기 하는 방식이 아니라, 전달하고자 하는 내용이다.

2.12 Activity 개발 시 주의 사항

context를 별도 멤버로 선언하고 관리하지 않도록 한다.

잘못된 사례

public class MyActivity extends Activity {
private Context mContext = null;
...
...
protected void onCreated(Bundle savedInstanceState){
    mContext = this;
}

context가 필요하면 getContext() 를 사용하라.

3. JavaTests Style Rules

3.1 Follow Test Method Naming Conventions

Test Method 의 이름을 설계할 때, test case를 "_" 뒤에 붙여줄 수 있다.

예:

testMethod_specificCase1 testMethod_specificCase2

void testIsDistinguishable_protanopia() {
    ColorMatcher colorMatcher = new ColorMatcher(PROTANOPIA)
    assertFalse(colorMatcher.isDistinguishable(Color.RED, Color.BLACK))
    assertTrue(colorMatcher.isDistinguishable(Color.X, Color.Y))
}