전자정부 표준프레임워크에서 제공하는 Code Inspection 도구인 PMD의 기본 사용법에 대하여 설명한다.
일괄적인 Inspection 작업 수행과 작업의 편의성을 위하여 Eclipse IDE의 PMD Perpective에서 Code Inspection 기능을 수행한다.
다음과 같은 과정으로 PMD Perpective로 전환할 수 있다.
Inspection을 수행하고자 하는 프로젝트를 선택하여 Inspection을 수행한다.
기본적으로 java 소스 코드를 대상으로 Inspection을 수행하며, 다음 조건의 경우는 Inspection을 수행하지 않는다.
Inspection을 수행한 후에는 Inspection의 결과를 확인하고 룰에 위배되는 코드를 수정할 수 있다.
PMD Perspective에서는 다음 그림과 같이 다양한 뷰에서 위배 결과를 확인할 수 있다.
Inspection 결과 해당 프로젝트 내에서 위배사항이 발생하였을 경우, 일반적인 오류가 있을때와 마찬가지로 Package Explorer 뷰의 프로젝트 아이콘과 하위 패키지 아이콘들이 또는 와 같이 붉은색 X자 모양의 상자가 추가된 아이콘으로 변경된다.
이 아이콘은 일반 오류 상황과 마찬가지로 해당 위배 내역이 수정될 때까지 정상상태의 아이콘으로 변경되지 않는다.
Problem 뷰에서도 Inspection의 위배 결과를 확인할 수 있다. PMD Perspective에서 다음과 같은 순서로 Problem 뷰를 열 수 있다.
위배결과 내용 중, Problem 뷰를 통하여 다음의 주요 항목을 확인하고 위배된 코드에 접근하여 수정할 수 있다.
항목 | 설명 |
---|---|
Description | 소소코드의 위배된 룰항목에 대한 상세 설명을 표시한다. |
Resource | 위배된 소스코드 이름을 표시한다. |
Path | 위배된 소스코드의 Path를 표시한다. |
Location | 소스코드에서 위배된 해당 Line을 표시한다. |
위 위배결과에서 해당 항목을 더블 클릭하면 Editor 영역에서 위배된 코드를 볼 수 있어, 바로 코드를 수정할 수 있다.
Violations Ovewview 뷰를 이용한 위배사항 확인은 Inspection 결과 리포팅에서 상세히 설명한다.
Inspection을 수행하면 위배된 결과는 수정하기 전까지 계속해서 소스 상에 남아있다.
소스 상에 Inspection의 위배사항이 남아 있으면 코드 상의 실제 오류(컴파일 오류 등)와 구분하기 힘들기 때문에 Inspection결과를 초기화 할 필요가 있다. 또는 Inspection을 재수행하기 위해 기존의 Inspection 결과를 초기화할 수 있다.
Inspection 결과를 초기화하기 위해서는 다음과 같이 프로젝트를 선택하고 초기화를 수행한다.
결과를 초기화하면 소스의 위배결과가 초기화되며, Problem 뷰, Violations Overview 뷰와 Package Explore 뷰에서도 사라진다.
전자정부 표준프레임워크에서는 Code Inpsection을 위한 룰셋으로 논리오류/구문오류/참조오류 영역을 대상으로 하는 총 44개의 룰을 표준으로 선정하였다.
전자정부 표준 Inspection 룰셋은 전자정부 표준 Inspection 룰셋 설치 지침을 참조하여 소스코드 Inspection 대상자의 PC에 설치하며, 개별 룰에 대한 설명과 오류코드(룰 위배 코드) 예제, 권장방안(위배 대응방안 또는 소스코드) 예제는 다음과 같다.
public abstract class Foo { void int method1() { ... } void int method2() { ... } // consider using abstract methods or removing // the abstract modifier and adding protected constructors }
public class Foo { public void bar() { int x = 2; if ((x = getX()) == 3) { System.out.println("3!"); } } }
public class StaticField { static int x; public FinalFields(int y) { x = y; // unsafe } }
class Scratch { void copy_a_to_b() { int[] a = new int[10]; int[] b = new int[10]; for (int i = 0; i < a.length; i++) { b[i] = a[i]; } // equivalent b = Arrays.copyOf(a, a.length); // equivalent System.arraycopy(a, 0, b, 0, a.length); int[] c = new int[10]; // this will not trigger the rule for (int i = 0; i < c.length; i++) { b[i] = a[c[i]]; } } }
package com.igate.primitive; public class PrimitiveType { public void downCastPrimitiveType() { try { System.out.println(" i [" + i + "]"); } catch(Exception e) { e.printStackTrace(); } catch(RuntimeException e) { e.printStackTrace(); } catch(NullPointerException e) { e.printStackTrace(); } } }
class Foo { void bar() { try { // do something } catch (Exception e) { e.printStackTrace(); } } }
public class Foo { // Try to avoid this: synchronized void foo() { // code, that doesn't need synchronization // ... // code, that requires synchronization if (!sharedData.has("bar")) { sharedData.add("bar"); } // more code, that doesn't need synchronization // ... } // Prefer this: void bar() { // code, that doesn't need synchronization // ... synchronized(this) { if (!sharedData.has("bar")) { sharedData.add("bar"); } } // more code, that doesn't need synchronization // ... } // Try to avoid this for static methods: static synchronized void fooStatic() { } // Prefer this: static void barStatic() { // code, that doesn't need synchronization // ... synchronized(Foo.class) { // code, that requires synchronization } // more code, that doesn't need synchronization // ... } }
public class Foo { void bar() { throw new NullPointerException(); } }
public class Foo { private String ip = "127.0.0.1"; // not recommended }
public class Bar { public void withSQL() { Connection c = pool.getConnection(); try { // do stuff } catch (SQLException ex) { // handle exception } finally { // oops, should close the connection using 'close'! // c.close(); } } public void withFile() { InputStream file = new FileInputStream(new File("/tmp/foo")); try { int c = file.in(); } catch (IOException e) { // handle exception } finally { // TODO: close file } } }
public interface ConstantInterface { public static final int CONST1 = 1; // violation, no fields allowed in interface! static final int CONST2 = 1; // violation, no fields allowed in interface! final int CONST3 = 1; // violation, no fields allowed in interface! int CONST4 = 1; // violation, no fields allowed in interface! } // with ignoreIfHasMethods = false public interface AnotherConstantInterface { public static final int CONST1 = 1; // violation, no fields allowed in interface! int anyMethod(); } // with ignoreIfHasMethods = true public interface YetAnotherConstantInterface { public static final int CONST1 = 1; // no violation int anyMethod(); }
public void doSomething() { try { FileInputStream fis = new FileInputStream("/tmp/bugger"); } catch (IOException ioe) { // not good } }
class Foo { { if (true); // empty if statement if (true) { // empty as well } } {} // empty initializer }
String x = "foo"; if (x.equals(null)) { // bad form doSomething(); } if (x == null) { // preferred doSomething(); }
class Foo { int myField = 1; // This is in camel case, so it's ok int my_Field = 1; // This contains an underscore, it's not ok by default // but you may allow it, or even require the "my_" prefix final int FinalField = 1; // you may configure a different convention for final fields, // e.g. here PascalCase: [A-Z][a-zA-Z0-9]* interface Interface { double PI = 3.14; // interface "fields" use the constantPattern property } enum AnEnum { ORG, NET, COM; // These use a separate property but are set to ALL_UPPER by default } }
public class Foo { public final int BAR = 42; // this could be static and save some space }
class Foo { abstract void bar(int myInt); // This is Camel case, so it's ok void bar(int my_i) { // this will be reported } void lambdas() { // lambdas parameters can be configured separately Consumer<String> lambda1 = s_str -> { }; // lambda parameters with an explicit type can be configured separately Consumer<String> lambda1 = (String str) -> { }; } }
public class Foo { void good() { SecretKeySpec secretKeySpec = new SecretKeySpec(Properties.getKey(), "AES"); } void bad() { SecretKeySpec secretKeySpec = new SecretKeySpec("my secret here".getBytes(), "AES"); } }
public class Foo { private int x; // could be final public Foo() { x = 7; } public void foo() { int a = x + 2; } }
public void bar(String string) { if (string != null && string.trim().length() > 0) { doSomething(); } }
// Avoid this, two buffers are actually being created here StringBuffer sb = new StringBuffer("tmp = "+System.getProperty("java.io.tmpdir")); // do this instead StringBuffer sb = new StringBuffer("tmp = "); sb.append(System.getProperty("java.io.tmpdir"));
class Foo { void bar() { int localVariable = 1; // This is in camel case, so it's ok int local_variable = 1; // This will be reported unless you change the regex final int i_var = 1; // final local variables can be configured separately try { foo(); } catch (IllegalArgumentException e_illegal) { // exception block parameters can be configured separately } } }
public class SecureSystem { UserData [] ud; public UserData [] getUserData() { // Don't return directly the internal array, return a copy return ud; } }
public class Foo { void bar() { if (a.equals(baz) && a != null) {} // a could be null, misplaced null check if (a != null && a.equals(baz)) {} // correct null check } }
public class Foo { // Should specify Locale.US (or whatever) private SimpleDateFormat sdf = new SimpleDateFormat("pattern"); }
public class Bar { // can be simplified to // bar = isFoo(); private boolean bar = (isFoo() == true); public isFoo() { return false;} }
class Foo {{ int x = 2; switch (x) { case 1: int j = 6; case 2: int j = 8; // missing default: here } }}
class Foo{ Logger log = Logger.getLogger(Foo.class.getName()); public void testA () { System.out.println("Entering test"); // Better use this log.fine("Entering test"); } }
public void doSomething() { }
import java.io.File; // not used, can be removed import java.util.Collections; // used below import java.util.*; // so this one is not used import java.lang.Object; // imports from java.lang, unnecessary import java.lang.Object; // duplicate, unnecessary public class Foo { static Object emptyList() { return Collections.emptyList(); } }
class Foo { { toString();; // one of these semicolons is unnecessary if (true); // this semicolon is not unnecessary, but it could be an empty block instead (not reported) } }; // this semicolon is unnecessary
public class Foo { private void bar(String howdy) { // howdy is not used } }
public class Something { private static int FOO = 2; // Unused private int i = 5; // Unused private int j = 6; public int addOne() { return j++; } }
public class Something { private void foo() {} // unused }
public class Foo { { int n = 0; n = (n); // here n = (n * 2) * 3; // and here n = n * (2 * 3); // and here } }
Inspection을 수행한 후 수행결과를 종합하여 리포팅할 수 있다.
Inspection을 수행한 후, 개발자 환경에서 바로 확인할 수 있는 통계 정보로써, 다음과 같은 화면을 가지고 있다.
Violations Overview 뷰의 화면 구성은 위배 사항의 통계 정보를 조회할 수 있는 그리드와 상단의 그리드의 내용을 제어할 수 있는 우측상단의 기능 버튼들로 구성된다.
개발자는 Violations Overview 뷰로 조회할 수 있는 이러한 유형별 통계 정보를 이용하여, 소스 코드의 품질 개선활동을 개발도구 상에서 바로 수행할 수 있다.
Inspection의 결과를 별도 파일로 리포팅하기 위해서는 프로젝트를 선택하고 리포팅 생성 작업을 수행한다.
Inspection의 별도 리포팅을 수행하게되면 다양한 형태의 리포팅 파일 들이 생성되는 것을 확인할 수 있다.
다음의 그림에서와 같이, 별도 리포팅을 수행한 프로젝트 루트 아래에 있는 reports 폴더에 CSV, HTML, TXT, XML 형태의 리포팅 파일이 생성된다.
Inspection 수행을 통해 위배된 결과를 생성된 리포팅 파일로 한 눈에 확인할 수 있다.
다음은 리포팅 파일 중, HTML 형태로 생성된 리포팅 파일을 웹 브라우저로 확인한 결과이다.
PMD의 리포팅 파일들은 기본적으로 UTF-8으로 인코딩되어 생성되므로, 다음과 같은 상황에서 한글이 깨질 수 있다.
Hudson PMD Plugin을 이용하면, Inspection 수행 결과 리포팅에 대한 통계 정보를 활용할 수 있다. Hudson에서 위배사항에 대한 통계 자료를 조회하기 위해서는 다음과 같은 순서로 확인할 수 있다.
Hudson PMD Plugin이 제공하는 다음과 같은 다양한 통계 정보를 토대로, 개인별/팀별 성과 측정 및 품질보증활동의 결과로써 활용할 수 있다.