들어가며
String은 Java에서 Object를 제외하고 제일 많이 쓰이는 클래스 중 하나입니다. 하지만 String을 다루기는 굉장히 귀찮은 일들이 많죠. 예를 들어서 StringBuilder
를 통해서 문자열을 더해줘야 한다거나, String의 메서드를 이용하면, 반드시 리턴을 받아야 한다는 등 말입니다.
이런 일들이 발생하는 이유는, String이 불변 객체이기 때문입니다. String은 왜 불변 객체일까를 알아보기 앞서서 자바에서의 불변 객체가 무엇인지에 대해서 살펴보고, 불변 객체의 어떤 특징 때문에 String을 불변 객체로 만들었을지 알아보겠습니다.
불변 객체란?
Java에서의 불변 객체는 종류가 굉장히 많습니다. 대표적인 예시로는, 앞서 언급된 String
부터, Integer
, Long
등의 Wrapper 클래스들이 있습니다.
불변 객체는 말 그대로 내부의 상태가 변하지 않는 객체를 의미합니다. 이런 불변 객체는 어떻게 만들고, 어떤 장점이 있을까요?
불변 객체 생성법
불변 객체를 만들기 위해선 다음과 같은 원칙을 따르면 됩니다.
- setter 메서드를 제공하지 않고, read-only 메서드를 제공한다. 참조에 의해서 변경될 수 있는 경우엔 방어적 복사를 한다.
- 클래스를 fianl로 선언하라. (상속을 하지 못하게 한다)
- 모든 변수에 private, final을 선언한다. 참조 객체인 경우, 해당 객체도 불변 객체로 만든다.
- 객체 생성을 위한 생성자 혹은 정적 팩토리 메서드를 만든다.
위의 규칙을 잘 따르면 다음과 같은 불변 객체를 만들 수 있습니다.
public final class ImmutableStudent {
private final String name;
private final int age;
private final Address address;
private final List<ImmutableStudent> friends;
private ImmutableStudent(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
this.friends = new ArrayList<>();
}
public static ImmutableStudent of(final String name, final int age, final Address address) {
return new ImmutableStudent(name, age, address);
}
public String getName() {
return name;
}
public List<ImmutableStudent> getFriends() {
return Collections.unmodifiableList(friends);
}
static final class Address{
private final String sido;
private final String gugun;
private final String dong;
public Address(String sido, String gugun, String dong) {
this.sido = sido;
this.gugun = gugun;
this.dong = dong;
}
public static Address of(final String sido, final String gugun, final String dong) {
return new Address(sido, gugun, dong);
}
}
}
불변 객체의 장점
불변 객체를 생성하는 방법을 알았으니, 이젠 불변 객체의 장점에 대해서 알아보겠습니다. 불변 객체의 장점으로 알려진 부분은 다음과 같습니다.
- Thread-Safe해서 멀티 쓰레드 환경에서 안전하다.
- 상태가 변하지 않아서 신뢰성이 상승한다.
- 내부 상태가 변하지 않기 때문에 Cache, Set, Map의 내부 요소로 활용 가능하다.
- GC의 성능을 향상시킬 수 있다.
1. Thread-Safe
멀티 쓰레드 환경에서 발생하는 동시성 문제는 write와 read가 동시에 발생할 때, 나타납니다. 하지만 불변 객체를 이용할 경우, write가 발생하지 않습니다.
2. 상태가 변하지 않아 신뢰성 상승
외부에서 객체의 값이 변하게 된다면, 객체의 상태를 예측하기와 변경 시점을 추적하기가 어렵습니다. 하지만 불변 객체의 경우 변하지 않은 상태를 가지기 때문에, 상태를 예측하기 쉽고, 오류가 발생할 경우 원인을 발견하기 쉽습니다.
아래와 같은 코드가 있다고 했을 때, 가변 객체의 경우, setter 메서드가 있기 때문에, 어떤 부분에서 KIM이라는 사람의 이름이 LEE로 바꼈는지 알기 힘듭니다. 하지만 불변 객체의 경우 KIM이라는 사람의 이름이 LEE가 될 수 있는 경우는 객체가 생성되는 순간이기 때문에, 빠르게 원인 파악이 됩니다.
public static void main(String[] args) {
MutablePerson kim = new MutablePerson("KIM");
// 1000 라인
kim.setName("LEE");
// 1000 라인
System.out.println(kim.getName()); // 왜 이름이 LEE여?
ImmutablePerson kimkim = new ImmutablePerson("LEE");
// 10000 라인
System.out.println(kimkim.getName()); // 왜 이름이 LEE여?
}
static class MutablePerson{
String name;
public MutablePerson(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
static class ImmutablePerson{
final String name;
public ImmutablePerson(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
3. Cache, Set, Map의 내부 요소로 활용
불변 객체는 Cache, Set, Map에서 사용될 경우, 이후 그 값이 변경될 일이 없어, Cache, Set, Map의 데이터를 갱신해주지 않아도 됩니다.
예를 들어 Integer가 불변 객체가 아닌 상황에서 Set<Integer>
에 들어있다면, Set의 요소인 Integer가 외부에서 값이 변경될 때, Set의 특징이 유지되고 있는지 체크되어야 할 것입니다.
4. GC의 성능 향상
객체의 값이 변경될 때, 가변 객체를 이용하는 것 보다 새로운 불변 객체를 이용하는 것이 GC의 성능을 더욱 향상시킬 수 있습니다. 이는 GC의 동작 방법 중 '최근에 생성된 객체는 일찍 사라진다.'에 따라서 오랫동안 유지된 가변 객체를 제거하는 것 보다, 자주 불변 객체를 삭제하는 편이 더 좋습니다.(Heap의 yg 영역을 지우는 것이 og를 지우는 것 보다 쉽다.)
그리고 오라클 문서에 따르면 객체를 만드는 것에 대한 비용이 과대평가 되어있고, 이는 불변 객체의 효율성으로 커버가 가능하다고 합니다.
Programmers are often reluctant to employ immutable objects, because they worry about the cost of creating a new object as opposed to updating an object in place. The impact of object creation is often overestimated, and can be offset by some of the efficiencies associated with immutable objects. These include decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption.
프로그래머는 객체를 제자리에서 업데이트하는 대신 새 객체를 생성하는 데 드는 비용을 걱정하기 때문에 불변 객체를 사용하는 것을 꺼리는 경우가 많습니다. 객체 생성의 영향은 종종 과대평가되며, 불변 객체와 관련된 일부 효율성으로 인해 상쇄될 수 있습니다. 여기에는 가비지 수집으로 인한 오버헤드 감소, 변경 가능한 개체를 손상으로부터 보호하는 데 필요한 코드 제거가 포함됩니다. - oracle
https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html
불변 객체 String
위의 내용을 통해 불변 객체의 장점에 대해서 알아보았다. 하지만 왜 String을 불변 객체로 이용해야 하는지에 대해서는 의문이 든다. String을 불변 객체로 이용해서 발생하는 불편을 많이 겪어보았기 때문이다.
장점
String을 불변 객체로 이용하는 이유는 크게 두 가지로, 성능과 보안이다.
성능
String은 자바에서 String Constant Pool
에 의해서 관리됩니다. ""
을 통해 생성된 String은 상수풀에 있는 객체를 할당 받아 새로운 String을 생성하지 않습니다. 이를 통해 String을 이용할 때 성능을 향상시키는데, 이때 String이 가변 객체라면 상수풀에 저장된 객체의 값이 변할 수 있기 때문에, 상수풀을 이용할 수 없을 것입니다.
보안
자바에서 String은 DB의 sql, 네트워크에서의 host, port, url등 민감한 정보를 담습니다. 이때 String이 가변 객체여서 이런 정보를 외부에서 변경한다면 심각한 피해가 발생할 것입니다.
아래 코드와 같이, second의 값이 변경되면 의도하지 않은 sql이 실행될 수 있습니다.
private void excuteUpdate(String username) {
String first = "UPDATE Customers SET Status = 'Active' ";
String second = " WHERE UserName = '";
String end = "'";
String sql = first + second + username + end;
// ....
second = " WHERE UserId = '";
// ....
}
정리
'String은 왜 불변 객체일까?' 라는 질문에 대해서 Java에서의 불변 객체의 정의와 특징을 알아보았고, String을 불변 객체로 삼으면서 얻을 수 있는 장점에 대해서 알아보았다. 정리하자면 아래와 같다.
불변 객체는 내부의 상태가 변하지 않는 객체이고, Thread-Safe하며, Cache, Map, Set등의 자료 구조에서 이용하기 좋고, 코드의 신뢰성을 보장하며, GC의 성능을 향상시킨다는 장점을 가진다.
String이 불변 객체인 이유는 상수풀을 이용할 수 있어 성능이 향상되고, 민감한 정보를 다루기 때문에, 보안이 좋아진다.
참고 자료
https://mangkyu.tistory.com/131
https://devoong2.tistory.com/entry/Java-%EB%B6%88%EB%B3%80-%EA%B0%9D%EC%B2%B4Immutable-Object-%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html
https://www.artima.com/articles/james-gosling-on-java-may-2001#part13
https://www.baeldung.com/java-string-immutable
https://devlog-wjdrbs96.tistory.com/247
https://readystory.tistory.com/139