본문 바로가기

Better SW Development

Java의 static, 득과 실, 어떻게 사용할 것인가?

ZoundryDocument

예전 C 시절에 그랬다.

'전역변수는 악마'라고. (Global variables are evil.)

흔히 '모든 범죄의 이면에는 여자와 돈이 있다'고 말하는데, 마찬가지로 '모든 버그의 이면에는 피곤한 개발자와 전역변수가 있다'고 말할 정도로 전역변수는 문제시 되었다.

그 시절엔 전역변수를 최대한 사용하지 않는게 일반적인 원칙이었다. 그래서 종종 function을 호출할 때 필요한 데이터나 참조를 모두 파라미터로 넘겨서 처리하곤 하였다. 그러다 보니, 프로그램이 성장해 나가면서 function 의 파라미터 선언부는 점점 길어질 수 밖에 없었다. 결과적으로 function의 선언이 복잡해 지거나, 경우에 따라서 필요없는 파라미터까지도 함께 넘겨야 하는 아쉬운 일이 종종 발생했다. 그래도 전역변수를 사용하는 것 보다는 낫다고 여겨졌었다.

어두운 동굴과 음습한 터널을 지나 어느 사이엔가 객체 지향의 시대가 도래했다.
(글쎄... 라며 반론할 수도 있겠지만, 적어도 객체지향 언어의 시대는 도래했지 않은가?)

객체지향 언어는 기본적으로 '클래스'라는 개념의 모듈화된 객체를 전면으로 내세워 언어적으로 지원했고, 이로 인해 프로그램 전범위에 걸쳐서 동일하게 접근해야 하는 전역변수의 필요성은 드라마틱하게 줄어들었다. 객체는 자신이 필요한 자원을 자기 내부에서 소비하도록 유도되었기 때문에, 그리고 그렇지 않은 경우에는 다른 객체를 이용해서 처리하면 되었기 때문에, 기존에 절차언어에서 겪어야 했던 '전역변수'로 인한 간섭이 최소화 되었다.

'클래스에도 일종의 전역변수 같은 맴버변수(=field variable)가 있잖아요?' 라고 물을 수도 있는데, 필드의 소유는 기본적으로 클래스(=모듈) 내로 한정되어 있다. 그리고 아주 기본적인 객체지향 원리를 준수함으로써, 맴버변수를 외부로 직접 오픈하는, 즉 public scope의 필드를 갖는 경우는 거의 만들지 않게 되었다. getter, setter 등을 이용하거나 특정한 접근메소드를 통해서만 한정적으로 접근가능하게 만들어 놓는게 일반적인 원리가 되었다. 따라서 한 클래스의 맴버변수의 값이 달라진다고 해서, 다른 클래스에서 받게 되는 영향은 극히 적다. 만일, 자신의 프로그램이 그렇지 않다면 객체지향 언어를 이용해 절차적으로 프로그래밍했기 때문일 가능성이 높다.

어쨌든 프로그래밍 언어를 사용해 비즈니스적인 로직을 구현함에 있어, 로직에 필요한 자원을 생성하고 소비하는 부분이 객체 각각에게로 한정 위임되었기 때문에, 구현 복잡도가 줄어들었고, 작성도 보다 손쉬워 졌다. ( 절차언어에서 전역변수 하나가 나오면, 그 변수를 제대로 다루기 위해서는 참조 하는 모든 부분의 연관성을 따져보고 앉아 있어야 하는데, 일부분 라이브라리라도 함께 사용하게 되는 경우에는 끔.찍.하다는 표현이 절로 나오는 상황에 빠지게 된다) 하지만 그렇다고 해도 전역변수가 주었던 일부 장점은 여전히 매력적으로 보일때가 종종 있다.

바로 공용자원(public shared resource)에 대한 접근이 그 중 하나이다.

네트워크, DB, 외부파일 등등은 프로그램 내부 여기저기에서 참조되거나 호출될 필요가 있는데, 매번 객체를 생성한다던가, 연결을 만들고, 자원을 여는 작업을 각각하는 건, 매우 비효율적이면서, 그리고 그로 인한 새로운 문제점을 야기시킬 가능성이 높은, 더러운 짓(Dirty factor)이 된다. 따라서 이런 자원들은 단일 접근점을 통해서 여러 부분에서 이용될 수 있다면, 비용도 줄이고, 효율도 높일 수 있다. 일종의 '전역변수'같은 통로를 통해 접근하는 편이 나을 수 있다는 말이다.

JAVA에는 이런 전역변수 형태의 접근을 가능하게 만들어 주는 static 이라는 키워드가 있는데, 이 키워드는 일반적인 클래스의 라이프 사이클을 넘어서는, 초 클래스적인 접근을 가능하게 만들어 주는 무시무시한 키워드이기도 하다.

일반적으로 클래스의 변수나 메소드는 해당 클래스가 인스턴스화 되기 전에는 접근하거나 사용할 수 없는데, static 으로 선언된 변수나 메소드는 해당 클래스의 인스턴스화와 상관없이 바로 접근이 가능하고, 이용이 가능하다.

클래스 안에 선언되어 있으나, 해당 클래스와 삶을 갖이 하지 않는 존재라...

어찌보면 뜬금없을 수 있는데, 그래서 그나마 혼란을 줄여주는 최소한의 규칙이 하나 있는데 그게 바로 'static 에서는 non-static 에 대한 접근을 제한한다'는 규칙이다.

public class Game {   
    public int score = 0;

    public static void main(String[] args) {
        score = 10;
    }
}

Cannot make a static reference to the non-static field score Game.java line 6

컴파일하면 위와 같은 에러가 나온다.

위와 반대의 경우, 즉 non-static 에서 static 호출은 에러는 아니가 경고(warning) 정도만 띄워준다. 그도 그럴 것이 static 은 인스턴스화와 상관없이 접근가능하기 때문에 non-static 으로의 접근시에는 해당 클래스가 인스턴스화가 안되어 있을 수 있기 때문에 에러가 나는 것이고, 그 반대는 당연히 인스턴스화가 이루어졌을 경우에만 해당 호출(= non-static에서의 호출)이 가능한 상황이 되기 때문에 에러까지는 아닌게 되는 것이다.

어쨌든 이런 static 의 도움으로 인해 자바는 전역변수를 갖는 것이 가능해 졌다. 어디서나, 언제나 마음대로 접근 가능! 이를 응용해서 위에서 말한 공용자원 접근을 제공하는 방식으로 싱글톤(Singleton)이라는 것이 있다. 흔히 다음과 같은 형태로 구현된다.


public class Game {   
    private int score;
    private static Game game;

    private Game(int score){
        this.score = score;
    }
   
    public static Game getInstance(){
        if( Game.game == null ){
            game = new Game(10);
        }
        return game;
    }

    public static void main(String[] args) {
        Game game = Game.getInstance();
        System.out.println("game.getScore(): " + game.getScore());
        game.setScore(100);
        System.out.println("game.getScore(): " + game.getScore());
        Game newGame = Game.getInstance();
        System.out.println("newGame.getScore(): " + newGame.getScore());
    }
   
    public int getScore(){
        return score;
    }
   
    public void setScore(int score){
        this.score = score;
    }
}

-- 결과 --
game.getScore(): 10
game.getScore(): 100
newGame.getScore(): 100

생성자를 private 으로 만들어서 new 로 생성할 수 없게 만들고, public static 메소드를 통해서만 객체에 접근 가능하게 만들어 준다.  getInstance() 메소드를 살펴 보면, 한 번 만들어진 Game 객체가 계속 유지되도록 유도하고 있는 걸 알 수 있다. 즉, 데이터베이스 연결이나 네트워크 연결등의 공용 자원 접근에 이런식의 static 응용을 하면 효율을 높일 수 있다.

자, 그런데 이런 건 긍정적인 경우이고, 부정적인 경우를 생각해 보자.

우선, 위 예제에서 보는 것 처럼 static 으로 접근하는 객체가 상태를 가지게 될 경우, 그 상태가 계속 유지된다는 점을 알 수 있다. 이게 큰 문제인데, 왜냐하면, 언제 어떤식으로, 어디에서 어떻게, 접근이 일어날 지 예측할 수 없는 static 이기 때문에 현재 상태를 가정할 수가 없다는 점이다.

즉 static 객체의 상태에 내가 10을 대입했다 하더라도, 바로 그 다음 순간 그대로 10이 유지되리란 보장이 없다. 특히 자바의 장점으로 내세우는 멀티스레드 환경에서는 동시에 여기저기서 접근이 일어날 수 있기 때문에 더더욱 난감해진다.

즉, 절차언어에서의 전역변수, 바로 그 유령이 되살아 나는 셈이다. (Return of the Jedi?)

이런 이유로, TDD에서도 static method 나 static 변수를 테스트 하기 어려운 케이스로 꼽고 있다. TDD는 기본적으로 상태에 기반한 행위의 결과를 비교하는 경우가 많기 때문에, 상태가 시시때때로 변할 수 있다는 가정하에서는, 혹은 그런 환경을 구축한다는 것 자체도, TDD를 어렵게 만들거나 곧잘 신뢰할 수 없게 만드는 요인이 되기 때문이다.

그럼 어떻게 하라구?

static 은 최대한 안 쓰는게 답이다.

에엣? 너무 무책임 한 것 아닌가?

OK. 손에 쥔 돌을 놓으시길. :)

다시 정리 하도록 하겠다.

#1. Rule
static 은 최대한 안 쓰는 게 답이다.

#2. Rule
static 을 쓸때는 상태를 저장, 혹은 변경하지 않는 방식으로 사용한다.

Math.PI <= static final 로 상태 변경을 근본적으로 막고 있다.
System.out.println() <= 상태를 저장하지 않는다.
String <= immutable 의 대표적인 예
그외 setter 를 지원하지 않고 field 값이 setting되면 그대로 고정되는 클래스들

혹자는 static 을 가독성을 위해서 쓴다고 말하는 사람도 있다. 위 예에서의 Math.PI 같은 식으로 말이다. 

ps. static block, static method, static variable 등등 종류가 많은데, 여기에서는 굳이 구분하지 않고, 납득할 만한 수준에서 편하게 표현했음을 미리 밝혀 둡니다. :)