Java

7주차 - package, import, classpath, 접근 제어자(access modifier)

voider 2021. 1. 2. 15:54

목표

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

학습할 것 (필수)

  • package 키워드
  • import 키워드
  • 클래스패스
  • CLASSPATH 환경변수
  • classpath 옵션
  • 접근지시자

Package

패키지는 관련된 파일을 넣어둔 폴더와 비슷하다. 패키지란 관련된 클래스들을 묶어서 그룹화 해놓은 것을 하나의 패키지라고 한다. 폴더 안에서 또 관심사를 나누는 폴더가 있을 수 있는 것처럼, 패키지 안에 패키지가 존재할 수 있다. 이를 통해 큰 틀로 묶고, 점점 세분화 하는 방식으로 패키지를 나눌 수 있다. 패키지 안의 패키지는 마침표(.)로 구분한다.

java.lang.String str;

패키지는 제공하는 것을 사용해도 되고 새롭게 정의해서 사용해도 된다. 패키지명은 소문자로 만들어서 클래스 이름과 구분할 수 있도록 하는 게 관례다. java.Lang.String이라면 Lang이 클래스인지 패키지인지 바로 구분할 수 없기 때문이다.

모든 클래스는 반드시 하나의 패키지에 속한다. 패키지를 지정하지 않았아도 자바는 unnamed라는 패키지를 제공하여 그곳에 속하게 한다.

패키지를 지정해줬다면 중요한 것은 반드시 첫 줄에 이 클래스가 어느package에 속하는 지 지정해야 한다. 단 한 번, 단 하나의 패키지만 지정할 수 있으며 두 번 정의할 수 없다.

import

같은 이름의 클래스가 여러 개 있더라도 패키지가 다르면 충돌이 발생하지 않는다. 자바에서는 패키지 명까지 포함하여 클래스를 구분하기 때문이다. 만약 ArrayList를 사용하고 싶다면 이렇게 해야 한다.

java.util.List list = new java.util.ArrayList();

어느 패키지에 List를 사용해야 하는지 명확하게 하기 위해 패키지명까지 명시해야 한다. 하지만 누구도 이렇게 사용하지 않는다. 우리는 import를 이용하여 패키지명을 생략하고 클래스 이름만 쓸 수 있다.

import java.util.*;
...
List list = new ArrayList();

이렇게 import를 해주면 어느 클래스의 List를 사용하는 것인지 컴파일러가 판단할 수 있다. import문에서 java.util.*이라고 했는데 *의 의미는 util패키지에 있는 모든 클래스를 imort한다는 뜻이다. 만약 서로 다른 패키지의 같은 이름을 가진 클래스를 쓸 일이 있다면 import보단 패키지명을 명시해서 구분하는 게 좀 더 좋은 방법일 것 같다.

기본으로 import되어 있는 패키지 java.lang

자바에서 몇 가지 패키지를 기본으로 제공한다. 그 중에 하나가 java라는 패키지를 제공한다. 이 패키지 내부에는 유용한 클래스를 제공하는 lang이라는 패키지가 있다. java.lang이 제공하는 유용한 클래스 중에는 흔히 사용하는 String, Ingeger, System같은 것들이 있다. 이런 클래스는 워낙 사용 빈도가 높기 때문에 import없이도 사용할 수 있도록 기본으로 설정 되어 있다. 그래서 String이나 System같은 클래스를 이용할 때 따로 import를 하지 않고 사용할 수 있다.

static import

만약 java.util.Objects라는 클래스가 제공하는 isNull()이라는 static 메소드를 여러 번 사용한다고 해보자. 그럼 곳곳에 Objects.isNull(...)이라는 코드가 도배될 것이다. 이럴 때 static import를 사용해서 Objects를 import해주면 간편하게 스태틱 메소드를 참조할 수 있다.

import static java.util.Objects.isNull;

이렇게 선언만 해주면 클래스를 명시하지 않고 isNull()메소드를 편하게 사용할 수 있다.

package와 import에 대해서 말했는데 선언하는 순서를 반드시 지켜야 한다.

1. 맨 첫 줄에 package. 단 한 번만 가능/

2. 그 다음 줄에 import 여러 번 여러 패키지를 import할 수 있음

3. 클래스 선언

궁금해서 package나 import를 클래스 밑에다 선언해봤는데 컴파일 에러가 발생한다.

아래는 위 규칙을 지킨 예시다.

package java.lang;

import java.io.ObjectStreamField;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
...더 많은 import문들 가능

public final class String....{}

클래스패스

클래스패스는 말 그대로 클래스의 경로다. JVM이 필요한 클래스를 찾을 때, 어느 디렉토리부터 찾을 것인지 기준을 주는 것이다. 클래스패스는 직접 지정할 수도 있고, 환경변수로 설정해둘 수도 있다.

CLASSPATH 환경변수

Linux사용자라면 터미널에 export명령으로 설정된 환경변수를 확인할 수 있다.

declare -x JAVA_HOME="/usr/lib/jvm/java-11-openjdk-amd64"

이렇게 JAVA_HOME이라는 이름으로 JDK 경로가 환경변수로 잡혀있는 것을 확인할 수 있다.

그런데 왜 이런 걸 설정해야 할까? IDE를 사용한다면 사실 클래스패스 환경변수를 잡아주지 않아도 에러가 발생하지 않는다. 하지만 GUI가 없는 환경에서도 자바 파일을 컴파일하고 실행해야 하는 일이 생길 수도 있다(고 한다). 우리가 터미널에서 javac나 java라는 명령을 사용할 수 있는 것은 JAVA_HOME의 환경변수로 JDK경로가 설정되어 있기 때문이다.

-classpath 옵션

Test라는 임의의 자바 파일을 만들어서 컴파일하고 실행해보자.

hoon@hoon-MS-7C51:~/test$ javac Test.java
hoon@hoon-MS-7C51:~/test$ ls
Test.class  Test.java

Test라는 자바파일을 컴파일해서 Test.class파일로 만들었다. 이제 java Test 명령으로 컴파일한 Test파일을 실행할 수 있다.

hoon@hoon-MS-7C51:~/test$ java Test  
Java CLASSPATH Test!!!!!!!!!

하지만 만약 다른 디렉토리 이를 테면 상위 디렉토리에서 이 파일을 실행하면 ClassNotFoundException이 발생한다.

hoon@hoon-MS-7C51:~$ java Test  
오류: 기본 클래스 Test을(를) 찾거나 로드할 수 없습니다.  
원인: java.lang.ClassNotFoundException: Test

이때 -classpath 옵션을 사용할 수 있다. 줄여서 -cp로 사용해도 된다.

hoon@hoon-MS-7C51:~$ java -classpath ./test Test  
Java CLASSPATH Test!!!!!!!!!

접근 제어자Access-Modifiers

자바에는 public, protected, default, private 이렇게 4개의 접근 제어자가 있다.

이미지 출처 : https://www.softwaretestingmaterial.com/access-modifiers-in-java/

  • public - 접근 제한 없음
  • protected 같은 패키지 내, 그리고 다른 패키지의 자손(sub)클래스에서만 접근 가능
  • default 같은 패키지 내에서만 접근 가능
  • private 같은 클래스 내에서만 접근 가능

이미지 출처 :   https://www.softwaretestingmaterial.com/access-modifiers-in-java/

캡슐화

왜 귀찮게 접근을 제어할까? 데이터를 보호하기 위해서다. 클래스 외부에서 데이터를 함부로 변경하지 못하도록 보호할 필요가 있다. 이것을 데이터 감추기Data Hiding이라고 한다. Data Hiding은 객체지향개념에서 캡슐화Encapsulation에 해당하는 내용이다.
내부에서만 이용하고, 외부에는 불필요한 부분을 감추기 위해 멤버를 private으로 지정하는데, 이것 역시 캡슐화에 해당한다.
만약 Time이라는 클래스를 이렇게 정의한다고 하자.

class Time {
  int hour;
  int minute;
  int second;

}

이러면 누가 언제 어디서든 값을 바꿀 수 있기 때문에 위험하고, 또 시간으로 들어와야 하는 값에 25이나 33 같은 유효하지 않은 값을 넣을지도 모른다. 이 문제를 해결하기 위해서 이렇게 쓴다.

public class Time {
    private int hour, minute, second;

    Time(int hour, int minute, int second) {
        setHour(hour);
        setMinute(minute);
        setSecond(second);
    }

    //getter, setter, toString...
}

public class TimeMain {  
    public static void main(String\[\] args) {  
    Time time = new Time(13, 47, 22);  
    //time.hour = 22; 이제 이런식으로 수정하지 않는다.  
    time.setHour(time.getHour()+1);  
    System.out.println(time);  
    }  
}

이런 식으로 하면 Time클래스의 멤버를 직접 수정할 수 없다. setter를 거쳐야 하기 때문에 유효성 검사를 통해 유효하지 않은 값은 return할 수 있다. 그리고 또 하나는 객체는 자율성을 가져야 한다. A라는 객체가 B라는 객체를 직접 수정할 수 있어서는 안 된다. 객체지향프로그래밍이라는 것은 메세지를 주고 받는 것이다. 메세지를 준다는 것은, 무언가를 처리해달라고 요청하는 것이다. 여기서 time.setHour(time.getHour()+1)라는 것은 Time오브젝트에게 현재 시간에서 한 시간을 더 해달라는 메세지를 보내는 것이다. 메세지를 수신한 Time오브젝트는 스스로, 자신의 의지로 타임을 1시간 증가시킨다.외부 오브젝트는 단순히 '요청'만할 뿐 직접적으로 무언가를 수정해서는 안 된다.

생성자의 접근 제어자

일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치하지만 다르게 지정할 수 있다.
private으로 지정하면 외부에서 생성자에 접근할 수 없다. 해당 클래스 내에 인스턴스를 return하는 static메서드를 선언해두면, private생성자여도 외부에서 인스턴스를 생성할 수 있다.

주의할 점은 private 생성자를 가진 클래스는 다른 클래스의 조상이 될 수 없다. 따라서 class 앞에 fianl을 명시적으로 붙이는 것이 좋다.

위에 말이 좀 어려웠는데 코드로 보면 쉽다.

final class Singleton {  
    private Singleton() {}

    public static Singleton getInstance() {
        return new Singleton();
    }
}

public class SingletonTest {  
    public static void main(String\[\] args) {  
        //Singleton singleton = new Singleton(); //ERROR  
        Singleton singleton = Singleton.getInstance();  
    }  
}

참고자료 : 자바의 정석

'Java' 카테고리의 다른 글

[Java]Exception  (0) 2021.02.07
8주차 - 인터페이스  (0) 2021.01.08
다이나믹 디스패치, 더블 디스패치  (0) 2020.12.26
6주차 - 상속  (0) 2020.12.25
5주차 - 클래스, 생성자, 객체, this, 메소드  (0) 2020.12.19