이번에 static 키워드를 쓰면서
static 함수를 사용하는 과정에서 변수가 static으로 변환이 되는 문제가 발생했다.
이에 따라 테스트코드가 다 깨져버려서 확인해보니 인스턴스들끼리 그 변수를 공유하는 문제가 발생한 것이다.
public class Data {
private static Type type;
private static int size;
private static List<Integer> index;
private String address;
private Data(){}
public static Data of (Type type, int size){
Data data = new Data();
data.type = type;
data.size = size;
index = new ArrayList<>();
return data;
}
정적 팩토리 메서드를 만드는 과정에서 인텔리제이에서 컴파일 에러가 났으니 코드를 수정하라고 권유를 했던 거 같다.
위 코드처럼 정적 팩토리 메서드 내에서 인스턴스를 만들고, 그 인스턴스 변수를 직접 수정하는 방식은 컴파일 에러가 뜨지 않는다.
public class Data {
private Type type;
private int size;
private List<Integer> index;
private String address;
private Data(){}
public static Data from(){
size =3;
}
다만 이런식으로 정적 메서드 안에서 필드 변수에 직접 접근을 하려고 하면 static으로 바꿔야 한다.
그렇지 않으면 컴파일 에러가 뜬다. 이 부분이 헷갈렸는데 정리를 하면서 다시 알게 됐다.
좀더 자세히 알기 위해서 static 개념을 정리해본다.
1."the keyword static means that the particular member belongs to a type itself, rather than to an instance of that type."
static은 인스턴스가 아니라 타입 그 자체에 속하는 멤버들을 말한다고 한다. 즉, 인스턴스들이 개별적으로 갖는 변수들이 아니라, 모두가 공유하는 변수 정도로 생각하면 될 거 같다.
static한 변수는 heap 메모리에 저장된다는 점도 기억하자.
static한 변수를 잘못 쓰면 공유해서는 안될 변수들을 공유하는 문제가 생길 수 있다.
private Type type;
private int size;
private List<Integer> index;
private static String address;
@DisplayName("타입을 설정하고 제거한다.")
@Test
void test4(){
Controller controller = settings();
String malloc = controller.malloc(new Type("string", 4), 2);
controller.malloc(new Type("int", 3), 3);
controller.free(malloc);
List<Data> data = controller.getData();
assertTrue(data.size()==1);
String malloc1 = controller.malloc(new Type("string", 4), 2);
assertTrue(malloc.equals(malloc1));
}
우선, malloc은 데이터를 힙에 저장하면서 상대 주소를 반환하는 메서드다. free는 이 상대 주소를 기준으로 힙에서 데이터를 찾고 삭제한다.
처음에 잘 작동하던 테스트코드가 갑자기 깨져서 원인을 찾아봤다.
원인은 static에 있었다.
데이터들은 각각 상대 주소가 달라야 한다. 하지만, 변수에 static이 붙어버려서
데이터들의 상대 주소가 동일해진 문제가 생긴 것이다.
당연히 테스트가 깨질 수밖에 없는 상황이다.
String malloc = controller.malloc(new Type("string", 4), 2);
controller.malloc(new Type("int", 3), 3);
controller.free(malloc);
//malloc을 호출하면 상대주소값이 반환된다.
//free 메서드는 이 상대 주소값을 기준으로 데이터를 리스트에서 삭제한다.
1)malloc으로 반환한 첫 주소는 410이다.
2)두번째 malloc으로 428의 주소를 반환했다.
3)이 주소는 static이라서, 모든 인스턴스가 이 주소를 공유하게 된다. 즉, 첫번째 데이터가 주소를 410이라고 반환했지만, 두번째 데이터가 주소를 428이라고 반환하면서 처음 쓰여진 데이터를 428로 덧씌운 상황이 된 것이다.
4)free메서드는 처음 반환한 410을 기준으로 데이터를 찾는다. 모든 데이터의 상대 주소는 428이라서 데이터를 찾지 못하고, 삭제도 하지 못한 것이다.
static 키워드를 지우지 않은 상태에서 디버깅을 해봤다.
디버깅을 해보면 각각의 데이터 인스턴스에는 상대 주소가 필드로 존재하지 않았다.
디버깅을 해보면 실제로 데이터가 삭제되지 않은 것을 볼 수 있었다.
static 키워드를 지워주면 테스트는 통과하게 된다.
static 키워드를 지워주니 삭제하려는 데이터가 정상적으로 삭제된 것을 확인할 수 있었다.
++
static 키워드를 address에서 빼주면 위 사진처럼 각각의 인스턴스에 필드값으로 설정이 된다.
그렇다면, static 키워드를 그대로 둔 채 마지막으로 설정한 주소값을 기준으로 데이터를 지워버린다면 어떤 데이터가 삭제될까?
코드자체가 발견한 가장 먼저의 데이터를 삭제 하는 방식으로 로직이 구성돼 있다.
String malloc = controller.malloc(new Type("string", 4), 2);//이 데이터가 먼저 들어갔으니, 먼저 삭제 된 것이다.
String malloc2 = controller.malloc(new Type("int", 3), 3);
2.static 필드를 어디서 쓸 수 있는지 한번 찾아봤다.
public class Car {
private String name;
private String engine;
public static int numberOfCars;
public Car(String name, String engine) {
this.name = name;
this.engine = engine;
numberOfCars++;
}
// getters and setters
}
//출처 https://www.baeldung.com/java-static
이런 식으로 차가 늘어날 때마다 static 변수를 하나씩 올려주는 방식으로도 쓸 수 있을 거 같다.
3.static 메서드에 대해서도 공부를 해봤다.
static methods also belong to a class instead of an object. So, we can call them without creating the object of the class in which they reside.
static 메서드는 인스턴스가 아니라 객체 자체에 속하는 메서드이고, 인스턴스를 생성하지 않고도 호출할 수 있다.
We also commonly use static methods to create utility or helper classes so that we can get them without creating a new object of these classes.
static 메서드는 주로 유틸리티나, 헬퍼 클래스를 사용할 때 쓰는 거 같다.
public class Calculator {
static int calculateSize(int count){
return (int) Math.ceil(count* Heap.BUFFER_SIZE / 8.0);
}
}
나도 평소에 인스턴스를 생성하지 않아도 될 거 같으면 static 메서드를 갖고 있는 유틸리티 클래스를 만들어서 사용했다.
보통은 인스턴스가 상태값을 갖고 있지 않아도 될 때, 간단한 계산을 하거나 출력을 할 때 static 메서드를 쓰고 있다.
static 메서드를 쓰는 상황은 아래 상황정도로 요약할 수 있을 거 같다.
to access/manipulate static variables and other static methods that don’t depend upon objects.
static methods are widely used in utility and helper classes.
4.static block은 평소에 잘 쓰지 않는데 이번에 정리해본다.
public class StaticBlockDemo {
public static List<String> ranks = new LinkedList<>();
static {
ranks.add("Lieutenant");
ranks.add("Captain");
ranks.add("Major");
}
static {
ranks.add("Colonel");
ranks.add("General");
}
}
static 블록에 대한 설명을 찾아보면
"This code inside the static block is executed only once: the first time the class is loaded into memory."라는 내용이 나온다.
즉, 클래스가 로딩될 때 처음 한번만 실행되는 블록인 것이다. 이 객체를 여러 번 생성하더라도 한번만 실행됨을 보장하는 것 같다.
그래서, 위의 코드를 보면 List 객체에 미리 지정된 값을 초기화해줄 때 사용할 수 있는 거 같다.
private static final Map<String, Character> codeToCharMap = new HashMap<>();
static {
codeToCharMap.put("00", '0');
codeToCharMap.put("01", '1');
codeToCharMap.put("02", '2');
codeToCharMap.put("03", '3');
codeToCharMap.put("04", '4');
codeToCharMap.put("05", '5');
codeToCharMap.put("06", '6');
codeToCharMap.put("07", '7');
codeToCharMap.put("08", '8');
codeToCharMap.put("09", '9');
}
이런식으로 해주면 이 클래스를 여러개 만들어도 static에 있는 작업은 한번만 동작한다고 한다.
위에서 테스트를 해봤다. code라는 인스턴스를 만들고, getCode를 해서 codeToCharMap를 반환했다. 그리고, map을 clear해봤다.
그 다음에 code1이라는 인스턴스를 다시 만들어서getCode를 해봤더니, 이렇게 가져온 map도 비어있었다. 즉, 인스턴스를 처음 생성할 때만 map에 요소들을 넣어주고, 그 다음에 인스턴스를 반복적으로 만들어도 static 블록에 있는 코드들이 실행되지 않는 것이다.
요약해보면,
if the initialization of static variables needs some additional logic apart from the assignment
(전체적인 로직과 별도로 어떤 추가적인 로직을 수행할 때)
if the initialization of static variables is error-prone and needs exception handling
(이 부분은 무슨 말인지 솔직히 잘 모르겠다)
다만 개인적으로는 static 메서드를 쓰는 게 객체지향적인 방식과 맞는건지 고민이 될 때가 있다. JK얘기를 듣고, 책에서 보더라도 객체지향적인 방식은 하나의 클래스에 데이터가 있고 그 데이터를 메서드로 조작하는 캡슐화가 필요하다.
하지만, static 메서드를 쓰면 다른 클래스에 있는 데이터를 조작해야 할 때가 있다. 또한, static 메서드가 여러개 모여있는 범용 클래스에는 여러 책임이 몰려 있을 때가 있다. 범용 클래스로 여기니 여러가지 잡다한 메서드가 다 들어갈 수 있는 것이다.
추가로, static 메서드는 함수형 프로그래밍의 방식과 거의 유사한데 객체지향과 맞지않는 지점들이 좀 있어서 고민이다. 적절히 쓰면 좋겠지만 static 메서드를 쓰는 걸 주의를 좀 해야할듯하다.
참고자료
-A Guide to the Static Keyword in Java | Baeldung