반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 플라이웨이트패턴
  • 러스트
  • 자바스크립트
  • MSSQL
  • 이펙티브자바
  • bean Validation
  • 작업기억공간
  • 서버리스 컴퓨팅
  • Java
  • NEXT JS
  • 싱글톤 패턴
  • pnpm
  • try-with-resources
  • prerendering
  • next js app
  • 자바 디자인패턴
  • 슬로우 쿼리
  • 알고리즘
  • 싱글톤
  • 이펙티브 자바 item9
  • 자료구조
  • item9
  • 추상 팩터리 패턴
  • 스프링 검증
  • 메이븐 라이프사이클
  • Javadoc 자바독 자바주석 주석 Comment
  • 자바스크립트 면접
  • Next.js
  • 디자인패턴
  • 자바
  • 빈 검증
  • 메이븐 페이즈
  • 느린 쿼리
  • 외래키 제약조건
  • 참조 해제
  • 자바스크립트 인터뷰
  • 싱글턴
  • Pre-rendering
  • 메이븐 골
  • 도커공식문서
  • item7
  • 팩터리 메서드 패턴
  • 프로그래머의 뇌
  • 이펙티브 자바
  • 자바 검증
  • serverless computing
  • item8
  • 객체복사
  • rust
  • 토비의 스프링

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

Java/이펙티브 자바

이펙티브 자바, 쉽게 정리하기 - item6. 불필요한 객체 생성을 피하라

2021. 12. 26. 23:50

불필요한 객체 생성을 피하라

객체 생성의 비용

  • 객체를 매번 생성하고 지우는 것은 반복적으로 발생했을 때 큰 비용이 될 수 있다.
    • 물론 현대 컴퓨터의 성능이 많이 좋아서 작은 객체는 큰 부담이 되지 않을 수도 있다.
  • 계속 같은 내용의 객체를 사용할 것이라면 불변 객체를 만들어놓고 재사용하는 것이 좋다.

불필요한 객체 생성의 예

String 객체의 예

String s = new String("bikini");

위 코드는 안티패턴이다. 결국 bikini라는 문자열을 사용하고 싶은 건데, 굳이 JVM 문자열 풀에서 가져오지 않을 이유가 없다.

String s = "bikini";

더 간결하면서도 더 옳은 코드이다.

Boolean 객체의 예

Boolean boolean = new Boolean(true);

과연 위 객체는 정말 필요한 걸까? 결국 Boolean 객체란 true와 false 2가지 상태밖에 존재하지 않는다.

@Deprecated(since="9", forRemoval = true)
public Boolean(boolean value) {
    this.value = value;
}

결국 위와 같이 Java 9 버전부터는 @Deprecated 된 것을 볼 수 있다. 객체 생성 대신 true, false primitive 타입을 쓰거나 Boolean.TRUE 혹은 Boolean.FALSE를 이용하면 미리 생성되어 있는 싱글턴 객체를 불러와 사용할 수 있다.

불필요한 객체 생성의 특징은 매번 같은 내용의 객체를 반복해서 사용하면서, 매번 새로 생성하는 경우이다.

정규표현식에서의 불필요한 객체 생성

기존 코드

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.matchs() 메서드는 정규표현식으로 해당 문자열이 내가 원하는 형태인지 확인하는 가장 쉬운 방법이다.

matches() 메서드 사용 중 놓치기 쉬운 부분

public static Pattern compile(String regex) {
    return new Pattern(regex, 0);
}
  • String.matches() 메서드의 구현을 따라가보면, 위처럼 Pattern 인스턴스를 생성하는 부분이 있다.
  • 위 과정에서 생성되는 Pattern 인스턴스는 한번 쓰고 버려져 가비지컬렉션된다.
  • 정규표현식에 해당되는 유한상태머신을 만들기 때문에 인스턴스 생성 비용이 높다.
  • 내부적으로 Pattern을 생성하는 로직을 밖으로 빼서 재활용하면 객체 생성을 1번만 하고 성능을 높일 수 있다.

개선된 코드로 성능 테스트해보기

package item6;

import org.junit.jupiter.api.Test;

import java.util.regex.Pattern;

public class Item6Test {
    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}))");
    }

    static class RomanNumerals {
        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();
        }
    }

    @Test
    public void regexTest() {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++) {
            boolean iiv = isRomanNumeral("IV");
        }

        long end = System.currentTimeMillis();

        System.out.println("매번 새로 생성 : " + (end - start) + "ms");
    }

    @Test
    public void regexTest2() {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000; i++) {
            boolean iiv = RomanNumerals.isRomanNumeral("IV");
        }

        long end = System.currentTimeMillis();

        System.out.println("객체 재활용 : " + (end - start) + "ms");
    }
}
  • regexTest 메서드에서는 매번 객체를 새로 생성해본다.
  • regexTest2 메서드에서는 정적 메서드를 이용하여 생성된 객체를 매번 재활용한다.
  • 반복 횟수는 동일하게 10000번이다.

결과

객체 재활용 : 6ms
매번 새로 생성 : 46ms

비록 6ms와 46ms로 현실세계에서는 별 것 아닌 시간이지만, 단순 비례로 계산해보면 거의 7.x배 차이가 난다. 동시접속자가 많은 서버라면, 이정도면 꽤나 유의미한 차이일 수 있다.

사실 Pattern 객체를 더 최적화하면, 사용하지 않을 때는 아예 생성되지 않도록 지연초기화를 할 수 있으나 지연 초기화로 코드가 복잡해지는 반면 성능상 이득이 크게 없을 수 있으므로 이는 적용하지 않는다.

언제 객체를 재생성할 필요 없이 재사용해도 될까?

재사용해도 될 때

  • 객체가 불변이라면 안심하고 재사용할 수 있다.

재사용을 조심해야 할 때

  • 간단히 말하면, 객체 내부의 내용이 변할 수 있을 때다.
  • 다른 스레드에 의해 해당 객체가 변해서 동시성 문제가 발생하게 되면 개발자가 원했던 결과와 다른 결과가 나오게 될 것이다.
  • 어댑터라는 개념에서는 특히 주의해야 한다.
    • 어댑터는 실제 작업을 뒷단 객체에 위임하고 자신은 인터페이스의 역할만 하는 것이다.
    • 어댑터는 사실상 View의 역할을 하기 때문에 실제 객체의 내용이 바뀌면 어댑터가 바라보는 내용도 바뀐다.

간단한 예제

  • Map 인터페이스의 keySet()은 Map 안에 들어있는 key를 Set 타입으로 반환한다.
  • 이 반환받은 Set을 저장해놓고 재활용해도 될까?
    • 안된다. keySet()이라는 메서드가 매번 Set을 생성한다고 보장할 수 없다.

keySet() 메서드의 구현을 살펴보자.

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new AbstractSet<K>() {
            public Iterator<K> iterator() {
                return new Iterator<K>() {
                    private Iterator<Entry<K,V>> i = entrySet().iterator();

                    public boolean hasNext() {
                        return i.hasNext();
                    }

                    public K next() {
                        return i.next().getKey();
                    }

                    public void remove() {
                        i.remove();
                    }
                };
            }

            public int size() {
                return AbstractMap.this.size();
            }

            public boolean isEmpty() {
                return AbstractMap.this.isEmpty();
            }

            public void clear() {
                AbstractMap.this.clear();
            }

            public boolean contains(Object k) {
                return AbstractMap.this.containsKey(k);
            }
        };
        keySet = ks;
    }
    return ks;
}
  • ks라는 변수에 해당 클래스가 가진 keySet 필드의 값을 할당한다.
  • ks가 null이라면
    • AbstractSet 클래스를 이용해 새로운 객체를 넣어 할당한다.
    • 만들어둔 객체는 재활용할 수 있도록 keySet 필드에 할당해둔다.
  • ks가 null이 아니라면
    • keySet 필드의 값을 재활용한다.

Map.keySet()으로 할당받은 Set은 어댑터의 대표적인 예이다. 원본 Map이 바뀌면 keySet도 바뀌므로, 항상 인지하고 있어야 한다. 또한, 매번 Map.keySet()을 하더라도 같은 객체를 반환할 것이다. Map 객체가 이를 재사용하고 있음을 인지하고 코딩하는 것이 좋다.

primitive 오토 박싱의 함정

primitive 오토 박싱은 primitive 타입을 객체화 시켜준다. 이로 인해, 일반 객체처럼 null 값을 할당해두는 것도 가능해지고, 제너릭스에 타입으로 줄 수도 있게 된다. 그러나, 이렇게 의도한 박싱도 있는 반면 나도 모르게 박싱이 되는 경우도 있다.

파라미터 타입에 의한 오토박싱

public static void unintendedBoxing1(Integer number1, Integer number2) {
  System.out.println(number1 + number2);
}

위와 같은 메서드가 있을 때, 일반 int 타입을 넘기더라도 파라미터 타입에 의해 오토 박싱이 일어난다. 이는 작은 작업에서는 별다른 두각을 나타내지 않지만, 이 메서드를 이용해 대량의 작업을 하면 분명히 그냥 int 타입을 활용하는 것보다 훨씬 느리게 될 것이다.

변수 타입에 의한 오토박싱

private static long unintendedBoxing2() {
  Long sum = 0L;

  for(long i=0; i<=Integer.MAX_VALUE; i++) {
    sum += i;
  }

  return sum;
}

위는 왜 오토 박싱이 발생하는 것일까? 그 이유는 모든 합계를 더해줄 sum을 Long으로 선언하였기 때문이다.

    /**
     * The value of the {@code Long}.
     *
     * @serial
     */
    private final long value;

Long 클래스 내부에는 저렇게 final로 불변 value를 가지고 있다. 그렇기 때문에 값을 바꾸는 행위는 사실 새로운 Long 객체를 한번 더 생성 해야만 가능한 것이다.

이렇게 의도치 않게, 불필요한 객체를 재생성해 낭비가 발생할 수 있다.

그렇다면 어떻게 코딩을 해야 할까?

primitive 타입을 사용해도 동작에 별다른 지장이 없는 경우엔 무조건 primitive타입을 사용하자.

객체 생성 자체를 최대한 피해야 할까?

정답은 아니다. JVM에서 작은 객체를 생성하고 회수하는 게 그렇게 큰 일은 아니다.

그러나 몇가지 확실히 객체 생성이 안좋을 때가 있다.

  • 첫째, 불필요한 객체 생성일 때
    • ex1) 같은 정규표현식 Pattern 객체를 재사용해도 무관할 때
    • ex2) primitive 타입만 사용해도 되는 경우인데, 불필요한 오토 박싱이 일어나 낭비를 줄 때
  • 둘째, 부하가 많은 서버 환경일 때
    • 이 경우엔 작은 낭비도 사용자들에 의해 큰 낭비가 되어버릴 수 있으니 내가 객체 생성을 새로 하고 있다는 것을 인지하고 있는 것이 좋다.

이후에 나오는 내용 중, 새로운 객체를 만들어야 한다면, 기존 객체를 재사용하지 마라 라는 내용도 있다. 무조건 xx해라. 라는 것은 없고 결국 상황에 맞는 코드를 사용하는 것이 가장 중요하다.

방어적 복사가 필요한 때 기존 객체를 재사용하는 것이 필요없는 객체를 반복 생성하는 것보다 훨씬 피해가 크다는 점을 분명히 인지해두어야 한다.

그렇다면 객체 풀을 만드는 것은 어떨까?

  • 요즘 JVM의 가비지 컬렉터는 상당히 최적화가 잘되어서 사용자가 어설프게 만든 객체 풀보다는 훨씬 빠르다.
    • 되려 코드의 복잡성만 증가할 수 있다.
반응형
저작자표시

'Java > 이펙티브 자바' 카테고리의 다른 글

이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라  (0) 2021.12.27
이펙티브 자바, 쉽게 정리하기 - item7. 다 쓴 객체 참조를 해제하라  (0) 2021.12.27
이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라  (0) 2021.12.25
이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라  (0) 2021.12.25
이펙티브 자바, 쉽게 정리하기 - item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라  (0) 2021.12.24
    'Java/이펙티브 자바' 카테고리의 다른 글
    • 이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라
    • 이펙티브 자바, 쉽게 정리하기 - item7. 다 쓴 객체 참조를 해제하라
    • 이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
    • 이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바