Java의 패키지와 제어자를 통한 캡슐화
패키지란 클래스나 인터페이스의 묶음을 뜻하며 서로 관련된 클래스끼리 그룹 단위로 묶어 패키지에 포함시킴으로써 효율적으로 관리할 수 있게 한다. 지금까지는 단순히 클래스 이름으로만 클래스를 구분했지만 사실 실제 이름은 클래스가 속한 모든 패키지명까지 포함한다. 이를 FQCN(Fully Qualified Class Name이라고 한다. 예를 들어 String 클래스의 FQCN은 java.lang.String
이다.
클래스가 물리적으로 하나의 클래스파일(.class
)인 것처럼 패키지는 물리적으로 하나의 디렉토리이다. 그래서 같은 이름의 클래스일지라도 서로 다른 패키지에 속하면 패키지명으로 구별이 가능하다.
클래스나 인터페이스의 소스파일(.java
)의 최상단에 package 패키지명;
으로 명시함으로써 패키지를 선언할 수 있다. 패키지 선언문은 반드시 소스파일에서 주석과 공백을 제외한 첫 번째 문장이어야 하고 하나의 소스파일에 단 한 번만 선언될 수 있으며 해당 소스파일에 포함된 모든 클래스나 인터페이스는 선언된 패키지에 속하게 된다. 패키지명은 대소문자 모두 허용하지만 원칙적으로 소문자로 하고 있다.
소스파일에 자신이 속할 패키지를 지정하지 않은 클래스는 자동적으로 ‘이름 없는 패키지(unnamed package)’에 속하게 되므로 따로 패키지를 선언하지 않아도 문제가 생기지는 않는다.
소스코드를 작성할 때 다른 패키지의 클래스를 사용하려면 FQCN을 사용해야 한다. 하지만 매번 패키지명을 붙여서 작성하기란 매우 불편한 일이다. 이를 해결하기 위해서 import문을 써준다. import문은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하게 한다. 컴파일 시에 컴파일러는 import문을 통해 소스파일에 사용된 클래스들의 패키지를 알아 낸 다음 모든 클래스명 앞에 패키지명을 붙여 준다. 따라서 소스코드 작성 시 클래스명에서 패키지명을 생략할 수 있다.
java.util.Date today = new java.util.Date();
///////////////////////////////////////////////////////////////
import java.util.Date;
.
.
.
Date today = new Date();
모든 소스파일(.java)에서 import문은 package문 다음에, 그리고 클래스 선언문 이전에 위치해야 하며 여러 번 선언할 수 있다. import문은 import 패키지명.클래스명;
또는 import 패키지명.*;
과 같이 선언할 수 있다. *
는 프로그래밍에서 ‘모든’을 뜻하며 여기서는 패키지에 속하는 모든 클래스를 사용하겠다는 뜻을 나타낸다.
double pi = Math.PI;
double d = Math.random()
///////////////////////////////////////////////////////////////
import static java.lang.Math.*;
double pi = PI;
double d = random();
다른 패키지의 클래스를 사용하려면 분명 FQCN이나 import문을 써야 한다고 했다. 그런데 생각해보면 우린 String 클래스나 System 클래스를 사용하면서 한번도 그런 적이 없다. 바로 이 두 가지가 모두 java.lang이라는 패키지에 속하기 때문인데 java.lang은 모든 클래스에서 암시적으로 선언되는 패키지이다.
modifier는 수식어라는 뜻을 가지고 있다. 자바에서는 이를 제어자라 부르며 클래스, 변수 또는 메서드의 선언부에 부가적인 의미를 부여하는데 사용한다. 영어 뜻대로 이들을 수식하는 것이다. 제어자의 종류는 크게 접근 제어자와 그 외의 제어자로 나눌 수 있다.
public
, protected
, default
, private
static
, final
, abstract
, native
, transient
, synchronized
, volatile
, strictfp
제어자들 간에 순서는 없지만 주로 접근 제어자를 제일 앞에 명시한다.
접근 제어자는 클래스 또는 멤버에 사용되어 해당하는 클래스 또는 멤버를 외부에서 접근하지 못하도록 제한한다. 기본 접근 제어자는 default로 접근 제어자가 명시되어 있지 않으면 모두 default 상태이다.
public
> protected
> (default)
> private
구분 | public | protected | (default) | private |
---|---|---|---|---|
같은 클래스 | ✔ | ✔ | ✔ | ✔ |
같은 패지키의 자식 클래스 | ✔ | ✔ | ✔ | ❌ |
같은 패키지 | ✔ | ✔ | ✔ | ❌ |
다른 패키지의 자식 클래스 | ✔ | ✔ | ❌ | ❌ |
다른 패키지 | ✔ | ❌ | ❌ | ❌ |
protected
는 패키지에 관계없이 상속 받은 자식 클래스라면 접근할 수 있다.접근 제어자를 사용하는 이유는 클래스 내부에 선언된 데이터를 보호하고 감추기 위해서이다.
예를 들어 커피를 나타내기 위한 Coffee 클래스가 있고 커피의 가격은 최소 3000원부터, 샷을 추가하면 추가 금액을 받도록 하려한다. 하지만 제한되지 않은 멤버 변수는 해당 클래스의 인스턴스를 생성한 다음, 멤버 변수에 직접 접근하여 다음과 같이 값을 변경할 수 있다.
public class Coffee{
int price;
int extraShot;
int calculatePrice() {
return price + (extraShot * 500);
}
}
/////////////////////////////////////////////////////////////////////
Coffee coffee = new Coffee();
coffee.price = 100;
coffee.extraShot = -1;
coffee.calculatePrice() // -400원이 됨.
위의 코드처럼 잘못된 값을 지정한다고 해도 이것을 막을 수 없다. 이런 경우 멤버 변수를 private
이나 protected
로 설정하고 멤버 변수의 값을 읽고 변경할 수 있는 public
메서드를 제공함으로써 간접적으로 멤버 변수의 값을 다룰 수 있도록 하는 것이 바람직하다.
public class Coffee{
private int price;
private int extraShot;
public void setPrice(int price) {
if(price < 3000) price = 3000;
this.price = price;
}
public void setExtraShot(int extraShot) {
if(extraShot < 0) extraShot = 0;
this.extraShot = extraShot;
}
public int getPrice() { return price; }
public int getExtraShot() { return extraShot; }
...
}
일반적으로 멤버 변수의 값을 읽는 메서드는 get멤버변수명()
으로, 멤버 변수의 값을 변경하는 메서드는 set멤버변수명()
으로 명명하고 각각을 getter / setter 라고 부른다.
이렇게 접근 제어자를 활용하면 데이터가 유효한 값을 유지하도록 하거나 데이터를 외부에서 함부로 변경하지 못하도록 할 수 있다. 또한 클래스 내에서 내부 작업을 위해 임시로 사용되는, 즉 외부에 공개할 필요가 없는 멤버를 감출 수도 있는데 이를 정보 은닉이라고 한다. 정보 은닉은 객체지향 프로그래밍의 기본 원리에 해당하는 캡슐화의 한 예로 캡슐화는 객체의 상태(변수)와 기능(메서드)을 묶어서 제공하는 것을 의미한다. 자바에서는 변수와 메서드를 하나로 묶어 클래스화함으로써 캡슐화를 구현한다. 캡슐화의 주요 목적은 외부에서 임의로 객체의 상태를 조작하는 작업을 방지하는 것인데 정보 은닉을 통해 그런 일로부터 객체를 보호할 수 있다.
static의 사전적 의미는 ‘고정된’이다. 자바에서 static
으로 선언된 멤버는 Run-Time Data Areas의 Method Area라는 메모리 영역에 생성된다. 이 메모리 영역은 JVM이 동작하기 시작할 때 생성되며 JVM의 모든 스레드에서 공유된다. 그러니까 처음부터 공유될 수 있도록 ‘고정된’ 것이다. static 멤버는 모든 인스턴스가 공통적으로 사용해야 하기 때문에 인스턴스를 생성하지 않고 바로 클래스명.static멤버명
으로 접근해서 사용한다. 그렇기에 static 멤버를 클래스 멤버라고도 부르는 것이다.
한 가지 주의할 점은 인스턴스 메서드는 static 멤버에 접근할 수 있지만 static 메서드는 인스턴스 멤버에 접근할 수 없다. 앞서 언급했다싶이 static 멤버는 JVM 동작이 시작되면 생성된다. 하지만 인스턴스 멤버는 인스턴스를 생성해야만 접근할 수 있으므로 static 멤버와의 생성 시기가 맞지 않는다. static 메서드에서 인스턴스 멤버를 사용하려고 하면 Non-static field '...' cannot be referenced from a static context
라는 에러가 발생하는 것을 확인할 수 있다.
public class StaticMembers {
// 클래스 변수(static 변수)
static int year = 2022;
// 클래스 초기화 블럭(static 초기화 블럭)
static {
// 클래스 변수의 복잡한 초기화 작업
...
}
// 클래스 메서드(static 메서드)
public static void main(String[] args) {
...
}
}
final
은 ‘마지막의’라는 의미로 변경될 수 없는 대상에 사용된다.
abstract
은 ‘추상적인’이라는 의미로 메서드 선언부만 작성하고 실제 수행 내용은 구현하지 않은 추상 메서드를 선언하는데 사용된다. 그리고 추상 클래스에 사용되어 클래스 내에 추상 메서드가 존재한다는 것을 쉽게 알 수 있게 한다. 추상 클래스는 아직 완성되지 않은 메서드가 존재하는 ‘미완성 설계도’이므로 인스턴스를 생성할 수 없다.
메서드와 접근 제어자에 대해서 알았다면 이제 자바의 main
메서드에 대해 파헤쳐 볼 수 있다. 자바 애플리케이션은 모두 public static void main(String[] args)
를 메서드 시그니처로 가지는 main
메서드를 포함하고 있어야 한다. main
메서드는 프로그램의 진입점(entry point)으로써 main
메서드를 가지고 있는 클래스를 JVM이 애플리케이션 실행 시 찾아 제일 먼저 실행시킨다. main
메서드에서 더이상 수행할 동작이 없는 경우 프로그램도 종료된다. 따라서 JVM이 접근할 수 있도록 접근에 제한이 없는 public
을, 인스턴스를 생성하지 않고 가장 먼저 접근할 수 있는 static
을, 메서드가 끝나면 프로그램이 종료되므로 반환 값이 없는 void
를 메서드 시그니처로 정한 것이다. String[] args
는 명령행 인자(command-line argument)로써 콘솔에서 프로그램을 실행하는 경우 넘겨줄 인자를 지정할 수 있다.