Static(스태틱)을 나는 왜 쓰고 있을까?

Frog Lemon·2025년 4월 6일
3

Java

목록 보기
2/3
post-thumbnail

나는 Static을 얕게 알고 있었다! 😵

우아한테크코스 레벨1 인터뷰를 통해, Static에 대한 나의 기준이 책이나 주변 크루와 같은 외부적인 요인에 의해 결정되어 왔고, 내가 이를 얕게 공부해왔다는 사실을 깨달았다. 이번 기회를 통해 Static에 대해 깊이 있게 공부하고, 언제, 왜 사용할지에 대한 나만의 기준을 세워보고자 한다.

나는 프리코스를 거치며 Parser, Validator와 같은 유틸리티 클래스를 사용하면서 Static의 편리함을 직접 경험했다. 입력값을 검증하거나 다른 형태로 변환하는 등, 간단하면서도 여러 곳에서 사용할 수 있다고 판단되는 경우 Static을 사용했다.

하지만, 이 기준은 굉장히 모호하지 않은가? '간단함'과 '여러 곳에서 사용됨'이라는 기준이 구체적으로 무엇을 의미하는지 누군가 묻는다면 제대로 대답할 수 있을까?

이처럼 안일하고 성의 없는 기준은 나의 성장을 방해하는 큰 요인 중 하나였다. 이번 기회를 통해, 얕게 알고 있던 나 자신을 되돌아보고, Static에 대해 제대로 공부해보자!

Static이란?

스태틱은 크게 4가지로 분류된다.
1. static 변수 : 모든 객체가 공유
2. static 메서드 : 객체 없이 호출 가능
3. static 블록 : 클래스 로드 시 한 번 실행
4. static 내부 클래스 : 외부 클래스 없이 사용 가능


🔻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 메서드

  • 객체 없이 호출 가능 (클래스명.메서드명()).
  • static 변수만 접근 가능 (인스턴스 변수는 사용 불가).

내가 위에서 말했던 유틸리티 클래스를 만들어 사용한 사례이다.

  • validateInputNullOrEmpty는 입력을 하지 않았거나 빈 값을 입력했는지를 검증한다.
  • parseStringToIntegerList는 문자열을 "," 기준으로 정수형 리스트로 변환한다.

이 원래 이 메서드들을 사용하려면 ValidatorParser 클래스를 인스턴스화 했어야 했지만, 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 블록을 사용할 수 있다.
  • 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. 생성자 실행!

🔢 실행 순서

  1. static 블록 → 클래스가 메모리에 로드될 때 단 한 번 실행

  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(열거형)은 자바에서 열거된 상수들을 다룰 수 있게 해주는 특별한 클래스이다. 내부적으로 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는 객체를 생성하지 않고 바로 사용하는 순수 유틸리티 클래스.
  • 내부 메서드는 모두 static으로 선언되어 있다.

아래는 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;
    }


🔻Collections / Arrays 클래스

Java에서 CollectionsArrays 클래스는 유용한 정적(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);
    }


🔻Singleton(싱글톤) 패턴

  • 싱글톤(Singleton) 패턴은 객체를 오직 하나만 생성하도록 보장하는 디자인 패턴이다.
  • 객체를 생성하지 않아도 메서드명.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 필드를 써야한다.

  • 즉, 불변 유틸리티 + 공유 자원이 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 사용 ✅static 사용 ❌
상태(필드)가 필요한가?
객체 생성이 필요한가?
재사용 가능한 기능인가?
전역적으로 공유되는 값인가?✅ (예: 캐시, 설정값)
profile
노력과 끈기를 추구합니다. 레몬이 좋아!

0개의 댓글