
안녕하세요:) 오늘은 김영한 강사님의
실전 자바 기본편 강의 요약 정리 2편입니다!
목차
06. 접근 제어자
07. 자바 메모리 구조와 static
08. final
(+05. 패키지)
패키지 규칙
● 패키지의 이름과 위치는 폴더(디렉토리) 위치와 같아야 한다. (필수)
● 패키지 이름은 모두 소문자를 사용한다. (관례)
● 패키지 이름의 앞 부분에는 일반적으로 회사의 도메인 이름을 거꾸로 사용한다.
(예를 들어, com.company.myapp 과 같이 사용)
또, 패키지는 계층구조를 이루더라도 서로 다른 패키지라는 점!
큰 애플리케이션은 패키지를 잘 구성하는 것이 중요.
06. 접근 제어자
접근 제어자는 클래스 외부에서 특정 필드나 메서드에 접근하는 것은 허용하거나 제한할 수 있다.
그렇다면 접근제어자는 왜 필요한 것일까?
예를 들어, 스피커의 음량이 100이 넘어가면 안되는 요구사항이 있다고 하자.
Speaker
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);
}
}
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();
}
}
메서드를 호출하면 100이상은 음량이 증가하지 않지만, volume필드에 직접 접근하여 200으로 값을 변경하면 100이 넘어가게 되는 것이다.
이럴때!! volume 필드의 외부 접근을 막을 수 있는 방법이 바로 접근 제어자 private
int volume; → private int volume;
이렇게 되면 해당 클래스 내부에서만 호출이 가능하다.
접근 제어자 종류
private : 모든 외부 호출을 막는다.
default (package-private): 같은 패키지안에서 호출은 허용한다.
protected : 같은 패키지안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
public : 모든 외부 호출을 허용한다.
제한 많은 순에서 적은 순으로 적자면 !
private → default → protected → public
접근 제어자는 필드, 메서드, 생성자에 사용이 되며 명시하지 않는다면 default가 적용됨!
(클래스 레벨에도 일부 접근 제어자를 사용할 수 있음)
클래스 레벨의 접근 제어자는 public, dafault만 사용 가능!
- 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 {
}
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();
}
}
=> 같은 패키지 이므로 접근 가능!
PublicClassOuterMain
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();
}
}
=> DefaultClass1 , DefaultClass2 는 다른 패키지이므로 접근 불가!
캡슐화
객체 지향 프로그래밍의 중요한 개념 중 하나!
: 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말함
캡슐화를 통해 데이터의 직접적인 변경을 방지할 수 있다.
그리고 이 캡슐화를 안전하게 완성할 수 있게 해주는 장치가 바로 접근 제어자인 것이다.
그럼 어떤걸 숨기고 보여야하느냐!
객체에는 속성(데이터)와 기능(메서드)가 있는데 필수로 숨겨야하는 것은 데이터이며 기능 역시 대부분은 숨기되 필수적으로 사용자가 필요한 기능만 외부에 노출한다.
package access;
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 boolean isAmountValid(int amount) {
// 금액이 0보다 커야함
return amount > 0;
}
}
package access;
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() : 잔고
이렇게 접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호할 수 있고, 개발자 입장에서 해당 기능을 사용하는 복잡도를 맞출 수가 있다.
07. 자바 메모리 구조와 static
자바 메모리 구조
크게 메서드 영역, 스택 영역, 힙 영역으로 나눌 수 있다.
● 메서드 영역: 클래스 정보 보관(이 클래스 정보가 붕어빵 틀)
● 스택 영역: 실제 프로그램이 실행되는 영역. 메서드 실행마다 하나씩 쌓임
● 힙 영역: 객체(인스턴스)가 생성되는 영역. (붕어빵 틀에서 생성된 붕어빵이 존재)
1) 메서드 영역(Method Area)
: 프로그램을 실행하는데 필요한 공통 데이터를 관리.
- 클래스 정보: 클래스의 실행 코드, 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재
- static 영역: static 변수들을 보관
- 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관 (프로그램에 "hello" 라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리)
2) 스택 영역(Stack Area)
: 자바 실행 시, 하나의 실행 스택이 생성. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함
❓스택프레임 : 스택 영역에 쌓이는 네모박스가 하나의 스택 프레임. 메서드 호출 시 하나의 스택 프레임이 쌓이고 메서드 종료 시 해당 스택 프레임 제거됨
(+ 각 쓰레드별로 하나의 실행 스택이 생성)
3) 힙 영역(Heap Area)
: 객체(인스턴스)와 배열이 생성되는 영역. 가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거됨
스택과 큐 자료구조
스택 구조
스택구조는 다음과 같이 후입 선출(LIFO, Last In First Out)의 방식을 따른다.
나중에 넣는 것이 가장 먼저 나오는 것이 후입 선출이며 이런 자료 구조를 스택이라고 한다.
큐 자료구조
반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출(FIFO, First In First Out)이라 하고 이런 자료구조를 큐라 한다. 예를 들어 선착순 이벤트를 하는데 고객이 대기해야 한다면 큐 자료구조를 사용해야한다.
이번 시간에 중요한 것은 스택! 프로그램 실행과 메서드 호출에는 스택 구조가 적합하다.
스택 영역
package memory;
public class JavaMemoryMain1 {
public static void main(String[] args) {
System.out.println("main start");
method1(10);
System.out.println("main end");
}
static void method1(int m1) {
System.out.println("method1 start");
int cal = m1 * 2;
method2(cal);
System.out.println("method1 end");
}
static void method2(int m2) {
System.out.println("method2 start");
System.out.println("method2 end");
}
}
위 코드를 실행한 결과는 아래와 같다.
main start
method1 start
method2 start
method2 end
method1 end
main end
즉 호출 그림으로 보자면 아래와 같다.
자바 실행 시 main() 실행 → main() 스택 프레임은 내부에 args 라는 매개변수를 가짐
method1() 을 호출(method1() 스택 프레임 생성) → m1 , cal 지역 변수(매개변수 포함)를 가지므로 스택 프레임에 포함
정리
● 스택 영역을 사용해서 메서드 호출과 지역 변수(매개변수 포함)를 관리한다.
● 메서드를 계속 호출하면 스택 프레임이 계속 쌓인다.
● 지역 변수(매개변수 포함)는 스택 영역에서 관리한다.
● 스택 프레임이 종료되면 지역 변수도 함께 제거된다.
● 스택 프레임이 모두 제거되면 프로그램도 종료된다.
스택 영역과 힙 영역
스택에서 힙을 관리한다는 것을 코드와 그림을 통해 확인해보자
Data
public class Data {
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
JavaMemoryMain2
public class JavaMemoryMain2 {
public static void main(String[] args) {
System.out.println("main start");
method1();
System.out.println("main end");
}
static void method1() {
System.out.println("method1 start");
Data data1 = new Data(10);
method2(data1);
System.out.println("method1 end");
}
static void method2(Data data2) {
System.out.println("method2 start");
System.out.println("data.value=" + data2.getValue());
System.out.println("method2 end");
}
}
/*실행결과
main start
method1 start
method2 start
data.value=10
method2 end
method1 end
main end*/
그림을 통해 동작 순서를 파악해보자.
method2()가 종료될 때 method2() 의 스택 프레임이 제거되면서 매개변수 data2 도 함께 제거된다.
method1() 역시 마찬가지다.
method1()이 종료된 직후, x001 참조값을 가진 Data 인스턴스를 참조하는 곳이 더는 없기 때문에 메모리만 차지하게 된다. 이때 GC(가비지 컬렉션)은 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거한다!
정리
● 지역변수는 스택 영역에, 객체는 힙 영역에 관리되는 것
이제 메서드 영역에 대한 부분이 남았는데 이를 위해서는 static키워드를 알아야한다.
static 변수
static키워드의 필요성을 이해하기 위해 '특정 클래스를 통해서 생성된 객체의 수를 세는' 단순한 프로그램을 만들어보자.
인스턴스 내부 변수에 카운트 저장
기존지식으로는 생성할 인스턴스 내부에 카운트를 저장하면 되나 생각할 것이다.
public class Data1 {
public String name;
public int count;
public Data1(String name) {
this.name = name;
count++;
}
}
이렇게 되면 객체마다의 카운트가 새로 만들어져 원하는 대로 값이 나오지 않는다.
그렇다면 어떻게 해야할까?
외부 인스턴스에 카운트 저장
Counter
public class Counter {
public int count;
}
Data2
public class Data2 {
public String name;
public Data2(String name, Counter counter) {
this.name = name;
counter.count++;
}
}
DataCountMain2
public class DataCountMain2 {
public static void main(String[] args) {
Counter counter = new Counter();
Data2 data1 = new Data2("A", counter);
System.out.println("A count=" + counter.count);
Data2 data2 = new Data2("B", counter);
System.out.println("B count=" + counter.count);
Data2 data3 = new Data2("C", counter);
System.out.println("C count=" + counter.count);
}
}
/* 실행결과
A count=1
B count=2
C count=3 */
이렇게 코드를 짜게 되면 Counter 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때 마다 값을 정확하게 증가시킬 수 있다.
하지만 Counter를 위한 클래스를 별도록 추가해서 사용하는 것이 비효율적으로 느껴진다.(생성자의 매개변수도 추가, 생성자도 복잡해짐)
그래서❗
공용으로 함께 사용하는 변수를 만들기 위해 static 키워드를 사용한다.
Data3
public class Data3 {
public String name;
public static int count; //static
public Data3(String name) {
this.name = name;
count++;
}
}
이렇게 멤버 변수에 static 을 붙이게 되면 static 변수, 정적 변수 또는 클래스 변수라 한다.
이러한 count 정적 변수에 접근하는 것 또한 클래스에 접근 하는 것처럼 .(dot)을 사용한다.
public class DataCountMain3 {
public static void main(String[] args) {
Data3 data1 = new Data3("A");
System.out.println("A count=" + Data3.count);
Data3 data2 = new Data3("B");
System.out.println("B count=" + Data3.count);
Data3 data3 = new Data3("C");
System.out.println("C count=" + Data3.count);
//추가
//인스턴스를 통한 접근
Data3 data4 = new Data3("D");
System.out.println(data4.count);
//클래스를 통한 접근
System.out.println(Data3.count);
}
}
용어 정리
public class Data3 {
public String name;
public static int count; //static
}
위 코드에서 name, count는 모두 멤버 변수
● 인스턴스 변수: static 이 붙지 않은 멤버 변수, 예) name
- 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있음
● 클래스 변수: static 이 붙은 멤버 변수, 예) count
- called 클래스 변수, 정적 변수, static 변수
- 인스턴스와 무관하게 클래스에 바로 접근해서 사용할수 있고, 클래스 자체에 소속되어 있음
생명주기 비교
- 지역 변수(매개변수 포함): 메서드가 종료되면 스택 프레임도 제거 되어 생존 주기가 짧다.
- 인스턴스 변수: 힙 영역을 사용하는데 이 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길다.
- 클래스 변수: 메서드 영역의 static 영역에 보관되는 변수이다. 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거된다. (클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성)
static 메서드
추가적으로 정적 메서드와 관련하여 기억해야할 사항은
정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점을 가지고 있지만
정적 메서드는 언제나 사용할 수 있는 것이 아니라는 것
■ static 메서드는 static 만 사용할 수 있다.(인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다.)
■ 반대로 모든 곳에서 static 을 호출할 수 있다. (접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static 을 호출할 수 있다.)
public class DecoData {
private int instanceValue;
private static int staticValue;
public static void staticCall() {
//instanceValue++; //인스턴스 변수 접근, compile error
//instanceMethod(); //인스턴스 메서드 접근, compile error
staticValue++; //정적 변수 접근
staticMethod(); //정적 메서드 접근
}
public void instanceCall() {
instanceValue++; //인스턴스 변수 접근
instanceMethod(); //인스턴스 메서드 접근
staticValue++; //정적 변수 접근
staticMethod(); //정적 메서드 접근
}
private void instanceMethod() {
System.out.println("instanceValue=" + instanceValue);
}
private static void staticMethod() {
System.out.println("staticValue=" + staticValue);
}
}
❓정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유
인스턴스처럼 참조값의 개념이 없기 때문!
특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다
08. final
final 키워드는 이름처럼 끝! 이라는 뜻으로 final 키워드가 붙으면 더는 값을 변경할 수 없다.
특정 변수의 값을 할당한 이후에 변경하지 않아야 한다면 final 을 사용하는 것!!
지역변수
package final1;
public class FinalLocalMain {
public static void main(String[] args) {
//final 지역 변수1
final int data1;
data1 = 10; //최초 한번만 할당 가능
//data1 = 20; //컴파일 오류
//final 지역 변수2
final int data2 = 10;
//data2 = 20; //컴파일 오류
method(10);
}
static void method(final int parameter) {
//parameter = 20; //컴파일 오류
}
}
→ final 을 지역 변수 선언시 바로 초기화 한 경우 이미 값이 할당되었기 때문에 값을 할당할 수 없다.
필드(멤버변수)
public class ConstructInit {
final int value;
public ConstructInit(int value) {
this.value = value;
}
}
→ final 을 필드에 사용할 경우 해당 필드는 생성자를 통해서 한번만 초기화 될 수 있다.
public class FieldInit {
static final int CONST_VALUE = 10;
final int value = 10;
}
→ final 필드를 필드에서 초기화하면 이미 값이 설정되었기 때문에 생성자를 통해서도 초기화 할 수 없다.
→ static 변수에도 final 을 선언할 수 있다.
- FieldInit 과 같이 final 필드를 필드에서 초기화 하는 경우, 모든 인스턴스가 다음 오른쪽 그림과 같이 같
은 값을 가진다. 하지만 모든 인스턴스가 같은 값을 사용하기 때문에 결과적으로 메모리를 낭비하게 된다.
- 또 같은 값을 계속 생성하는 것은 명확한 중복이기에 이때 static영역을 활용해 static final을 사용하는 것!
- static 영역은 단 하나만 존재하는 영역이기에 필드에 final+필드 초기화를 사용하는 경우 static을 사용하는 것이 효과적!
상수(Constant)는 변하지 않고, 항상 일정한 값을 갖는 수를 말하며 static final키워드를 사용함!
자바 상수 특징
○ static final 키워드를 사용
○ 대문자를 사용하고 구분은 _ (언더스코어)로
○ 필드를 직접 접근해서 사용
+이런 상수들은 애플리케이션 전반에서 사용되기 때문에 public 를 자주 사용
오늘도 자바의 중요한 개념들에 대해
정리해보는 시간을 가져보았습니다:)
다음에 이어서 가져오도록 하겠습니다
오늘도 화이팅🙌

'자바' 카테고리의 다른 글
[자바의 정석] 변수, 배열, 객체 지향 파트 요약 정리 (챕터2, 5, 6 정리) (0) | 2025.03.16 |
---|---|
자바 실전개념편 3(상속, 다형성) (1) | 2024.06.02 |
자바 실전개념편 1(클래스와 데이터 ~ 생성자) (0) | 2024.05.19 |
자바 입문하기_기본 개념 (변수~메소드) (0) | 2024.05.05 |