우아한테크코스 체스 미션을 수행할 때, 편의성 때문에 무의식적으로 static으로 선언한 메소드를 남발하곤 했다. 코드 리뷰어분께서 static 메소드를 사용했을 때 얻을 수 있는 이점이 무엇인지, 또 그것을 사용했을 때 유의해야할 점이 무엇인지를 생각해보라고 하셨다. 따라서 static 메소드를 사용하는 나만의 기준을 세워보기로 결정했다.
static 메소드를 사용하는 가장 대표적인 경우는 정적 팩토리 메소드를 사용할 때이다. 따라서 정적 팩토리 메소드의 장점은 대부분 static 메소드의 장점에 속한다고 생각한다.
정적 팩토리 메소드를 사용할 때의 이점은 이 글에 정리해 놓았다.
예를 들어 가위, 바위, 보 셋 중 하나를 랜덤으로 내는 프로그램을 짠다고 가정해 보자.
1, 2, 3 중의 하나의 숫자를 랜덤으로 추출해 가위, 바위, 보를 결정한다고 전략을 세워본다.
public enum RockPaperSissors {
ROCK(1),
PAPER(2),
SISSORS(3);
private final int num;
RockPaperSissors(int num) {
this.num = num;
}
public static RockPaperSissors from(int indexNum){
return Arrays.stream(values())
.filter( value -> value.num == indexNum )
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 인덱스입니다"));
}
}
그렇다면 번호를 추출하는 객체를 만들 수 있을 것이다. 여기서 숫자를 추출하는 메소드를 static으로 선언한다.
public class RandomNumberGenerator {
private static final Random random = new Random();
private RandomNumberGenerator() {
}
public static int generateRandomNumber() {
return random.nextInt(3) + 1;
}
}
다음과 같은 상황에서 "숫자를 랜덤하게 추출한다"라는 메소드는 게임이 여러번 실행될 경우 반복적으로 사용될 가능성이 클 것이다.
따라서 이 때 static 메소드를 사용하면 메소드를 사용할 때 마다 객체를 생성할 필요가 없어서 불필요한 객체의 추가 생성을 막을 수 있다고 생각한다.
public class Main {
public static void main(String[] args) {
int randomNumber = RandomNumberGenerator.generateRandomNumber();
RockPaperSissors rockPaperSissors = RockPaperSissors.from(randomNumber);
System.out.println(randomNumber);
System.out.println(rockPaperSissors.toString());
}
}
위에서 재사용성이 높을 경우 불필요한 객체 생성을 방지한다고 했지만, 반대로 재사용성이 낮은 static 메소드를 남발하는 경우 문제가 생긴다.
static을 선언한 변수나 메소드의 경우, 프로그램의 시작과 동시에 static 영역에 할당된다. static 영역에 할당되게 되면 그 메모리는 프로그램이 끝낼 때 까지 유지된다.
따라서 단순 편의성을 위해 static 메소드를 남발하다가 static 영역이 지나치게 커지는 경우, 메소드의 재사용성이 높지 않은 경우라면 불필요한 메모리가 크게 낭비되는 꼴이 된다.
동일한 추상화 계층에 속한 여러 구현체가 있다고 생각해 보자. 이 때 오버라이딩을 통해 특정 구현체만의 방법론으로 객체의 행동을 구현할 수 있다. 이는 객체지향의 다형성을 지키기 위한 좋은 방법이다.
그러나 static으로 선언한 메소드는 오버라이딩이 불가능하다. 따라서 동일한 추상화 계층에서 각 구현체들이 자신만의 방법론으로 동작을 선언할 수 없다. 요구사항의 변경에 따라 행동이 바뀔 경우, 새로운 구현체를 만들어 갈아 끼우는 것이 불가능하고, 기존 코드를 수정해야 하는 불편함이 생길 수 밖에 없을 것이다.
위 단점과 이어지는 내용이지만 바텀업 방식으로 추상화를 진행할 경우 생길 수 있는 문제다. 프로그래밍을 진행하다가 여러 개의 클래스가 비슷한 성격임을 느낄 수 있다. 이 때 추상화의 필요성을 느끼게 된다.
그러나 인터페이스 혹은 추상 클래스를 만드려고 할 때, static 메소드는 그 자체로 추상화시킬 수 없다. 혹 인터페이스에 static 메소드를 사용할 경우 동작 방식이 드러나게 되므로 캡슐화 또한 침해된다.
결국 장단점이 명확한 이 static 메소드는 언제 사용해야 한까? 나만의 기준을 몇 가지 확립해 보았다.
위의 "메모리 효율" 장단점에서 명확하게 드러나는 차이점은 결국 "재사용성"이다.
static 메소드가 재사용되는 경우가 많다면, 불필요한 객체 생성 없이 메모리를 아낄 것이다. 반대로 static 메소드가 재사용이 되지 않은 상태에서 남발된다면, 불필요한 메모리가 계속해서 static 영역에 존재할 것이다.
따라서 해당 메소드가 내 애플리케이션에서 얼마나 잦게 호출되는지를 제일 먼저 고려해보면 좋을 것 같다.
애매하지만 잘 판단해야 할 영역 같다. 추상화 계층에서 "행동"을 수행하는 코드 행위는 드러나면 안 된다. 객체지향의 캡슐화와 다형성을 지키기 위한 규칙과 같다. 따라서 만약 인터페이스에서 static 메소드를 사용하려 할 때, 객체의 "행동" 영역, 즉 어떠한 역할을 수행하는 메소드라면 사용하지 않아야 한다. 또 반드시 절대적으로 같은 동작만 다른 구현체에서도 수행됨이 보장될 때만 사용해야 할 것이다.
정적 팩토리 메소드가 주는 여러 가지 이점을 이용하기 위해 static 메소드를 사용하는 사람들이 많을 것이고, 나 또한 그렇다. 따라서 정적 팩토리 메소드의 탄생을 고려하는 순간에 static의 장단점 역시 생각해봐야 할 것이다.
정적 팩토리 메소드는 객체 생성에 이름을 부여하고, 매개변수에 따라 원하는 성격의 객체를 탄생시키고, 캐시된 자원을 활용할 때 큰 이점이 있다. 정적 팩토리 메소드의 장점에 부합하는 순간에 static 메소드의 사용을 결정하자