학습 목표 : 자바의 패키지에 대해 학습하세요.
package 란? 클래스의 묶음이다.
패키지 안에는 클래스 또는 인터페이스를 포함시킬 수 있으며, 서로 관련된 클래스들끼리
그룹 단위
로 묶어 클래스를 효율적으로 관리할 수 있다.
클래스의 실제 이름(full name)은 패키지명을 포함한 것이다.
예를 들어, String 클래스의 실제 이름은 java.lang.String
이다.
이것을 보통 FQCN(Fully Qualified Class Name)
라고 한다.
String s1 = "hello";
java.lang.String s2 = "hello"; // FQCN
클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리
이다.
java.lang.String 클래스는 물리적으로 디렉토리 java의 서브디렉토리인 lang에 속한 String.class 파일이다. String 클래스는 rt.jar 또는 src.zip 파일에 압축되어 있다.
rt.jar는 JRE에 포함되어 있으며, JRE가 없어진 버전 이후로는 src.zip에서 확인 가능하다.
package 패키지명;
패키지명 작성 규칙은 다음과 같다.
패키지 선언이 포함된 클래스를 컴파일할 경우, 단순히 javac ClassName.java
로 컴파일하면
패키지 폴더가 생성되지 않는다.
-d 옵션을 추가하고 패키지가 생성될 경로를 다음과 같이 지정한다.
javac -d . // 현재 폴더에 생성
javac -d ../bin // 현재 폴더와 같은 위치의 bin 폴더에 생성
javac -d home/tmp // home/tmp 폴더에 생성
모든 패키지는 반드시 하나의 패키지에 포함되어야 하는데, 패키지가 존재하지 않다면
자바에서 기본으로 제공하는 이름없는 패키지(unnamed package or default package)
가 패키지가 된다.
package com.jihan.javastudycode.week7;
public class PackageTest {
public static void main(String[] args) {
System.out.println("hello!");
}
}
com.jihan.javastudycode.week7 패키지에 존재하는 파일이다.
public class UnnamedPackage {
public static void main(String[] args) {
System.out.println("Unnamed package");
}
}
패키지 경로가 없는 unnamed package이다.
그럼 PackageTest에서 이름없는 패키지에 있는 UnnamedPackage 클래스를 사용할 수 있을까?
사용 불가능 하다. 컴파일 에러가 발생한다.
그렇다면 같은 unnamed(default) package에 있는 파일 끼리는 가능할까?
이름없는 패키지에서는 같은 default package의 파일과 com.jihan.javastudycode.week7 패키지의 파일 모두 사용 가능하다.
요약하자면 다음과 같다.
- 하나의 소스파일에는 첫 번째 문장으로 단 한번의 패키지 선언만을 허용
- 모든 클래스는 반드시 하나의 패키지에 속해야 한다
- 패키지는 점(.)을 구분자로 하여 계층구조로 구성
- 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.
import 문 ? 컴파일러에게 소스파일에 사용될 클래스의 패키지 정보를 제공
다른 패키지의 클래스를 사용할 경우 패키지명이 포함된 클래스 이름을 사용해야 한다.
하지만 긴 패키지명을 다 쓰기는 번거로워서 import문
을 사용해 사용하려는 클래스의 패키지를 미리 명시한다.
import문을 사용하고나면 소스코드 내에서 해당 클래스를 사용할 때 패키지명을 생략하고 사용할 수 있다.
1. package문
2. import문
3. 클래스 선언
import 문은 package문 다음에, 클래스 선언문 이전에 위치해야 한다.
import문은 package와 달리 한 소스파일에 여러 번 선언할 수 있다. 당연히 한 소스파일에서 여러 패키지에 있는
클래스를 사용하기 때문이다.
import 패키지명.클래스명;
or
import 패키지명.*;
import 할 때 클래스명 까지 작성하는 방법과 '*' 기호를 사용하여 해당 패키지의 모든 일치하는 클래스를 맵핑할 수 있다.
import java.util.Date;
import java.util.ArrayList;
import java.util.Calendar;
or
import java.util.*;
💡 '*'은 하위 패키지의 클래스 까지 모두 포함하는 것은 아니다.
package com.jihan.javastudycode.week7;
public class PackageTest {
public static void main(String[] args) {
String test = "hello";
System.out.println("hello!");
}
}
위의 코드를 보면 String과 System.out.println을 사용했음에도 불구하고 어떠한 import 문이 보이지 않는다
왜 그런것일까?
import java.lang.*;
정답은 모든 소스파일에 위의 코드가 묵시적으로 선언되어있기 때문이다. java.lang 패키지의 경우 매우 자주 사용되는
중요한 클래스들이 속한 패키지이기 때문에 따로 import 문을 지정하지 않아도 된다.
위와 같이 자바에서 제공하는 Java API에 포함된 클래스들의 패키지들을 Built-in package
라고 한다. built-in package에는 다음과 같은 것들이 있다.
java
lang
awt
javax
swing
net
io
util
sql
...
static import ? static 멤버를 호출할 때 클래스 이름을 생략할 수 있다.
특정 클래스의 static 멤버를 자주 호출 할 경우 편리하며, 코드도 간결해진다.
import static java.lang.Math.random;
public class PackageTest {
public static void main(String[] args) {
System.out.println(random());
}
}
Math.random() static 메소드를 import 하여 random()으로 사용하는 예제이다.
주로 static import는 Test 코드를 짤 때 많이 등장했던 것 같다.
클래스패스? 클래스를 찾기위한 경로
JVM이 클래스 파일을 찾는 기준이 되는 파일 경로를 의미한다.
.java 파일을 작성하고 이 파일이 컴파일 된 .class 파일을 JVM이 찾을 때 이 classpath가 사용된다.
classpath는 콜론(:)으로 구분된 디렉토리 및 파일 목록이다.
💡 리눅스, Mac 은 콜론(:)이며, 윈도우는 세미콜론(;)이다.
classpath에 사용가능한 값은 다음 3가지가 있다.
세 가지 유형을 모두 적용하면 classpath는 다음과 같게 된다.
/home/user/java/classes:/home/user/java/classes/myclasses.zip:/home/user/java/classes/myclasses.jar
classpath 환경변수는 다음과 같이 OS에 따라 설정할 수 있다.
windows
시스템 설정 > 환경변수
리눅스, 유닉스 계열이 시스템
/etc/profile 에 추가
그러나 이 방법은 추천되지 않는다.
모든 프로젝트에서 이 환경변수를 바라볼 것이다. 좋지않다!
대신, IDE나 빌드도구를 통해 클래스패스를 프로젝트 별로 설정할 수 있다.
보통 IDE가 자동으로 설정해준다.
인텔리제이의 경우 File -> Project Structure -> Project Settings -> Modules 탭 확인
출처: https://gintrie.tistory.com/67
CLASSPATH 환경변수를 등록하는 방법 말고 java와 javac 명령어 옵션을 사용하여 지정할 수도 있다.
java -classpath(cp) path javac -classpath(cp) path
커맨드 라인에서 -classpath 옵션을 사용하여 실습하는 예제를 만들어보자.
실습을 위해 홈 디렉토리에 tmp 폴더를 생성하고 그 폴더 아래에
1. a_package와 b_package라는 폴더 생성 2. 프로젝트의 클래스 패스는 ~/tmp
가 될것이며 a_package와 b_package는 각각 패키지 경로이다.
package a_package;
public class Hello {
static void print() {
System.out.println("Hello");
}
}
a_package 폴더에 Hello.java 클래스 파일을 생성한다. Hello.java 클래스 파일안에는
static 메소드인 print()가 있다.
package b_package;
import a_package.Hello;
public class Test {
public static void main(String[] args) {
Hello.print();
}
}
b_package 폴더에 Test.java 클래스 파일을 생성한다.
Test.java 클래스 파일은 main 메소드에서 Hello의 print() 메소드를 호출한다.
이제 terminal 혹은 cmd 창을 띄워서 컴파일을 시도해보자.
👉 목표 : b_pacakage 아래에 있는 Test.java 파일을 실행해보자.
javac Test.java
명령어로 Test.java 파일을 컴파일한다. 왜 에러가 발생했을까? 이유는 위의 명령어에 javac -classpath . Test.java
생략된 부분이 존재한다.
-classpath 옵션을 지정하지 않으면 자동으로 현재경로(.)으로 생략되어 붙는데,
현재경로인 ~/tmp/b_package에서 a_package.Hello 파일을 찾을 수 없기 때문에 에러가 발생한 것이다.
이 문제를 해결하기 위해 -classpath 옵션을 조정해보자.
-classpath ~/tmp
현재령로 대신에 처음 실습에서 언급한 ~/tmp
를 classpath로 설정했다.이제 java 명령어로 컴파일된 파일을 구동시켜보자.
~/tmp 외에 ~/another 이라는 클래스패스가 새로 생겼고, ~/another/test/Hi.java 파일을
Test.java에서 Hi.print()로 호출한다.
package test;
public class Hi {
public static void print() {
System.out.println("Hi!!!!");
}
}
package b_package;
import a_package.Hello;
import test.Hi;
public class Test {
public static void main(String[] args) {
Hello.print();
Hi.print();
}
}
클래스패스가 여러개 이므로 콜론 ':'
으로 구분하자. (윈도우 세미콜론 ';' )
참고로 ~/tmp:~/another
가 아니라 ~/tmp:/Users/jihan/another
인 이유는
절대 경로
를 사용하지 않으면 에러가 발생한다. /Users/jihan/tmp:/Users/jihan/another 는 물론 가능하다.
멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다.
private
같은 클래스
내에서만 접근이 가능
default
같은 패키지
내에서만 접근이 가능
protected
같은 패키지
내에서, 다른 패키지의 자손
클래스에서 접근 가능
public
접근 제한이 없음
표로 나타내면 다음과 같다.
제어자 | 같은 클래스 | 같은 패키지 | 자손 클래스 | 전체 |
---|---|---|---|---|
public | v | v | v | v |
protected | v | v | v | |
(default) | v | v | ||
private | v |
접근 범위가 넓은 쪽에서 좁은 쪽으로 왼쪽부터 나열하면 다음과 같다.
public > protected > (default) > private
💡 default 라는 것은 아무 접근 제어자도 붙어이지 않은 상태
대상 | 사용가능한 접근 제어자 |
---|---|
클래스 | public, (default) |
메서드 | public, protected, (default), private |
멤버변수 | public, protected, (default), private |
지역변수 | 없음 |
클래스나 멤버에 접근 지시자를 사용하는 이유는 클래스의 내부에 선언된 데이터를 보호
하기 위해서이다.
또 다른 이유로는, 클래스 내에서만 사용되는 내부 작업을 위해 임시로 사용되는 멤버변수나 작업을 처리
하기 위한
메소드 등의 멤버들을 클래스 내부에 감추기 위해서이다.
정리하자면 다음과 같다.
- 외부로부터 데이터 보호
- 외부에서 불필요한, 내부적으로만 사용되는 부분을 감추기 위해
public class Person {
int age;
String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
}
public class AccessModifier {
public static void main(String[] args) {
Person p = new Person(20, "김자바");
p.age = 30;
System.out.println(p.age);
}
}
예제를 보면, Person 클래스가 있고, AccessModifier의 main 메소드에서 Person 인스턴스를 생성한다.
나이 20살의 김자바라는 사람을 만들어내는데, 아랫줄에 바로 나이를 30으로 바꿔버렸다.
출력해보면 김자바의 나이가 30살이 되어버렸다???
만약, 처음 생성한 인스턴스의 나이를 변경할 수 없게 바꾸려면 어떻게 해야할까?
이름은 개명을 한다고 가정하에 변경가능하게 하자.
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
}
Person 클래스를 다음과 같이 변경했다. age와 name에 접근지시자 private
을 사용하여,
해당 클래스 내부에서만 접근 가능하도록 변경한다.
우리는 나이를 변경하고 싶지 않다. 대신 나이를 출력 하고는 싶을 수 있다. private를 붙였기 때문에
당연히 다른 클래스에 p.age
코드는 에러일 것이다.
이때, getAge() 와 같은 게터(getter) 메소드
를 작성한다. 아주 많이들 작성해왔을 것이고, 손으로 작성하기 보다는
IDE 툴에서 기본적으로 다 제공을 한다(인텔리제이, 이클립스). 이것도 귀찮으면 lombok을 사용하자.
getAge()를 보면 age를 리턴하고 있으므로 외부에서 age값 변경은 할 수 없지만, age 값을 확인할 수 있다.
name의 경우 개명을 할 수도 있으니, setName() 과 같은 세터(setter) 메소드
를 작성한다.
p.name
으로 하면되지 왜 setter가 필요할까? 라고 생각할 수도 있는데
아무나 이름을 다 바꿔줘서는 안된다. 개명한 사람일 경우에만 이름을 바꿔줘야하는 요구사항이 들어왔을 경우,
setName()에서 이 사람이 개명을 한 사용자 인지 체크 후 이름을 변경해 줄 수 있다.
setter와 getter는 주로 쌍으로 작성하는 경우가 많으나, 필요한 경우 각각 하나씩 작성할 수도 있다.
생성자에 접근 제어자를 사용하여 인스턴스의 생성을 제한할 수 있다.
보통 생성자의 접근 제어자는 클래스의 접근 제어자와 같지만, 다르게 지정할 수 있다.
public class Singleton {
private static Singleton s = new Singleton();
private Singleton () {}
public static Singleton getInstance() {
return s;
}
}
디폴트 생성자에 private 키워드를 사용하여 인스턴스 생성을 막는다
.
getInstance() 메소드에서 new Singleton()으로 만든 객체를 리턴한다.
이때, static 키워드가 붙은 이유는 인스턴스 생성을 하지 않고 호출하기 위함이다.
생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 왜냐하면, 자손클래스의 인스턴스를 생성할때,
조상클래스의 생성자를 호출하는 것이 불가능하기 때문이다.
그래서 클래스 앞에 final
을 추가하여 상속할 수 없는 클래스라는 것을 알리는 것이 좋다.