[Java] Package

8 minute read

Published:

In this post, basic concpet of package is introduced.

Java Package

Package 는 클래들을 논리적으로 묶는 이름공간이다.

Package Declaration

다음과 같이 자바 소스코드에 Package 선언을 할 수 있다.

package com.example.utils;

public class MyUtil{
  // some java code ...
}

패키지 루트는 package 선언의 시작점이 되는 디렉터리로, 내가 선언한 package의 한 단계 상위 폴더이다. 즉, 패키지 루트는 package 이름과 실제 디렉토리 구조가 이어지기 시작하는 최상위 디렉터리이다.

예를 들어, 다음과 같은 파일 구조가 있다고 해보자.

project-root/
 └── src/              ← 패키지 루트
      └── com/
          └── example/
              ├── utils/
              │    └── MyUtil.java
              └── app/
                   └── Main.java

package com.example.utils; 라는 패키지 선언을 했다면, com/ 디렉터리가 패키지 루트 바로 아래 있어야 하고 이때 패키지 루트는 src 이다.

Compile & Run command with Package

다음의 명령어로 Compile 할 수 있다.

javac -d <출력할디렉토리> -cp <클래스패스> <소스파일>
  • 클래스 패스 란, JVM 이 .class 파일을 찾는 기준 경로이다. -cp 뒤에 지정한 경로를 기준으로 패키지 구조(import 뒤에 나온 패키지 구조)를 따라 들어가서 찾는다. -cp 옵션의 값을 지정하지 않는다면 현재 디렉터리(.) 이 클래스 패스가 된다.
  • Main.java에서 import com.example.utils.MyUtil; 로 MyUtil을 import 하고 있다고 생각하자. Java 컴파일러는 (위 디렉터리 예시에서) Main.java -> Main.class 를 만드는 과정에서 참조하는 다른 클래스 (MyUtil) 를 찾아야 한다.

위 컴파일 명령어를 위 프로젝트 구조에 대입하여 보면 다음과 같이 할 수 있다.

cd project-root
javac -d out src/com/example/utils/MyUtil.java src/com/example/app/Main.java
  • -d out 이라 작성했으므로 현재 디렉터리 아래에 out 이라는 디렉터리를 만들고, 패키지 구조 그대로 .class 파일이 저장된다. -d 옵션을 적지 않으면 .java 파일이 위치한 곳에 .class 파일이 함께 생성된다.

  • 위 경우엔 -cp 값을 설정하지 않았으므로 클래스 패스가 현재 디렉터리가 된다. 그러면 Main.java에서 MyUtil을 참조할 때, project-root/com/example/utils/ 에서 MyUtil.class 가 있는지를 확인할 것이다. 하지만 이 경로는 src가 빠져있으므로 잘못된 경로이다. 그럼에도 컴파일이 되는 이유는 이미 동시에 컴파일 중인 파일 목록 안에 MyUtil.java 가 있기 때문이다.

  • 이번엔 다음과 같은 경우를 생각해보자.

    cd project-root
    javac -cp src -d out src/com/example/app/Main.java
    

    이번에는 -cp를 지정했지만, Main.java만 컴파일을 시도하고 있다. 따라서, 이 시점에 MyUtil.java는 컴파일 되지 않았고 따라서 .class 파일도 존재하지 않는다.

    • 컴파일러는 MyUtil을 찾을 때 먼저 클래스 패스 + 패키지 구조인 src/com/example/utils/MyUtil.class 존재 여부 확인한다.

    • .class가 없으면 -sourcepath에서 .java를 찾는다.-sourcepath 기본값은 클래스패스와 동일하다.

      즉,src/com/example/utils/ 에서 src/com/example/utils/MyUtil.java를 찾는다.

      따라서 javac은 MyUtil.java를 자동으로 읽어와서 같이 컴파일한다.

    project-root/
     ├── src/
     │    └── com/
     │         └── example/
     │              ├── utils/
     │              │    └── MyUtil.java
     │              └── app/
     │                   └── Main.java
     └── out/
          └── com/
               └── example/
                    ├── utils/
                    │    └── MyUtil.class
                    └── app/
                         └── Main.class
      
    

    이때, 출력할디렉토리의 경로는 꼭 위처럼 패키지 루트와 동등할 필요 없다. 어느 경로를 지정해도 상관없다.

  • 같은 디렉터리의 .java 파일을 모두 컴파일 하고 싶을 때는 다음과 같이 *을 이용할 수 있다.

    javac -d out src/com/example/app/*.java
    
  • Compile을 할 파일 위치를 쓸 때 패키지 루트가 포함되도록 적어야 하고 현재 어느 위치에서 컴파일을 하는지는 상관 없다.

    예를 들어, 현재 위치가 project-root/src/com/example 라 해보자. 여기서 cd 로 이동하지 않고 바로 컴파일 명령어를 적고 싶다면 아래와 같이 할 수 있다.

    javac -d ../../../out ../../com/example/utils/MyUtil.java ../../com/example/app/Main.java
    

    반대로, project-root 보다 상위 폴더에서 컴파일 명령어를 적는 다고 가정하자. 현재 위치가 project-root의 한단계 상위 디렉터리인 /home/user 라고 해보자. 그럼 다음과 같이 컴파일 할 수 있다.

    javac -d project-root/out project-root/src/com/example/app/Main.java project-root/src/com/example/utils/MyUtil.java
    

다음의 명령어로 실행할 수 있다.

java -cp <패키지루트> <패키지명.클래스명>

이를 위 프로젝트 구조에 대입해보면 다음과 같이 할 수 있다.

cd project
java -cp out com.example.app.Main
  • <패키지루트> 란, 컴파일 단계에서 -d 로 지정한 루트 디렉토리 (이 경우 out) 을 의미한다.
    • 소스코드에서 패키지 선언을 할 때의 패키지루트와 다르다.
  • 두 번째 인자로는 말 그대로 <패키지.클래스명> 을 적으면 되는데, 파일의 경로를 적는 컴파일 단계와 다르게, 패키지루트 이후의 패키지 경로만을 적으면 된다.
    • classpath 란 JVM이나 Compiler가 .class 코드를 어디서 찾을지 알려주는 경로 목록이다. -cp 옵션 뒤에 오는 인자가 classpath로 설정된다.

Access Modifier, Import & Package

project-root1/                       ← 첫 번째 프로젝트 루트
 └── src1/                           ← 패키지 루트
      └── com/
           └── example/
                ├── utils/
                │    └── MyUtil.java       (package com.example.utils)
                │
                └── app/
                     └── Main.java         (package com.example.app)
										 └── Test.java				 (package com.example.app)


project-root2/                       ← 두 번째 프로젝트 루트
 └── src2/                           ← 패키지 루트
      └── org/
           └── demo/
                └── app/
                     └── MainApp.java         (package org.demo.app)

다음과 같은 디렉터리 구조를 보자.

  • 같은 패키지 안에 있는 클래스라면 import 없이 바로 사용 가능하다.
    • Main.java 에서 Test.java의 클래스를 사용하고 싶다고 하자. 둘은 같은 패키지에 있으므로 별도의 import 없이 아래와 같이 사용가능하다.

      package com.example.app;
          
      public class Main {
          public static void main(String[] args) {
              Test t = new Test(); // no need to import
              t.run();
          }
      }
      
    • Class (Test)의 Access Modifier

      • public, default : 모두 접근 가능
    • 멤버 필드, 메서드의 Access Modifier

      • public, protected, default : 접근 가능
      • private : (다른 파일에서) 접근 불가능
  • 다른 패키지 안에 있는 클래스라면 import 를 해야한다.

    • Main.java에서 MyUtil.java의 클래스를 사용하고 싶다고 하자. 다음과 같이 import 하여 사용 가능하다.

      package com.example.app;
          
      import com.example.utils.MyUtil;   // need to import
          
      public class Main {
          public static void main(String[] args) {
              MyUtil util = new MyUtil();
              util.sayHello();
          }
      }
      

      이 때, 두 클래스가 속한 패키지의 패키지 루트 (클래스 패스, src1) 가 동일하므로 아래와 같이 컴파일 하면 된다.

      cd project-root
      javac -cp src -d out src1/com/example/app/Main.java
      
    • Class (MyUtil)의 Access Modifier

      • public : 접근 가능
      • default 접근 불가능
    • 멤버 필드, 메서드의 Access Modifier : (class가 public 이라는 가정 하에)

      • public : 접근 가능
      • protected : 다른 패키지라 하더라도 상속 받은 클래스인 경우 (Main이 Test를 상속받은 경우) 접근 가능, 아니면 불가능
      • default, private : 접근 불가능
  • 다른 패키지 안에 있는 클래스이고 두 클래스가 속한 패키지의 패키지 루트가 다른 경우, 컴파일 과정이 달라진다.

    • Project-root 1의 Main.java 에서 Project-root 2의 MainApp.java 를 import 하고 싶다고 하자. 우선, 패키지가 다르므로 다음과 같이 import 하여 사용 가능하다.

      package com.example.app;
          
      import org.demo.app.MainApp;   // (4) import
          
      public class Main {
          public static void main(String[] args) {
              MainApp otherMain = new MainApp();
              otherMain.run();
          }
      }
      

      이 때, 두 클래스가 속한 패키지의 패키지 루트가 다르므로 아래와 같이 MainApp 의 클래스 패스를 추가해 주어야 한다.

      cd /home/user/project-root1
      javac -d out -cp src1:/home/user/project-root2/src2 \
          src1/com/example/app/Main.java
      

      이 때, : 를 이용하면 여러 개의 클래스 패스를 지정할 수 있다. src1을 하나의 클래스 패스로 지정하고 (Main에서 Test도 import 하고 있다고 생각하면 필요하다), MainApp.java의 클래스 패스도 추가해 준다. \ 는 줄바꿈을 의미하고 마지막에 컴파일할 파일의 주소를 적는다.

Naming Collision

class의 이름이 같아서 충돌이 생길 수 있다. 예를 들어, 가장 최근의 예시에서 MainApp.java가 아니라 Main.java 라고 생각해보자. 그리고 project 1의 Main class에서 project 2의 Main class를 import 하고 싶다고 하자. 이런 경우, 다음과 같이 import를 하면 이름 충돌 문제가 생긴다.

package com.example.app;

import org.demo.app.Main;   

public class Main {        
    public static void main(String[] args) {
        Main otherMain = new Main();   // 어떤 Main 인지 모호함
    }
}

따라서, 다음과 같이 import를 아예 안 쓰고, 경로 전체를 써주면 충돌을 없앨 수 있다. 이를 FQCN(Fully Qualified Class Name) 이라고 한다.

package com.example.app;

public class Main {
    public static void main(String[] args) {
        org.demo.app.Main otherMain = new org.demo.app.Main(); // FQCN 사용
        otherMain.run();
    }
}

Access Modifier

위에서 Access Modifier에 따른 접근 가능 여부를 따졌는데 그 규칙에 해당하는 표는 아래와 같다.

Access ModifierClassPackageSubclassGlobal
PublicOOOO
ProtectedOOOX
DefaultOOXX
PrivateOXXX

Default Package

Default Package 에서의 default는 access modifier 에서 등장하는 default 와 다른 개념이다. package 선언이 없으면 그 클래스는 default package에 속한다. default package에 있는 클래스는 오직 같은 default package 안에서만 접근 가능하다.

  • 같은 디렉토리의 defalut 패키지 : 접근 가능

    project/
     ├─ Hello.java   (package 없음. default package)
     └─ Main.java    (package 없음. default package)
    

    별도의 package 설정을 하지 않은 class는 default package 에 속하게 된다. 이 때, default package 에 속한 서로 다른 클래스는 서로 별도의 import 없이 접근 가능하다.

    public class Main {
        public static void main(String[] args) {
            Hello h = new Hello(); // 별도의 import 없이 접근 가능!
            h.hi();
        }
    }
    

    이 경우에도 컴파일을 하려면 위의 내용과 동일하게

    cd project
    javac Main.java
    

    이렇게 컴파일하면 현재 디렉터리가 클래스 패스로 지정되어 Hello.java를 함께 컴파일 해준다.

  • 다른 디렉토리의 default 패키지 : 접근 불가

    project/
     ├─ Hello.java           (package 없음. default package)
     └─ subdir/
         └─ SubMain.java     (package 없음. default package)
    

    이 경우 서로 다른 디렉토리에 있는 default package 클래스는 JVM이 연결하지 못한다. 즉, import 문법도 안 되고, 그냥 접근 자체가 불가능하다.

Leave a Comment