개요
자바에서 변수는 선언 위치와 제한자에 따라 특성과 용도가 달라집니다.
이를 이해하면 코드 설계에서 효율적인 전략을 세울 수 있습니다.
스프링을 이용해 서버를 구현할때 더 효율적인 인스턴스 생성이나 변수 관리를 할수도 있고 통과가 타이트한 알고리즘 문제를 풀때에도 적극적으로 이용할수 있습니다. (무조건 알아둬야 된다는뜻)
클래스 변수 (Static으로 선언된 변수)
- 정의: static 키워드로 선언된 변수. 클래스 로딩 시 한 번만 생성되고, 모든 객체가 공유.
- 위치: 클래스 내부, 메서드 외부.
- 특징
- 클래스에 귀속되고, 객체가 공유하는 변수.
- 프로그램 종료 시까지 유지.
- 사용 전략:
- 모든 객체가 공통으로 사용할 값(상수, 카운터 등)을 관리.
- 메모리 절약 및 데이터 일관성 유지
public static 의 경우
: 상수로 사용. 변경 불가능하며, public으로 선언 시 전역 상수처럼 사용 가능.
프레임워크나 전역적으로 사용되는 공용 util 함수.
public class MathUtils {
public static final double PI = 3.14159; // 누구나 참조 가능
public static int add(int a, int b) {
return a + b;
}
}
- 전역 함수처럼 활용 가능.
- 테스트 시 결합도가 높아질 수 있으니 주의(목(Mocks) 대체가 어려움)
public static final 의 경우
: 상수로 사용. 변경 불가능하며, public으로 선언 시 전역 상수처럼 사용 가능.
전역적인 공유 자원이나 전역 설정 값을 관리
*private static 의 경우 - 싱글턴이나 전역 캐싱
: 외부에서 접근 불가. 클래스 내부에서 공통 데이터를 관리하는 데 유용. 추가/삭제 가능한 경우
*일부러 Bean이 아닌 로우레벨로 관리할때.
캐시, 카운터(알고리즘 cnt++), 스레드 풀
public class InternalCache {
private static Map<String, String> cache = new HashMap<>(); // private static
public static void put(String key, String value) {
cache.put(key, value);
}
public static String get(String key) {
return cache.get(key);
}
}
- 외부에서는 InternalCache.cache 자체에 직접 접근 불가
- 메서드 통해서만 조작하게 함으로써 캡슐화를 유지한다.
또한 싱글턴에도 적용 가능함
public class SingletonService {
private static SingletonService instance = new SingletonService();
private SingletonService() {}
public static SingletonService getInstance() {
return instance;
}
}
- 외부에서 생성자를 호출할 수 없고, getInstance()를 통해 하나의 객체만 공유.
- private static final로 선언하면 재할당이 불가능해져 안전성이 높아진다.
- *정적 팩토리 메서드를 적용하는 이유
static 키워드와 final 키워드를 결합하면, 클래스 전역에서 활용하는 상수를 한 번만 정의해 효율과 일관성을 동시에 확보
private static final 의 경우
: 외부에서 접근 불가. 클래스 내부에서 공통 데이터를 관리하는 데 유용. 의도적으로 해당 클래스 내부에서만 사용하는 변수라는걸 명시가능
알고리즘 풀이
(클래스가 1개로 주어짐, 사실 public 해도 상관없는데, 명시적으로 단일 클래스 내부에서만 사용하면 private 선언하는 습관을 들이자. *왜 이런 선언습관이 좋을까?)
public class ArrayProcessor {
// 모든 배열에 적용할 상수 값 (공통된 데이터)
private static final int ADDITION_CONSTANT = 10;
public static int[] processArray(int[] inputArray) {
int[] result = new int[inputArray.length];
for (int i = 0; i < inputArray.length; i++) {
result[i] = inputArray[i] + ADDITION_CONSTANT;
}
return result;
}
public static void main(String[] args) {
int[] inputArray = {1, 2, 3, 4};
int[] processedArray = processArray(inputArray);
for (int value : processedArray) {
System.out.print(value + " "); // 출력: 11 12 13 14
}
}
}
- ADDITION_CONSTANT는 상수로, 전역적으로 값이 고정되어야 하므로 static final로 선언.
- 메모리 절약과 데이터 변경 방지를 위해 클래스 레벨에서 정의.
클래스 변수(static)와 인스턴스 변수를 비교할 때, 공통 데이터 공유와 객체별 상태 관리라는 두 가지 관점을 함께 고려해야 합니다.
인스턴스 변수(Static X )
- 정의: 클래스의 객체가 생성될 때마다 생성되는 변수.
- 위치: 클래스 내부, 메서드 외부.
- 특징:
- 각 객체마다 고유의 값을 가짐.
- 객체가 제거되면 메모리에서 해제.
- 사용 전략:
- 객체의 고유 상태(예: 이름, 나이 등)를 저장.
- 데이터 은닉과 불변성 확보로 안전한 객체 설계
public 의 경우
: 다른 클래스에서 직접 참조와 수정이 가능
-> 따라서 잘 이용하지 않는다 왜일까?
캡슐화가 깨져서 (다른 클래스에서 수정 참조가 가능하다.) 값의 변화를 추적하기 어렵다.
-> 유지보수가 어렵다.
객체지향에 반하는 사용법이다.
public final의 경우
: 다른 클래스에서 읽기는 가능하다. 재할당(수정)은 불가능하다. 인스턴스 생성시에 값이 확정되고 변하지 않는다.
-> 이또한 잘 이용하지 않는다. 왜일까?
변수에 직접 접근을 허용한다. 객체 내부의 데이터가 외부에 노출이 되는데 객체 지향에 반한다.
내부 구현이 외부에 드러날 수도 있다.
차라리 static 을 붙여서 전역적 상수로 이용해 확실하게 전역적으로 클래스 수준에서 변하지 않는 상수임을 명시하는게 좋다.
private의 경우
: 캡슐화 원칙에 따라 변수 보호, 접근자 메서드로만 접근 가능. 클래스 내부에서만 접근 가능하다.
*[중요] 스프링에서는 어노테이션이나 도메인 메서드를 이용해 외부와 소통하게 한다. = 메서드
"필드를 숨기고, 메서드로 조작" REAL 객체지향
클래스내 선언한 식별자와 속성등 필드를 private 로 두고 이를 조작하는 메서드를 public 으로 제공한다.
public class Order {
private Long orderId; // 외부에 직접 노출되지 않음
private int quantity;
public Order(Long orderId, int quantity) {
this.orderId = orderId;
this.quantity = quantity;
}
public Long getOrderId() {
return orderId;
}
public int getQuantity() {
return quantity;
}
public void changeQuantity(int newQuantity) {
this.quantity = newQuantity;
}
}
- 어디서 어떻게 값이 변경되는지 추적이 쉽고, 유지보수성과 안전성 향상.
- *보일러플레이트 코드(getter/setter)가 늘어날 수 있지만, Lombok 등으로 개선 가능.
인스턴스 변수를 final로 선언하면, 불변 객체 패턴을 쉽게 구현할 수 있으며 이는 동시성 문제를 완화할수 있다.
private final 의 경우
: 캡슐화 원칙에 따라 변수 보호, 인스턴스 생성 시점에만 필드 값이 설정되고, 재할당 불가능.
불변 객체(Immutable Object)를 구성하거나, 특정 필드를 절대 바꾸지 않겠다는 의도를 명확히 표현할수 있다.
*VO 모델을 설계할때도 쓰인다.
public class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
// plus, minus 등 새 객체 반환 메서드
}
- 변경 불가능하므로 멀티스레드 안전, 로직 안정성.
- 값이 변해야 하는 시점이 있다면 매번 새 객체를 만들어야 하므로, 성능/메모리 부담 가능성이 있지만 명시를 통해 개발자끼리 소통을 한다거나 안정성 측면을 고려한다면 적용해야한다.
인스턴스 변수의 스코프가 넓어지면 예기치 않은 부작용이 발생할 수 있으므로, 지역 변수로 대체 가능한 부분은 적극 활용하여 코드 복잡도를 줄입니다.
지역 변수
- 정의: 메서드나 블록 내부에서 선언된 변수.
- 위치: 메서드, 생성자, 블록 내부.
- 특징:
- 메서드 실행 중에만 존재하며, 종료 시 메모리에서 해제.
- 반드시 초기화 필요.
- 사용 전략:
- 메서드 내의 임시 값, 연산 결과를 저장.
- 메모리 사용을 최소화하며 스코프를 제한.
final 의 경우
: 변수 값을 변경할수 없고 상수처럼 사용.
매개 변수
- 정의: 메서드 호출 시 전달되는 값.
- 위치: 메서드 선언부.
- 특징:
- 메서드 호출 시 전달된 값으로 초기화.
- 호출 스택에 저장되며, 메서드 실행이 끝나면 메모리에서 해제.
- 사용 전략:
- 메서드 간 데이터 전달.
- 불필요한 값 변경 방지로 데이터 안정성 강화.
final 의 경우
: 변수 값을 변경할수 없고 상수처럼 사용.
마무리
자바에서 변수는 선언 위치와 제한자에 따라 특성과 용도가 크게 달라집니다.
클래스 변수, 인스턴스 변수, 지역 변수, 매개변수 각각은 특정 상황에서 적합하게 사용되어야 하며, 변수의 static, final, private 등의 제한자는 설계 의도를 명확히 드러내는 데 중요한 역할을 합니다.
- 클래스 변수(static)는 공통 데이터 관리나 전역 상수 선언에 유용하며, 메모리 절약과 데이터 일관성을 제공합니다.
- 인스턴스 변수는 객체마다 고유한 상태를 저장하며, 객체 지향 설계의 기본이 됩니다.
- 지역 변수와 매개변수는 스코프를 제한해 불필요한 메모리 사용을 줄이고, 메서드 간 데이터 전달에서 안정성을 강화합니다.
특성에 따라서 캡슐화 원칙을 준수하고 변수의 가시성을 제한하며, 필요한 경우 final을 활용해 불변성을 부여하면 코드의 안정성과 유지보수성이 크게 향상됩니다.
이러한 설계 원칙은 스프링 기반 서버 구현, 알고리즘 문제 해결 등 다양한 상황에서 유효하기에 숙지해놓는것을 추천합니다.
'Java' 카테고리의 다른 글
Java 조각모음 [4] (1) | 2025.01.23 |
---|---|
예제로 배우는 결국 알아야하는 JVM 메모리 구조(순한맛) (0) | 2025.01.22 |
예제로 배우는 자바가 배열을 생성하는 원리 이해하기 (0) | 2025.01.21 |
Java 조각모음 [3] (0) | 2025.01.20 |
예제로 배우는 자바 String Pool 이해하기 (0) | 2025.01.20 |