객체 데이터 보호 (Feat. Kotlin)

jYur·2022년 9월 29일
0

Q. 왜 객체 내부의 데이터를 보호하는가?
A. 객체 내부 데이터에 대한 책임은 온전히 그 객체가 져야 한다. 다시 말해, 객체 내부 데이터는 객체 외부에서 변경할 수 있도록 허용하면 안 된다. 객체 내부에서 변경할 수 있다고 외부에서도 변경할 수 있으면 안 된다는 말이다.

Q. 아니 복잡하고 추상적인 건 됐고, 뭐 땜에 그렇게 해야 되냐고?
A. 객체 내부 데이터를 맘껏 읽게 코드 짜 놓으면 나중에 그 객체 내 변수 타입, 이름이나 마음대로 바꿀 수 있겠냐? 뭐 하나만 바꿔도 여기저기 오류 터지고 난리날 걸?
그리고 읽기만 하면 다행이지 편하다고 바로 내부 변수에 접근해서 값 덮어쓰도록 짜 봐. 객체 커지고 복잡해지면 실수 안 하겠어? 객체가 중간에 안 깨지고 쭉 의도한 대로 동작한다고 장담할 수 있어?


뭐 그냥 데이터는 private으로 제한하고 필요하면 public getter로 접근할 수 있게 해 주면 되는 거 아니야? 라고 할지도 모르겠다. 예로부터 "객체 지향"하면 바로 따라 나왔던 게 getter, setter이긴 하니까.

getter, setter 사용이 객체 지향을 저해하는 문제점에 대해선 다른 글에서 다루겠다.

그런데 단순히 Java의 List, Kotlin의 MutableList 같은 타입의 데이터에만 위 방법을 적용해 봐도 안 먹힌다는 걸 알게 될 것이다.

Java에서의 객체 내부 데이터 보호


Primitive type 데이터 보호

가볍게 짚고 넘어가자. 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
    }
}
  1. 각인private이기 때문에내_태블릿 객체 외부인 데이터_보호_테스트()함수에서 직접 접근할 수 없다. 읽을 수도, 쓸 수도 없다.
  2. 각인() 함수가 반환하는 데이터는 복사본이다. 따라서 변경해도 원본(각인)에 영향을 미칠 수 없다.
  3. 내_태블릿 객체 외부에서는 오직 공개된, 설계자의 의도를 담아 제공된 변경_각인() 함수를 통해서만 간접적으로 변경할 수 있다.

primitive type은 이런 식으로 보호할 수 있다. 하지만 배열 같은 건?
List도 같은 방법으로 보호할 수 있을까?

(Java)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(이메일목록);
    }

Kotlin에서의 객체 내부 데이터 보호


코틀린에서는 어떻게 하는지 한번 살펴보자. 사실 이것 때문에 쓴 글이다.

Primitive type 데이터 보호

private setter를 사용하는 방법과 getter를 사용하는 방법 두 가지를 소개하겠다.

1. private setter 활용

참고로 코프링에서 이 방법을 사용한다면 알고 있어야 할 것이 하나 있는데 먼저 방법 설명부터하고 마지막에 다시 얘기하겠다.

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을 없애면서 varval로 바꿔도 동작은 같음)

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, 현구막

2. getter 사용

이 방법은 네이밍이 좀 거슬릴 수 있지만(꼭 앞에_를 붙여야 하는 건 아님) 바로 다음에 보게 될 "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이라서 에러가 발생하므로 객체 설계자가 의도하지 않은 동작이라는 걸 객체 사용자가 알 수 있다.

(Kotlin)MutableList 데이터 보호

코틀린에는 애초에 타입이 ListMutableList 두 가지로 나뉘어 있어서 참 좋다.

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
    }
}

0개의 댓글