일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 입문(무료)
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch13
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- 스프링 db2 - 데이터 접근 기술
- 게시글 목록 api
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch14
- 스프링 mvc2 - 타임리프
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch9
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch3
- 타임리프 - 기본기능
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch4
- @Aspect
- 스프링 mvc1 - 스프링 mvc
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 수제비 실기
- Today
- Total
나구리의 개발공부기록
자바 메모리 구조와 static, 자바 메모리 구조, 스택과 큐 자료 구조, 스택 영역, 스택 영역과 힙 영역, static 변수, static 메서드 본문
자바 메모리 구조와 static, 자바 메모리 구조, 스택과 큐 자료 구조, 스택 영역, 스택 영역과 힙 영역, static 변수, static 메서드
소소한나구리 2025. 1. 8. 18:55출처 : 인프런 - 김영한의 실전 자바 - 기본편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 자바 메모리 구조
1) 자바 메모리 구조
(1) 비유
- 자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있음
- 메서드 영역: 클래스 정보를 보관, 이 클래스 정보가 붕어빵 틀이라고 볼 수 있음
- 스택 영역: 실제 프로그램이 실행되는 영역, 메서드를 실행할 때마다 하나씩 쌓임
- 힙 영역: 객체(인스턴스)가 생성되는 영역, new 명령어를 사용하면 이 영역을 사용하며 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간이라고 이해하면 됨, 참고로 배열도 이 영역에 생성됨
(2-1) 실제 구조
(2-2) 메서드 영역(Method Area)
- 프로그램을 실행하는데 필요한 공통 데이터를 관리, 해당 영역은 프로그램의 모든 영역에서 공유함
- 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재함
- static 영역: static 변수들을 보관함(뒤에서 자세히 설명)
- 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관함, 예를 들어 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리함
이 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리함
(2-3) 스택 영역(Stack Area)
- 자바 실행 시, 하나의 실행 스택이 생성되며 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함함
- 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이며 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고 메서드가 종료되면 해당 스택 프레임이 제거됨
(2-4) 힙 영역(Heap Area)
- 객체(인스턴스)와 배열이 생성되는 영역
- 가비지 컬렉션(GC)이 이루어지는 주요 영역이며 더이상 참조되지 않는 객체는 GC에 의해 제거됨
** 참고
- 문자열을 다루는 문자열 풀은 메서드 영역이였으나 자바 7부터 힙 영역으로 이동했음
- 스택 영역은 더 정확하게는 각 쓰레드별로 하나의 실행 스택이 생성되어 쓰레드 수 만큼 스택 영역이 생성됨
- 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나이며 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있음
(3) 메서드 코드는 메서드 영역에
- 자바에서 특정 클래스로 100개의 인스턴스를 생성하면 힙 메모리에 100개의 인스턴스가 생기며 각각의 인스턴스는 내부에 변수와 메서드를 가짐
- 같은 클래스로부터 생성된 객체라도 인스턴스 내부의 변수 값은 서로 다를 수 있지만 메서드는 공통된 코드를 공유하기 때문에 객체가 생성될 때 인스턴스 변수에는 메모리가 할당되지만 메서드에 대한 새로운 메모리 할당은 없음
- 즉, 메서드는 메서드 영역에서 공통으로 관리되고 실행되며 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행함
2. 스택과 큐 자료 구조
1) 스택과 큐 자료 구조
(1) 스택(Stack) 구조
- 후입 선출(LIFO, Last In First Out)
- 블럭을 네모난 통에 넣는다고 생각해보면 위쪽에만 열려있기 때문에 위쪽으로 블럭을 넣고 위쪽으로 블럭을 빼야함
- 이렇게 입구가 하나로 가장 나중에 넣은 것이 가장 먼저 나오는 것을 후입 선출이라하는데 이런 자료 구조를 스택이라 함
- 1(입력) -> 2(입력) -> 3(입력) -> 3(출력) -> 2(출력) -> 1(출력)
(2) 큐(Queue) 자료 구조
- 선입 선출(FIFO, First In First Out)
- 후입 선출과 반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라하며 이런 자료 구조를 큐라고 함
- 1(입력) -> 2(입력) -> 3(입력) -> 1(출력) -> 2(출력) -> 3(출력)
(3) 정리
- 이런 자료 구조는 각자 필요한 영역이 있으며 선착순 이벤트를 진행시 고객이 대기해야한다면 스택구조를 사용하면 먼저 들어온 사용자가 가장 마지막에 꺼내지므로 큐 자료 구조를 사용해야함
- 프로그램 실행과 메서드 호출에는 스택 구조가 적합함
3. 스택 영역
1) 스택 영역
(1) JavaMemoryMain1
- main()메서드 내부에서 method1을 호출하고 method1에서 method2를 호출하는 구조
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
*/
(2) 호출 그림
- 처음 자바 프로그램을 실행하면 main()을 실행하며 이때 main()을 위한 스택 프레임이 하나 생성됨
- main() 스택 프레임은 내부에 args라는 매개변수를 가지는데 args는 뒤에서 다룸(외부에서 입력값을 주는 용도)
- main()은 method1()을 호출하고 method1() 스택 프레임이 생성됨
- method1()은 m1, cal 이라는 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함됨
- method1()은 method2()를 호출하고 method2() 프레임이 생성됨
- method2()는 m2 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수가 스택 프레임에 포함됨
(3) 종료 그림
- method2()가 종료되면 method2() 스택 프레임이 제거되고 매개변수 m2도 제거되고 프로그램은 method1()로 돌아가는데 method1()을 처음부터 시작하는 것이 아니라 method1()에서 method2()를 호출한 지점으로 돌아감
- method1()이 종료되면 method1() 스택 프레임이 제거되고 지역변수(매개변수 포함)도 제거되며 프로그램은 main()으로 돌아감
- main()이 종료되면 더 이상 호출할 메서드가 없고 스택 프레임도 완전히 비워져 자바는 프로그램을 정리하고 종료함
(4) 정리
- 자바는 스택 영역을 사용하여 메서드 호출과 지역 변수(매개변수 포함)을 관리함
- 메서드를 계속 호출하면 스택 프레임이 계속 쌓이고 지역 변수(매개변수 포함)는 스택 영역에서 관리함
- 스택 프레임이 종료되면 지역 변수도 함께 제거되고 스택 프레임이 모두 제거되면 프로그램도 종료됨
4. 스택 영역과 힙 영역
1) 스택 영역과 힙 영역이 함께 사용되는 경우
(1) Data
package memory;
public class Data {
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
(2) JavaMemoryMain2
- main() -> method1() -> method2() 순서로 호출하는 단순한 코드이며 method1()에서 Data 클래스의 인스턴스를 생성하고 method2()를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달
package memory;
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
*/
(3) 실행 순서
- 1. 처음 main() 메서드를 실행하고 main() 스택 프레임이 생성됨
- 2. main()에서 method1()을 실행하고 method1() 스택 프레임이 생성됨
- method1()은 지역 변수로 Data data1을 가지고 있으므로 이 지역 변수도 스택 프레임에 포함됨며 new Data(10)메서드를 사용하여 힙 영역에 Data 인스턴스를 생성하고 참조값을 data1에 보관함 - 3. method1()은 method2()를 호출하면서 Data data2 매개변수에 data1에 보관한 참조값을 넘김
- method1()에 있는 data1과 method2()에 있는 data2는 둘다 같은 참조값을 가지고 있으므로 같은 인스턴스를 참조함 - 4. method2()가 종료되면 method2()의 스택 프레임이 제거되면서 매개변수 data2도 함께 제거됨
- 5. method1()이 종료되면 method1()의 스택 프레임이 제거되면서 지역변수 data1도 함께 제거됨
- method1()이 종료된 직후의 상태
- Data 인스턴스를 참조하는 곳이 없으므로 사용되는 곳도 없음
- 결과적으로 프로그램에서 더는 사용하지 않는 메모리만 차지하는 객체가 되어버림
- GC(가비지 컬렉션)이 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거함
** 참고
- 힙 영역 외부가 아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 됨
(4) 정리
- 지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리됨
5. static 변수
1) 인스턴스 내부 변수에 카운트 저장
(1) Data1
- 생성된 객체의 수를 세기 위한 목적으로 객체가 생성될 때마다 생성자를 통해 인스턴스의 멤버 변수인 count값을 증가시키도록 작성
- 예제를 단순하게 만들기 위해 필드에 public을 사용하였으며 예제에서 필드에 public을 사용하면 예제를 간단히 하기 위해서 적용한 것이라고 보면됨
package static1;
public class Data1 {
public String name;
public int count;
public Data1(String name) {
this.name = name;
count++;
}
}
(2) DataCountMain1
- 그러나 객체를 생성하고 카운트 값을 출력해보면 이 프로그램은 당연히 기대한대로 작동하지 않는데 객체를 생성할 때마다 Data1 인스턴스는 새로 만들어지므로 인스턴스에 포함된 count 변수도 새로 만들어지기 때문임
- 처음 Data1("A") 인스턴스를 생성하면 count 값은 0으로 초기화되고 생성자에서 count++을 호출했으므로 count의 값은 1이됨
- Data2("B") 인스턴스를 생성하면 완전히 새로운 인스턴스를 생성하며 마찬가지로 count의 값은 1이 되고 Data3("C") 인스턴스를 생성할 때도 마찬가지로 count의 값은 1이 됨
- 즉, 인스턴스에 사용되는 멤버 변수 count 값은 인스턴스끼리 서로 공유되지 않기 때문에 원하는 답을 구할 수 없는데 이 문제를 해결하려면 변수를 서로 공유 해야함
package static1;
public class DataCountMain1 {
public static void main(String[] args) {
Data1 data1 = new Data1("A");
System.out.println("A count = " + data1.count);
Data1 data2 = new Data1("B");
System.out.println("B count = " + data2.count);
Data1 data3 = new Data1("C");
System.out.println("C count = " + data3.count);
}
}
/* 실행 결과
A count = 1
B count = 1
C count = 1
*/
2) 외부 인스턴스에 변수에 카운트 저장
(1) Counter
- 카운트 값을 저장하는 별도의 객체
- 해당 객체를 공유해서 필요할 때마다 카운트 값을 증가 시킴
package static1;
public class Counter {
public int count;
}
(2) Data2
- 여기에는 count 멤버 변수가 없는 대신 생성자에서 Counter인스턴스를 추가로 전달 받음
- 생성자가 호출되면 counter인스턴스에 있는 count 변수의 값을 하나 증가시킴
package static1;
public class Data2 {
public String name;
public Data2(String name, Counter counter) {
this.name = name;
counter.count++;
}
}
(3) DataCountMain2
- Counter 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때마다 값을 정확하게 증가시킬 수 있음
- Data2("A") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시키고 count 값은 1이 됨
- Data2("B") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시키고 count 값이 2가 됨
- Data2("C") 인스턴스를 생성해도 마찬가지로 Counter 인스턴스에 있는 count 값을 하나 증가시키고 count 값이 3이 됨
- 결과적으로 Data2의 인스턴스가 3개 생성되고 count 값도 인스턴스 숫자와 같은 3으로 정확하게 측정되었는데, 이렇게 하게 되면 아래와 같은 불편한 점들이 있음
- Data2 클래스와 관련된 일인데 Counter라는 별도의 클래스를 추가로 사용해야함
- 생성자의 매개변수도 추가되고, 생성자가 복잡해지며 생성자를 호출하는 부분도 복잡해짐
package static1;
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
*/
3) Static 변수 사용
(1) Data3
- 변수 타입 앞에 static 키워드를 붙이면 static 변수, 정적 변수, 클래스 변수라고 함
- 객체가 생성되면 생성자에서 정적 변수 count의 값을 하나 증가시킴
package static1;
public class Data3 {
public String name;
public static int count; // static 변수
public Data3(String name) {
this.name = name;
count++;
}
}
(2) DataCountMain3
- 코드를 보면 count 정적 변수에 접근하는 방법이 조금 특이한데 Data3.count와 같이 클래스명에 .(dot)을 사용하여 마치 클래스에 직접 접근하는 것처럼 사용함
- 기대하는 바와 같이 count값이 누적되어 증가되고 있음
package static1;
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);
}
}
/* 실행 결과
A count = 1
B count = 2
C count = 3
*/
(3) 그림으로 이해
- 1. Data3("A") 인스턴스를 생성하면 생성자가 호출되고 생성자에있는 count++코드가 호출되며 static 변수인 count의 값이 하나 증가함
- static 이 붙은 멤버 변수 즉, 예제에서의 count 변수는 인스턴스 영역에 생성되지 않고 메서드 영역에서 이 변수를 관리함
- 메서드 영역에 있는 count의 값이 하나 증가하게 됨 - 2. Data3("B") 인스턴스를 생성하면 생성자가 호출되고 마찬가지로 count++코드가 있으므로 메서드 영역에 있는 count 변수의 값이 하나 증가함
- 3. Data3("C") 도 동일하게 실행되며 메서드 영역에 있는 count 변수의 값이 하나 증가하게 됨
- 4. 최종적으로 메서드 영역에 있는 count 변수의 값은 3이됨
- static이 붙은 정적 변수에 접근하려면 Data3.count와 같이 클래스명 + .(dot) + 변수명으로 접근하면 되며 Data3의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스명을 생략할 수 있음
- static 변수를 사용하면 공용으로 변수를 사용할 수 있어 편리하게 문제를 해결할 수 있게 됨
(4) 정리
- static 변수는 클래스인 붕어빵 틀이 특별히 관리하는 변수라고 이해하면 됨
- 붕어빵 틀은 1개이므로 클래스 변수도 1개만 존재하는 반면 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재할 수 있음
4) 용어 정리
public class Data3 {
public String name;
public static int count; // static 변수
}
(1) 멤버 변수(필드)의 종류
- 예제 코드에서 name, count는 둘다 멤버 변수(필드)이며 static이 붙은 것과 아닌 것에 따라 분류 할 수 있음
- 인스턴스 변수: static이 붙지 않은 멤버 변수, ex) name
- static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고 인스턴스에 소속되어서 인스턴스 변수라고 함
- 인스턴스 변수는 인스턴스를 만들 때마다 새로 만들어짐 - 클래스 변수: static이 붙은 멤버 변수, ex) count
- 클래스 변수, 정적 변수, static 변수 등으로 부르며 모든 용어를 사용함
- static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고 클래스 자체에 소속되어 있어서 클래스 변수라고 함
- 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어지며 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용됨
5) 변수와 생명주기
(1) 지역 변수(매개변수 포함)
- 스택 영역에 있는 스택 프레임 안에 보관됨
- 메서드가 종료되면 스택 프레임도 제거되고 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거됨
- 지역 변수는 생존 주기가 짧음
(2) 인스턴스 변수
- 인스턴스에 있는 멤버 변수를 인스턴스 변수라 하며 힙 영역을 사용함
- 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 일반적으로는 지역 변수보다 생존 주기가 긺
(3) 클래스 변수
- 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이며 메서드 영역은 프로그램 전체에서 사용하는 공용공간임
- 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성이되고 JVM이 종료될때 까지 생명주기가 이어져 가장 긴 생명주기를 가짐
- static이 정적이라는 이유는 바로 여기에 있는데 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고 제거되는 반면, static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고 프로그램 종료 시점에 제거되므로 정적 변수는 이름그대로 정적임
6) 정적 변수 접근법
(1) DataCountMain3 - 추가
- static 변수는 클래스를 통해 바로 접근할 수도 있고 인스턴스를 통해 접근할 수도 있음
- 실행해보면 둘의 차이는 없고 둘다 결과적으로 정적 변수에 접근함
package static1;
public class DataCountMain3 {
public static void main(String[] args) {
// ... 기존 코드 동일
Data3 data4 = new Data3("D");
System.out.println(data4.count); // 인스턴스를 통한 접근
System.out.println(Data3.count); // 클래스를 통한 접근
}
}
/* 실행결과(생략된 코드의 실행결과 제외)
4
4
*/
(2) 올바른 정적 변수 접근 방법
- 그러나 정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는데 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문임
- 정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확하므로 정적 변수에 접근 할 때는 클래스를 통해서 접근해야함
6. static 메서드
1) 인스턴스 메서드
(1) DecoUtil1
- 특정 문자열의 앞뒤에 *을 붙여서 꾸며주는 deco()메서드를 가지고 있음
package static2;
public class DecoUtil1 {
public String deco(String str) {
return "*" + str + "*";
}
}
(2) DecoMain1
- 앞서 개발한 deco() 메서드를 호출하기 위해 DecoUtil1의 인스턴스를 먼저 생성해야함
- 그러나 deco()라는 기능은 멤버 변수도 없고 단순히 기능만 제공할 뿐임
- 인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 큰데 이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공함
- 즉, 인스턴스를 계속 생성하는 의미가 없음
package static2;
public class DecoMain1 {
public static void main(String[] args) {
String s = "hello java";
DecoUtil1 utils = new DecoUtil1();
String deco = utils.deco(s);
System.out.println("before: " + s);
System.out.println("after: " + deco);
}
}
/* 실행 결과
before: hello java
after: *hello java*
*/
2) static 메서드
(1) DecoUtil2
- 메서드 앞에 static을 붙여서 정적 메서드를 만들 수 있음
- 이렇게 static이 붙은 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해서 바로 호출할 수 있음
package static2;
public class DecoUtil2 {
static public String deco(String str) {
return "*" + str + "*";
}
}
(2) DecoMain2
- static이 붙은 정적 메서드는 객체 생성 없이 클래스명 + .(dot) + 메서드 명으로 바로 호출할 수 있으며 정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용할 수 있게 됨
- 클래스 메서드: 메서드 앞에도 static을 붙일 수 있는데 이것을 정적 메서드 또는 클래스 메서드라고 함
- 정적 메서드라는 용어는 static이 정적이라는 뜻이기 때문이고 클래스 메서드라는 용어는 인스턴스 생성 없이 마치 크래스에 있는 메서드를 바로 호출하는 것 처럼 느껴지기 때문임 - 인스턴스 메서드: static이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있어서 인스턴스 메서드라고 부름
package static2;
public class DecoMain2 {
public static void main(String[] args) {
String s = "hello java";
String deco = DecoUtil2.deco(s);
System.out.println("before: " + s);
System.out.println("after: " + deco);
}
}
/* 실행결과는 동일 */
3) 정적 메서드 사용법
(1) static 메서드는 static만 사용할 수 있음
- 클래스 내부의 기능을 사용할 때 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있음
- 클래스 내부의 기능을 사용할 때 정적 메서드는 인스턴스 변수나 인스턴스 메서드를 사용할 수 없음
(2) 반대로 모든 곳에서 static을 호출할 수 있음
- 정적 메서드는 공용 기능이므로 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있음
(3-1) DecoData
- 접근 제어자를 활용하여 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었음
- instanceValue: 인스턴스 변수
- staticValue: 정적 변수(클래스 변수)
- instanceMethod(): 인스턴스 메서드
- staticMethod(): 정적 메서드(클래스 메서드)
package static2;
public class DecoData {
private int instanceValue;
private static int staticValue;
public static void staticCall() {
// instanceValue++; // 인스턴스 변수 접근 불가, compile error
// instanceMethod(); // 인스턴스 메서드 접근 불가, compile error
staticValue++; // 정적 변수 접근 OK
staticMethod(); // 정적 메서드 접근 OK
}
public void instanceCall() {
instanceValue++; // 인스턴스 변수 접근 OK
instanceMethod(); // 인스턴스 메서드 접근 OK
staticValue++; // 정적 변수 접근 OK
staticMethod(); // 정적 메서드 접근 OK
}
private void instanceMethod() {
System.out.println("instanceValue=" + instanceValue); }
private static void staticMethod() {
System.out.println("staticValue=" + staticValue);
}
}
(3-2) DecoData - staticCall() 메서드
- staticCall()메서드는 정적메서드이므로 static만 사용할 수 있음
- 정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생함
(3-3) DecoData - instanceCall() 메서드
- instanceCall()메서드는 인스턴스 메서드이므로 인스턴스 변수, 인스턴스 메서드를 사용할 수 있음
- 또한 모든 곳에서 공용인 static을 호출할 수 있으므로 정적 변수, 정적 메서드에도 접근할 수 있음
(4) DecoDataMain
- 정적 메서드와 인스턴스 메서드를 각각 호출
- staticValue는 스태틱 변수이므로 각 메서드에서 공용으로 사용하여 값이 누적이 되고 instanceValue는 인스턴스 변수이므로 해당 인스턴스에서만 사용되는 변수이므로 값이 인스턴스마다 다르게 사용됨
package static2;
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("1. 정적 호출");
DecoData.staticCall();
System.out.println("2. 인스턴스 호출1");
DecoData data1 = new DecoData();
data1.instanceCall();
System.out.println("3. 인스턴스 호출3");
DecoData data2 = new DecoData();
data2.instanceCall();
}
}
/* 실행 결과
1. 정적 호출
staticValue=1
2. 인스턴스 호출1
instanceValue=1
staticValue=2
3. 인스턴스 호출3
instanceValue=1
staticValue=3
*/
(5) 정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유
- 정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있어서 인스턴스처럼 참조값의 개념이 없음
- 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데 정적 메서드는 참조값 없이 호출하므로 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없음
- 당연히 아래처럼 객체의 참조값을 직접 매개변수로 전달하면 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있음
public static void staticCall(DecoData data) {
data.instanceValue++;
data.instanceMethod();
}
4) 용어 정리
(1) 멤버 메서드의 종류
- 인스턴스 메서드: static이 붙지 않은 멤버 메서드
- 클래스 메서드: static이 붙은 메서드
- 클래스 메서드, 정적 메서드, static 메서드등으로 부름 - static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고 인스턴스에 소속되어 있으므로 인스턴스 메서드라함
- static이 붙은 멤버 메서드는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고 클래스 자체에 소속되어 있어 클래스 메서드라 함
- 해당 설명은 멤버 변수에도 똑같이 적용됨
(2) 정적 메서드 활용
- 정적 메서드는 객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행할 때 주로 사용함
- 예를 들어 간단한 메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용함
- 수학의 여러가지 기능을 담은 클래스를 만들 수 있는데 이 경우 인스턴스 변수 없이 입력한 값을 계산하고 반환하는 것이 대부분인데 이럴 때 정적 메서드를 사용해서 유틸리티성 메서드를 만들면 좋음
5) 정적 메서드 접근법
(1) 클래스를 통해서 접근 해야 함
- static 변수에서 다뤘던 것처럼 static 메서드 메서드도 인스턴스를 통해서도 접근할 수 있는데 권장하지 않으며 이유도 동일함
- static 변수, static 메서드는 인스턴스에 접근하는 것처럼 오해하지 않도록 클래스를 통해서 접근해야 함
6) static import
(1) DecoDataMain - static import 적용
- 정적 메서드를 사용할 때 스태틱 메서드를 자주 호출해야한다면 static import 기능을 사용할 수 있음
- 특정 클래스의 정적 메서드 하나만 적용하려면 import static static2.DecoData.staticCall; 처럼 생략할 메서드 명을 적어주면 되며 특정 클래스의 모든 메서드에 적용하려면 *을 사용하면 됨
- static import는 정적 변수에도 사용할 수 있음
package static2;
import static static2.DecoData.staticCall; // 특정 메서드를 지정
import static static2.DecoData.*; // 해당 클래스의 모든 메서드를 static import
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("1. 정적 호출");
staticCall(); // 클래스 명이 생략 됨
staticCall();
staticCall();
staticCall();
}
}
7) main() 메서드는 정적 메서드
(1) main() 메서드
- 인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main() 메서드임
- main()메서드는 프로그램을 시작하는 시작점이 되는데, 객체를 생성하지 않아도 main() 메서드가 작동했던 이유가 main()메서드가 static이기 때문임
- 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있으므로 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했음
7. 문제와 풀이
1) 구매한 자동차 수
(1) 요구사항 및 문제
- 다음 코드를 참고해서 생성한 차량 수를 출력하는 프로그램을 작성
- Car 클래스를 작성
package static2.ex;
public class CarMain {
public static void main(String[] args) {
Car car1 = new Car("K3");
Car car2 = new Car("G80");
Car car3 = new Car("Model Y");
Car.showTotalCars(); //구매한 차량 수를 출력하는 static 메서드
}
}
실행 결과
차량 구입, 이름: K3
차량 구입, 이름: G80
차량 구입, 이름: Model Y
구매한 차량 수: 3
(2) 정답
package static2.ex;
public class Car {
private static int totalCars;
private String name;
public Car(String name) {
this.name = name;
totalCars++;
System.out.println("차량 구입, 이름: " + name);
}
static void showTotalCars() {
System.out.println("구매한 차량 수: " + totalCars);
}
}
실행 결과
차량 구입, 이름: K3
차량 구입, 이름: G80
차량 구입, 이름: Model Y
구매한 차량 수: 3
2) 수학 유틸리티 클래스
(1) 문제
- 다음 기능을 제공하는 배열용 수학 유틸리티 클래스(MathArrayUtils)를 생성
- sum(int[] array): 배열의 모든 요소를 더하여 합계를 반환
- average(int[] array): 배열의 모든 요소의 평균값을 계산
- min(int[] array): 배열에서 최소값을 찾음
- max(int[] array): 배열에서 최대값을 찾음
(2) 요구사항
- MathArrayUtils은 객체를 생성하지 않고 사용해야하며, 누군가 실수로 MathArrayUtils의 인스턴스를 생성하지 못하도록 막아야함
- 실행 코드에 static import를 사용해도 됨
package static2.ex;
public class MathArrayUtilsMain {
public static void main(String[] args) {
int[] values = {1, 2, 3, 4, 5};
System.out.println("sum=" + MathArrayUtils.sum(values));
System.out.println("average=" + MathArrayUtils.average(values));
System.out.println("min=" + MathArrayUtils.min(values));
System.out.println("max=" + MathArrayUtils.max(values));
}
}
실행 결과
sum=15
average=3.0
min=1
max=5
(3) 정답
package static2.ex;
public class MathArrayUtils {
// 인스턴스 생성 막기
private MathArrayUtils() {
}
public static int sum(int[] array) {
int sum = 0;
for (int value : array) {
sum += value;
}
return sum;
}
public static double average(int[] array) {
return (double) sum(array) / array.length;
}
public static int max(int[] array) {
int max = array[0];
for (int value : array) {
if (value > max) {
max = value;
}
}
return max;
}
public static int min(int[] array) {
int min = array[0];
for (int value : array) {
if (value < min) {
min = value;
}
}
return min;
}
}
'인프런 - 실전 자바 로드맵 > 실전 자바 - 기본편' 카테고리의 다른 글
접근제어자, 접근 제어자 이해, 접근 제어자 종류, 접근 제어자 사용(필드,메서드,클래스레벨) 캡슐화 (0) | 2025.01.04 |
---|---|
패키지, 패키지(시작 / import) 패키지 규칙, 패키지 활용 (0) | 2024.12.28 |
생성자, 생성자 - 필요한 이유, this, 생성자 - 도입, 기본 생성자, 생성자 - 오버로딩과 this() (1) | 2024.12.28 |
객체 지향 프로그래밍, 절차 지향 프로그래밍, 클래스와 메서드 (0) | 2024.12.25 |
기본형과 참조형, 기본형 vs 참조형, 참조형과 메서드 호출, 변수와 초기화, null, NullPointerException (1) | 2024.12.21 |