꾸준한 스터디
article thumbnail

같은 기능을 하는 객체는 하나를 재사용하는 방법을 고려해야 한다.

 

Wrapper Class

자바의 wrapper class 인 Byte, Short, Integer 등 이있다.

primitive type 을 wrapper class로 변환할때, new와 valueOf를 통한 인스턴스 생성이 있는데,

팩터리 메서드인 valueOf를 이용하게 되면 캐싱을 이용하여 불필요한 객체 생성을 막고 있다.

public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
		return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

한가지 유의할 점은 불변 객체가 아닌 가변 객체라면 변경되지 않을 것임을 보장해야 한다.

 

정규표현식

정규표현식을 이용하여 주로 이메일 등 정해진 패턴에 일치하는지 유효성 검증에 많이 이용된다.

compileOnCall 메서드는 호출시 matches를 이용하여 Pattern.matches를 호출하는 방식이다.

public class Regexp {

    public void compileOnCall(String email) {
        long startTime = System.nanoTime();
        for(int i = 0; i < 100; i++) {
            email.matches("^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,8}$");
        }
        System.out.println("Compile On Call     >> " + (System.nanoTime() - startTime));
    }
    
    // String.matches 호출시 Pattern.matches 호출
    public static boolean matches(String regex, CharSequence input) {
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(input);
        return m.matches();
    }
}

 

compileBeforeCall은 String.mathes를 호출시 호출되는 Pattern 인스턴스 초기화 과정을

미리 캐시를 해두어 확인하는 방식이다.

public class Regexp {
	
    private final Pattern emailPattern = Pattern.compile("^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,8}$");

    public void compileBeforeCall(String email) {
        long startTime = System.nanoTime();
        for(int i = 0; i < 100; i++) {
            emailPattern.matcher(email).matches();
        }
        System.out.println("Compile Before Call >> " + (System.nanoTime() - startTime));
    }
}

 

아래는 두가지 경우를 100회 호출하였을때 성능 비교 지표이다.

public class Main {
    public static void main(String[] args) {
        Regexp regexp = new Regexp();
        // 매번 정규식 컴파일
        regexp.compileOnCall("yim3370@gmail.com");
        // 정규식 초기화 공유
        regexp.compileBeforeCall("yim3370@gmail.com");

    }
}

10배 이상 차이나는 결과이다.

지연초기화

불필요한 초기화를 방지하기 위해 필요한 순간에 초기화를 진행하는 지연초기화 방식이 존재한다.

간단한 지연초기화 방식을 구현한 예제이다.

public class ClassA {

    private ClassB classB;

    public ClassA() {
        this.classB = new ClassB();
    }
}

public class LazyInitializationA {

    private ClassB classB;

    public ClassB getLazyInitializationB() {
        if(classB == null) {
            this.classB = new ClassB();
        }
        return classB;
    }
}

ClassA는 생성자 호출시 ClassB를 생성자 호출한다.

LazyInitializationA는 getLazyInitializationB 메서드 호출시 ClassB의 초기화 여부를 판단하여 동작한다.

 

아래는 두가지 방식의 메모리 사용량 비교이다.

public class Main {
    public static void main(String[] args) {

        // 초기화 당시 생성
        Runtime.getRuntime().gc();
        for (int i = 0; i < 100000; i++) {
            new ClassA();
        }
        long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("초기화 당시 생성할 경우 메모리 사용량                   >> " + used);

        // 지연 초기화
        Runtime.getRuntime().gc();
        for (int i = 0; i < 100000; i++) {
            new LazyInitializationA();
        }
        used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("지연 초기화를 이용하여 호출시 생성할 경우 메모리 사용량    >> " + used);

    }
}

2배 정도 차이나는 것을 확인할 수 있다.

 

Adpter Pattern

마지막으로 어뎁터 패턴을 이용한 불필요한 객체 생성을 방지하는 방법이다.

Map interface의 keyset 메서드는 새로운 객체를 생성하는 것이 아닌 매번 같은 객체를 반환한다.

public Set<K> keySet() {
	Set<K> ks = keySet;
	if (ks == null) {
		ks = new KeySet();
		keySet = ks;
	}
	return ks;
}

아래는 예제는 keyset 메서드를 통해 반환된 Set<K>이 동일한 객체인지 비교하는 예제이다.

public class Main {
    public static void main(String[] args) {

        Map<String, Object> map = new HashMap<>();
        Set<String> setAdapterA = map.keySet();
        Set<String> setAdapterB = map.keySet();

        System.out.println(setAdapterA == setAdapterB);
        System.out.println(setAdapterA.hashCode() == setAdapterB.hashCode());
        System.out.println(setAdapterA.equals(setAdapterB));

    }
}

 

Auto Boxing

명시적인 형변환이 아닌 묵시적으로 큰 타입으로  자동 형변환이 되는 등의 경우 성능에 영향을 미친다.

Wrapper, AutoBoxing, 동일 타입 연산에 대한 성능 비교 예제이다.

public class Primitive {
    private long longValue = 0L;

    public void sumLongValue() {
        long startTime = System.nanoTime();
        for(int i = 0; i < 100000; i++) {
            longValue++;
        }
        System.out.println("Primitive type add int  >> " + (System.nanoTime() - startTime));

        startTime = System.nanoTime();
        for(long i = 0; i < 100000; i++) {
            longValue += i;
        }
        System.out.println("Primitive type add long >> " + (System.nanoTime() - startTime));
    }
}

public class Wrapper {
    private Long longValue = 0L;

    public void sumLongValue() {
        Long startTime = System.nanoTime();
        for(int i = 0; i < 100000; i++) {
            longValue += i;
        }
        System.out.println("Wrapper Class Long      >> " + (System.nanoTime() - startTime));
    }
}

Wrapper > AutoBoxing > Same Type

 

항상 같은 객체를 재사용하는 것이 장점 만을 가지는 것이 아니다.

같은 객체를 재사용할 때에는 참조를 유의하여야하며, 변화가 되어야 할 경우 새로운 객체를 생성하여 사용해자.

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!