Java

Java 코드 성능/메모리 사용량 최적화

ZZJJing 2025. 5. 7. 22:11
반응형

■ 코드 성능 최적화 해보기 

1. 프로파일링을 통한 병목 지점 파악

  • 시간 측정 코드
    long startTime = System.nanoTime();
    myMethod();
    long endTime = System.nanoTime();
    System.out.println("실행 시간: " + (endTime - startTime) / 1000000 + "ms");
  • 전문 프로파일링 도구: VisualVM, JProfiler, YourKit 활용
  • 핵심: 전체 코드의 10%가 90%의 성능 문제를 일으키는 경우가 많음

2. 반복문 최적화

// 최적화 전
for (int i = 0; i < list.size(); i++) {
    // 매번 list.size() 호출
}

// 최적화 후
int size = list.size();
for (int i = 0; i < size; i++) {
    // size 한 번만 계산
}

// 더 나은 방법
for (Object item : list) {
    // 향상된 for문 사용
}

3. 계산 결과 캐싱

public class CachedCalculator {
    private Map<String, Double> cache = new HashMap<>();
    
    public double calculate(String key) {
        if (cache.containsKey(key)) {
            return cache.get(key);
        }
        
        double result = heavyComputation(key);
        cache.put(key, result);
        return result;
    }
}

4. 적절한 자료구조 선택

  • 검색이 빈번할 때: HashMap, HashSet (O(1) 검색)
  • 정렬이 중요할 때: TreeMap, TreeSet
  • 순차 접근만 필요할 때: ArrayList
  • 큐/스택 동작이 필요할 때: LinkedList, ArrayDeque
// 검색이 빈번한 경우 HashMap 활용
Map<String, Customer> customerMap = new HashMap<>();
customerMap.put(customer.getId(), customer);
Customer found = customerMap.get(targetId); // O(1) 검색

5. 문자열 처리 최적화

// 최적화 전
String result = "";
for (String s : strings) {
    result += s; // 매번 새 String 객체 생성
}

// 최적화 후
StringBuilder builder = new StringBuilder();
for (String s : strings) {
    builder.append(s);
}
String result = builder.toString();

■ 메모리 사용량 최적화 해보기 

1. 적절한 데이터 타입 사용

대문자/ 소문자 한자의 차이가 크다!

// 최적화 전
Long[] array = new Long[1000000]; // 객체 타입, 더 많은 메모리 사용

// 최적화 후
long[] array = new long[1000000]; // 기본 타입, 메모리 효율적

2. 객체 재사용 (객체 풀링)

public class StringBuilderPool {
    private final ThreadLocal<StringBuilder> builderPool = 
        ThreadLocal.withInitial(StringBuilder::new);
    
    public String concatenate(List<String> strings) {
        StringBuilder builder = builderPool.get();
        builder.setLength(0); // 재사용 전 초기화
        
        for (String s : strings) {
            builder.append(s);
        }
        
        return builder.toString();
    }
}

3. 메모리 누수 방지

// 메모리 누수 위험 있는 코드
public class EventManager {
    private List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
    // 리스너 제거 메서드 없음 - 누수 발생
}

// 개선된 코드
public class EventManager {
    private List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
    
    public void removeListener(EventListener listener) {
        listeners.remove(listener);
    }
}

4. 대용량 데이터 처리

// 청크 단위 처리
public void processLargeData(List<Data> allData) {
    int chunkSize = 1000;
    
    for (int i = 0; i < allData.size(); i += chunkSize) {
        List<Data> chunk = allData.subList(
            i, Math.min(i + chunkSize, allData.size())
        );
        
        processChunk(chunk);
    }
}

 

■ 실전 최적화 기술

아직 잘 모르겠지만 일단 메모! 

1. JVM 튜닝

# 힙 크기 설정
java -Xms1g -Xmx2g MyApplication

# G1 가비지 컬렉터 사용
java -XX:+UseG1GC MyApplication

# 메모리 사용 모니터링
jstat -gc <pid> 1000

2. 병렬 처리 활용

// 병렬 스트림 활용
List<Result> results = dataList.parallelStream()
                             .map(this::processItem)
                             .collect(Collectors.toList());

// CompletableFuture 활용
CompletableFuture<Result> future1 = CompletableFuture.supplyAsync(() -> processTask1());
CompletableFuture<Result> future2 = CompletableFuture.supplyAsync(() -> processTask2());
CompletableFuture.allOf(future1, future2).join();

3. 리소스 관리

// try-with-resources 사용
try (InputStream is = new FileInputStream(file);
     OutputStream os = new FileOutputStream(output)) {
    // 리소스 자동 정리
}

// 명시적 리소스 해제
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
// 사용 후
((DirectBuffer) buffer).cleaner().clean();

4. 효율적인 I/O 처리

// 버퍼링 I/O
try (BufferedReader reader = new BufferedReader(new FileReader(file), 8192)) {
    // 8KB 버퍼로 읽기 - 성능 향상
}

// NIO 활용
FileChannel channel = FileChannel.open(Paths.get("large.dat"), 
                                      StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
while (channel.read(buffer) != -1) {
    buffer.flip();
    // 처리
    buffer.clear();
}

■ 최적화 체크리스트

  1. 측정 먼저: 가정이 아닌 프로파일링을 통해 실제 병목 지점 확인
  2. 변경 후 재측정: 최적화 효과 검증
  3. 복잡성 대비 이득: 코드 복잡성 증가 대비 성능 향상 가치 평가
  4. 가독성 유지: 지나친 최적화로 유지보수성 희생 금지
  5. 핵심 로직 집중: 자주 실행되는 코드, 고비용 연산 집중 최적화

 

 

728x90
반응형