패키지(Package)

패키지는 관련된 클래스, 인터페이스, 하위 패키지들을 묶어놓은 상자와 같습니다. 각 패키지는 고유한 이름을 가지고 있으며, 관련 있는 항목들을 그룹화하여 관리하기 용이하도록 만들어줍니다. 파일 시스템에서 살펴보면 패키지가 디렉터리라는 사실을 알 수 있는데, 같은 패키지에 속해있는 클래스의 소스 파일은 같은 디렉터리에 있는 것을 볼 수 있습니다.

상단에 보이는 src 디렉터리는 자바 IDE(이클립스, 인텔리제이 등)가 소스 파일을 저장하는 곳이며 이 디렉터리 자체는 패키지가 아닙니다. src 디렉터리 아래에 있는 모든 하위 디렉터리는 자바 패키지와 동일합니다. 디렉터리 안에 또 다른 디렉터리가 있을 수 있는 것처럼, 패키지 아래에도 또 다른 하위 패키지가 있을 수 있습니다. 예를 들어서 com.oracle.util이라는 패키지에 Lock이란 클래스가 속해있다면 아래와 같은 디렉터리 구조를 상상할 수 있습니다.

자바에는 크게 두 종류의 패키지가 있는데, 하나는 자바에서 기본적으로 제공하는 패키지와 우리가 만드는 사용자 정의 패키지가 있습니다. 여기서는 사용자 정의 패키지를 만드는 법을 살펴봅니다.

패키지 생성하기

소스 코드에서 패키지는 아래와 같이 지정할 수 있습니다. package문은 소스 코드의 맨 앞에 작성해야 합니다. 예를 들어서, package문 앞에 클래스 선언이나 import문이 있으면 컴파일 에러가 발생합니다.

package 패키지명;

그리고 패키지에 클래스를 추가하려면 패키지와 동일한 디렉터리에 소스 파일을 넣어야 에러가 발생하지 않습니다.

패키지명 작명 관습

이 내용은 오라클 자바 문서에서 확인할 수 있습니다. 자바 패키지명은 클래스나 인터페이스와 이름이 겹치는 것을 방지하기 위해서 항상 소문자로 작성합니다. 그리고 회사는 역순 도메인명 표기법(reverse domain name notation)을 따라 도메인 이름을 뒤집어서 패키지명을 시작합니다. 예를 들어서 example.com의 프로그래머가 만든 mypackage 패키지의 이름은 com.example.mypackage을 사용합니다.

역순 도메인명 표기법(reverse domain name notation)

역순 도메인명 표기법은 프로그래밍 언어, 시스템, 프레임워크에서 쓰이는 컴포넌트, 패키지, 타입, 파일 이름에 관한 작명 관습이다. 역순 DNS 문자열은 등록된 도메인명을 기준으로 하며, 그룹화를 위해서 순서를 반대로 뒤집는다. 예를 들어서, 제품 "MyProduct"를 만드는 회사가 도메인 example.com을 가지고 있다면 해당 제품을 식별하기 위한 이름으로 역순 DNS 문자열인 "com.example.MyProduct"를 쓸 수 있다. 모든 도메인명은 모든 등기된 소유주마다 고유하므로 역순 DNS명은 네임스페이스 충돌을 없애는 간단한 방법이다.

한 회사 내에서는 이름이 겹치는 것을 방지하기 위해서 com.example.region.mypackage와 같이 회사의 이름 뒤에 지역이나 프로젝트 이름을 넣는 식으로 처리합니다.

하지만 도메인 이름에 하이픈(-)이나 기타 특수문자가 포함되어 있거나, 숫자로 시작하거나, 패키지명에 int 같은 자바 키워드가 포함된 경우는 패키지명으로 사용할 수 없으니 아래와 같이 밑줄 기호(_)를 추가하는 것을 권장합니다.

패키지 만들기

인텔리제이(IntelliJ)

인텔리제이에서는 프로젝트 창에서 원하는 디렉터리를 우클릭 후 New > Package를 눌러서 패키지를 만들 수 있습니다. 참고로 일일이 마우스를 움직여서 만드는게 번거롭다면 Ctrl+Alt+Insert를 눌러서 현재 디렉터리에 패키지를 만들거나, Ctrl+1을 누르고 프로젝트 창에 포커스를 잡은 뒤 원하는 위치로 이동하여 Alt+Insert를 누르는 방법도 있습니다.

이클립스(Eclipse)

이클립스에서 패키지를 만드는 방법은 다음과 같습니다. 좌측에 보이는 Package Explorer에서 프로젝트를 우클릭하여 'New > Package'를 누릅니다.

그리고 Name에 원하는 패키지명을 입력하고 'Finish' 버튼을 누르면 Package Explorer에서 패키지가 만들어진 것을 확인할 수 있습니다. 여기서 패키지를 우클릭하여 'New > Class'를 누르면 패키지에 클래스를 추가할 수 있습니다.

예제 살펴보기

abc 패키지

src 디렉터리에 abc란 패키지를 만들고 그 안에 Foo.java와 Bar.java를 추가해봅시다. 인텔리제이에서 확인하면 아래와 같아야 합니다.

그 다음에는 Foo.java에 다음과 같이 작성합시다.

package abc;

public class Foo {
    private int length;
    int[] data;
    protected String[] tags;
    public String name;
}

Bar.java에는 다음과 같이 작성합시다. 우리가 접근 제어자에서 살펴봤던 것처럼 동일 패키지에 있는 다른 클래스의 public, protected, package-private 멤버에 접근할 수 있습니다.

package abc;

public class Bar {
    private final Foo foo;

    public Bar(Foo foo) {
        this.foo = foo;
    }

    public void doSomething() {
        System.out.println("Bar.doSomething");

		/* private은 동일 패키지에서도 접근 불가 */
        // foo.length = 10;
        /* default(package-private)는 동일 패키지에서 접근 가능 */
        foo.data = new int[10];
        /* protected는 동일 패키지에서도 접근 가능 (혹은 다른 패키지에 있는 자식 클래스에서) */
        foo.tags = new String[10];
        /* public은 어디에서나 접근 가능 */
        foo.name = "abc";
    }

    public static void main(String[] args) {
        Bar bar = new Bar(new Foo());
        bar.doSomething();
    }
}

xyz 패키지

이번에는 src 디렉터리에 xyz란 패키지를 새로 만들고 그 안에 Bar.java를 추가해봅시다. 인텔리제이에서 살펴보면 아래와 같아야 합니다.

그리고 Bar.java는 아래와 같이 작성합시다. 이 클래스는 abc.Foo를 상속받기 때문에 protected로 제한된 멤버에는 접근할 수 있으나, default(package-private)로 제한된 멤버에는 접근할 수 없습니다.

package xyz;  
  
import abc.Foo;

public class Bar extends Foo {
    public void doSomething() {
        System.out.println("Bar.doSomething");

        /* private은 동일 패키지에서도 접근 불가 */
        // foo.length = 10;
        /* default(package-private)는 동일 패키지에서 접근 가능 */
        // data = new int[10];
        /* protected는 동일 패키지에서도 접근 가능 (혹은 다른 패키지에 있는 자식 클래스에서) */
        tags = new String[10];
        /* public은 어디에서나 접근 가능 */
        name = "abc";
    }
}

pqr 패키지

마지막으로 xyz 패키지 아래 pqr이란 하위 패키지를 새로 만들고 그 안에 Qux.java를 추가해봅시다. 인텔리제이에서 살펴보면 아래와 같아야 합니다.

그리고 Qux.java는 아래와 같이 작성합시다.

package xyz.pqr;

public class Qux {
    public static void main(String[] args) {
        Bar bar = new Bar();
        bar.doSomething();
    }
}

그러면 인텔리제이 기준으로 "'Bar'라는 심볼을 해석할 수 없습니다(Cannot resolve symbol 'Bar')"라는 컴파일 에러가 발생할 것입니다. 이는 abc 패키지에도 Bar가 있고, xyz 패키지에도 Bar가 있어서 어느 패키지의 Bar를 가리키는지 알 수 없기 때문입니다. 따라서 아래와 같이 직접 완전수식 이름(fully qualified name)으로 입력해주어야 합니다. 그러면 컴파일 에러가 사라진 것을 볼 수 있습니다.

완전수식 이름(fully qualified name)

클래스의 완전수식 이름은 패키지명이 앞에 붙은 클래스명을 말합니다. 예를 들어서, Example 클래스가 com.example.subpackage 패키지에 있는 경우 클래스의 완전수식 이름은 com.example.subpackage.Example를 말합니다.

xyz.Bar bar = new xyz.Bar();
bar.doSomething();

혹은 다음과 같이 import문을 사용하여 모호함을 제거할 수도 있다. 참고로 F2를 눌러 에러가 발생한 곳으로 이동하고 Alt+Enter를 누른 뒤 'Import class'를 눌러도 됩니다.

package xyz.pqr;

import xyz.Bar; // 여기!

public class Qux {
    public static void main(String[] args) {
        Bar bar = new Bar();
        bar.doSomething();
    }
}

import

타입 임포트(Type Import)

이미 위에서 대충 짐작이 드셨겠지만, 다른 패키지에 있는 멤버를 사용하려면 우선 사용할 패키지를 import 해야 쓸 수 있습니다. import는 아래와 같이 사용합니다.

// 해당 패키지에 있는 모든 멤버(클래스, 인터페이스, 애노테이션 등)에 접근할 수 있음
import 패키지명.*;

// 해당 패키지에 있는 특정 멤버에 접근할 수 있음
import 패키지명.멤버명;

예를 들어 패키지 com.example에 있는 모든 멤버에 접근하려면 아래와 같이 쓸 수 있습니다.

import com.example.*;

com.example 패키지에 있는 Example이라는 특정 멤버에 접근하려는 경우 아래와 같이 쓸 수 있습니다.

import com.example.Example;

여기서 주의할 점은 import문은 아래와 같이 package문이 끝나고 난 다음에 써야 한다는 것입니다. 예를 들어서, package문 아래나 클래스 선언 위에 작성하면 컴파일 에러가 발생하게 됩니다.

package com.example;

import com.example.subpackage.*;

public class Example {
	// ...
}

여러 번 import하면 프로그램이 느려질 수 있을까요?

컴파일 시간이 조금은 느려질 수는 있으나 프로그램 실행 중 성능에는 영향을 주지 않습니다. 즉 (1), (2), (3)은 모두 컴파일 후에는 차이가 없습니다. 그저 컴파일러가 소스 파일에서 Arrays를 마주쳤을 때 이 클래스가 어느 패키지에 있는지 보조 정보를 제공해주는 것뿐입니다. 실제로 클래스 파일(.class)에는 import 관련 내용이 없습니다.

// (1)
import java.util.*;
System.out.println("arr: " + Arrays.toString(arr));

// (2)
import java.util.Arrays;
System.out.println("arr: " + Arrays.toString(arr));

// (3)
System.out.println("arr: " + java.util.Arrays.toString(arr));

정적 임포트(Static Import)

정적 멤버를 가져오는 static import도 있습니다. 이 static import를 사용하면 정적 멤버(정적 필드, 정적 메서드 등)에 접근할 때 패키지명 뿐만 아니라 클래스명도 생략할 수 있게 됩니다. 예를 들어서, java.lang.Math 클래스에는 로그, 삼각함수, 제곱근 등과 같은 기본 수치 연산을 지원하는 여러 정적 메서드가 정의되어 있습니다. 이런 정적 메서드들을 빈번하게 호출하거나 복잡한 식을 작성할 때마다 클래스명이 계속해서 반복되는 것을 보면, 뭔가 조금이라도 줄일 수 있는 방법은 없을지 고민해보게 됩니다. 클래스명이 빠져도 메서드명을 보면 기능을 바로 파악할 수 있기에 클래스명을 생략할 수 있다면 좋지 않을까요?

public class PackageTutorial {
    public static void main(String[] args) {
        double a = 123.45, b = 234.56;
        System.out.println("floor(a) = " + Math.floor(a)); // 123.0
        System.out.println("ceil(b) = " + Math.ceil(b)); // 235.0
        System.out.println("round(a) = " + Math.round(a)); // 123
        // ...
    }
}

이럴 때 바로 다음과 같이 static import를 사용할 수 있습니다.

// 해당 클래스에 있는 특정 정적 멤버(정적 필드, 정적 메서드, 정적 클래스 등)
import static 패키지명.클래스명.정적멤버명;

// 해당 클래스에 있는 모든 정적 멤버
import static 패키지명.클래스명.*;

static import를 예제에 적용하면 아래와 같이 수정할 수 있습니다.

import static java.lang.Math.*;

public class PackageTutorial {
    public static void main(String[] args) {
        double a = 123.45, b = 234.56;
        System.out.println("floor(a) = " + floor(a)); // 123.0
        System.out.println("ceil(b) = " + ceil(b)); // 235.0
        System.out.println("round(a) = " + round(a)); // 123
        // ...
    }
}

하지만 이를 과도하게 사용하면 어느 클래스에서 왔는지 알 수 없기 때문에 코드 가독성이 떨어지는 문제점이 있어서 자주 사용되지는 않습니다. 사용 시에는 가독성을 크게 해치지 않는 범위 내에서 적절하게 사용하는 것이 중요합니다.

주의해야 할 점

패키지 선언은 하나만

소스 코드마다 패키지 선언은 하나만 올 수 있습니다. 즉, 하나의 클래스가 여러 개의 패키지에 속할 수는 없습니다. 아래와 같이 작성하면 에러가 발생합니다.

package abc;
package xyz;

하지만 import문은 아래와 같이 여러 번 사용할 수 있습니다.

import java.util.*;
import java.lang.*;

하위 패키지는 같이 import 되지 않는다

자바 표준 패키지인 java.util에는 날짜나 시간을 나타내는 클래스, 의사난수를 만드는 클래스, 배열을 조작하는 데 쓸 수 있는 클래스 등 유용한 유틸리티 클래스들이 모여있습니다. 물론 이 아래에도 다양한 하위 패키지들이 존재합니다. 예를 들어서 동시성 프로그래밍(concurrent programming)에 유용한 클래스들은 java.util.concurrent 패키지에 모여있습니다. 여기서 java.util 패키지도 가져오고 싶고, 그 하위 패키지인 java.util.concurrent 패키지도 가져오고 싶다면 import java.util.* 처럼 작성하면 되지 않을까요?

import java.util.*;

public class PackageTutorial {
    public static void main(String[] args) {
        int[] data = {5, 7, 8, 2, 3, 4, 9, 1, 6};
        Arrays.sort(data); // 배열을 오름차순으로 정렬한다.
        // 배열을 간편하게 출력한다.
        System.out.println("data: " + Arrays.toString(data));

		// 원자적 연산을 지원하는 동기화 관련 클래스로 이는 추후에 살펴본다.
        AtomicInteger atomicData = new AtomicInteger(3);
        System.out.println("atomicData: " + atomicData.get());
    }
}

하지만 11행에서 에러가 발생합니다. 무엇이 잘못된 걸까요? 1행의 'import java.util.*'은 java.util 패키지 바로 아래에 있는 클래스를 모두 import 하는 문장입니다. 여기서 java.util의 하위 패키지에 있는 클래스들은 import 되지 않습니다. 하위 패키지에 있는 클래스를 사용하려면 import문 아래와 같이 별도로 작성해야 한다는 점을 기억해주세요.

import java.util.*;
// java.util.concurrent.atomic 패키지 바로 아래에 있는 클래스들을 모두 import함
import java.util.concurrent.atomic.*;

public class PackageTutorial {
    public static void main(String[] args) {
	    // ...
        // 이 클래스는 java.util.concurrent.atomic 패키지 바로 아래에 있다.
        AtomicInteger atomicData = new AtomicInteger(3);
        // ...
    }
}