본문 바로가기
Study/Java

[Java] Java API 생성자 구현 및 검증 방식 (String, BigDecimal)

by 검프 2021. 11. 9.

우테코 초기, 객체 검증, 할당 및 방어적 복사에 관련된 논의를 했던 적이 있어요. 논의에 대한 결과와 원문을 블로그에 남겨보려해요.

원문 : https://silly-elderberry-dd6.notion.site/Java-Java-API-8f4dceee5c84440288a5424a6326e457

https://user-images.githubusercontent.com/48986787/107109142-f75b7200-6880-11eb-9c8e-600e7e55b603.png

생성자 코드 구현시, 검증로직을 어떤 순서로 진행하는게 좋을지에 대한 질문의 제이슨의 답변이었습니다.

왜이리 멀리보고 있었지?

답변을 듣고, 들었던 생각이었습니다.

선배 자바 개발자분들이 멀리 있는 것이 아니라, 자바 코드 내에 살아 숨 쉰다는 사실을 까맣게 잊고 있었습니다.

(질문을 한 제가 부끄러워지는 듯한 느낌이었습니다.. ㅠㅠ)

그냥 쓰는 것이 아닌, 알고 쓰자.

String, BigDecimal은 어떻게 생성자 코드가 구현되고 또 검증의 순서는 어떻게 되는지 코드를 읽어보며 이해해보려 했습니다.

자바 버전

jdk 1.8.0_275

목차

  • String은 어떻게 검증되어 생성될까?
  • BigDecimal은 어떻게 검증되어 생성될까?
  • 논의해볼 부분

String은 어떻게 검증되어 생성될까?

java.lang 패키지의 String 클래스를 먼저 살펴 보기로 했습니다.

클래스 선언 및 인스턴스 변수 확인

https://user-images.githubusercontent.com/48986787/107106316-51067100-686e-11eb-8e6d-f2c4fcefd304.png

여기서 저는 3가지를 알 수 있었는데요. (밑에 두개는 아직 잘 모르겠어요..ㅎㅎ)

첫번째, String class는 상속할 수 없다.

final Class로 선언되어 있기때문에 다른곳에서 extends로 상속 할 수 없습니다.

두 번째, 문자 저장시 불변 char 배열을 쓴다.

String은 결국 클래스이기 때문에 프리미티브 타입인 char을 통해 데이터를 저장한다는 것을 알 수 있습니다. 또한 불변을 위해 final을 쓴 것을 확인할 수 있습니다.

마지막, hash는 불변이 아니다.

hashCode는 고유한 값이지 주솟 값은 아니기 때문에 불변이 아닙니다.

무한정 생성되는 String과 달리 int는 한정적이기 때문에 value 값이 달라도 hash 값이 중복될 수 있습니다.

생성자 확인

String을 인스턴스화 하기위해 2가지의 생성 방법이 있는데요

String pobi = "포비";
String brown = "포비";
String jason = new String ("포비");
String cu = new String ("포비");

위의 생성 방식의 차이는 무엇일까요? 제이슨의 수업에서 확인할 수 있었습니다.

리터럴로 생성할 경우, String constant pool에
new String()으로 생성 할 경우 Heap에 새로운 주소 할당

와닿지 않을 것 같아 그림으로 그려보았습니다.

https://user-images.githubusercontent.com/48986787/107108201-8c5a6d00-6879-11eb-8988-8a9651e5da2c.png

리터럴로 생성될 경우 String Constant Poll에 저장되기 때문에 같은 값일 시 같은 주소를 공유하고,

new String()으로 생성될 시 Heap에 새로운 공간이 할당되는 것을 확인할 수 있습니다.

그렇기 때문에 아래 두개의 테스트가 통과 할 수 있습니다.

@DisplayName("같은 이름의 문자열이 저장될 때 두 변수는 같다")
    @Test
    void literalSameTest() {
        //given
        String pobi = "포비";
        String brown = "포비";

        //then
        assertThat(pobi).isEqualTo(brown);
        assertThat(pobi).isSameAs(brown);
    }
@DisplayName("같은 이름의 문자열로 String객체가 생성될 때, 두 변수가 같은지 비교한다")
    @Test
    void objectSameTest() {
        //given
        String pobi = new String ("포비");
        String brown = new String ("포비");

        //then
        assertThat(pobi).isEqualTo(brown);
        assertThat(pobi).isNotSameAs(brown);
    }

그럼 Heap의 할당된 걸 String Constant Poll로 옮기지 못하나?

intern()메소드를 활용하면 옮길수 있습니다.

https://user-images.githubusercontent.com/48986787/107108427-0fc88e00-687b-11eb-8be4-8dc4131ee2da.png

테스트도 정상 작동하는 것을 볼 수 있습니다.

@DisplayName("intern() 정상 작동한다")
    @Test
    void internTest() {
        //given
        String pobi = "포비";
        String jason = new String ("포비").intern();

        //then
        assertThat(pobi).isEqualTo(jason);
        assertThat(pobi).isSameAs(jason);
    }

생성자 구현 로직 확인

드디어 구현 로직을 살펴보는 과정까지 왔습니다. ( 자바 버전에 따라 조금 차이가 날 수 있으니, 위에서 명시한 버전을 꼭 확인해주세요! )

String pobi = "포비" 는 어떻게 동작하는거지?

https://user-images.githubusercontent.com/48986787/107108615-9fbb0780-687c-11eb-9302-a3b9abd0b660.png

String Class 문서에 따르면

String str = "abc"; 는

char data[] = {'a','b','c'};

String str = new String(data); 와 같다고 합니다.

검증은 어떻게?

https://user-images.githubusercontent.com/48986787/107108676-135d1480-687d-11eb-8f12-1243cae4b6e8.png

https://user-images.githubusercontent.com/48986787/107108702-4acbc100-687d-11eb-8fbc-d72e42003646.png

https://user-images.githubusercontent.com/48986787/107108727-78b10580-687d-11eb-81f2-7fb07b14623f.png

String 클래스에선 값을 복사하고, System에서 필요한 검증을 한 후, 할당을 하는 것을 볼 수 있네요

제이슨의 질문에 대한 첫번째 답이 나왔습니다.

String 클래스의 String(char value[]) 생성자는 복사 -> 검증(System에서 필요한) -> 할당 한다.


BigDecimal은 어떻게 검증되어 생성될까?

클래스 선언 확인

인스턴스 변수는 이후 BigDecimal을 따로 정리할때 하기로 하고, 클래스 선언만 간단히 확인해보겠습니다.

https://user-images.githubusercontent.com/48986787/107108826-553a8a80-687e-11eb-896a-febbcffe8bdf.png

여기서 알수 있는 2가지는

첫 번째, Number 클래스를 상속받았다.

두 번째, 순서 비교를 위해 Comparable를 구현했다.

생성자 확인

저는 BigDecimal을 생성할 때 주로 String으로 인자를 넘겨주는데요, 테스트 먼저 확인하겠습니다.

@DisplayName("BigDeciaml 저장 및 더하기 확인")
    @Test
    void bigDeciamlSaveTest() {
        //given
        String five = "5";
        String ten = "10";

        //when
        BigDecimal fiveBigDecimal = new BigDecimal(five);
        BigDecimal tenBigDecimal = new BigDecimal(ten);
        BigDecimal sum = fiveBigDecimal.add(tenBigDecimal);

        //then
        assertThat(sum.intValue()).isEqualTo(15);
    }

문자열이 숫자로만 이루어져있다면, 정상적으로 생성 되는 것을 확인할 수 있습니다.

그렇다면, String이 비어있거나, 숫자 값이 아닌경우 에러가 발생해야 할 것입니다.

@DisplayName("문자열이 숫자가 아니라면 에러가 발생한다.")
    @Test
    void bigDeciamlSaveErrorTest() {
        //given
        String five = "five";
        String empty = "";

        //then
        assertThatThrownBy(() -> new BigDecimal(empty))
                .isInstanceOf(NumberFormatException.class);

        assertThatThrownBy(() -> new BigDecimal(five))
                .isInstanceOf(NumberFormatException.class);
    }

생성자 구현 로직 확인

위의 검증은 어떻게 진행될까요, BigDecimal 클래스에서 확인해보겠습니다.

https://user-images.githubusercontent.com/48986787/107151761-adb97700-69a7-11eb-97e2-1336473bfff9.png

https://user-images.githubusercontent.com/48986787/107151849-291b2880-69a8-11eb-81fc-f2ed5cd8ebf9.png

베이스 생성자의 toCharArray에서, 방어본을 만든 후 → 검증(System에서 필요한) → 반환을 하는 과정을 볼 수 있습니다.

https://user-images.githubusercontent.com/48986787/107109074-700dfe80-6880-11eb-9b68-1df40f5ae2d1.png

결국, BigDecimal(String)에서 넘어온 복사가 끝난 값이 인자(char[] in)로 들어오기 때문에 방어적 복사가 이루어졌다고 볼 수 잇습니다. 복사된 값으로 BigDecimal에서 필요한 검증을 하는 모습을 볼 수 있습니다.

그렇다면 public BigDecimal(char[] in, int offset, int len, MathContext mc)으로 생성하는 것이랑 BigDecimal(String)의 차이점은 복사를 하는것 뿐인데, 뭐가 다른걸까요?

https://user-images.githubusercontent.com/48986787/107152296-7dbfa300-69aa-11eb-971b-b9c85905ea33.png

문서에 따르면, BigDeicmal(String)보다 조금더 나은 성능을 기대할 수 있다고 합니다. (아무래도 값 복사를 하지 않기 때문이겠죠 → 더 넓게 볼 수 있는 관점을 주신 제이, 감사합니다.)

여기서는 CU의 질문의 답이 조금이나마 될 수 있을 듯 합니다.

https://user-images.githubusercontent.com/48986787/107152402-fa528180-69aa-11eb-91b6-ea1aa025cbc7.png

결론적으로, 제이슨의 질문에 대한 두번째 답이 나왔습니다.

BigDecimal(String) 생성자는 값을 할당 하기 전 복사본을 만들고, 검증을 한다.
또한 BigDecimal은 상황에 따라 값 복사 → 검증 → 할당 혹은 복사없이 검증 → 할당 한다.


논의해볼 부분

완태검증 먼저 vs 항당 후 검증에 대한 질문을 남겨주셨는데요, 이부분은 저도 이렇다 할 정답이 떠오르지 않아 논의할 부분으로 남겨두려 합니다.

https://user-images.githubusercontent.com/48986787/107109257-20303700-6882-11eb-9acd-6ccde1552fcb.png

7개월 후의 늦은 결론

"이후에 결론을 내자!", 했던게 벌써 7개월 가량이 지나버렸네요((저때는 글의 어투도 달랐네요 ㅎㅎ). "검증 후 할당 vs 할당 후 검증"을 논의해보는 것이었는데요.

저의 결론은, "검증 후 할당"을 하자 입니다.
할당을 먼저 하게 되면, 할당을 위해 준비되어야 할 객체 생성 비용과 할당된 객체의 검증 과정의 비용이 커질거라 생각하기 때문이에요. 또한, 실패했을 때, 쓸모없는 객체가 힙 영역에 추가 추가 되어 GC에 의해 해제되겠네요 :)

지금 저의 결론이 정답이라고는 생각하지 않지만, 위와 같은 이유가 나름 명확하다 생각하기에.. 당분간은 유지될거라 생각해요!

여러분들의 생각은 어떤가요?

댓글