첫 미션을 하면서 리뷰어가 계속해서 남긴 코멘트가 있었다.
"왜 static을 사용했나요?" "static 없이 구현해 보세요" "static factory method를 써야 할까요?"
사실 그전까지는 static이 "클래스 생성 없이 쓸 수 있다" 정도로만 알고 있었는데, 이번 기회를 통해 깊게 알아볼 수 있었다.
❯ Static
static이란 '정적'이라는 키워드로 클래스, 필드, 메서드에 모두 적용할 수 있다.
이때 자바 메모리 영역에 대한 이해가 필요하다. 크게 세 가지로 구분할 수 있으며 각 특징은 아래와 같다.
Static 영역 | Stack 영역 | Heap 영역 |
- 클래스 정보나 static으로 선언된 것 저장 - |
- 원시타입의 데이터 저장 - Heap 영역에 생성된 Object 타입의 데이터 참조값(주소)이 저장 - 함수가 호출될 때 사용되는 메모리로 기능 수행이 끝나면 자동으로 반환됨 |
- Object 타입의 데이터 저장 - Stack 영역에 저장된 주소값을 통해 접근 - 호출이 종료되어도 사라지지 않음 (GC에 의해 메모리에서 해제) |
즉, static 필드는 객체 생성 이전에 클래스 로딩 시점에 static 영역에 초기화된다. 프로그램이 실행되는 동안 계속해서 유지된다.
모든 인스턴스에서 동일한 값을 공유하게 되며 만약 한 인스턴스에서 이 값을 변경한다면 다른 인스턴스에서도 변경된 값을 참조하게 된다.
반면 instance 필드는 해당 인스턴스에서만 사용할 수 있는 것을 의미한다. 인스턴스별로 다른 값을 가질 수 있다.
+ Static 영역과 관련하여 수정된 내용이 있어 추가한다.
Java 8 이전에는 Permanent(PermGen)에서 정적 멤버, 클래스 메타 데이터 등을 저장했다.
그러나 PermGen이 제한적인 크기를 갖고있어 메모리 부족 문제가 생길 수 있다.
(사용자가 영역을 설정해주는것이 가능하기는 하나 매번 용량을 예측해서 사용하는것이 쉽지는 않다.)
그래서, Java 8부터 PermGen영역이 없어지고 MetaSpace 영역이 생겼다고한다.
MetaSpace는 힙이 아닌 Native 메모리 영역으로 취급된다고 한다. 따라서 MetaSpace의 크기는 자동으로 조절된다.
(Native는 OS레벨에서 관리, 힙은 JVM에서 관리)
이렇게되면서 MetaSpace에서는 클래스 메타데이터만을 저장하고, 정적 멤버는 힙에서 관리하게 되었다고한다.
따라서 static 인스턴스도 GC의 관리를 받게 되었고, 참조를 잃으면 GC에 의해 제거될 수 있다고한다.
참고자료: JEP 122: Remove the Permanent Generation
코드 예시를 통해 살펴보면 아래와 같다.
static 키워드가 붙은 필드는 일반 메서드에서도 사용이 가능하지만
static 키워드가 붙지 않은 인스턴스 필드는 static 메서드에 사용이 불가능하다.
(아직 인스턴스가 생성되지 않은 상태이므로 사용할 수 없다)
public class Example {
// 정적 필드, 클래스 변수(Static Field)
public static String classText = "class";
// 인스턴스 필드(Instance Field)
public String instanceText = "instance";
// 정적 메서드(Static Method)
public static void printWithStatic() {
System.out.println(classText);
System.out.println(instanceText); // 인스턴스 필드 사용 불가
}
// 인스턴스 메서드(Instance Method)
public void printWithoutStatic() {
System.out.println(classText); // 정적 필드 사용 가능
System.out.println(instanceText);
}
public class ExampleApp {
public static void main(String[] args) {
System.out.println(Example.classText);
System.out.println(Example.instanceText); // 클래스에서 인스턴스 필드 접근 불가
Example.printWithStatic(); // 클래스로 직접 호출 가능
Example.printWithoutStatic(); // 클래스로 호출 불가
Example example = new Example();
example.printWithoutStatic(); // 인스턴스 통해 호출 가능
System.out.println(example.instanceText); // 인스턴스 통해 필드 접근 가능
}
}
}
그렇다면 Static 키워드를 사용하는 장 / 단점은 어떤 것이 있을까.
▸ 장점
1. 메모리 측면에서 효율적일 수 있다.
만약 여러 인스턴스에서 공통된 필드/메서드를 사용해야 할 경우 static이 유리할 수 있다.
stack 메모리 영역에 저장되어 고정된 메모리 영역을 사용하고, 매번 인스턴스를 생성하지 않아도 되기 때문에 메모리 낭비를 줄일 수 있다.
2. 속도가 빠를 수 있다.
인스턴스를 생성하지 않고 바로 사용이 가능하기 때문에 실행 시간에 일부 성능 개선이 될 수 있다.
‣ 단점
1. 메모리 측면에서 비효율적 일수 있다.
장점과 반대되는 개념인데, static 키워드의 경우 프로그램 시작부터 종료 시까지 메모리에 할당된 채로 존재하게 된다.
만약 프로그램에서 많은 static을 사용하게 된다면 오히려 메모리에 악영향을 줄 수 있다.
2. 객체지향적이지 못하다
static은 객체를 생성하지 않고도 접근이 가능하다.(접근제어자에 따라 일부 다를 수는 있다.)
이 특징은 객체 데이터들이 캡슐화되어야 한다는 객체지향 프로그래밍 원칙에 위배될 수 있다.
3. 다중 스레드에서 안정성이 보장되지 않는다
static의 경우 모든 스레드에서 접근이 가능하다. 만약 여러 스레드에서 접근해서 데이터를 변경한다면 예기치 않은 결과가 나올 수도 있다.
이 경우 추가적인 Synchronized 처리가 필요할 수 있다.
장, 단점을 비교하고 있지만 이는 상황에 따라 달라질 수 있다.
위에서도 완전하게 단언하지 못하는 것처럼 상황에 따라 장점이 클 수도 있고, 단점이 클수도 있다.
어느 하나가 좋다 / 나쁘다기보다는 현재 내 코드에 적절한 방식이 어떤 방식인지 살펴보고 적용하는 것이 중요하다.
❯ 정적 팩토리 메서드(static factory method)
static을 활용한 대표적인 사례가 static factory method이다.
이펙티브 자바(조슈아 블로크 저)에서 가장 처음으로 나오는 내용이 다음과 같다.
생성자 대신 정적 팩토리 메서드를 고려하라
이름과 현재 위치를 필드로 갖는 Car라는 클래스가 있다고 가정하자. 일반적으로 아래와 같은 생성자를 사용하게 된다.
public class Car {
private String name;
private int position;
public Car(String name, int position) {
this.name = name;
this.position = position;
}
}
생성자를 사용하면 아래 방식으로 인스턴스를 생성하게 된다.
public class Application {
public static void main(String[] args) {
Car car = new Car("차량이름", 0);
}
}
그렇다면 정적 팩터리 메서드를 적용해 보자.
사용자 위치 초기값은 0이라고 가정하고 이름만 받아와서 생성할 수 있도록 작성하였다.
public class Car {
private String name;
private int position;
// 생성자를 private으로 바꾸면 외부에서 생성자를 통한 인스턴스 생성을 막을 수 있다.
private Car(String name, int position) {
this.name = name;
this.position = position;
}
// "이름" 속성만 받아와서 원하는 객체를 생성할 수 있다.
public static Car from(String name){
return new Car(name, 0);
}
}
정적 팩토리 메서드를 적용해서 인스턴스를 생성하면 아래와 같다.
public class Application {
public static void main(String[] args) {
Car car = Car.from("차량이름");
}
}
그렇다면 이렇게 정적 팩터리 메서드를 사용했을 때의 장/단점에 대해 알아보자. (이펙티브 자바 내용 참고)
▸ 장점
1. 이름을 가질 수 있다.
위 예시에서는 from()을 사용했지만, 생성자에 넘기는 매개변수나 생성되는 객체의 특성을 메서드 명에 담을 수 있다.
2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
반복된 동작의 경우 불필요한 객체 생성 없이 수행할 수 있다.
3. 반환 타입의 하위타입 객체를 반환할 수 있는 능력이 있다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
3번에서 이어지는 내용인데, 매개변수에 따라 반환 타입을 다르게 설정해 줄 수도 있다. 반환하는 클래스가 반환타입의 하위타입이기만 하면 어떤 클래스이던지 반환할 수 있다.
5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
▸ 단점
1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
보통 정적 팩토리 메서드를 사용할 때에는 객체 생성을 정적 팩토리 메서드를 통해서만 하도록 기본 생성자를 private으로 둔다.
이 경우 상속하려는 곳에서 생성자를 호출하지 못하게 되므로 상속이 불가능하다.
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
생성자처럼 API 문서에 바로 드러나지 않아 사용자가 직접 정리를 해주어야 한다.
문서로 직접 정리하거나 메서드 규약을 따라 짓는 방식으로 문제를 완화해야 한다.
이처럼 static, static factory method 모두 각각의 장단점이 있어 필요한 경우에만 사용하는 것이 중요하다.
예를 들어, 이번 미션에서 생성자를 여러 개 사용하고 있었는데 "필요한 매개변수 혹은 생성 로직에 따라 이름을 정해주는 건 어떨까요?"라는 피드백을 받기도 했다. 이 때는 정적 팩토리 메서드를 통해 특징을 나타내는 게 도움이 될 수 있다.
또한 게임 진행을 위해 random 값을 생성하는 로직이 있다고 하자. 이 부분은 모든 인스턴스에서 공통적으로 사용하며, 반복해서 동작하는 부분이므로 static을 적용해서 객체 생성 없이 바로 사용하는 것이 효율적일 수 있다.
반면 기본 생성자만으로도 생성이 가능한 객체를 정적 팩토리 메서드로 생성하게 되면 오히려 객체 지향적이지 못할 수 있다.
참고자료
이펙티브 자바 - 조슈아 블로크 저
'우아한테크코스 > 레벨 1 - Java' 카테고리의 다른 글
[GitHub] 코드리뷰가 익숙하지 않은 분들을 위한 GitHub에서 코드리뷰 하는법 (0) | 2024.03.30 |
---|---|
[JAVA] 상태 패턴(State Pattern) with BlackJack (21) | 2024.03.19 |
[JAVA] 캐싱 활용하기 (0) | 2024.03.11 |
[JAVA] VO(Value Object) vs DTO(Data Transfer Object) with record (13) | 2024.03.05 |
[JAVA] 함수형 인터페이스 개념과 예시 (0) | 2024.02.21 |