<< Previous | Home

[번역] 객체지향 프로그래밍으로 유틸리티 클래스를 대체하자.

유틸리티 클래스를 안써본 사람은 거의 없을겁니다. 유틸리티 클래스가 좋다, 나쁘다는 논쟁꺼리지만, 객체지향의 관점에서 볼때 그래도 생각해볼 꺼리가 된다고 생각해, OOP Alternative to Utility Classes"라는 아티클을 저자 허락하에 번역해 봅니다.

유틸리티 클래스(또는 헬퍼 클래스)는 정적 메소드만을 가지고 있고, 상태를 내포하지 않는 "구조"이다. Apache Commons의 StringUtils, IOUtils, FileUtils과 Guava의 Iterables, Iterators, 그리고 JDK7의 Files 등이 유틸리티 클래스가 좋은 예다.

유틸리티 클래스는 많은 곳에서 사용되는 공통 기능을 제공하기 때문에, 이런 설계 방법은 Java(또는 C#, Ruby 등) 세계에서는 매우 인기있다.

여기엔, DRY 원칙을 따르고 중복을 피하는 것을 원한다. 그래서 유틸리티 클래스에 공통 코드를 넣고, 필요에 따라 재사용한다.
// This is a terrible design, don't reuse
public class NumberUtils {
  public static int max(int a, int b) {
    return a > b ? a : b;
  }
}
정말, 이것이 편리한 기술인가?

유틸리티 클래스는 악이다

그러나, 객체 지향의 세계에서 유틸리티 클래스는 아주 나쁜(심하게 나쁘다고 생각하는 사람도 있을지도 모른다) 방법이다.

이 주제에 대해서는 많은 논란이 있다. 일부 들면, Nick Malik의 헬퍼 클래스는 악인가?", Simon Hart의 왜 헬퍼클래스, 싱글톤, 유틸리티 클래스는 대체로 나쁘낙?", Marshal Ward의 유틸리티 클래스를 피하기", Dhaval Dalal의 유틸 클래스를 죽여라!", Rob Bagby의 "헬퍼 클래스는 문제의 징후다."

게다가, StackExchange에는 유틸리티 클래스에 대한 질문이 몇가지 있다. 예를 들어, 유틸리티 클래스가 악이라면 공통 코드를 어디에 두어야 하나?", 유틸리티 클래스는 악이다" 등이다.

이러한 논쟁을 요약해보면, 유틸리티 클래스는 적절한 객체가 아니라는 것이다. 그래서 객체 지향의 세계에선 적합하지 않다. 유틸리티 클래스는 당시 사람들이 기능 분할 패러다임에 익숙해져 있었기 때문에 절차적 언어에서 계승되었다.

여러분이 이 주장에 동의하고 유틸리티 클래스를 사용하는 것을 중지하고 싶어한다것을 가정하고 유틸리티 클래스를 어떻게 적절한 객체로 대체하는지를 예를 들면서 보여주겠다.

절차적 프로그램의 예

예를 들어, 텍스트 파일을 읽고, 행단위로 분할하고, 각 라인을 손질(공백제거 등)하고, 그 결과를 다른 파일에 저장하고 싶다고 한다. 이것은 Apache Commons의 FileUtils과 함께 구현되어 있다.
void transform(File in, File out) {
  Collection src = FileUtils.readLines(in, "UTF-8");
  Collection dest = new ArrayList<>(src.size());
  for (String line : src) {
    dest.add(line.trim());
  }
  FileUtils.writeLines(out, dest, "UTF-8");
}
위의 코드는 예뻐 보인다. 그러나, 이것은 절차적 프로그래밍이며, 객체 지향이 아니다. 코드의 각 라인에서 데이터(byte와 bit)를 조작하고 컴퓨터의 어디에서 데이터를 가지고, 어디에 쓸 것인지를 명시적으로 지시하고 있다. 즉, 실행 절차를 정의하고 있다.

객체 지향적 대안

객체 지향 패러다임에서는 객체를 인스턴스화하여 합성해야(컴포즈) 한다. 이것은 객체가 언제, 어떻게 객체 자신이 원하는 방식으로 데이터를 관리해야하기 때문이다. 추가적인 정적 메소드를 호출하는 대신, 요구하는 행동을 제공할 수 있는 객체를 생성해야 한다.
public class Max implements Number {
  private final int a;
  private final int b;
  public Max(int x, int y) {
    this.a = x;
    this.b = y;
  }
  @Override
  public int intValue() {
    return this.a > this.b ? this.a : this.b;
  }
}
다음은 절차적 메소드 호출:
int max = NumberUtils.max(10, 5);
다음은 객체지향적인 방법이 된다.
int max = new Max(10, 5).intValue();
둘 다 같은가? 아님 그렇지도 않은가? 좀 더 읽어 주었으면 한다.

데이터 구조 대신 객체

저라면 위와 같은 파일 변환 기능을 객체 지향 방식으로 다음과 같이 설계한다.
void transform(File in, File out) {
  Collection src = new Trimmed(
    new FileLines(new UnicodeFile(in))
  );
  Collection dest = new FileLines(
    new UnicodeFile(out)
  );
  dest.addAll(src);
}
FileLines는 Collection을 구현하고, 파일의 읽기및 쓰기 함수를 내포하고 있다. FileLines 인스턴스는 문자열의 컬렉션으로 정확하게 작동하고 모든 I/O 처리를 은폐하고 있다. 이 인스턴스를 반복하면 파일이 읽혀진다. 이 인스턴스에 addAll()하면 파일에 기록된다.

Trimmed도 Collection을 구현하고, 문자열 컬렉션을 내포하고 있다(Decorator 패턴). 한행이 검색될 때마다 트림된다.

Trimmed이나, FileLines, UnicodeFile은 파일 변환에 기능에 참여하는 모든 클래스는 작지만, 각각 자신의 하나의 기능을 담당하는, 즉 단일 책임 원칙에 완벽하게 따르고 있다.

우리 측, 즉 라이브러리의 사용자에서 보면 이것은 그렇게 중요하지 않을지도 모르지만, 라이브러리 개발자에서 보면 중요하다. 80개 이상의 메소드를 가진 3000라인의 유틸리티 클래스인 FileUtils의 readLines()보다 FileLines의 클래스가 개발과 유지 보수, 단위 테스트가 더 쉽다. 심각하게, 그 소스 코드를 봐라.

객체 지향 접근 방식은 지연 실행을 가능하게 한다. in 파일은 데이터가 필요할 때까지 읽지 않는다. I/O 오류로 out을 여는데 실패했다면 파일은 터치조차 되지 않는다. 모든 것은 addAll()을를 호출한 다음에 시작된다.

두번째 조각의 마지막 줄을 제외한 모든 라인은 작은 객체를 인스턴스화하고 큰 객체를 합성하고 있다. 이 객체 합성은 데이터 변환을 일으키지 않기 때문에 CPU 비용은 오히려 낮다.

또한 첫번째 스크립트가 O(n)으로 움직이는 반면, 두번째 스크립트는 분명히 O(1)의 계산량으로 움직인다. 이런 이유는 첫번째 스크립트에서는 데이터에 대한 절차적 접근을 했기 때문이다.

객체 지향의 세계에서는 데이터라는 것은 없다. 객체와 그 행위만이 있다!

넋두리

오늘은 블로그 글을 쓰고 싶어서 그동안 메모해 두었던것들, 생각나는 것들, 책 읽은 것들을 정리, 인용해서 채워나갑니다. ^^

Leadership and Management -- The Two Creations(리더십과 관리능력, 두 개의 창의성)

회사를 경영하는 것은 참 쉽지 않네요. 오늘은 초심으로 돌아가보고자 The 7 Habits of Highly Effective People"라는 책을 들었는데, 그중에서 예전에는 몰라는데 지금 보니 기록해보고 싶은 글귀가 있어 메모를 해 봅니다.

You can quickly grasp the important difference between the two if you envision a group of producers cutting their way through the jungle with machetes. They're the producers, the problem solvers. They're cutting through the undergrowth, clearing it out.
The managers are behind them, sharpening their machetes, writing policy and procedure manuals, holding muscle development programs, bringing in improved technologies, and setting up working schedules and compensation programs for machete wielders.
The leader is the one who climbs the tallest tree, surveys the entire situation, and yells, "Wrong jungle!"
But how do the busy, efficient producers and managers often respond? "Shut up! We're making progress."
As individuals, groups, and businesses, we're often so busy cutting through the undergrowth we don't even realize we're in the wrong jungle. And the rapidly changing environment in which we live makes effective leadership more critical than it has ever been -- in every aspect of independent and interdependent life.

정글 속에서 도끼로 길을 개척하는 작업팀을 생각해 보면, 리더십과 관리능력의 차이를 곧 알 것이다. 작업 팀은 생산에 종사하고 현장에서 문제를 해결하는 사람들이다. 그들은 실제로 덤불을 베어 길을 개척해 나간다. 관리의 역할은 후방에 있으며, 도끼의 날을 예리하게 갈고, 정책및 절차 매뉴얼을 만들고, 근육 강화 훈련을 개발하고, 새로운 기술을 도입하며, 작업 일정 및 보상 체계를 만든다. 리더의 역할은 정글에서 가장 높은 나무에 올라가, 전체를 둘러보고, "이 정글이 아니잖아!"라고 외친다.

하지만, 현실은 바쁘면서도 효율적인 우리 작업자와 관리자는 종종 어떻게 반응합니까? "닥쳐! 우리 지금 만들고 있잖으냐? 개인, 그룹, 기업으로서, 우리는 정글속 덤불을 절단하는데 너무 바쁜 나머지 우리가 잘못된 정글에서 작업하고 있다는 것은 인지하지 못하고 있다. 지금 우리는 아주 급변하는 환경속에 살고 있어서, 무엇보다도 지금까지 해왔던것보다 더 효과적인 리더십이 중요하다.

"The 7 Habits of Highly Effective People" 중에서.

개발 문화에 대해

요즘 Github에 스타트업에서도 필요한 개발 문화를 만들기 위해 우선 필요한 것들을 이것 저것 적고 있는데 문화를 만들기 또한, 쉽지 않네요.

요즘, 스트트업들은 Cloud, Slack, GitHub, Intellij, Mac등과 같은 도구들을 잘 지원해 주고 또, 일반적으로 잘 다루고 있기도 합니다만. 그런다고 개발이 잘 진행되지는 않는것 같습니다. 뭔가 도구를 도입하면 문제가 해결한다는 것은 착각이고, 정작 중요한 것은 개발 조직의 구조와 관리, 그리고 리더십 등 올바르게 움직이게 하는 것인데, 이런 것이 기업 문화가 아닌가 생각됩니다.

바라봐야할 곳

문제 해결을 위해서, 문제보다, 문제 해결 노력보다 먼저 방법을 생각해 버리는 경우가 많아지고 있습니다. 일 예로, 서비스를 하나 구상하고 있는데, 제가 가지고 있는 기술중에서 방법들을 엮어보려는 시도를 하는 모습을 자주 봅니다. 도구와 방법에 이끌려 창의력을 옥죄고 있고 자신이 가지고 있는 방법이나 기술이 만능이라고 암묵적으로 생각해서 그런걸까요?

또, 소프트웨어는 목적을 달성하기 위한 도구이기는 하지만, 소프트웨어가 목적 자체를 바꿀 수는 없을까요? 같은 생각을 해야할 것 같아요. 그리고 목적 달성에 굳이 소프트웨어가 아닐 수도 있다는 생각도 중요한거 같습니다.

[번역] 왜 Null이 나쁜가?

우리가 개발을 하면서 항상 마주하는게 NULL 체크인데(잊고 있을수도 있지만), NULL 체크를 해야하는지 말아야하는지 등의 부담을 개발자에게 전가시키는 것은 안좋은 방법이라고 알고 있다. 그래서 왜 Null의 반환이 안좋은지를 잘 이해시킨 포스트가 있어 소개한다. "Why NULL is Bad?".

Java에서 NULL을 사용하는 아주 단순한 예이다.
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return null;
  }
  return new Employee(id);
}

이 메서드 무엇이 잘못되었는가? 객체 대신 NULL을 반환할 수 있다는 것이 잘못된 것이다. NULL은 객체 지향 패러다임의 끔찍한 관습이며, 온 힘을 다해 피해야할 것 중에 하나이다. 이에 대해서는 많은 의견이 이미 발표되어 있다. 예를 들어, Tony Hoare의 Null References, The Billion Dollar Mistake와 David West의 저서 Object Thinking에서 전반적으로 언급되고 있다.

여기에서 모든 논거를 정리해 NULL의 사용을 피하고 적절한 객체 지향 구조로 바꾸는 방법의 예를 소개하고 싶다.

기본적으로 NULL을 대신할 수 있는 것은 두가지가 있다.

하나는 Null Object 디자인 패턴이다. (가장 좋은 방법은 하나의 불변 객체로 만드는 것이다.)
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return Employee.NOBODY;
  }
  return Employee(id);
}

또 하나는 객체를 돌려줄 수 없는 경우에 예외를 던지고 fail-fast하는 방법이다.
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    throw new EmployeeNotFoundException(name);
  }
  return Employee(id);
}

자 그럼, NULL을 반대하는 논거를 살펴 보자.

위 Tony Hoare의 발표와 David West의 저서 이외에, 나는 이 포스트를 쓰기 전에 다음의 책이나 글들을 읽었다.

Ad-hoc(임기응변적인) 오류 처리
입력으로 객체를 받은 경우는 항상 그것이 NULL 아닌지, 또한 유효한 객체 참조 여부를 확인하지 않으면 안된다. 그것을 확인하는 것을 잊어버리면 NullPointerException(NPE)으로 인해 런타임때 실행을 중지시켜 버릴 우려가 있다. 따라서 여러분의 로직은 복수의 확인 처리나 if/then/else 분기에 코드가 오염되어 버린다.
// this is a terrible design, don't reuse
Employee employee = dept.getByName("Jeffrey");
if (employee == null) {
  System.out.println("can't find an employee");
  System.exit(-1);
} else {
  employee.transferTo(dept2);
}

이것은 C나 다른 명령문을 늘어 놓는 절차형 언어에 있어서 예외적인 상황에 대응하는 방법이다. OOP는 주로 이런 임시 오류 처리의 블록을 없앨 목적으로 예외 처리를 도입했다. OOP에서는 예외 처리를 어플리케이션 레벨에서 에러 핸들러에 맡기는 것으로, 코드를 매우 깨끗하고 간결하게 해준다.
dept.getByName("Jeffrey").transferTo(dept2);

NULL 참조는 절차적 언어에서 계승된 것이라고 인식하고 Null 객체 또는 예외를 대신에 사용한다.

Ambiguous Semantic(모호한 의도)
위 메소드의 목적을 분명하게 전달하기 위해서 getByName()은 getByNameOrNullIfNotFound()라고 명명되어져야 한다. 이와 같은 이름을 가진 객체 또는 NULL을 반환하는 모든 함수가 있어야 된다. 그렇지 않다면 누군가가 모호한 코드를 읽게 된다. 그래서 코드의 의도를 명확하게 하기 위해 함수에 긴 이름을 붙이게 한다.

이 모호성을 제거하기 위해 함수는 실제 객체를 반환하거나 혹은 Null 객체를 반환하거나 예외를 반환한다.

성능을 고려하면 NULL을 반환해야하는 경우도 있다라고 주장하는 사람이 있을지도 모른다. 예를 들어, Java의 Map 인터페이스의 get() 메서드는 지정된 요소가 없는 경우 NULL을 반환한다.
Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;

이 코드는 Map이 NULL을 사용하고 있는 덕분에 map을 한번밖에 검색하지 않는다. 만약 Map의 get()이 요소가 발견되지 않을 때에 예외를 던지도록 하면 다음과 같은 코드가 된다.
if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search

분명히, 이 방법은 처음보다 2배 느리다. 그런데, 어떻게 하라고?

Map 인터페이스는 (저자을 공격하는 것은 아니지만) 설계에 문제가 있다. 그 get() 메소드는 Iterator를 반환해야 한다. 그러한 경우는 다음과 같은 코드가 된다.
Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();

참고로 C++ 표준 라이브러리의 map::find() 함수는 이렇게 설계되어 있다.

컴퓨터 사고 vs. 객체 사고
Java의 객체는 데이터 구조를 가리키는 포인터로 NULL은 아무것도 가리키지 않는 포인터(Intel x86 프로세서에서는 0x00000000)임을 아는 사람에게는 if (employee == null)이라는 문장은 이해할 수 있다.

그러나 만약 우리가 객체가 되었다고 생각하면 이 문장은 상당히 의미가 없는 것이 된다. 객체 관점에서 위 코드는 다음과 같다.
- 여보세요, 소프트웨어 부서입니까?
- 예.
- Jeffrey랑 이야기하고 싶습니다.
- 잠시만 기다려주십시오...
- 여보세요?
- 당신은 NULL입니까?

대화의 마지막 질문이 이상하지 않은가?

대신에 만약 Jeffrey와 연결을 요청한 후 전화가 끊어지면 우리에게 문제(예외)가 발생했다는 것을 안다. 이 시점에서 다시 한번 전화해보거나 Jeffrey에게로 연결되지 않아서 큰 일을 못하게 되면 상사에게 보고한다.

또는, Jeffrey는 아니지만 소프트웨어 부서의 사람에게 대략적인 질문에 대답할 수 있는 사람에게 전화를 할 수도 있고, Jeffrey 밖에 모르는 내용이라면 거부할지도 모른다(Null Object).

지연 실패(Slow Failing)
빠른 실패 대신에 위의 코드는 천천히 죽이려 한다. 중간에 다른 객체를 죽이면서. 문제가 발생했기 때문에 예외 처리를 빨리 시작해야 한다고 주위에 알리는 대신, 클라이언트부터 오류를 숨기고 있다.

이 논의는 앞에 기술한 "임시 오류 처리(Ad-hoc Error Handling)"에 가깝다.

코드는 가능한 한 허술한 것이 좋다. 필요할 때 멈춰야 한다.

메소드는 다루는 데이터에 대해서 가능한 한 엄격하게 만들어져야 한다. 주어진 데이터가 불충분하거나 메소드의 사용 방법에 위배되면 예외를 던지도록 해야한다.

그렇지 않으면, 공통적인 행위를 하거나 모든 호출에서 항상 예외를 던지는 Null Object를 반환한다.
public Employee getByName(String name) {
  int id = database.find(name);
  Employee employee;
  if (id == 0) {
    employee = new Employee() {
      @Override
      public String name() {
        return "anonymous";
      }
      @Override
      public void transferTo(Department dept) {
        throw new AnonymousEmployeeException(
          "I can't be transferred, I'm anonymous"
        );
      }
    };
  } else {
    employee = Employee(id);
  }
  return employee;
}

가변적이면서 불완전한 객체
일반적으로 객체는 불변으로 설계하는 것이 바람직하다. 이것은 객체룰 인스턴스화할 때 필요한 모든 정보를 받고 그 수명 주기 전반에 걸쳐 상태를 바꾸지 않는다는 것을 의미한다.

NULL은 지연 로딩을 할 때 종종 사용되는 객체를 불완전, 가변객체를 만든다. 다음이 그 예다.
public class Department {
  private Employee found = null;
  public synchronized Employee manager() {
    if (this.found == null) {
      this.found = new Employee("Jeffrey");
    }
    return this.found;
  }
}

이 기술은 널리 사용되고는 있지만, OOP의 안티 패턴이다. 주된 이유는 실행 환경의 성능 문제 책임을 객체에 전가했기 때문이다. 본래 그것은 Employee 객체가 걱정해야 할 부분은 아니다.

객체가 자신의 상태를 관리하고 자신의 역할에 대한 행동을 공개하는 대신 반환 캐시를 신경써야 한다. 이것이 지연로드의 의미이다.

캐시는 employee(직원)이 사무실에서 하는 일이 아니지 않나?

해결책? 지연로드는 위의 예 같은 원시적인 방법으로는 사용하지 마라. 대신 캐시 문제를 어플리케이션의 다른 레이어에 옮겨라.

예를 들어, Java라면, AOP를 사용할 수 있다. 예를 들어, jcabi-aspects는 @Cacheable 어노테이션이 메소드의 반환 값을 캐쉬하고 있다.
import com.jcabi.aspects.Cacheable;
public class Department {
  @Cacheable(forever = true)
  public Employee manager() {
    return new Employee("Jacky Brown");
  }
}

나의 이 분석에 납득하고 더이상 NULL을 쓰기를 끝내길 바란다.
Tags : , ,

스타트업에 필요한 코드 품질 관리

코드 품질 관리 부분은 개발자가 소홀히 하기 쉬운 부분이고, 대기업에서는 어느 정도 품질관리 부서가 있어서 프로세스화 되어 있는데, 작은 기업들은 개발자의 성향에 의존하는 환경에 놓여 있어, 개발자가 코드 리뷰하기 전에 남에게 내 놓을 수 있는 최소한의 품질이 보장되는 코드를 양산하도록 도와주는 정적 분석 도구에 대한 사용 가이드를 정리해 보았습니다.

정적 분석 도구를 통해 우리는 기존에 공유된 코딩 스타일을 위반하거나, 잠재적 버그의 가능성이 있는 코드들, 클래스나 패키지간의 종속성 등 설계상의 문제가 되는 코드들, 중복되고 보안에 위반되 코드들의 문제를 쉽게 찾아 고칠 수 있습니다. 그리고 이 도구들은 품질을 올려주는 것이 아니라, 최소한의 문제되는 코드들을 클리어 해 주는 것에 만족해야 합니다. 품질을 올려주는 것은 개인의 몫이 커 스스로 노력해야 합니다. ^^ 여기서는 기본적으로 Java, Javascript 언어에 한정합니다.

이후 개선된 내용은 github에서 현행화되여 github를 참조해 주시면 고맙겠습니다.

Checkstyle

CheckStyle은 코딩 스타일에서의 벗어난 코드들을 알려준다. 기본적으로 내장된 코딩룰은 Google's Style과 Unix Style이 있지만, 커스터마이징이 가능해 자기 조직만의 CheckStyle을 Code Formatter와 함께 정의해서 사용할 수 있게 해준다.

1. 설정
wiseeco-checkstyle.xml 파일을 다운로드한 다음, pom.xml에 아래 사항을 기술해 준다.
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-checkstyle-plugin</artifactId>
    <version>${checkstyle-version}</version>
    <configuration>
        <configLocation>wiseeco-checkstyle.xml</configLocation>
    </configuration>
</plugin>

2. 설정 정보
-
blocks
  • AvoidNestedBlocks : 중첩된 블록 체크.
  • EmptyBlock : 빈 블록 체크.
  • LeftCurly : 중괄호 {의 위치 체크.
  • RightCurly : 오른쪽 중괄호 }의 위치 체크.
- Coding
  • EmptyStatement : 빈줄 확인.
  • HiddenField : 동일한 클래스 내에서 정의된 필드와 같은 이름의 지역 변수와 메소드 매개 변수가 존재하는지 여부를 체크.
  • IllegalInstantiation : 잘못된 인스턴스 사용 체크.
  • ModifiedControlVariable : for 문의 증감 부분에 설명 된 변수를 블록으로 변경하고 있는지 여부 체크.
  • SimplifyBooleanExpression : 중복 연산 체크.
  • IllegalCatch : 부적절한 Catch 사용 체크.
  • FallThrough : case문에 break문이 정의되지 않은 부분 체크.
- imports
  • AvoidStarImport : ".*" 형식의 import 문장을 체크.
  • IllegalImport : import하면 안되는 패키지 체크.
  • RedundantImport : 중복 import 문을 체크.
  • UnusedImports : 프로그램에서 사용되지 않는 import 문을 체크.
- Naming Conventions
  • ConstantName : 상수(static, final)의 명명 규칙을 설정. 기본값은 ^[A-Z][A-Z0-9]+(_[A-Z0-9]+)*$.
  • LocalFinalVariableName : final 로컬 변수 명명 규칙을 설정(카멜 표기법). ^[a-z][a-z0-9][a-zA-Z0-9]*$.
  • MemberName : static이 아닌 필드(변수) 명명 규칙을 설정(카멜 표기법). ^[a-z][a-z0-9][a-zA-Z0-9]*$.
  • MethodName : 메소드의 명명 규칙을 설정(카멜 표기법). ^[a-z][a-z0-9][a-zA-Z0-9]*$.
  • ParameterName : 매개 변수의 명명 규칙(카멜 표기법). ^[a-z][a-z0-9][a-zA-Z0-9]*$.
  • StaticVariableName : static변수 명명 규칙을 설정(카멜 표기법). ^[a-z][a-z0-9][a-zA-Z0-9]*$.
  • TypeName : 클래스나 인터페이스의 명명 규칙을 설정(파스칼 표기법). ^[A-Z][a-zA-Z0-9]+$.
  • PackageName : 패키지의 명명 규칙을 설정. ^[a-z]+(\.[a-z][a-z0-9]*)*$.
- whitespace
  • MethodParamPad : 메소드 정의 부분(호출 부분도 포함)의 공백을 체크.


FindBugs

FindBugs는 Maryland 대학에서 만들어진 자바를 위한 정적 분석 툴이며, 9개 이상의 카테고리와 400개가 넘는 항목(버그 패턴)이 등록되어 있어 잠재적인 버그를 알려주며, ruleset의 커스트마이징이 가능해 기업마다 원하는 형태로 커스터마이징 할 수 있다.

1. 설정
findbugs-exclude-filter.xml파일을 다운로드한 다음(include파일은 지정안해도 됨), pom.xml에 아래 사항을 기술해 준다.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>findbugs-maven-plugin</artifactId>
    <version>${findbugs-version}</version>
    <configuration>
        <excludeFilterFile>findbugs-exclude-filter.xml</excludeFilterFile>
    </configuration>
</plugin>

2. 오류 메세지 정보
- Correctness(문제소지가 있는 코드)
  • Possible null pointer dereference in method on exception path : 예외가 발생했을 경우에 null 포인터가 참조될 가능성 있음.
  • Nullcheck of value previously dereferenced : 중복 널 체크.
  • Uninitialized read of field in constructor : 생성자 초기회 이전에 필드를 사용하고 있음.
  • Invocation of toString on an array : 배열을 toString하고 있음.[C@16f0472 같은 의미없는 문자열)
- Bad practice(나쁜 습관의 코드)
  • Method ignores results of InputStream.read() : InputStream.read() 메소드의 반환 값을 무시하고 있음.
  • Method names should start with a lower case letter : 함수명이 카멜케이스가 아님.
  • Method may fail to close stream : 메소드에서 생성된 스트림이 close되지 않고 종료될 가능성이 있음.
- Experimental
  • Method may fail to clean up stream or resource on checked exception : 메소드에서 생성된 스트림이나 리소스가 close되지 않은 채 메소드가 종료 될 가능성이 있음.
- Internationalization(지역특성을 고려하지 않는 코드)
  • Reliance on default encoding : 디폴트 인코딩 지정 안했을 경우. String.getBytes(), new String(bytes) 등 인코딩 정보를 빠진 경우 디폴트 인코딩 정보 등록해야 함.
- Multithreaded correctness(멀티쓰레드에 안전하지 않는 코드)
  • Call to static DateFormat : DateFormat 객체의 사용은 멀티 스레드에 안전하지 않음.
- Performance(성능에 영향을 주는 코드)
  • Boxed value is unboxed and then immediately reboxed : Box된 값이 바로 Unbox 되는 부분이 있음.
  • Boxing/unboxing to parse a primitive : primitive 타입을 강제 Box, Unbox 한 부분이 있음.
  • Unread field : 읽혀지지 않는 필드가 존재함.
- Dodgy code(부정확하거나 에러를 유발할 수 있는 코드)
  • Result of integer multiplication cast to long : 정수(int)의 곱셈 결과를 long으로 변환함.
  • Dead store to local variable : 로드되지 않는 로컬 변수에 쓰기 작업을 수행하고 있음.
  • Parameter must be non-null but is marked as nullable : 파라미터는 널이 아니어야하는데 널일수도 있음.
  • Check for oddness that won't work for negative numbers : 음수에 대해서는 동작하지 않을수도 있음.
  • Exception is caught when Exception is not thrown : Exception이 발생하지 않는 곳에서 Exception을 캐치하고 있음.
  • Switch statement found where default case is missing : default문이 없음.
  • Useless object created : 의미없는 객체 생성문이 있음.
  • Dead store of null to local variable : 로컬 변수에 null을 할당해서 GC대상이 되게 하는 방법은 java 6.0이상에서는 더이상 불필요함.


ESLint

ESLint는 Javascript의 알려진 버그들을 알려준다. 그리고 룰셋 정보도 많고, 가장 활발하게 소스가 업데이트되고 있는 Javascript 정적 분석기라고 생각되어 도입하게 되었다.

1. ESLint의 특징
  • 모든 규칙을 자유롭게 on/off 할 수 있음.
  • 자신의 프로젝트에 맞게 사용자 정의 규칙을 만들어서 사용 가능.
  • 풍부한 내장 규칙 이외에 많은 플러그인이 공개되어 있음.
  • ECMAScript 2015(ES6)를 지원.
  • React의 JSX 기법을 지원.
  • Babel과 연계하여 ECMAScript 2016(ES7) 이후의 구문과 Flow형 주석에도 대응함.
  • 지속적으로 소스 업데이트가 진행되고 있음.

2. 설치 및 사용법
> npm install -g eslint
> eslint  -v
> eslint --init
{
    "rules": {
        "linebreak-style": [
            2,
            "unix"
        ],
        "semi": [
            2,
            "always"
        ]
    },
    "env": {
        "browser": true
    },
    "extends": "eslint:recommended"
}

사용 방법은 아래와 같이 해당 자바스크립트를 돌려주면 된다.
> eslint src/main/webapp/resources/app/common/utils/helpers.js
"extends": "eslint:recommended" 라인만 있어도 소스 분석해 문제점을 나열해 준다. 그리고 ES6에서 추가된 언어 기능에 관한 규칙이 많이 있어, 이것을 활용하려면 env 안에 "es6": true 를 추가해 주면 된다.

3. 설정 정보
- Possible Errors
  • comma-dangle : 마지막 쉼표있을 경우 경고.(예: [1, 2, 3,])
  • no-cond-assign : 조건식안에 대입식이 있을 경우 경고.(예 : if(a = 10) {})
  • no-console : 소스에 console 로그가 있으면 경고.
  • no-constant-condition : 조건식이 상수라면 경고.(예 : if (true) {})
  • no-control-regex : 정규 표현식 중에 ASCII 제어 문자가 존재하면 경고.
  • no-debugger : debugger 문 경고.
  • no-dupe-args : 인수 이름이 중복되면 경고.(예 : function foo (a, a) {})
  • no-dupe-keys : 속성 이름이 중복되면 경고.(예 : var foo = {a : 0, a : 1};)
  • no-duplicate-case : case 절이 중복되면 경고.(예 : switch (foo) {case 0 : case 0 :})
  • no-empty-character-class : 정규 표현식에 널문자 집합이 존재하면 경고.(예 : var foo = /abc[]/;)
  • no-empty : 빈 블럭이 있으면 경고.
  • no-ex-assign : catch 절에 예외 변수에 다시 대입문이 있으면 경고.(예 : try {} catch (err) {err = 0;})
  • no-extra-boolean-cast : 조건식 안에 !!연산을 경고.(예 : if (!!a) {})
  • no-extra-semi : 불필요한 세미콜론을 경고.(예 : var a = 0;;)
  • no-func-assign : 함수에 대입하면 경고.(예 : function foo () {} foo = 0;)
  • no-inner-declarations : 함수 이외의 블록 안에 함수 선언을 경고.(예 : if (a) {function foo () {}})
  • no-invalid-regexp : RegExp 생성자에 잘못된 문자열을 작성하면 경고.(예 : new RegExp ("[/");)
  • no-irregular-whitespace : 부적절한 공백을 경고.
  • no-negated-in-lhs : in 연산자의 좌변 값이 예기치 않은 연산자인 경우에 경고.(예 : if (! a in foo) {})
  • no-obj-calls : 함수가 아닌 내장 변수에 대한 함수 호출을 경고.
  • no-regex-spaces : 정규식 파싱동안 연속적인 공백이 존재한다면 경고.
  • no-sparse-arrays : 스파스 배열 리터럴을 경고.(예 : var a = [0, 1,, 3];)
  • no-unreachable : 도달할 수없는 코드에 경고.(예 : function foo () {return; var a = 0;})
  • use-isnan : NaN 과의 비교를 경고.(예 : if (foo == NaN) {})
  • valid-typeof : typeof 비교를 경고.(예 : if (typeof a == "strong") {})
- Best Practices
  • no-fallthrough : switch 구문 fall through를 경고.(예 : switch (a) {case 0 : foo(); default: bar();})
  • no-octal : 8진수 리터럴을 경고.(예 : var a = 0755;)
  • no-redeclare : 변수의 재 선언을 경고.(예 : var a = 0; var a = 1;)
- Variables
  • no-delete-var : 변수에 delete 식을 경고.
  • no-undef : 정의되지 않은 변수에 대한 참조를 경고.
  • no-unused-vars : 선언했지만, 사용되지 않은 변수를 경고.
- Stylistic Issues
  • no-mixed-spaces-and-tabs : 공백과 탭이 혼재하고 있는 경우에 경고.

코드 리뷰의 기본 원칙들

1. 리뷰도 프로젝트의 공식일정과 자원에 포함시킨다.
2. 목표는 사람(사람에 대한 선입견 포함)이 아니라 산출물(프로그램 등)을 검토한다.
3. 문제를 명확하게 할 순 있어도 모두 해결하려고 하지 않는다.
4. 성능과 이해도에 영향을 주지 않는 한 그 사람의 스타일을 문제제기하지 않는다.
5. 리뷰는 2시간을 넘지 않는다.
6. 초기에는 비공식 리뷰나 교육을 자주하고 일정 시간이 지나면 공식 리뷰로 전환한다.
7. 필요하면 체크리스트를 만들어서 리뷰에 임한다.
8. 토론을 장려하고 반론은 대다수가 동의하면 진행하자.

[번역] Java에서 String 클래스가 왜 final 혹은 Immutable인가?

5 Reasons of Why String is final or Immutable in Java

Java에서 String이 왜 final 혹은 Immutable인지에 대해서 적당한 사유를 정리해 준 사이트(Why String Class is made Immutable or Final in Java - 5 Reasons")가 있어 번역, 정리해 봅니다.

왜 String 클래스가 final이었는지의 진짜 이유는 James Gosling이 말했던 보안적인 부분을 제외하더라도, Java 언어 디자이너가 가장 잘 알고 있겠지만, 여기서는 내 자신이 왜 Java의 String이 Final이고 Immutable인지에 대한 몇가지 이유를 제시한다.

1) String Pool
Java 디자이너는 모든 종류의 Java 어플리케이션에서 가장 많이 사용되는 데이터 타입이 String이 될 것이라고 예측했었고, 그리고 그것이 처음부터 최적화가 필요한 이유라는 것을 알았다. 첫번째 아이디어는 String pool에 String 리터럴을 포함하는 것이었다. 목표는 String 객체를 공유해, 템프러리하게 생성된 String 객체를 줄여주는 것이었다. 공유를 하기 위해서는 String 클래스는 Immutable class이어야 한다. 서로 알 수 없는 두 영역에서 mutable 객체의 공유는 불가능하다. 예를 가정해보면, 두 참조 변수가 동일한 String 객체를 참조한다.
String s1 = "Java";
String s2 = "Java";
지금 s1을 "Java"에서 "C++"객체로 변경하면 참조 변수 s2="C++"이 된다. String을 immutable하게 하는 것으로, String 리터럴의 공유가 가능하게 된다. 즉, String pool의 주요한 아이디어는 String을 final 또는 immutable하게 해야만 Java에서 String pool을 구현할 수 있다.

2) Security
Java는 모든 서비스 레벨에서 보안 환경을 제공한다는 명확한 목표를 가지고 있어서, String이 특히 전반적인 보안에 중요하다. String은 다수의 Java 클래스에 매개 변수로 널리 사용되고 있으며, 예를 들어 네트워크 연결시, 호스트 및 포트가 String으로 되어 있고, Java에서 파일을 읽어들이기 위한 파일이나 디렉토리 경로도 String으로 되어있고 데이터베이스 연결에 필요한 URL 등이 문자열로 되어 있다. 만약 이 String이 immutable 하지 않다면, 사용자는 시스템의 특정 파일에 대한 액세스 권한을 얻은 후 PATH의 변경이 가능하게 되며, 이것은 심각한 보안 문제를 일으킨다. 마찬가지로 네트워크 시스템과 데이터베이스에 연결하는 동안 String이 mutable하게 되면 보안 위협상태에 놓이게 된다. 또한 mutable String을 인수로 취하는 리플렉션에서도 마찬가지로 보안 문제를 일으킨다.

3) Use of String in Class Loading Mechanism
String을 final이나 Immutable해야하는 다른 이유는 class loading mechanism에서 자주 사용되기 때문이라는 점도 있다. String이 Immutable하지 않다고 하면 공격자는 이점을 이용하게 되며, java.io.Reader 등 Java 표준 클래스의 로드 요청시에 악성 com.unknown.DataStolenReader 클래스로 변경하게 할 수 있다. String이 final이나 Immutable함으로써 적어도 JVM은 올바른 클래스들을 로드할 수 있게 된다.

4) Multithreading Benefits
Concurrency나 멀티 스레드는 Java의 핵심 기능이므로 String 객체의 스레드 안전성을 고려할 때 당연한 이유가 된다. String 널리 쓰이게 될 것으로 예측 되었기 때문에, Immutable함으로써 외부에서 동기화 필요를 없애고 여러 스레드 간에 String을 공유하는 부분에서 코드를 깨끗하게 정리해 준다. 이 기능은 복잡하고 오류가 발생하기 쉬운 concurrency 코드를 쉽고 간단하게 해준다. String은 concurrency하기 때문에 스레드간에 공유가 가능하고, 결과적으로 읽기 쉬운 코드가 된다.

5) Optimization and Performance
클래스를 Immutable한 경우의 장점은 클래스가 일단 생성되면 변경이 불가능하다는 점이다. 이점은 캐시 등의 많은 성능 최적화의 길을 열 수 있게 된다. String 자신이 변경되지 않는다는 것을 알고 있기 때문에 String 해시 코드를 캐시한다. 해시 코드의 계산은 지연(lazy)하여 수행되고 일단 생성되면 캐시된다. 간단한 경우는 String 객체의 hashCode() 메소드를 처음 호출시에 해시 코드가 계산되고, 그 이후의 hashCode는 계산된 캐시 값을 반환하는것을 사용한다. 따라서 String은 Hashtable과 HashMap 등 Map 기반으로 해시가 자주 사용하는 경우에는 성능이 향상된다. 해시 코드 캐시는 String이 자신의 내용에 의존하기 때문에 Immutable이나 final 없이는 불가능하다.

Pros and Cons of String being Immutable or Final in Java
위의 이점을 통해 Java에서 String을 final로 하면 또 다른 이점이 있다. String은 HashMap과 Hashtable 같은 해시 기반 컬렉션의 키로써 가장 많이 사용된다. Immutable은 HashMap 키의 필수로 요구하지는 않지만, Immutable이 mutable보다 오브젝트를 사용하는 편이 더 안전하다. 그 이유는 만약 mutable 오브젝트의 상태가 HashMap에서 변경되는 경우 equals()와 hashCode() 메소드 변경후의 특성에 의해 영향을 받아 다시 뒤로 되돌아가는 취소가 어렵다. 클래스가 Immutable한 경우 해시 기반 컬렉션내에 저장 될 때, 상태 변경의 위험은 없게 된다. 또 다른 중요한 장점은 이미 스레드 안전성에 대해 이미 언급했다. String은 Immutable하므로 외부에서 동기화를 고려하지 않고 스레드간에 안전하게 공유할 수 있다. 이는 동시 코드의 가독성이 높아 오류의 가능성을 줄일 수 있다.

하지만 많은 장점에도 불구하고 Immutable에는 단점 또한 가지고 있다. 예로, 비용이 소요된다. String은 Immutable하기 때문에 템프러리하게 사용할 객체를 많이 생성하게 되면 GC에 영향을 미친다. Java 디자이너도 그 점은 인식하고 풀에 String 리터럴을 저장해 String의 GC를 줄일 수 있는 대안으로 생각해 왔다. 그러나, String 풀에서 오브젝트를 가져올 수 없는 new String()이라는 생성자를 사용하지 않고 String을 생성하도록 주의해야 한다. 일반적인 Java 어플리케이션은 다수의 객체를 생성해 GC가 빈번하게 일어난다. 풀에 저장하는 String은 GC와 관련하여 숨겨진 위험이 있다. String 풀은 Java Heap의 PermGen이라는 공간에 배치되어 Java Heap에 비해 상당히 제한되는 영역이다. 다수의 String 리터럴은 이 공간을 즉시에 채워 버려, 결과적으로 Java.lang.OutOfMemoryError : PermGen Space를 야기시킨다. 다행히, Java 언어 프로그래머는 이 문제를 인식하고, Java 7 이상에서는 String pool은 일반적인 힙 공간으로 이동되었다. 이곳은 PermGen에 비해 매우 큰 공간이다. String을 final로 하면 또하나의 다른 단점으로는 확장성 제약이다. 일반적인 상황에서는 거의 필요하지 않다 해도, 기능 확장을 위해 String을 확장할 수 없으며, java.lang.String 클래스를 확장하고 싶은 사람들에게는 여전히 제약 사항 중에 하나이다.

위 5가지 이유는 확실히 "왜 Java의 String 클래스는 final이나 Immutable인가?(Why String class has been made Final and Immutable in Java)"에 대한 팁이다. 그리고 Integer, Long, Double, Float 등의 래퍼 클래스도 Immutable이고 Final이다.

Trello, Github, Slack을 활용한 개발 프로세스

개발 흐름


이 프로세스 문서의 현행화는 Github에서 진행되니 추후에는 Github을 방문해 주시면 고맙겠습니다.

개발 프로세스(Trello, Github, Slack)

1. Trello Card 만들기
1.1 기본적인 Trello 흐름
  • 먼저 Trello에서 개발해야 할 기능을 [To Do(Story)]라는 이름의 리스트에 카드로 만들고,
  • 해당 스토리(카드)를 개발자가 구현에 들어가면 [Doing(WIP)] 리스트에 카드를 옮기고,
  • 리뷰에 들어가면 [Review(Sprint1)] 리스트로 옯기고,
  • 개발 브랜치가 병합하여 테스트를 완료하면 [Done(Sprint1)] 리스트에 카드를 옮기고 해당 기능을 클로즈한다.


1.2 Trello카드 내용은 Description란에는 이슈 링크를 걸어주거나 wiki 링크를 걸어줘, 해당 스토리의 정보를 알 수 있도록 해준다. 그리고 Spec을 참조하여 Checklist를 추가해 완료조건을 기술해 개발해가면서 하나씩 처리해 나간다.


1.3 카드 처리 및 이동시 Slack Alert를 줘 실시간으로 처리가 가능하도록 설정한다.


2. Branch 만들고 Pull Request, Merge 하기
2.1 소스 리모트와 동기화
> git checkout master && git pull origin master && git fetch -p origin
2.2 브랜치명을 만들고 브랜치로 이동.
> git checkout -b dev_standard
2.3 작업 후 커밋.
> git commit -a -m "[VOY-201] README git 사용법 추가"
# 커밋 작성은 issue ID를 넣고 내용은 구체적으로 제시한다.
2.4 작업을 완료 후 master branch로 변경하여 remote에 새로운 변경 사항을 master에 반영
> git checkout master
> git pull origin master && git fetch -p origin
2.5 변경사항이 있다면 dev_standard branch에서 rebase를 수행
> git checkout dev_standard
> git rebase master
- rebase 중 충돌이 발생하면 아래 수행하고 아니면 넘어감.
> git add .
> git rebase --continue
2.6 Pull Request 요청하기 전에 Trello 카드도 [Review] 리스트로 옮기고 Checklist 하나를 더 만들어서 Review 내용(리뷰 담당자 포함)을 기술하면 Trello 알림을 통해 담당자가 리뷰어임을 알게 된다. 아니면 Trello 카드를 만들때 Review Checklist를 미리 만들어서 리뷰 대상자를 등록한다.


2.7 Pull Request 요청
> git push origin dev_standard
- push 후 Github의 repository로 이동해서 Compare & pull request 버튼 클릭하고 코멘트[To close VOY-201(jira issue 번호), 혹은 github issue 사용하면 #1234로] 남기고 Create pull request 버튼 클릭한다.


- 이때 Pull Request 정보가 Slack을 통해 담당자에게 보내지게 되고 리뷰를 수행한다.


- 리뷰 수행 후 수정사항이 있으면 수정 후 Pull Request를 다시 보내고, 수정 사항이 없으면 Github에서 Merge pull request를 클릭하고 Confirm merge 버튼을 클릭해서 merge를 완료한다.

- Github에서 merge가 완료되면, Delete branch 버튼을 클릭하거나 아래 로컬에서 커맨드로 원격 브랜치를 삭제한다.(선택사항)
> git push origin :dev_standard
2.8 Review 후 수정사항이 있는 경우 수정한 다음 2.7을 재 수행한다.

2.9 로컬 master 동기화
> git checkout master && git pull origin master && git fetch -p origin
- 로컬 브랜치 삭제(선택사항)
> git branch -d dev_standard
3. 배포하기
- 테스트 코드를 돌리고, jenkins나 자체 배포 도구를 활용하여 운영 서버에 소스를 배포한다.

4. 배포후 확인
- 기능 테스트를 눈으로 확인하면서 화면의 깨짐, 데이터의 정확성, 브라우저 호환성 등을 점검한다.
- Selenium 도구를 통해 브라우저단에서 테스트를 할 수 있는데, 이 Selenium이 구동한 브라우저의 결과 화면을 아이컨텍해서 봄으로써 어느 정도 테스트 자동화를 할 수 있다.

5. Trello 카드 Done 리스트로 이동
- 배포후 확인에서 이상이 없다면 Trello의 카드를 [Done(Sprint1 - 날짜기간)] 리스트에 이동시키고, 이슈를 close한다.