패키지
패키지
타입을 더 찾기 쉽고 쓰기 쉽게 만들고, 이름 충돌을 피하며, 접근을 제어하기 위해 프로그래머는 연관된 타입들을 **패키지(package)**로 묶어요.
정의: 패키지는 접근 보호와 네임스페이스 관리를 제공하는, 연관된 타입들의 묶음이에요. 여기서 타입이란 클래스, 인터페이스, 열거형, 애노테이션 타입을 가리켜요. 열거형과 애노테이션 타입은 각각 클래스와 인터페이스의 특수한 종류라서, 이 섹션에서는 흔히 그냥 클래스와 인터페이스라고 부를게요.
본문
Java 플랫폼의 일부인 타입들은 기능별로 클래스를 묶어 둔 여러 패키지의 구성원이에요. 핵심 클래스는 java.lang에, 읽고 쓰기(입출력) 관련 클래스는 java.io에 있고, 이런 식으로 정렬돼 있죠. 여러분 자신의 타입도 패키지에 넣을 수 있어요.
원, 사각형, 선, 점 같은 그래픽 객체를 나타내는 클래스들을 작성했다고 해볼게요. 그리고 마우스로 드래그할 수 있는 클래스들이 구현할 인터페이스 Draggable도 작성했다고 합시다.
//in the Draggable.java file
public interface Draggable {
...
}
//in the Graphic.java file
public abstract class Graphic {
...
}
//in the Circle.java file
public class Circle extends Graphic
implements Draggable {
. . .
}
//in the Rectangle.java file
public class Rectangle extends Graphic
implements Draggable {
. . .
}
//in the Point.java file
public class Point extends Graphic
implements Draggable {
. . .
}
//in the Line.java file
public class Line extends Graphic
implements Draggable {
. . .
}
이 클래스들과 인터페이스를 패키지로 묶어야 할 이유가 여러 가지 있어요.
- 여러분과 다른 프로그래머가 이 타입들이 서로 연관돼 있다는 걸 쉽게 알 수 있어요.
- 여러분과 다른 프로그래머가 그래픽 관련 기능을 제공하는 타입을 어디서 찾을지 알 수 있어요.
- 패키지가 새로운 네임스페이스를 만들기 때문에, 타입의 이름이 다른 패키지의 타입 이름과 충돌하지 않아요.
- 패키지 안의 타입끼리는 서로 자유롭게 접근하게 하면서, 패키지 밖의 타입에는 접근을 제한할 수 있어요.
패키지 만들기
패키지를 만들려면 패키지 이름을 정하고(명명 규칙은 다음 섹션에서 다뤄요), 패키지에 넣고 싶은 타입(클래스·인터페이스·열거형·애노테이션 타입)을 담은 모든 소스 파일의 맨 위에 그 이름으로 된 package 문을 넣으면 돼요.
package 문(예: package graphics;)은 소스 파일의 첫 줄이어야 해요. 각 소스 파일에는 package 문이 하나만 있을 수 있고, 그 문은 파일 안의 모든 타입에 적용돼요.
참고: 하나의 소스 파일에 여러 타입을 넣으면 그중 하나만 public일 수 있고, 그것은 소스 파일과 이름이 같아야 해요. 예를 들어
Circle.java파일에public class Circle을,Draggable.java파일에public interface Draggable을,Day.java파일에public enum Day를 정의할 수 있어요.
public 타입과 같은 파일에 non-public 타입을 넣을 수도 있어요(강력히 권장되진 않지만, non-public 타입이 작고 public 타입과 밀접하게 연관됐다면요). 하지만 패키지 밖에서 접근할 수 있는 건 public 타입뿐이에요. 최상위 non-public 타입은 모두 패키지 전용(package private)이 되죠.
앞선 섹션의 그래픽 인터페이스와 클래스들을 graphics라는 패키지에 넣으려면, 소스 파일이 여섯 개 필요해요.
//in the Draggable.java file
package graphics;
public interface Draggable {
. . .
}
//in the Graphic.java file
package graphics;
public abstract class Graphic {
. . .
}
//in the Circle.java file
package graphics;
public class Circle extends Graphic
implements Draggable {
. . .
}
//in the Rectangle.java file
package graphics;
public class Rectangle extends Graphic
implements Draggable {
. . .
}
//in the Point.java file
package graphics;
public class Point extends Graphic
implements Draggable {
. . .
}
//in the Line.java file
package graphics;
public class Line extends Graphic
implements Draggable {
. . .
}
package 문을 쓰지 않으면 타입은 **무명 패키지(unnamed package)**에 들어가요. 일반적으로 무명 패키지는 작거나 임시적인 애플리케이션용이거나, 개발을 막 시작했을 때만 써요. 그 외에는 클래스와 인터페이스가 이름 있는 패키지에 속해야 해요.
패키지 이름 짓기와 명명 규칙
전 세계의 프로그래머가 Java로 클래스와 인터페이스를 작성하다 보면, 서로 다른 타입에 같은 이름을 쓰는 일이 생기기 마련이에요. 사실 앞선 예가 그렇죠. java.awt 패키지에 이미 Rectangle 클래스가 있는데 Rectangle 클래스를 정의했으니까요. 그래도 두 클래스가 서로 다른 패키지에 있다면 컴파일러는 같은 이름을 허용해요. 각 Rectangle 클래스의 **정규화된 이름(fully qualified name)**은 패키지 이름을 포함해요. 즉 graphics 패키지의 Rectangle 클래스의 정규화된 이름은 graphics.Rectangle이고, java.awt 패키지의 Rectangle 클래스는 java.awt.Rectangle이에요.
이 방식은 잘 동작하지만, 두 명의 독립된 프로그래머가 자기 패키지에 같은 이름을 쓰면 문제가 생겨요. 그걸 막는 건 뭘까요? **규칙(convention)**이에요.
패키지 이름은 클래스나 인터페이스의 이름과 충돌하지 않도록 전부 소문자로 써요.
회사는 패키지 이름을 뒤집은 인터넷 도메인 이름으로 시작해요. 예를 들어 example.com의 프로그래머가 만든 mypackage라는 패키지는 com.example.mypackage로 쓰죠.
한 회사 안에서 생기는 이름 충돌은 그 회사 안의 규칙으로 처리해야 해요. 회사 이름 뒤에 지역이나 프로젝트 이름을 붙이는 식으로요(예: com.example.region.mypackage).
Java 언어 자체의 패키지는 java. 또는 javax.로 시작해요.
때로는 인터넷 도메인 이름이 유효한 패키지 이름이 아닐 수도 있어요. 도메인 이름에 하이픈이나 다른 특수 문자가 포함된 경우, 패키지 이름이 숫자나 Java 이름의 시작으로 쓰기 불법인 다른 문자로 시작하는 경우, 또는 패키지 이름이 int 같은 예약 키워드를 포함하는 경우가 그렇죠. 그럴 때 권장되는 규칙은 밑줄을 추가하는 거예요. 예를 들어:
| 도메인 이름 | 패키지 이름 접두사 |
|---|---|
hyphenated-name.example.org |
org.example.hyphenated_name |
example.int |
int_.example |
123name.example.com |
com.example._123name |
패키지 멤버 사용하기
패키지를 구성하는 타입들을 **패키지 멤버(package member)**라고 불러요.
패키지 밖에서 public 패키지 멤버를 사용하려면 다음 중 하나를 해야 해요.
- 멤버를 정규화된 이름으로 참조하기
- 패키지 멤버를 import하기
- 멤버가 속한 패키지 전체를 import하기
각각은 상황에 따라 알맞게 쓰이는데, 이어지는 섹션에서 하나씩 설명할게요.
멤버를 정규화된 이름으로 참조하기
지금까지 이 튜토리얼의 대부분의 예제는 Rectangle, StackOfInts처럼 타입을 단순 이름으로 참조했어요. 패키지 멤버의 단순 이름은, 작성 중인 코드가 그 멤버와 같은 패키지에 있거나 그 멤버가 import됐다면 쓸 수 있어요.
하지만 다른 패키지의 멤버를 쓰려는데 그 패키지가 import되지 않았다면, 패키지 이름이 포함된 멤버의 정규화된 이름을 써야 해요. 앞선 예의 graphics 패키지에 선언된 Rectangle 클래스의 정규화된 이름은 이렇습니다.
graphics.Rectangle
이 정규화된 이름으로 graphics.Rectangle의 인스턴스를 만들 수 있어요.
graphics.Rectangle myRect = new graphics.Rectangle();
정규화된 이름은 가끔 쓸 때는 괜찮아요. 하지만 이름을 반복해서 써야 한다면, 매번 전체를 타이핑하는 게 지루하고 코드도 읽기 어려워져요. 그럴 땐 멤버나 그 패키지를 import하고 단순 이름을 쓰면 됩니다.
패키지 멤버 import하기
현재 파일에 특정 멤버를 import하려면, 타입 정의 앞이면서 package 문(있다면) 뒤에 import 문을 두면 돼요. 앞선 섹션에서 만든 graphics 패키지에서 Rectangle 클래스를 import하는 방법은 이렇습니다.
import graphics.Rectangle;
이제 Rectangle 클래스를 단순 이름으로 참조할 수 있어요.
Rectangle myRectangle = new Rectangle();
graphics 패키지의 멤버를 몇 개만 쓴다면 이 방식이 잘 맞아요. 하지만 패키지의 타입을 많이 쓴다면 패키지 전체를 import하는 편이 좋아요.
패키지 전체 import하기
특정 패키지에 포함된 모든 타입을 import하려면, 별표(*) 와일드카드 문자와 함께 import 문을 사용해요.
import graphics.*;
이제 graphics 패키지의 어떤 클래스나 인터페이스든 단순 이름으로 참조할 수 있어요.
Circle myCircle = new Circle();
Rectangle myRectangle = new Rectangle();
import 문의 별표는 보시다시피 패키지 안의 모든 클래스를 지정할 때만 쓸 수 있어요. 패키지 안의 클래스 일부만 매칭하는 데는 쓸 수 없어요. 예를 들어 다음은 graphics 패키지에서 A로 시작하는 모든 클래스를 매칭하지 않아요.
// does not work
import graphics.A*;
오히려 이건 컴파일 오류를 일으켜요. import 문으로는 보통 단일 패키지 멤버나 패키지 전체만 import해요.
참고: 덜 흔한 또 다른 import 형태는, 바깥 클래스의 public 중첩 클래스를 import하게 해줘요. 예를 들어
graphics.Rectangle클래스에Rectangle.DoubleWide,Rectangle.Square같은 유용한 중첩 클래스가 있다면, 다음 두 문으로Rectangle과 그 중첩 클래스를 import할 수 있어요.import graphics.Rectangle; import graphics.Rectangle.*;두 번째 import 문은
Rectangle자체를 import하지 않는다는 점에 주의하세요.
또 다른 덜 흔한 import 형태인 정적 import(static import) 문은 이 섹션의 끝에서 다룰게요.
편의상 Java 컴파일러는 소스 파일마다 두 개의 패키지 전체를 자동으로 import해요.
java.lang패키지- 현재 패키지(현재 파일의 패키지)
패키지의 겉보기 계층 구조
처음에는 패키지가 계층 구조처럼 보이지만, 실제로는 그렇지 않아요. 예를 들어 Java API에는 java.awt 패키지, java.awt.color 패키지, java.awt.font 패키지, 그리고 java.awt로 시작하는 다른 패키지들이 많이 있어요. 하지만 java.awt.color 패키지나 java.awt.font 패키지, 그 외 java.awt.xxxx 패키지들은 java.awt 패키지에 포함되지 않아요. java.awt(Java Abstract Window Toolkit)라는 접두사는 연관된 여러 패키지의 관계를 드러내기 위해 쓰일 뿐, 포함 관계를 나타내는 게 아니에요.
java.awt.*를 import하면 java.awt 패키지의 모든 타입은 import되지만, java.awt.color나 java.awt.font, 그 외 java.awt.xxxx 패키지는 import되지 않아요. java.awt.color의 클래스와 타입도 java.awt의 것들과 함께 쓰려면, 두 패키지를 모두 그 파일들까지 포함해 import해야 합니다.
import java.awt.*;
import java.awt.color.*;
이름 모호성
한 패키지의 멤버가 다른 패키지의 멤버와 이름을 공유하고 두 패키지 모두 import됐다면, 각 멤버를 정규화된 이름으로 참조해야 해요. 예를 들어 graphics 패키지는 Rectangle이라는 클래스를 정의했고, java.awt 패키지에도 Rectangle 클래스가 있어요. graphics와 java.awt가 모두 import됐다면, 다음 코드는 모호해요.
Rectangle rect;
이런 상황에서는 정확히 어떤 Rectangle 클래스를 원하는지 밝히기 위해 멤버의 정규화된 이름을 써야 해요. 예를 들어:
graphics.Rectangle rect;
정적 import 문
한두 개의 클래스에서 static final 필드(상수)와 정적 메서드에 자주 접근해야 하는 상황이 있어요. 그런 클래스의 이름을 매번 접두사로 붙이다 보면 코드가 지저분해지죠. 정적 import(static import) 문은 사용하려는 상수와 정적 메서드를 import해서, 클래스 이름을 접두사로 붙일 필요를 없애줘요.
java.lang.Math 클래스는 PI 상수와 많은 정적 메서드를 정의해요. 사인, 코사인, 탄젠트, 제곱근, 최대, 최소, 지수 등을 계산하는 메서드들이죠. 예를 들어:
public static final double PI = 3.141592653589793;
public static double cos(double a) {
...
}
보통은 다른 클래스에서 이들을 쓰려면 클래스 이름을 접두사로 붙입니다.
double r = Math.cos(Math.PI * theta);
static import 문을 사용하면 java.lang.Math의 정적 멤버를 import해 Math 클래스 이름을 붙일 필요가 없어져요. Math의 정적 멤버는 개별적으로 import할 수도 있고,
import static java.lang.Math.PI;
묶음으로 import할 수도 있어요.
import static java.lang.Math.*;
일단 import되면 정적 멤버를 접두사 없이 사용할 수 있어요. 예를 들어 앞선 코드 조각은 이렇게 바뀌어요.
double r = Math.cos(PI * theta);
물론 자주 쓰는 상수와 정적 메서드를 담은 자신만의 클래스를 만들고 정적 import 문을 쓸 수도 있어요. 예를 들어:
import static mypackage.MyConstants.*;
참고: 정적 import는 아주 드물게 사용하세요. 정적 import를 남용하면 코드를 읽고 유지보수하기 어려워져요. 코드를 읽는 사람이 특정 정적 객체를 어떤 클래스가 정의했는지 모르게 되거든요. 제대로 쓰면 정적 import는 클래스 이름 반복을 없애 코드를 더 읽기 좋게 만들어줘요.
패키지 정리하기
타입을 위한 패키지를 만들려면, 그 타입(클래스·인터페이스·열거형·애노테이션 타입)을 담은 소스 파일의 첫 번째 문으로 package 문을 두면 돼요.
다른 패키지에 있는 public 타입을 사용하려면 세 가지 선택지가 있어요.
- 타입의 정규화된 이름을 사용하기
- 타입을 import하기
- 타입이 멤버로 속한 패키지 전체를 import하기
패키지의 소스 파일·클래스 파일의 경로 이름은 패키지 이름을 그대로 따라요.
컴파일러와 JVM이 여러분의 타입에 대한 .class 파일을 찾을 수 있도록 CLASSPATH를 설정해야 할 수도 있어요.