Q. 왜 객체 내부의 데이터를 보호하는가?
A. 객체 내부 데이터에 대한 책임은 온전히 그 객체가 져야 한다. 다시 말해, 객체 내부 데이터는 객체 외부에서 변경할 수 있도록 허용하면 안 된다. 객체 내부에서 변경할 수 있다고 외부에서도 변경할 수 있으면 안 된다는 말이다.
Q. 아니 복잡하고 추상적인 건 됐고, 뭐 땜에 그렇게 해야 되냐고?
A. 객체 내부 데이터를 맘껏 읽게 코드 짜 놓으면 나중에 그 객체 내 변수 타입, 이름이나 마음대로 바꿀 수 있겠냐? 뭐 하나만 바꿔도 여기저기 오류 터지고 난리날 걸?
그리고 읽기만 하면 다행이지 편하다고 바로 내부 변수에 접근해서 값 덮어쓰도록 짜 봐. 객체 커지고 복잡해지면 실수 안 하겠어? 객체가 중간에 안 깨지고 쭉 의도한 대로 동작한다고 장담할 수 있어?
뭐 그냥 데이터는 private
으로 제한하고 필요하면 public
getter로 접근할 수 있게 해 주면 되는 거 아니야? 라고 할지도 모르겠다. 예로부터 "객체 지향"하면 바로 따라 나왔던 게 getter, setter이긴 하니까.
getter, setter 사용이 객체 지향을 저해하는 문제점에 대해선 다른 글에서 다루겠다.
그런데 단순히 Java의 List
, Kotlin의 MutableList
같은 타입의 데이터에만 위 방법을 적용해 봐도 안 먹힌다는 걸 알게 될 것이다.
가볍게 짚고 넘어가자. Integer나 String 같은 primitive type 데이터를 보호한다면 보통 아래와 같은 형태의 코드일 것이다.
class 아이패드 {
private String 각인;
public 아이패드(String 각인) { // 생성자
this.각인 = 각인;
}
public String 각인() { // 외부에서 내부 데이터 `각인`에 접근할 수 있도록 해 주는 getter
return 각인;
}
public void 변경_각인(String 새_각인) { // 사실 여기선 setter나 다름없긴 함
// `새_각인`의 유효성을 검사하는 코드 생략
각인 = 새_각인;
}
}
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class JavaTest {
@Test
void 데이터_보호_테스트() {
아이패드 내_태블릿 = new 아이패드("사과 씨앗");
내_태블릿.각인 = ""; // ERROR! [1]
내_태블릿.각인() = ""; // 문법 ERROR!
String 각인 = 내_태블릿.각인(); // [2]
각인 = "";
내_태블릿.변경_각인(""); // [3]
assertThat(내_태블릿.각인()).isEqualTo(""); // OK
}
}
각인
은 private
이기 때문에내_태블릿
객체 외부인 데이터_보호_테스트()
함수에서 직접 접근할 수 없다. 읽을 수도, 쓸 수도 없다.각인()
함수가 반환하는 데이터는 복사본이다. 따라서 변경해도 원본(각인
)에 영향을 미칠 수 없다.내_태블릿
객체 외부에서는 오직 공개된, 설계자의 의도를 담아 제공된 변경_각인()
함수를 통해서만 간접적으로 변경할 수 있다.primitive type은 이런 식으로 보호할 수 있다. 하지만 배열 같은 건?
List
도 같은 방법으로 보호할 수 있을까?
List
데이터 보호아래 코드를 보자.
import java.util.ArrayList;
import java.util.List;
class 아이패드 {
private final List<String> 이메일목록 = new ArrayList<>();
public 아이패드(String... 이메일들) {
for (String 이메일 : 이메일들) {
this.이메일목록.add(이메일);
}
}
public List<String> 이메일목록() { // getter
return 이메일목록;
}
}
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class JavaTest {
@Test
void 데이터_보호_테스트() {
아이패드 내_태블릿 = new 아이패드("gg@gmail.com");
assertThat(내_태블릿.이메일목록())
.containsExactly("gg@gmail.com"); // OK
내_태블릿.이메일목록().add("apple@apple.com"); // 내부 데이터 직접 수정
assertThat(내_태블릿.이메일목록())
.containsExactly("gg@gmail.com", "apple@apple.com"); // OK
}
}
private
에다 final
까지 추가해 보았지만 소용없다.
final
은이메일목록
변수가 다른 리스트를 참조하지 못하도록 막을 뿐 참조하고 있는 리스트가 변하는 것은 막지 못한다.
객체 외부(데이터_보호_테스트()
함수)에서 getter를 통해 내부 이메일목록
에 접근하여 마음대로 이메일을 추가하고 있다.
getter가 내부 데이터를 반환할 때,
primitive type은 값 복사가 일어났지만
List
는 참조(주소) 복사가 일어났기 때문이다. 다시 말해, 외부에서 원본에 접근할 수 있는 것이다.(더 자세한 내용은 C언어의 포인터 부분을 학습)
그럼 어떻게 해야 할까?
내부 리스트가 immutable(원소 추가/삭제 불가)인 경우는 당연히 외부에서 수정 불가능하겠지만 내부에서도 수정 불가능하므로 유용하지 않다.
getter에서 return 이메일목록;
이 원본(이메일목록
)에 접근할 수 있도록 하는 것이 원인이므로 이 부분을 아래처럼 고쳐 볼 수 있다.
public List<String> 이메일목록() { // getter
// return 이메일목록;
return new ArrayList<>(이메일목록);
}
C++이라면 몰라도 Java에서 주소가 복사되는 내부 동작을 변경할 수는 없다. 그래서 원본 대신 복사본의 주소가 복사되도록 만들었다. 하지만 이 경우, 외부에서는 원본으로 착각할 수 있다.
객체 외부 입장에서는 인터페이스가 명확한 것이 좋다. 그래서 반환할 때만이라도 immutable한 타입으로 반환하여 .add()
같은 함수를 호출할 수 없도록 만드는 게 제일 좋지 않나 싶다.
public List<String> 이메일목록() { // getter
// return 이메일목록;
// return new ArrayList<>(이메일목록);
return Collections.unmodifiableList(이메일목록);
}
코틀린에서는 어떻게 하는지 한번 살펴보자. 사실 이것 때문에 쓴 글이다.
private setter를 사용하는 방법과 getter를 사용하는 방법 두 가지를 소개하겠다.
참고로 코프링에서 이 방법을 사용한다면 알고 있어야 할 것이 하나 있는데 먼저 방법 설명부터하고 마지막에 다시 얘기하겠다.
class 아이패드(각인: String) {
var 각인: String = 각인
private set // setter
fun 변경_각인(새_각인: String) {
// 각인에 부적절한 단어가 포함되어 있는지 검사하는 코드 생략
this.각인 = 새_각인
}
}
위 코드에서 클래스 이름 오른쪽 괄호는 생성자를 정의하는 것이다.
자세한 내용은 코틀린 생성자에 대한 설명을 참고
각인
은 var
이므로 수정할 수 있다. 그리고 원래는 private
이 아니기 때문에 내부, 외부 모두에서 수정할 수 있었으나 setter를 private
으로 둠으로써 외부에서 수정만 못하도록 막았다.
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
class KotlinTest {
@Test
fun 데이터_보호_테스트() {
val 내_태블릿 = 아이패드("사과 씨앗")
내_태블릿.각인 shouldBe "사과 씨앗" // OK
내_태블릿.각인 = "" // ERROR! the setter is private in '아이패드'
내_태블릿.변경_각인("")
내_태블릿.각인 shouldBe "" // OK
}
}
그런데 MutableList
데이터는 이 방법으로 보호할 수 없다.
class 아이패드 {
var 이메일목록: MutableList<String> = mutableListOf()
private set
}
(참고로, 위 코드에서 private set
을 없애면서 var
를 val
로 바꿔도 동작은 같음)
class KotlinTest {
@Test
fun 데이터_보호_테스트() {
val 내_태블릿 = 아이패드()
내_태블릿.이메일목록 shouldContainExactly listOf()
내_태블릿.이메일목록.add("gg@gmail.com")
내_태블릿.이메일목록 shouldContainExactly listOf("gg@gmail.com")
}
}
객체 내부, 외부에서 같은 변수(이메일목록
)에 직접 접근하기 때문에 내부에서 요소를 추가하거나 뺄 수 있다면 외부에서도 똑같이 가능하다.
private setter는 open
된 property에는 사용할 수 없다.
Private setters are not allowed for open properties
그런데 코프링에서 lazy loading을 위해 아래와 같이 하는 경우가 있다.
// build.gradle.kts
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
}
allOpen
으로 open
된 엔티티는 property들도 다 open
된다. 따라서 private setter를 사용할 수 없다.
(그럼에도 property에 final
을 추가로 명시하면 컴파일 에러가 사라지긴 하는데, 런타임에서 다음과 같은 에러가 뜰 것이다.
org.hibernate.HibernateException: Getter methods of lazy classes cannot be final
)
이 경우, protected setter를 사용해야 한다. 참고 Kotlin Entity All open, 현구막
이 방법은 네이밍이 좀 거슬릴 수 있지만(꼭 앞에_
를 붙여야 하는 건 아님) 바로 다음에 보게 될 "MutableList 데이터 보호"에도 사용된다.
class 아이패드(private var _각인: String) {
val 각인: String
get() = _각인 // getter
fun 변경_각인(새_각인: String) {
_각인 = 새_각인
}
}
내부 데이터가 저장되는 _각인
은 private
이므로 외부에서 직접 접근할 수 없고 var
이므로 내부에서 값을 변경할 수 있다.
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
class KotlinTest {
@Test
fun 데이터_보호_테스트() {
val 내_태블릿 = 아이패드("사과 씨앗")
내_태블릿.각인 shouldBe "사과 씨앗" // OK
내_태블릿.각인 = "" // ERROR! Val cannot be reassigned
내_태블릿.변경_각인("")
내_태블릿.각인 shouldBe "" // OK
}
}
한편, 외부에서는 각인
을 통해 내부 데이터를 읽을 수 있지만 값을 덮어쓰려고 하면 각인
이 val
이라서 에러가 발생하므로 객체 설계자가 의도하지 않은 동작이라는 걸 객체 사용자가 알 수 있다.
MutableList
데이터 보호코틀린에는 애초에 타입이 List
와 MutableList
두 가지로 나뉘어 있어서 참 좋다.
class 아이패드 {
private val _이메일목록: MutableList<String> = mutableListOf()
val 이메일목록: List<String>
get() = _이메일목록
fun 추가_이메일(이메일: String) {
// email 주소가 올바른지 검사하는 코드 생략
_이메일목록.add(이메일)
}
}
실제 데이터가 저장되는 _이메일목록
의 타입이 MutableList
이기 때문에 원소 추가/삭제 등의 행위가 가능하다. 하지만 private
이라 외부에서는 직접 접근이 불가능하여 공개된 이메일목록
을 통해서만 접근할 수 있는데 이메일목록
의 타입은 (immutable)List
이기 때문에 결국 외부에서는 원소 추가/삭제 등의 행위를 할 수 없다.
import io.kotest.matchers.collections.shouldContainExactly
import org.junit.jupiter.api.Test
class KotlinTest {
@Test
fun 데이터_보호_테스트() {
val 내_태블릿 = 아이패드()
내_태블릿.이메일목록 shouldContainExactly listOf() // OK
내_태블릿.이메일목록.add("aa@apple.com") // ERROR! Unresolved reference: add
내_태블릿.추가_이메일("aa@apple.com")
내_태블릿.이메일목록 shouldContainExactly listOf("gg@gmail.com", "aa@apple.com") // OK
}
}