우아한테크코스 레벨1 인터뷰를 통해, Static에 대한 나의 기준이 책이나 주변 크루와 같은 외부적인 요인에 의해 결정되어 왔고, 내가 이를 얕게 공부해왔다는 사실을 깨달았다. 이번 기회를 통해 Static에 대해 깊이 있게 공부하고, 언제, 왜 사용할지에 대한 나만의 기준을 세워보고자 한다.
나는 프리코스를 거치며 Parser
, Validator
와 같은 유틸리티 클래스를 사용하면서 Static의 편리함을 직접 경험했다. 입력값을 검증하거나 다른 형태로 변환하는 등, 간단하면서도 여러 곳에서 사용할 수 있다고 판단되는 경우 Static을 사용했다.
하지만, 이 기준은 굉장히 모호하지 않은가? '간단함'과 '여러 곳에서 사용됨'이라는 기준이 구체적으로 무엇을 의미하는지 누군가 묻는다면 제대로 대답할 수 있을까?
이처럼 안일하고 성의 없는 기준은 나의 성장을 방해하는 큰 요인 중 하나였다. 이번 기회를 통해, 얕게 알고 있던 나 자신을 되돌아보고, Static에 대해 제대로 공부해보자!
스태틱은 크게 4가지로 분류된다.
1. static 변수 : 모든 객체가 공유
2. static 메서드 : 객체 없이 호출 가능
3. static 블록 : 클래스 로드 시 한 번 실행
4. static 내부 클래스 : 외부 클래스 없이 사용 가능
- 클래스가 메모리에 로드될 때 한 번만 생성됨.
- 모든 객체가 동일한 값을 공유함.
- 객체가 없어도 클래스명.변수명으로 접근 가능.
class Example {
static int count = 0; // 클래스 변수 (공유됨)
Example() {
count++; // 객체가 생성될 때마다 증가
}
void showCount() {
System.out.println("Count: " + count);
}
}
public class Main {
public static void main(String[] args) {
Example e1 = new Example();
Example e2 = new Example();
e1.showCount(); // Count: 2
e2.showCount(); // Count: 2
}
}
- 객체 없이 호출 가능 (클래스명.메서드명()).
- static 변수만 접근 가능 (인스턴스 변수는 사용 불가).
내가 위에서 말했던 유틸리티 클래스를 만들어 사용한 사례이다.
validateInputNullOrEmpty
는 입력을 하지 않았거나 빈 값을 입력했는지를 검증한다.parseStringToIntegerList
는 문자열을 "," 기준으로 정수형 리스트로 변환한다.이 원래 이 메서드들을 사용하려면 Validator
나 Parser
클래스를 인스턴스화 했어야 했지만, static 메서드
로 선언 하면 클래스명.메서드명() 형태로 간편하게 사용할 수 있다.
import java.util.List;
import java.util.ArrayList;
class Validator {
public static void validateInputNullOrEmpty(String input) {
if (input == null || input.trim().isEmpty()) {
throw new IllegalArgumentException("입력은 필수 입니다.");
}
}
}
class Parser {
public static List<Integer> parseStringToIntegerList(String input) {
List<Integer> result = new ArrayList<>();
String[] parts = input.split(",");
for (String part : parts) {
try {
result.add(Integer.parseInt(part.trim()));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("숫자를 입력해 주세요.");
}
}
return result;
}
}
public class Main {
public static void main(String[] args) {
String input = "1, 2, 3, 4, 5";
Validator.validateInputNullOrEmpty(input); // 입력값 검증
List<Integer> numbers = Parser.parseStringToIntegerList(input); //문자열 -> 정수형 리스트로 변환
System.out.println("Parsed numbers: " + numbers); // 출력: [1, 2, 3, 4, 5]
}
}
- 클래스가 로드될 때 한 번 실행됨.
- 주로 초기화 작업에 사용됨.
- 한 클래스 안에 여러개의 static 블록을 사용할 수 있다.
- static 블록의 순서대로 로딩한다.
class Example {
static String value;
static {
value = "첫번째 스태틱 블록";
System.out.println(value);
}
static {
value = "두번째 스태틱 블록";
System.out.println(value);
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Example.value);
}
}
/*
<출력결과>
첫번째 스태틱 블록
두번째 스태틱 블록
두번째 스태틱 블록
*/
💡만약 클래스 내부에 스태틱 블록
, 인스턴스 블록
, 생성자
가 있다면 출력 순서는 어떻게 될까?
public class Example {
static {
System.out.println("1. static 블록 실행!");
}
{
System.out.println("2. 인스턴스 블록 실행!");
}
public Example() {
System.out.println("3. 생성자 실행!");
}
public static void main(String[] args) {
System.out.println("메인 메서드 시작!");
new Example();
System.out.println("-----------");
new Example();
}
}
1. static 블록 실행!
메인 메서드 시작!
2. 인스턴스 블록 실행!
3. 생성자 실행!
-----------
2. 인스턴스 블록 실행!
3. 생성자 실행!
🔢 실행 순서
static 블록 → 클래스가 메모리에 로드될 때 단 한 번 실행
인스턴스 블록 → 객체가 생성될 때마다 생성자보다 먼저 실행
생성자 → 인스턴스 블록 이후 실행됨
- 외부 클래스의 인스턴스 없이 사용 가능.
class Example {
static class Inner {
void display() {
System.out.println("예제의 내부 클래스에서 출력합니다!");
}
}
}
public class Main {
public static void main(String[] args) {
Example.Inner obj = new Example.Inner();
obj.display(); // 예제의 내부 클래스에서 출력합니다!
}
}
평소에 우리는 Staitc을 알게 모르게 자주 사용하고 있었다. 어느 부분에서 사용하고 있었을지 알아보자.
클래스명.상수명
으로 접근 가능하다.우아한테크코스의 미션을 진행하면서 공통적으로 쓰이는 예외메시지를 한곳에서 관리하고 싶을때 사용했었다.
public class ErrorMessage {
public static final String NULL_POINTER = "NULL 참조가 발생했습니다.";
public static final String INVALID_INPUT = "잘못된 입력입니다.";
public static final String USER_NOT_FOUND = "사용자를 찾을 수 없습니다.";
public static final String IMPOSSIBLE_POSITION = "이동 불가능한 위치입니다.";
}
Enum(열거형)은 자바에서 열거된 상수들을 다룰 수 있게 해주는 특별한 클래스이다. 내부적으로 Enum은 여러 개의 static final 필드를 통해 상수들을 정의하며, 이들은 클래스가 로드될 때 static 블록을 통해 초기화된다.
public enum Day {
MONDAY, TUESDAY, WEDNESDAY;
}
위의 Day 클래스는 Enum클래스로 선언되어 있다.
겉으로는 왜 static으로 구성돼 있는지 잘 드러나지 않지만,
디컴파일을 통해 내부 구조를 보면 다음과 같다:
[디컴파일된 Day.class]
public final class Day extends Enum<Day> {
public static final Day MONDAY;
public static final Day TUESDAY;
public static final Day WEDNESDAY;
private static final Day[] VALUES;
static {
MONDAY = new Day("MONDAY", 0);
TUESDAY = new Day("TUESDAY", 1);
WEDNESDAY = new Day("WEDNESDAY", 2);
VALUES = new Day[] { MONDAY, TUESDAY, WEDNESDAY };
}
public static Day[] values() {
return VALUES.clone();
}
private Day(String name, int ordinal) {
super(name, ordinal);
}
}
Enum 클래스의 필드를 보면 모두 static 변수로 선언되어 있다. 그리고 static 블록에서 초기화를 하고 있다. 그렇기에 클래스명.변수명
으로 사용이 가능하다.
public class Main {
public static void main(String[] args) {
Day today = Day.MONDAY;
System.out.println("오늘은 " + today + "입니다.");
}
//출력: 오늘은 MONDAY입니다.
}
아래는 Math클래스의 사용 예시이다. Math.메서드명()
으로 사용가능하다.
public class MathPractice {
public static void main(String[] args) {
int absValue = Math.abs(-1);
System.out.println(absValue);
}
//출력: 1
}
Math 클래스 내부의 메서드들을 살펴보면 전부 Static으로 선언되있다. 아래는 몇가지 Math의 메서드을 가져와 봤다.
@IntrinsicCandidate
public static int abs(int a) {
return (a < 0) ? -a : a;
}
@IntrinsicCandidate
public static int min(int a, int b) {
return (a <= b) ? a : b;
}
@IntrinsicCandidate
public static double max(double a, double b) {
if (a != a)
return a; // a is NaN
if ((a == 0.0d) &&
(b == 0.0d) &&
(Double.doubleToRawLongBits(a) == negativeZeroDoubleBits)) {
// Raw conversion ok since NaN can't map to -0.0.
return b;
}
return (a >= b) ? a : b;
}
Java에서 Collections와 Arrays 클래스는 유용한 정적(static) 메서드들을 제공하여
컬렉션 및 배열을 쉽게 조작할 수 있도록 도와준다.
public class CollectionPractice {
public static void main(String[] args) {
// Arrays.asList 는 static 메서드
List<Integer> numberList = Arrays.asList(1, 3, 2, 9, 4, 5);
// Collections.sort 도 static 메서드
Collections.sort(numberList);
System.out.println("정렬된 리스트: " + numberList);
}
// 출력: 정렬된 리스트: [1, 2, 3, 4, 5, 9]
}
예시를 위해 몇가지 메서드들을 가져와봤다.
// 배열을 리스트로 변환
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
//정렬 메서드
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
//리스트 랜덤 메서드
public static void shuffle(List<?> list) {
Random rnd = r;
if (rnd == null)
r = rnd = new Random(); // harmless race.
shuffle(list, rnd);
}
메서드명.getInstance()
로 호출 가능하고, 어디서든 동일한 인스턴스에 접근할 수 있다.public class Singleton {
// static으로 유일한 인스턴스를 선언
private static Singleton instance = new Singleton();
private Singleton() {}
// static 메서드를 통해 인스턴스를 제공
public static Singleton getInstance() {
return instance;
}
}
[사용예시]
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
s1.doSomething();
System.out.println(s1 == s2); // true (동일 인스턴스(객체))
}
}
인스턴스 필드나 상태를 가지지 않고, 범용적인 기능 제공(유틸리티성)을 목적으로 하는 클래스는
객체 생성 없이 사용될 수 있도록 static 메서드로 구성한다.
단! 캐싱
이나 설정값
처럼 전역적으로 공유해야 하는 데이터는 static 필드
를 써야한다.
즉, 불변 유틸리티 + 공유 자원이 static을 사용하는 핵심 영역이다.
public class Counter {
private static int count = 0;
public static void increment() {
count++;
}
public static int getCount() {
return count;
}
}
위의 메서드를 static으로 선언하게 되면, 모든 Counter
인스턴스는 동일한 값을 공유하게 된다. 따라서 서로 다른 객체에서 increment()
를 호출해도 count
값은 공통으로 증가하게 된다.
기준 | static 사용 ✅ | static 사용 ❌ |
---|---|---|
상태(필드)가 필요한가? | ❌ | ✅ |
객체 생성이 필요한가? | ❌ | ✅ |
재사용 가능한 기능인가? | ✅ | ❌ |
전역적으로 공유되는 값인가? | ✅ (예: 캐시, 설정값) | ❌ |