Programming/Java

[Youtube][이팩티브 자바] #6 불필요한 객체를 만들지 말자

bisi 2020. 5. 23. 10:37

백기선님의 유투브 강의 내용을 정리하였습니다.

 

백기선님 Github 바로가기

Youtube 바로가기

 

 

강의내용 필기 


 

주제 6 : 불필요한 객체를 만들지 말자.

 

기능적으로 동일한 객체를 새로 만드는 대신 객체 하나를 재사용하는 것이 대부분 적절하다. 재사용하면 더 빠르고 있어보인다(new라는 코드를 안써서..?). 불변객체는 항상 재사용할 수 있다.

 

 

문자열 객체 생성

 

자바의 문자열 String을 new로 생상하면 항상 새로운 객체를 만들게 된다. 다음가 같이 String 객체를 생성하는 것이 올바르다. 

 

public class StringTest {

    public static void main(String[] args) {
        String s1 = new String("log");
		String s2 = "log";

        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}

s1 보다는 s2로 써라

s1==s2는 FALSE 출력함 -> 주소값 끼리의 비교 

s1.equals(s2) TRUE 출력 함-> string equals는 그안에 값을 비교하도록 오버라이딩 되어 있음.  

 

 

 

문자열 리터럴을 재사용하기 때문에 해당 자바 가상 머신에 동일한 문자열 리터럴이 존재한다면 그 리터럴을 재사용한다.

 

static 팩토리 메소드 사용하기 

자바 9에서 deprecated 된 Boolean(String) 대신 Boolean.valueOf(String) 같은 static 팩토리 메소드를 사용할 수 있다. 생성자는 반드시 새로운 객체를 만들어야 하지만 팩토리 메소드는 그렇지 않다.

 

public class StringTest {

    public static void main(String[] args) {
        Boolean true1 = Boolean.valueOf("true");
        Boolean true2 = Boolean.valueOf("true");

//Boolean이 리턴한건 Boolean.TRUE이기 때문에 true1과 true2는 같다.
        System.out.println(true1 == true2);
        System.out.println(true1 == Boolean.TRUE);
    }
}

 

 

무거운 객체 

만드는데 메모리나 시간이 오래 걸리는 객체 즉 "비싼 객체"를 반복적으로 만들어야 한다면 캐시해두고 재사용할 수 있는지 고려하는 것이 좋다. 

 

정규 표현식으로 예제로 살펴보자. 문자열이 로마 숫자를 표현하는지 확인하는 코드는 다음과 같다.

static boolean isRomanNumeral(String s) {
        return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
    }

넘어온 문자열이 string의 matches를 활용하여 맞는지 아닌지 확인한다.

 

정규식을 받아서 컴파일을 함. 근데 그 과정이 굉장히 비싸다.

 

정규식이 변함이 없는 이상 패턴을 미리 만들어 여러번 사용하면 된다. 

public class RomanNumber {

    private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }

}

위의 코드 처럼  static으로 컴파일 해놓고 isRomanNumeral에 재사용하면 된다. 반복적으로 사용하기 때문에 성능적으로 이득을 볼 수 있다. 

 

어댑터 

불변 객체인 경우에 안정하게 재사용하는 것이 매우 명확하다. 하지만 몇몇 경우에 분명하지 않은 경우가 있다. 오히려 반대로 보이기도 한다. 어댑터를 예로 들면, 어댑터는 인터페이스를 통해서 뒤에 있는 객체로 연결해주는 객체라 여러개 만들 필요가 없다. 

 

Map 인터페이스가 제공하는 keySet은 Map이 뒤에 있는 Set 인터페이스의 뷰를 제공한다. keySet을 호출할 때마다 새로운 객체가 나올거 같지만 사실 같은 객체를 리턴하기 때문에 리턴 받은 Set 타입의 객체를 변경하면, 결국엔 그 뒤에 있는 Map 객체를 변경하게 된다.

 

public class UsingKeySet {

    public static void main(String[] args) {
        Map<String, Integer> menu = new HashMap<>();
        menu.put("Burger", 8);
        menu.put("Pizza", 9);

        Set<String> names1 = menu.keySet();
        Set<String> names2 = menu.keySet();

        names1.remove("Burger");//이렇게 삭제하면 set뿐만 아니라 map까지도 영향을 준다.
        System.out.println(names2.size()); // 1
        System.out.println(menu.size()); // 1
    }
}

names1와 names2는 같다. 왜냐하면 menu에 있는 keyset의 같은 객체를 리턴하기 때문이다. 

하지만 이런 코드는 혼란을 줄 수 있으므로 좋다고 보기는 어렵다. 이 사실을 팀원들 모두가 알고 있다면 문제는 없지만, 안전한 방법은 set을 카피해서 쓰는 방법이 있다. (Depensive copy라는 기술 활용)

 

오토박싱

 

불필요한 객체를 생성하는 또 다른 방법으로 오토박싱이 있다. 오토박싱은 프로그래머가 프리미티브 타입과 박스 타입을 섞어 쓸 수 있게 해주고 박싱과 언박싱을 자동으로 해준다. 

ex ) long, int, char -> Long, Integer, Character 으로 자동으로 레퍼런스 타입으로 변환해주는 것. 

 

오토박싱은 프리미티브 타입과 박스 타입의 경계가 안보이게 해주지만 그렇다고 그 경계를 없애주진 않는다.

public class AutoBoxingExample {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        Long sum = 0l;
        for (long i = 0 ; i <= Integer.MAX_VALUE ; i++) {
            sum += i;
        }
        System.out.println(sum);
        System.out.println(System.currentTimeMillis() - start);
    }
}

위 코드에서 sum 변수의 타입을 Long으로 만들었기 때문에 불필요한 Long 객체를 2의 31제곱개 만큼 만들게 되고 대략 6초 조금 넘게 걸린다. (수많은 객체를 새로 생성하는 것이므로 오래 걸림)

타입을 프리미티브 타입으로 바꾸면 600밀리초로 약 10배 이상의 차이가 난다. 

 

불필요한 오토박싱을 피하려면 박스 타입 보다는 프리미티브 타입을 사용해야한다.

 

이번 아이템으로 인해 객체를 만드는 것은 비싸며 가급적이면 피해야한다는 오해를 해서는 안된다. 특히 방어적인 복사(Depensive copying)를 해야 하는 경우에도 객체를 재사용하면 심각한 버그와 보안성에 문제가 생긴다. 객체를 생성하면 그저 스타일과 성능에 영향을 줄 뿐이기 때문에, 객체를 언제 어떻게 사용할지는 신중하게 고민하길 권유한다.