자바를 사용해 코딩을 하다보면 static
이라는 키워드를 보게 됩니다!
대표적으로 public static void main()
과 같은 구문에서도 볼 수 있죠!
오늘은 이 static
키워드가 무엇을 의미하고 어떻게 동작하는지 알아보겠습니다!
자바의 static
자바에서 static
키워드는 클래스 수준에서 변수를 선언하거나 혹은 메서드를 정의할 때 사용합니다.
static
의 사용을 통해 클래스의 인스턴스에 속하는 것이 아닌 클래스 자체에 속하는 멤버를 만들 수 있습니다.
예를 들어볼까요?
static 변수
static 변수
는 정적 변수
혹은 클래스 변수
라고도 합니다. 클래스의 모든 인스턴스가 공유하는 변수 입니다.
public class Counter {
private static int count = 0;
public Counter() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
System.out.println("생성된 인스턴스의 수: " + c1.getCount()); // 출력: 생성된 인스턴스의 수: 3
System.out.println("생성된 인스턴스의 수: " + c2.getCount()); // 출력: 생성된 인스턴스의 수: 3
System.out.println("생성된 인스턴스의 수: " + c3.getCount()); // 출력: 생성된 인스턴스의 수: 3
}
}
위 코드를 보면 Counter
라는 객체는 count
라는 변수를 가지고 있습니다. 이 변수는 static
으로 선언되어 있으므로 모든 인스턴스가 공유합니다.
즉, Counter
의 인스턴스가 생성될 때 마다 count
가 증가합니다.
이게 어떻게 가능할까요?
기존의 생성된 객체의 변수는 인스턴스 생성 시에 힙 영역
에 생성되는 것으로 지난 글에서 알 수 있었습니다.
하지만, static
으로 생성되는 변수는 인스턴스 생성시 힙 영역에 생성되는 것이 아닌, 클래스 로드시 바로 메서드 영역
에 생성되게 됩니다.
위 그림과 같이 메서드 영역에서 count
라는 정적 변수가 관리되므로, 해당 인스턴스에 묶여 있는 속성이 아니고 공유할 수 있게 되는 형태입니다.
따라서 위 코드와 같이 특정 클래스의 인스턴스가 몇 개 생성되었는지를 추적할 수가 있는 것이지요.
static 메서드
static 메서드
는 마찬가지로 정적 메서드
혹은 클래스 메서드
라고도 불리웁니다. 클래스 메서드는 인스턴스를 생성하지 않고도 호출할 수 있는 메서드입니다.
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
public static int subtract(int a, int b) {
return a - b;
}
}
public class Main {
public static void main(String[] args) {
int sum = MathUtils.add(5, 3);
int difference = MathUtils.subtract(5, 3);
System.out.println("합: " + sum); // 출력: 합: 8
System.out.println("차: " + difference); // 출력: 차: 2
}
}
위 예제에서 MathUtils
클래스의 add
와 subtract
메서드는 static
으로 선언되어 있는 정적 메서드로 인스턴스의 생성이 필요 없이 이름만으로 직접 호출할 수 있습니다.
그래서, 내부에 속성을 가지고 있을 필요가 없는 유틸리티성 메서드를 정의할 때 유용하게 사용됩니다.
이와 같이 static 키워드는 클래스 수준의 변수를 선언하거나 메서드를 정의할 때 사용되고, 기존의 인스턴스를 생성하여 사용하는 인스턴스 변수나 메서드와는 다르게 동작합니다. 이처럼 static을 사용되는 예시를 살펴보았는데 static을 사용했을 때의 장점을 한 번 알아볼까요?
Static의 장점
1. 메모리 효율성
static 변수는 클래스 로드 시 메모리에 할당되며, 프로그램이 종료될 때까지 하나의 메모리 공간만 사용합니다. 이는 인스턴스 변수가 일일이 각자의 메모리 주소를 차지하지 않고 하나의 메모리 주소를 공유하면서 사용할 수 있게되기 때문에 메모리 낭비를 줄일 수 있습니다.
public class Config {
public static String appName = "My Application";
public static Strign version = "1.0.0";
}
public class Main {
public static void main(String[] args) {
System.out.println("어플리케이션 이름: " + Config.appName);
System.out.println("버전: " + Config.version);
}
}
위 코드의 예시 처럼 Config 클래스를 여러 번 참조하더라도 인스턴스를 따로 생성하지 않아 메모리 공간을 절약할 수 있습니다.
2. 유틸리티 클래스
static 메서드를 사용하면 굳이 인스턴스화하지 않아도 되는 객체들을 만들 필요가 없이 유틸리티 클래스를 만들어 사용할 수 있습니다. 유틸리티 클래스는 공통적인 기능만을 제공하는 메서드를 모아놓을 때 특히 유용한데요, 예를 들어 비즈니스 로직을 위한 문자열 조작, 날짜 계산, 수학 연산 등의 기능을 만들 때 유용하답니다.
public class StringUtils {
public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
public static String toUpperCase(String str) {
return str.toUpperCase();
}
}
public class Main {
public static void main(String[] args) {
String original = "hello";
String reversed = StringUtils.reverse(original);
String uppercased = StringUtils.toUpperCase(original);
System.out.println("원래 문자열: " + original);
System.out.println("뒤집은 문자열: " + reversed);
System.out.println("대문자 문자열: " + uppercased);
}
}
위 예시는 문자열을 뒤집거나 대문자로 변환하는 기능을 가진 유틸리티 클래스의 예시입니다 :)
상수 정의
static 키워드와 final 키워드를 함께 사용하면 변하지 않는 상수(constant)를 정의할 수 있습니다. 이는 코드에서 반복적으로 사용되는 값들을 중앙에서 관리하고 쉽게 변경 가능하도록 도와줍니다.
public class Constants {
public static final double PI = 3.141592653589793;
public static final String ERROR_MESSAGE = "에러가 발생하였습니다.";
}
public class Main {
public static void main(String[] args) {
double radius = 5;
double circumference = 2 * Constants.PI * radius;
System.out.println("원의 둘레: " + circumference);
System.out.println("에러 메세지: " + Constants.ERROR_MESSAGE);
}
}
싱글톤 패턴의 구현
static 키워드를 사용하면 싱글톤 패턴을 쉽게 구현할 수 있습니다. 싱글톤 패턴이란 클래스의 인스턴스를 하나만 생성하고 이를 전역적으로 접근할 수 있도록 하는 디자인 패턴입니다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void showMessage() {
System.out.println("안녕 나는 싱글톤 객체야!");
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.showMessage();
}
}
이와 같이 static 키워드는 효율적인 메모리 관리, 유틸리티성 클래스 작성, 상수 정의, 싱글톤 패턴 구현 등 다양한 용도로 사용될 수있습니다. 이를 통해 코드의 가독성과 유지보수성을 높이고 불필요한 메모리 사용을 줄이며, 중앙 집중식으로 관리하는 코드를 작성할 수 있게 됩니다.
하지만 이렇게 간편하게 사용할 수 있다고 무분별하게 static을 사용하면 안되겠죠?
Static의 단점
1. 인스턴스 멤버와의 혼동
static 멤버는 클래스 레벨에서 접근되므로 인스턴스 멤버와는 다르게 동작합니다. 하지만 static 키워드를 무분별하게 사용했을 경우 예상과 다르게 동작할 수 있으므로 꼭 static하게 관리되어야 하는 변수나 메서드만 static으로 선언하는 것이 좋습니다.
2. 객체 지향 설계 원칙 위배
객체 지향 프로그래밍의 주요 원칙 중 하나는 객체의 상태와 행동을 캡슐화하는 것입니다. static을 사용해 선언한 변수나 메서드는 해당 객체에 상태와 행동을 저장하는 것이 아닌 객체 외부에 상태나 행동을 정의하고 이를 공유하려하기 때문에 static 키워드를 너무 남발하게 된다면, 객체 지향 설계의 장점인 상속, 다형성 등을 활용하기가 어렵습니다.
3. 메모리 관리
static 멤버는 클래스 로드 시 메서드 영역에 상주하면서 해당 어플리케이션이 종료될 때 까지 메모리를 차지하게 됩니다. 이는 메모리를 낭비하는 메모리 누수의 원인이 될 수 있습니다.
public class MemoryLeak {
private static List<String> dataList = new ArrayList<>();
public static void addData(String data) {
dataList.add(data);
}
}
위 코드에서 dataList는 프로그램이 종료될 때 까지 메모리 공간을 차지합니다. 만약 이 리스트에 엄청 많은 데이터가 추가된다면 메모리를 심각하게 낭비할 수 있겠죠.
기존의 방법처럼 인스턴스 멤버로 생성되면 스택 영역에서 참조 제거되고, 힙 영역에서 gc에 의해 정리되지만 static으로 생성되면 JVM이 자동으로 메모리를 관리하는 영역에서 벗어나게 됩니다.
4. 동시성 문제
static 멤버는 여러 스레드에서 동시에 접근할 수 있으므로 멀티 스레드 환경에서 동시성 문제를 일으킬 수 있습니다. 적절한 동기화 방안 없이 static 변수를 수정하면 경쟁 상태(race condition)에 놓이게 될 수 있습니다.
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
}
위 코드는 synchronized 키워드를 활용하여 increment 메서드에 동기화 방안을 마련하였습니다. 만약 해당 키워드 없이 설계되었다면 여러 스레드가 동시에 count 변수에 접근하여 예기치 않은 결과가 나올 수 있습니다.
5. 테스트의 어려움
static으로 선언된 변수나 메서드는 단위 테스트 시 모킹이 어렵습니다.
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
public class MathService {
public int sumThreeNums(int a, int b, int c) {
int cal = MathUtils.add(a, b)
return MathUtils.add(cal, c);
}
}
public class MathServiceTest {
@Test
public void testSumThreeNums() {
int cal = MathUtils.add(1, 2);
int result = MathUtils.add(cal, 3);
assertEquals(6, result);
}
}
위 예제에서는 MathService를 테스트 하는 테스트 코드입니다. 단위 테스트 특성 상 외부의 의존은 전부 제거하거나 모킹하고 MathService만을 검증해야 하지만, static 메서드를 모킹하기 어려워 결국 MathUtils에 의존이 생기고 해당 테스트 코드는 MathUtils에 문제가 생겼다면 MathService를 제대로 검증하지 못하는 형태가 됩니다.
코드의 유연성 저하
static으로 선언된 멤버는 다형성을 활용하기 어렵기 때문에 코드의 유연성이 저하됩니다.
public class Config {
public static String dbUrl = "jdbc:mysql://localhost:3306/db"
}
public class Main {
public static void main(String[] args) {
System.out.println("데이터베이스 주소: " + Config.dbUrl);
}
}
위와 같이 db 접속 주소를 정적 변수로 선언하였다고 가정하였을 때, 다른 환경에서 데이터베이스 주소를 바꿔야 하는 경우, 직접 코드를 수정하는 방법만 사용할 수 있게 됩니다.
결론적으로 static 키워드는 강력한 기능을 제공하지만 무분별하게 사용하면 여러 문제를 발생시키기도 합니다.
따라서 static을 사용할 때는 다음과 같이 고려하여 사용하는 것이 좋습니다.
- 실제로 클래스 수준에서 관리해야 하는 데이터인가? (인스턴스 간의 공유가 필요한가?)
- 동시성 문제가 발생할 수 있는가? 그렇다면 어떻게 동기화를 할 것인가?
- 테스트하기 좋은 구조인가?
- 유지보수하기에 무리가 없는 구조인가?
static을 더 똑똑하게 사용하는 개발자가 되도록 더욱 정진해야겠습니다!
'Language > Java ☕️' 카테고리의 다른 글
자바의 메모리 구조 (0) | 2024.05.22 |
---|---|
자바의 가비지 컬렉션 (0) | 2024.02.02 |
String vs StringBuffer vs StringBuilder (0) | 2024.02.02 |
Double과 Float를 사용하면 안되는 이유 (0) | 2024.02.01 |
JDK vs JRE vs JVM (0) | 2024.02.01 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆