주제 : 김영한님의 자바 실전 강의 총 정리
내용 : 접근 제어자, 메모리 구조와 static
자바는 public, private 같은 접근 제어자를 제공한다. 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다.
스피커의 음량은 절대로 100을 넘으면 안되는 요구사항이 있다.
스피커 객체를 만들어보자.
public class Speaker { int volume; public 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); } }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 = 200; // 필드에 직접 접근 speaker.showVolume(); } }
- 앞서
volumeUp()과 같은 메서드를 만들어서 음량이 100을 넘지 못하도록 개발했지만volume필드에 직접 접근해서 원하는 값을 설정하면 얼마든지 조종이 가능하다. 이를 해결하기 위해서volume접근 제어자를private으로 수정해줘야 한다.
접근 제어자의 종류
- private : 모든 외부 호출을 막는다.
- default : 같은 패키지안에서 호출은 허용한다.
- protected : 같은 패키지 안에서 호출은 허용한다. 패키지가 달라도 상속 관계의 호출은 허용한다.
- public : 모든 외부 호출을 허용한다.
접근 제어자의 핵심은 속성과 기능을 외부로부터 숨기는 것이다.
private은 나의 클래스 안으로 속성과 기능을 숨길 때 사용, 외부 클래스에서 해당 기능을 호출할 수 없다.default는 나의 패키지 안으로 속성과 기능을 숨길 때 사용, 외부 패키지에서 해당 기능을 호출할 수 없다.protected는 상속 관계로 속성과 기능을 숨길 때 사용, 상속 관계가 아닌 곳에서 해당 기능을 호출할 수 없다.public은 기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.
예시 : volume 필드를 Speaker 클래스 외부에서는 접근하지 못하게 막아보자
package access; public class Speaker { priavte int volume; //private 사용 ... }
priavte접근 제어자는 모든 외부 호출을 막는다. 따라서, priavte이 붙은 경우 해당 클래스 내부에서만 호출할 수 있다. 즉, volume 필드는 이제 Speaker 내부에서만 접근할 수 있다.
public은 모든 접근을 허용하기 때문에 필드, 메서드 모두 접근할 수 있다.default는 같은 패키지에서 접근할 수 있다.access.b.AccessOuterMain은access.a.AccessData와 다른 패키지이다. 따라서default접근 제어자에 접근할 수 없다.private은AccessData내부에서만 접근할 수 있다
필드, 메서드 레벨의 접근 제어자
package access.a; public class AccessData{ public int publicField; int defaultField; private int privateField; public void publicMehotd(){ 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 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(); } }
- 클래스 레벨의 접근 제어자 규칙
- 클래스 레벨의 접근 제어자는 public, default만 사용할 수 있다.
private,protected는 사용할 수 없다.- public 클래스는 반드시 파일명과 같아야 한다.
- 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있다.
- 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.
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()을 보면 각각의 클래스를 사용하는 예를 보여준다.PublicClass는public접근 제어다. 따라서 어디서든 사용할 수 있다.DefaultClass1,DefaultClass2와는 같은 패키지에 있으므로 사용할 수 있다.
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는 같은 패키지이다. 따라서 접근할 수 있다.
캡슐화는 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말한다. 캡슐화를 통해 데이터의 직접적인 변경을 방지하거나 제한할 수 있다. 그럼 어떤 것을 숨기고 어떤 것을 노출해야 할까?
1. 데이터를 숨겨라
- 객체에는 속성(데이터)과 기능(메서드)이 있다. 캡슐화에서 가장 필수로 숨겨야 하는 것은 속성(데이터)이다.
Speaker의volume을 떠올려보자. 객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있다. 결국 모든 안전망을 다 빠져나가게 된다. 따라서, 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다.2. 기능을 숨겨라
- 객체의 기능 중에서 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있다. 이런 기능도 모두 감추는 것이 좋다.
static 변수는 메서드 영역에서 관리
자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있다.
- 메서드 영역 : 클래스 정보를 보관한다. 이 클래스 정보가 붕어빵 틀이다.
- 스택 영역 : 실제 프로그램이 실행되는 영역이다. 메서드를 실행할 때마다 하나씩 쌓인다.
- 힙 영역 : 객체가 생성되는 영역이다.
new명령어를 사용하여 이 영역을 사용한다. 쉽게 이야기해서 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간이다. 참고로 배열도 이 영역에 생성된다.
- 메서드 영역 : 메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.
- 클래스 정보 : 클래스의 실행 코드, 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재한다.
- static 영역 : static 변수들을 보관한다.
- 런타임 상수 풀 : 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다.
- 스택 영역 : 자바 실행 시, 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
- 스택 프레임 : 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
- 힙 영역 : 객체(인스턴스)와 배열이 생성되는 영역이다. 가비지 컬렉션(GC)이 이루어지는 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.
static 키워드는 주로 멤버 변수와 메서드에 사용된다.
먼저 멤버 변수에 static키워드가 왜 필요한지 이해하기 위해 예제를 만들어보자.
인스턴스 내부 변수에 카운트 저장
package static1; public class Data1{ public String name; public int count; public Data1(String name){ this.name = name; count ++; } }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); } }이 프로그램은 당연히 기대한 대로 인스턴스에 사용되는 멤버 변수 count 값이 인스턴스끼리 서로 공유되지 않는다. 객체를 생성할 때마다 Data1 인스턴스는 새로 만들어진 그리고 인스턴스에 포함된 count 변수도 새로 만들어지기 때문이다.
외부 인스턴스에 카운트 저장
package static1; public class Counter { public int count; }package static1; public class Data2 { public String name; public Data2(String name, Counter counter) { this.name = name; counter.count++; } }
- 기존 코드를 유지하기 위해 새로운
Data2클래스를 만들었다. 여기에는count멤버 변수가 없다. 대신에 생성자에서Counter인스턴스를 추가로 전달 받는다.- 생성자가 호출되면
counter인스턴스에 있는count변수의 값을 하나 증가시킨다.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인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때 마다 값을 정확하게 증가시킬 수 있다.
그런데 여기에는 약간 불편한 점들이 있다.
Data2클래스와 관련된 일인데,Counter라는 별도의 클래스를 추가로 사용해야 한다.- 생성자의 매개변수도 추가되고, 생성자가 복잡해진다. 생성자를 호출하는 부분도 복잡해진다.
static 변수 사용
특정 클래스에서 공용으로 함께 사용할 수 있는 변수를 만들 수 있다면 편리할 것이다.
static키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있다.
객체가 생성되면 생성자에서 정적 변수 count 의 값을 하나 증가시킨다
public class Data3 { public String name; public static int count; // static public Data3(String name){ this.name = name; count ++; } }
static int count부분을 보자. 변수 타입(int) 앞에static키워드가 붙어있다.- 이렇게 멤버 변수에
static을 붙이게 되면static변수, 정적 변수 또는 클래스 변수라 한다.- 객체가 생성되면 생성자에서 정적 변수
count의 값을 하나 증가시킨다.
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); } }
- 코드를 보면
count정적 변수에 접근하는 방법이 조금 특이한데Data3.count와 같이 클래스명에.을 사용한다. 마치 클래스에 직접 접근하는 것처럼 느껴진다.
static이 붙은 멤버 변수는 메서드 영역에서 관리한다.
static이 붙은 멤버 변수count인스턴스 영역에 생성되지 않는다. 대신에 메서드 영역에서 이 변수를 관리한다.Data3("A")인스턴스를 생성하면 생성자가 호출된다.- 생성자에는
count++코드가 있다.count는static이 붙은 정적 변수이다. 정적 변수는 인스턴스 영역이 아니라 메서드 영역에서 관리한다. 따라서 메서드 영역에 있는count의 값이 하나 증가한다.
정리
static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀(메서드 영역)이 특별히 관리하는 변수이다. 붕어빵 틀은 1개이므로 클래스 변수도 하나만 존재한다. 반면에, 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재한다.
용어 정리
public class Data3 { public String name; public static int count; //static }예제 코드에서
name,count는 둘다 멤버 변수이다.
멤버 변수(필드)는static이 붙은 것과 아닌 것에 따라 다음과 같이 분류할 수 있다.
멤버 변수(필드)의 종류
인스턴스 변수 :static이 붙지 않은 멤버 변수, 예)name
static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라 한다. 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.
클래스 변수 :
static이 붙은 멤버 변수, 예)count
클래스 변수, 정적 변수,static변수등으로 부른다. 용어를 모두 사용하니 주의하자
static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다. 따라서 클래스 변수라 한다.
클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용된다.
변수와 생명주기
- 지역 변수(매개변수 포함): 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임도 제거 되는데 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 지역 변수는 생존 주기가짧다.
- 인스턴스 변수: 인스턴스에 있는 멤버 변수를 인스턴스 변수라 한다. 인스턴스 변수는 힙 영역을 사용한다. 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길다.
- 클래스 변수: 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성된다. 그리고 JVM이 종료될 때 까지 생명주기가 어어진다. 따라서 가장 긴 생명주기를 가진다.
이번에는 static이 붙은 메서드에 대해 알아보자.
package static2; public class DecoUtil1 { public String deco(String str) { String result = "*" + str + "*"; return result; } }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); } }
- 앞서 개발한
deco()메서드를 호출하기 위해서는DecoUtil1의 인스턴스를 먼저 생성해야 한다. 그런데deco()라는 기능은 멤버 변수도 없고, 단순히 기능만 제공할 뿐이다. 인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 큰데, 이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공한다.
static 메서드
이 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해서 바로 호출할 수 있다.
public class DecoUtil2 { public static String deco(String str) { String result = "+" + str + "+"; return result; } } public class DecoMain2 { public static void main(String[] args) { String s = "hello java"; // DecoUtil2 utils = new DecoUtil2(); 인스턴스 생성 필요X String deco = DecoUtil2.deco(s); System.out.println("before: " + s); System.out.println("after: " + deco); } }실행 결과
before: hello java
after: +hello java+
static 메서드2
정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있다. 하지만 정적 메서드는 언제나 사용할 수 있는 것이 아니다.
정적 메서드 사용법
1.static메서드는static만 사용할 수 있다.
클래스 내부의 기능을 사용할 때, 정적 메서드는static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다. 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다.2. 반대로 모든 곳에서
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); } }
- 정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값의 개념이 없다. 따라서, 저엊ㄱ 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.
멤버 메서드의 종류
인스턴스 메서드:
static이 붙지 않은 멤버 메서드
static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 메서드라 한다.클래스 메서드:
static이 붙은 멤버 메서드
클래스 메서드, 정적 메서드,static메서드등으로 부른다.
static이 붙은 멤버 메서드는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다. 따라서 클래스 메서드라 한다.
[문1] 구매한 자동차 수
다음 코드를 참고해서 생성한 차량 수를 출력하는 프로그램을 작성하자
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 메서드 } }
정답
public class Car { private static int totalCars; // 반드시 static private String name; public Car(String name){ System.out.println("차량 구입, 이름:" + name); this.name = name; totalCars++; } public static void showTotalCars(){ System.out.println("구매한 차량 수:" + totalCars); } }
여기서 private static int totalCars;를 반드시 static으로 선언해야 하는 이유는 메서드 영역에서 해당 변수를 관리하여 계속해서 차량 수를 추적하기 때문입니다.
[문2] 수학 유틸리티 클래스
다음 기능을 제공하는 배열용 수학 유틸리티 클래스(MathArrayUtils)를 만드세요.
sum(int[] array): 배열의 모든 요소를 더하여 합계를 반환합니다.average(int[] array): 배열의 모든 요소의 평균값을 계산합니다.min(int[] array): 배열에서 최소값을 찾습니다.max(int[] array): 배열에서 최대값을 찾습니다.요구사항
MathArrayUtils은 객체를 생성하지 않고 사용해야 합니다.MathArrayUtils의 인스턴스를 생성하지 못하게 막으세요.(중요)
- 이렇게 함으로써 클래스의 메서드들을 호출할 때 객체를 생성하지 않아도 되므로 메모리를 절약하고, 코드를 더 간결하고 명확하게 만들 수 있습니다.
- 실행 코드에
static import를 사용해도 됩니다.
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)); } }public class MathArrayUtils { private MathArrayUtils() { //private 인스턴스 생성을 막는다. } public static int sum(int[] values) { int total = 0; for (int value : values) { total += value; } return total; } public static double average(int[] values) { return (double) sum(values) / values.length; } public static int min(int[] values) { int minValue = values[0]; for (int value : values) { if (value < minValue) { minValue = value; } } return minValue; } public static int max(int[] values) { int maxValue = values[0]; for (int value : values) { if (value > maxValue) { maxValue = value; } } return maxValue; } }