Java 기초 10 - 패키지, 접근 제어자
패키지
컴퓨터는 보통 파일을 분류하기 위해 폴더, 디렉토리라는 개념을 제공한다. 자바도 이런 개념을 제공하는데, 이것이 바로 패키지다.
패키지 사용
package pack;
public class Data {
public Data() {
System.out.println("패키지 pack Data 생성");
}
}
- 패키지를 사용하는 경우 항상 코드 첫줄에 package pack과 같이 패키지 이름을 적어주어야 한다.
- 여기서는 pack 패키지에 Data 클래스를 만들었다.
- 이후에 Data 인스턴스가 생성되면 생성자를 통해 정보를 출력한다.
package pack.a;
public class User {
public User() {
System.out.println("패키지 pack.a 회원 생성");
}
}
- pack 하위에 a라는 패키지를 제작
- pack.a 패키지에 User 클래스 제작
- 이후에 User 인스턴스가 생성되면 생성자를 통해 정보를 출력한다.
참고: 생성자에 public을 사용, 다른 패키지에서 이 클래스의 생성자를 호출하려면 public을 사용해야 한다.
package pack;
public class PackageMain1 {
public static void main(String[] args) {
Data data = new Data();
pack.a.User user = new pack.a.User();
}
}
pack 패키지 위치에 PackageMain1 클래스를 제작
- 사용자와 같은 위치 : PackageMain1과 Data는 같은 pack이라는 패키지에 소속 됨, 같은 패키지에 있는 경우에는 패키지 경로 생략 가능
- 사용자와 다른 위치 : PackageMain1과 User는 서로 다른 패키지, 패키지가 다른 경우 pack.a.User와 같이 패키지 전체 경로를 포함해서 클래스를 적어야 됨.
패키지 - import
import
패키지의 전체 경로를 불편하기 때문에 import를 사용
package pack;
import pack.a.User;
public class PackageMain2 {
public static void main(String[] args) {
Data data = new Data();
User user = new User();
}
}
코드 첫줄에는 package를 사용, 다음 줄에는 import를 사용할 수 있다.
import 사용 시 다른 패키지에 있는 클래스를 가져와서 사용할 수 있다(import 사용 시 패키지 경로를 생략하고 사용할 수 있음).
특정 패키지에 포함된 모든 클래스를 포함해서 사용하고 싶은 경우 import *을 사용하면 된다.
package pack;
import pack.a.*;
public class PackageMain2 {
public static void main(String[] args) {
Data data = new Data();
User user = new User(); // import 사용으로 패키지 명 생략 가능
User2 user2 = new User2();
}
}
클래스 이름 중복
패키지 덕에 클래스 이름이 같아도 패키지 이름으로 구분해서 같은 이름의 클래스를 사용할 수 있음
package pack.b;
public class User {
public User() {
System.out.println("패키지 pack.b 회원 생성");
}
}
package pack;
import pack.a.User;
public class PackageMain3 {
public static void main(String[] args) {
User userA = new User();
pack.b.User UserB = new pack.b.User();
}
}
같은 이름의 클래스가 있으면 import는 둘 중 하나만 선택 가능. 자주 사용하는 클래스를 import하고 나머지를 패키지를 포함한 전체 경로를 적어주면 됨.
패키지 규칙
패키지 규칙
- 패키지의 이름과 위치는 폴더(디렉토리) 위치와 같아야 한다(필수).
- 패키지의 이름은 모두 소문자를 사용한다(관례).
- 패키지의 이름 앞 부분에는 일반적으로 회사의 도메인 이름을 거꾸로 사용(예: com.company.myapp) - 관례
- 필수는 아니지만 수 많은 외부 라이브러리가 함께 사용되면 같은 패키지에 같은 클래스 이름이 존재할 수 있음(도메인 이름을 거꾸로 사용하는 것으로 문제 방지)
- 오픈소스나 라이브러리를 만들어 외부에 제공한다면 지키는 것이 좋음
- 제작한 애플리케이션을 다른 곳에 공유하지 않고, 직접 배포한다면 보통 문제가 되지 않음
패키지와 계층 구조
패키지는 보통 다음과 같이 계층 구조를 이룬다.
- a
- b
- c
이 경우 총 3개의 패키지가 존재한다.
a, a.b, a.c
계층 구조상 a 패키지 하위에 a.b 패키지와 a.c 패키지가 있음. 이것은 우리 눈에 보기에 계층 구조를 이룰 뿐이며 a패키지와 a.b, a.c 패키지는 완전히 다른 패키지임.
따라서 a패키지에서 다른 패키지의 클래스가 필요하면 import를 사용해야 한다.
패키지가 계층 구조를 이루더라도 모든 패키지는 서로 다른 패키지임.
패키지 활용
큰 애플리케이션의 패키지 구성(예시)
전체 구조도
- com.helloshop
- user
- User
- UserService
- product
- Order
- OrderService
- OrderHistory
- user
패키지를 구성할 때 서로 관련된 클래스는 하나의 패키즈로 모으고, 관련이 적은 클래스는 다른 패키지로 분리하는 것이 좋다.
접근 제어자
접근 제어자 이해
자바는 public, private 같은 접근 제어자(access modifier)를 제공함, 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다.
예제) 스피커 소프트웨어 개발(스피커 음량은 100을 넘기면 부품이 고장나므로 절대 넘으면 안 됨).
package access;
public class Speaker {
int volume;
Speaker(int volume) {
this.volume = volume;
}
void volumeUp() {
if (volume >= 100) {
System.out.println("음량을 증가할 수 없습니다. 최대 음량입니다.");
} else {
volume += 10;
System.out.println("음량을 10 증가합니다.");
}
}
void volumeDown() {
volume -= 10;
System.out.println("volumeDown 호출");
}
void showVolume() {
System.out.println("현재 음량: " + volume);
}
}
생성자를 통해 초기 음량 값을 지정할 수 있음.
package access;
public class SpeakerMain {
public static void main(String[] args) {
Speaker speaker = new Speaker(90);
speaker.showVolume();
speaker.volumeUp();
speaker.showVolume();
speaker.volumeUp();
speaker.showVolume();
}
}
새로운 개발자가 기존 코드를 이어받아 수정하게 될 때 기존 요구사항을 모르고 Speaker 클래스의 volume 필드를 직접 사용해 필드 값을 제한 수치를 넘게 설정하고 실행할 수 있음.
SpeakerMain - 필드 직접 접근 수정
package access;
public class SpeakerMain {
public static void main(String[] args) {
Speaker speaker = new Speaker(90);
speaker.showVolume();
speaker.volumeUp();
speaker.showVolume();
speaker.volumeUp();
speaker.showVolume();
// 필드에 직접 접근
System.out.println("volume 필드 직접 접근 수정");
speaker.volume = 200;
speaker.showVolume();
}
}
Speaker 객체를 사용하는 사용자는 Speaker의 volume 필드와 메서드에 모두 접근할 수 있다.
앞서 volumeUp()과 같은 메서드를 만들어서 음량이 100이 넘지 못하도록 기능을 개발했지만 소용이 없다. Speaker를 사용하는 입장에서는 volume 필드에 직접 접근해서 원하는 값을 설정할 수 있기 때문.
이런 문제를 근본적으로 해결하기 위해서는 volume 필드의 외부 접근을 막을 수 있는 방법이 필요하다.
접근 제어자 이해 2
문제를 근본적으로 해결하는 방법은 volume필드를 Speaker 클래스 외부에서 접근하지 못하게 막는 것이다.
Speaker - volume 접근 제어자를 private로 수정
package access;
public class Speaker {
private int volume; // private 사용
...
}
private 접근 제어자는 모든 외부 호출을 막는다. 따라서 private이 붙은 경우 해당 클래스 내부에서만 호출할 수 있다.
volume 필드를 private을 사용해서 Speaker 내부에 숨겼다, 외부에서 volume 필드에 직접 접근할 수 없게 막은 것(volume 필드는 이제 Speaker 내부에서만 접근할 수 있다).
Speaker코드 실행 시 IDE에서 speaker.volume = 200 부분에 컴파일 오류가 발생한다(volume 필드는 private으로 설정되어 외부에서 접근할 수 없음).
Speaker 클래스 개발자가 처음부터 private을 사용해서 volume 필드의 외부 접근을 막아두었다면 새로운 개발자도 volume필드에 직접 접근하지 않고, 메서드를 통해서 접근했을 것임, 결과적으로 스피커 고장 문제가 발생하지 않았을 것(접근 제어자의 필요 이유).
접근 제어자 종류
자바는 4가지 종류의 접근 제어자를 제공함.
접근 제어자의 종류
- private : 모든 외부 호출을 막는다.
- default (package-private) : 같은 패키지 안에서 호출은 허용한다.
- protected : 같은 패키지 안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
- public : 모든 외부 호출을 허용한다.
순서대로 private이 가장 많이 차단하고, public이 가장 많이 허용한다.
private → default → protected → public
package-private
접근 제어자를 명시하지 않으면 같은 패키지 안에서 호출을 허용하는 default 접근 제어자가 적용.
default는 해당 접근 제어자가 기본값으로 사용되서 붙여진 이름, package-private가 더 정확한 표현, 해당 접근 제어자를 사용하는 멤버는 동일한 패키지 내의 다른 클래스에서만 접근이 가능하기 때문.
접근 제어자 사용 위치
접근 제어자는 필드와 메서드, 생성자에 사용됨.
추가로 클래스 레벨에도 일부 접근 제어자를 사용할 수 있음.
접근 제어자 예시
public class Speaker { // 클래스
private int volume; // 필드
public Speaker(int volume) {} // 생성자
public void volumeUp() {} // 메서드
...
}
접근 제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것
- private은 나의 클래스 안으로 속성과 기능을 숨길 때 사용, 외부 클래스에서 해당 기능을 호출할 수 없다.
- default는 나의 패키지 안으로 속성과 기능을 숨길 때 사용, 외부 패키지에서 해당 기능을 호출할 수 없다.
- protected는 상속 관계로 속성과 기능을 숨길 때 사용, 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.
- public은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.
접근 제어자 사용 - 필드, 메서드
패키지 위치가 매우 중요함(패키지 위치에 주의!)
필드, 메서드 레벨의 접근 제어자
package access.a;
public class AccessData {
public int publicField;
int defaultField;
private int privateField;
public void publicMethod() {
System.out.println("publicMethod 호출 " + publicField);
}
void defaultMethod() {
System.out.println("defaultMethod 호출 " + defaultField);
}
private void privateMethod() {
System.out.println("privateMethod 호출 " + privateField);
}
public void innerAccess() {
System.out.println("내부 호출");
publicField = 100;
defaultField = 200;
privateField = 300;
publicMethod();
defaultMethod();
privateMethod();
}
}
- 패키지 위치는 package access.a 이다. 패키지 위치를 꼭 맞춰야 함(주의).
- 순서대로 public, default, private을 필드와 메서드에 사용
- 마지막에 innerAccess( )가 있는데, 이 메서드는 내부 호출을 보여 줌(내부 호출은 자기 자신에게 접근하는 것, private 포함 모든 곳에 접근 가능)
외부에서 이 클래스에 접근할 경우(동일 패키지)
package access.a;
public class AccessInnerMain {
public static void main(String[] args) {
AccessData data = new AccessData();
// public 호출
data.publicField = 1;
data.publicMethod();
// 같은 패키지 default 호출
data.defaultField = 2;
data.defaultMethod();
// private 호출 불가
// data.privateField = 3;
// data.privateMethod();
data.innerAccess();
}
}
- 패키지 위치는 package access.a이다. 패키지 위치 맞추기(주의).
- public은 모든 접근 허용(필드, 메서드 모두 접근 가능)
- default는 같은 패키지에서 접근 가능(AccessInnerMain은 AccessData와 같은 패키지)
- private은 Access 내부에서만 접근할 수 있음(호출 불가).
- AccessData.innerAccess( ) 메서드는 public, 외부 호출 가능(메서드는 외부에서 호출되었으나, 메서드는 AccessData 내부에 있음, 자신의 private 필드와 메서드에 접근 가능).
외부 패키지에서 클래스에 접근하는 경우
package access.b;
import access.a.AccessData;
public class AccessOuterMain {
public static void main(String[] args) {
AccessData data = new AccessData();
// public 호출
data.publicField = 1;
data.publicMethod();
// 다른 패키지 default 호출 불가
//data.defaultField = 2;
// data.defaultMethod();
// private 호출 불가
// data.privateField = 3;
// data.privateMethod();
data.innerAccess();
}
}
- 패키지 위치는 package access.b이다. 패키지 위치 맞추기(주의)
- public은 모든 접근을 허용(필드, 메서드 모두 접근 가능)
- default는 같은 패키지에서만 접근 가능(access.b.AccessOuterMain은 access.a.AccessData와 다른 패키지로 default 접근 제어자에 접근 불가)
- private는 AccessData 내부에서만 접근 가능(호출 불가)
- AccessData.innerAccess( ) 메서드는 public으로 외부 호출 가능(메서드는 외부에서 호출되지만, 해당 메서드는 자신의 private 필드와 메서드에 접근 가능)
접근 제어자 사용 - 클래스 레벨
클래스 레벨의 접근 제어자 규칙
- 클래스 레벨의 접근 제어자는 public, default만 사용할 수 있다.
- private, protected는 사용할 수 없음
- public 클래스는 반드시 파일명과 이름이 같아야 한다.
- 하나의 자바 파일에 public 클래스 하나만 등장할 수 있음.
- 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.
publicClass
package access.a;
public class PublicClass {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
DefaultClass1 class1 = new DefaultClass1();
DefaultClass2 class2 = new DefaultClass2();
}
}
class DefaultClass1 {
}
class DefaultClass2 {
}
- 패키지 위치 package access.a 패키지 위치 맞추기
- PublicClass라는 이름의 클래스 만듦. 이 클래스는 public 접근 제어자. 따라서 파일과 클래스의 이름이 반드시 같아야 한다(public 클래스는 외부 접근 가능).
- DefaultClass1, DefaultClass2는 default 접근 제어자. 이 클래스는 default이기에 같은 패키지 내부에서만 접근 가능
- PublicClass의 main()을 보면 각각의 클래스 사용하는 예를 보여줌.
PublicClassInnerMain
package access.a;
public class PublicClassInnerMain {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
DefaultClass1 class1 = new DefaultClass1();
DefaultClass2 class2 = new DefaultClass2();
}
}
- 패키지 위치는 package access.a임, 패키지 위치 맞추기
- PublicClass는 public 클래스(외부 접근 가능)
- PublicClassInnerMain와 DefaultClass1, DefaultClass2는 같은 패키지(접근 가능)
package access.b;
//import access.a.DefaultClass1;
import access.a.PublicClass;
public class PublicClassOuterMain {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
// 다른 패키지 접근 불가
// DefaultClass1 class1 = new DefaultClass1();
// DefaultClass2 class2 = new DefaultClass2();
}
}
- 패키지 위치는 package access.b 패키지 위치 맞추기
- PublicClass는 public 클래스(외부 접근 가능)
- PublicClassOuterMain와 DefaultClass1, 2는 다른 패키지(접근할 수 없다).
캡슐화
캡슐화(Encapsulation)은 객체 지향 프로그래밍의 중요 개념 중 하나.
데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말함.
캡슐화를 통해 데이터의 직접적인 변경을 방지 및 제한 가능.
속성과 기능을 하나로 묶고, 외부에 꼭 필요한 기능만 노출하고 나머지는 모두 내부로 숨기는 것.
캡슐화를 안전하게 완성할 수 있게 해주는 장치가 접근 제어자
1. 데이터 숨기기
객체에는 속성(데이터)과 기능(메서드)가 있음. 캡슐화에서 필수로 숨겨야 하는 것은 속성(데이터)임.
객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있게 돼 캡슐화가 깨진다.
객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.
2. 기능을 숨기기
객체의 기능 중 외부에서 사용하지 않고 내부에서만 사용하는 기능은 모두 감추는 것이 좋음.
사용자 입장에서 꼭 필요한 기능만 외부에 노출하고 나머지 기능은 모두 내부로 숨기자.
정리하면 데이터는 모두 숨기고, 기능은 꼭 필요한 기능만 노출하는 것이 좋은 캡슐화이다.
예제 BankAccount
package access.b;
public class BankAccount {
private int balance;
public BankAccount() {
balance = 0;
}
// public 메서드 : deposit
public void deposit(int amount) {
if (isAmountValid(amount)) {
balance += amount;
} else {
System.out.println("유효하지 않은 금액입니다.");
}
}
// public 메서드 : withdraw
public void withdraw(int amount) {
if (isAmountValid(amount) && balance - amount >= 0) {
balance -= amount;
} else {
System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
}
}
// public 메서드 : getBalance
public int getBalance() {
return balance;
}
// private 메서드 : isAmountValid
private boolean isAmountValid(int amount) {
// 금액이 0보다 커야함
return amount > 0;
}
}
BankAccountMain
package access;
import access.b.BankAccount;
public class BankAccountMain {
public static void main(String[] args) {
BankAccount account = new BankAccount();
account.deposit(10000);
account.withdraw(3000);
System.out.println("balance = " + account.getBalance());
}
}
은행 계좌 기능을 다루는 코드, 다음 기능을 가짐.
private
- balance : 데이터 필드는 외부에 직접 노출하지 않음. BankAccount가 제공하는 메서드를 통해서만 접근 가능
- isAmountValid() : 입력 금액을 검증하는 기능은 내부에서만 필요한 기능.
public
- deposit() : 입금
- withdraw() : 출금
- getBalance() : 잔고
BankAccount를 사용하는 입장에서는 단 3가지 메서드만 알면 된다. 나머지 복잡한 내용은 모두 BankAccount 내부에 숨어있다.
접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, BankAccount를 사용하는 개발자 입장에서도 해당 기능을 사용하는 복잡도를 낯출 수 있음.
문제 풀이
1. 최대 카운터와 캡슐화
MaxCounter 클래스를 제작
이 클래스는 최대값을 지정하고 최대값 까지만 값이 증가하는 기능을 제공
- int count : 내부에서 사용하는 숫자, 초기값은 0
- int max : 최대값, 생성자를 통해 입력
- increment() : 숫자를 하나 증가함
- getCount() : 지금까지 증가한 값을 반환.
요구사항
- 접근 제어자를 사용해서 데이터를 캡슐화
- 해당 클래스는 다른 패키지에서도 사용할 수 있어야 함
MaxCounter
package access.ex;
public class MaxCounter {
private int count;
private int max;
public MaxCounter(int count) {
max = count;
}
public void increment() {
if (max > count) {
count += 1;
} else {
System.out.println("최대값을 초과할 수 없습니다.");
}
}
/* public void increment() {
// 검증 로직
if (count >= max) {
System.out.println("최대값을 초과할 수 없습니다.");
return;
}
// 실행 로직
count ++;
} */
public int getCount() {
return count;
}
}
CounterMain
package access.ex;
public class CounterMain {
public static void main(String[] args) {
MaxCounter counter = new MaxCounter(3);
counter.increment();
counter.increment();
counter.increment();
counter.increment();
int count = counter.getCount();
System.out.println(count);
}
}
2. 쇼핑 카트
ShoppingCartMain 코드가 작동하도록 Item, ShoppingCart 클래스 완성
요구 사항
- 접근 제어자를 사용해서 데이터를 캡슐화
- 해당 클래스는 다른 패키지에서도 사용할 수 있어야 함
- 장바구니에는 상품을 최대 10개만 담을 수 있다.
- 10개 초과 등록시: 장바구니가 가득 찼습니다. 출력
Item 클래스
package access.ex;
public class Item {
private String name;
private int price;
private int quantity;
public Item(String name, int price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public String getName() {
return name;
}
public int getTotalPrice() {
return price * quantity;
}
}
- Item(...) : Item 생성자, 클래스 내의 private 멤버 변수 값을 변경하는데 사용, 다른 패키지에서도 사용할 수 있게 public 접근 제어자 사용.
- getName() : 멤버 변수인 name을 반환하는 메서드
- getTotalPrice() : 상품의 총 가격을 계산 후 반환
ShoppingCart 클래스
package access.ex;
public class ShoppingCart {
private Item[] items = new Item[10];
private int itemCount;
public void addItem(Item item) {
if (itemCount < items.length) {
this.items[itemCount] = item;
itemCount++;
} else {
System.out.println("장바구니가 가득찼습니다.");
}
}
public void displayItems() {
System.out.println("장바구니 상품 출력");
for (int i = 0; i < itemCount; i++) {
Item item = items[i];
System.out.println("상품명:" + item.getName() + ", 합계:" + item.getTotalPrice());
}
System.out.println("전체 가격 합:" + calculateTotalPrice());
}
private int calculateTotalPrice() {
int totalPrice = 0;
for (int i = 0; i < itemCount; i++) {
Item item = items[i];
totalPrice = item.getTotalPrice();
}
return totalPrice;
}
}
- addItem(Item item) : itemCount 값이 item.length의 값보다 작을 경우 items 배열에 item 값을 저장하는 메서드 itemCount의 값이 item.length보다 클 경우 문구 출력
- displayItems() : 장바구니에 들어 있는 상품의 이름과 합계 가격을 출력하는 메서드, items에 들어있는 값을 Item 클래스의 getName()과 getTotalPrice()를 사용해 출력
- items의 값을 Item item에 넣는 이유 : items의 값으로는 String name, int price, int quantity가 있고 Item 객체의 값으로 들어갈 때 Item 클래스의 멤버 변수의 값을 변경하고 그 변경된 값을 getName(), getTotalPrice() 두 개의 메서드로 가져올 수 있음
- calculateTotalPrice() : 장바구니에 들어 있는 상품의 총 가격을 계산해서 반환하는 메서드 shoppingCart 클래스에서만 사용되므로 private 접근 제어자 부여
ShoppingCartMain
package access.ex;
public class ShoppingCartMain {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
Item item1 = new Item("마늘", 2000, 2);
Item item2 = new Item("상추", 3000, 4);
cart.addItem(item1);
cart.addItem(item2);
cart.displayItems();
}
}
정리