[7주차] 패키지

janjanee·2022년 8월 1일
0
post-thumbnail

2021.01.14 작성글 이전

7. 패키지

학습 목표 : 자바의 패키지에 대해 학습하세요.

7-1. package 키워드

package

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 선언

package 패키지명;

패키지명 작성 규칙은 다음과 같다.

  • 숫자로 시작해서 안되며 _, $ 를 제외한 특수 문자를 사용해서는 안된다.
  • java로 시작하는 패키지는 자바 표준 API에서만 사용하므로 사용해서 안된다.
  • 모두 소문자로 작성하는 것이 관례이다.

패키지 선언이 포함된 클래스를 컴파일할 경우, 단순히 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 클래스를 사용할 수 있을까?

image

사용 불가능 하다. 컴파일 에러가 발생한다.

그렇다면 같은 unnamed(default) package에 있는 파일 끼리는 가능할까?

image

이름없는 패키지에서는 같은 default package의 파일과 com.jihan.javastudycode.week7 패키지의 파일 모두 사용 가능하다.

요약하자면 다음과 같다.

  • 하나의 소스파일에는 첫 번째 문장으로 단 한번의 패키지 선언만을 허용
  • 모든 클래스는 반드시 하나의 패키지에 속해야 한다
  • 패키지는 점(.)을 구분자로 하여 계층구조로 구성
  • 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리이다.

7-2. import 키워드

import

import 문 ? 컴파일러에게 소스파일에 사용될 클래스의 패키지 정보를 제공

다른 패키지의 클래스를 사용할 경우 패키지명이 포함된 클래스 이름을 사용해야 한다.
하지만 긴 패키지명을 다 쓰기는 번거로워서 import문을 사용해 사용하려는 클래스의 패키지를 미리 명시한다.

import문을 사용하고나면 소스코드 내에서 해당 클래스를 사용할 때 패키지명을 생략하고 사용할 수 있다.

import 선언

1. package2. import3. 클래스 선언

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에는 다음과 같은 것들이 있다.

static import문

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 코드를 짤 때 많이 등장했던 것 같다.

7-3. 클래스패스

클래스패스? 클래스를 찾기위한 경로

JVM이 클래스 파일을 찾는 기준이 되는 파일 경로를 의미한다.

.java 파일을 작성하고 이 파일이 컴파일 된 .class 파일을 JVM이 찾을 때 이 classpath가 사용된다.

classpath는 콜론(:)으로 구분된 디렉토리 및 파일 목록이다.

💡 리눅스, Mac 은 콜론(:)이며, 윈도우는 세미콜론(;)이다.

classpath에 사용가능한 값은 다음 3가지가 있다.

  1. /home/user/java/classes와 같은 디렉토리
  2. myclasses.zip과 같은 zip 파일
  3. myclasses.jar와 같은 jar(자바 아카이브) 파일

세 가지 유형을 모두 적용하면 classpath는 다음과 같게 된다.

/home/user/java/classes:/home/user/java/classes/myclasses.zip:/home/user/java/classes/myclasses.jar

출처 : https://effectivesquid.tistory.com/entry/%EC%9E%90%EB%B0%94-%ED%81%B4%EB%9E%98%EC%8A%A4%ED%8C%A8%EC%8A%A4classpath%EB%9E%80

7-4. CLASSPATH 환경변수

classpath 환경변수는 다음과 같이 OS에 따라 설정할 수 있다.

  • windows

    시스템 설정 > 환경변수

  • 리눅스, 유닉스 계열이 시스템

    /etc/profile 에 추가

그러나 이 방법은 추천되지 않는다. 모든 프로젝트에서 이 환경변수를 바라볼 것이다. 좋지않다!

대신, IDE나 빌드도구를 통해 클래스패스를 프로젝트 별로 설정할 수 있다.
보통 IDE가 자동으로 설정해준다.

image

인텔리제이의 경우 File -> Project Structure -> Project Settings -> Modules 탭 확인

출처: https://gintrie.tistory.com/67

7-5. -classpath 옵션

CLASSPATH 환경변수를 등록하는 방법 말고 java와 javac 명령어 옵션을 사용하여 지정할 수도 있다.

java -classpath(cp) path javac -classpath(cp) path

커맨드 라인에서 -classpath 옵션을 사용하여 실습하는 예제를 만들어보자.

실습을 위해 홈 디렉토리에 tmp 폴더를 생성하고 그 폴더 아래에
1. a_package와 b_package라는 폴더 생성 2. 프로젝트의 클래스 패스는 ~/tmp가 될것이며 a_package와 b_package는 각각 패키지 경로이다.

  • a_package/Hello.java
package a_package;

public class Hello {
    static void print() {
        System.out.println("Hello");
    }
}

a_package 폴더에 Hello.java 클래스 파일을 생성한다. Hello.java 클래스 파일안에는
static 메소드인 print()가 있다.

  • b_package/Test.java
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 파일을 실행해보자.

image

  1. ~/tmp/b_package로 이동하자. (윈도우라면 본인이 만든 c:\tmp 또는 d\tmp 어디든)
  2. javac Test.java 명령어로 Test.java 파일을 컴파일한다.
  3. 에러발생! -> Test.java 파일에서 import 한 a_package.Hello가 존재하지 않는다.

왜 에러가 발생했을까? 이유는 위의 명령어에 javac -classpath . Test.java 생략된 부분이 존재한다.
-classpath 옵션을 지정하지 않으면 자동으로 현재경로(.)으로 생략되어 붙는데,
현재경로인 ~/tmp/b_package에서 a_package.Hello 파일을 찾을 수 없기 때문에 에러가 발생한 것이다.

이 문제를 해결하기 위해 -classpath 옵션을 조정해보자.

image

  1. -classpath ~/tmp 현재령로 대신에 처음 실습에서 언급한 ~/tmp를 classpath로 설정했다.
  2. 정상적으로 컴파일 완료되어 Test.class 파일이 생성되었다.

이제 java 명령어로 컴파일된 파일을 구동시켜보자.

image

  1. 또 에러발생! 에러 발생의 원인은 두 가지가 있다.
    1. FQCN으로 클래스 명을 작성할 것
    2. javac의 문제와 동일하게 -classpath 옵션을 주지 않아서 발생.

image

  1. 위의 두 문제를 해결하여 위와같이 입력하면 정상적으로 실행되는 것을 확인할 수 있다.
  2. -cp 옵션은 -classpath와 동일하다.

추가실습

~/tmp 외에 ~/another 이라는 클래스패스가 새로 생겼고, ~/another/test/Hi.java 파일을
Test.java에서 Hi.print()로 호출한다.

  • ~/another/test/Hi.java
package test;

public class Hi {
    public static void print() {
        System.out.println("Hi!!!!");
    }
}
  • ~/tmp/b_package/Test.java
package b_package;

import a_package.Hello;
import test.Hi;

public class Test {
    public static void main(String[] args) {
        Hello.print();
	      Hi.print();
    }
}

image

  1. 클래스패스가 여러개 이므로 콜론 ':'으로 구분하자. (윈도우 세미콜론 ';' )

  2. 참고로 ~/tmp:~/another가 아니라 ~/tmp:/Users/jihan/another인 이유는

    절대 경로를 사용하지 않으면 에러가 발생한다. /Users/jihan/tmp:/Users/jihan/another 는 물론 가능하다.

7-6. 접근지시자

멤버 또는 클래스에 사용되어, 해당하는 멤버 또는 클래스를 외부에서 접근하지 못하도록 제한하는 역할을 한다.

  • private

    같은 클래스 내에서만 접근이 가능

  • default

    같은 패키지 내에서만 접근이 가능

  • protected

    같은 패키지 내에서, 다른 패키지의 자손 클래스에서 접근 가능

  • public

    접근 제한이 없음

표로 나타내면 다음과 같다.

제어자같은 클래스같은 패키지자손 클래스전체
publicvvvv
protectedvvv
(default)vv
privatev

접근 범위가 넓은 쪽에서 좁은 쪽으로 왼쪽부터 나열하면 다음과 같다.

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을 추가하여 상속할 수 없는 클래스라는 것을 알리는 것이 좋다.

References

profile
얍얍 개발 펀치

0개의 댓글