728x90
반응형
728x90
반응형
반응형

 

1. String Literal

  • 자바에서 "hello", "abc", "쿠폰"처럼 쌍따옴표로 직접 입력한 문자열
  • JVM의 String Constant Pool(문자열 상수 풀)에 저장됨
  • 같은 literal을 사용하면 동일한 인스턴스 재사용
  • 메모리 절약 + 빠름
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true

 

2. String 생성자 사용 (new)

 

  • 항상 heap 메모리새 인스턴스를 생성
  • 상수 풀에 있는 "hello"는 그대로 존재하지만, 생성자는 그것을 복사한 새로운 객체를 만듦
String s1 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s3); // false (주소 다름)

 


 

  • 특별한 이유가 없다면 항상 "hello" 같은 literal 방식 사용
  • new String("...")은 피하는 것이 좋음. 필요할 땐 .intern()을 써서 상수 풀로 옮길 수 있음

 

728x90
반응형
반응형

Call by Value vs Call by Reference

  • Call by Value: 인자로 전달된 변수의 만 복사하여 사용. 원본 변수에는 영향을 주지 않는다.
  • Call by Reference: 인자로 전달된 변수의 메모리 주소(참조값, reference)를 전달. 함수 내에서 값이 변경되면 원본 변수에도 영향을 미친다.

자바는 Call by Value

자바의 메서드 호출 방식은 항상 Call by Value다. 즉, 메서드가 인자를 받을 때 원본 변수의 값을 복사하여 전달한다.

기본 타입 (Primitive Type)

기본 타입(예: int, double, char 등)은 Call by Value로 동작하여 원본 변수에 영향을 주지 않는다. 

참조 타입 (Reference Type)

객체(Object)와 같은 참조 타입의 경우, 객체의 참조값(메모리 주소)이 값으로 전달된다. 따라서 참조하는 객체의 속성을 변경하면 원본 객체에도 영향을 준다. 하지만 참조 자체를 변경해도(새로운 객체를 할당하면) 원본에는 영향을 주지 않는다.

class Person {
    String name;
}

public class Example {
    public static void changePerson(Person p) {
        p = new Person(); // 새로운 객체를 할당
        p.name = "Charlie";
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.name = "Bob";

        changePerson(person);
        System.out.println(person.name); // 여전히 Bob (새로운 객체는 원본에 영향을 주지 않음)
    }
}

 

changePerson 함수 안에서 p가 새로운 객체를 가리키도록 변경되었지만, 이것은 메서드 내부의 p 변수가 가리키는 참조가 바뀐 것일 뿐, 원래 person 변수에는 영향을 주지 않는다. 만약 메서드 내부에서 새로운 객체를 만들어 원본에도 반영하고 싶다면, 리턴 값을 활용하여 원본 변수를 직접 변경해야 한다.

public static Person changePerson(Person p) {
    return new Person("New Person");
}

public static void main(String[] args) {
    Person person = new Person("Original");
    person = changePerson(person); // 리턴 값을 원본 변수에 할당
    System.out.println(person.name); // "New Person"
}

객체의 참조값(메모리 주소)을 복사하여 전달하기 때문에, 객체 내부 값은 변경할 수 있지만, 객체 자체를 변경할 수는 없다.

 

즉, Java는 "Call by Value of Reference"


유투브 쉬운코드

728x90
반응형
반응형

환경: springframework 4.2, java8

 

자바와 레디스를 연결하기 위한 레디스 클라이언트는 크게 3개가 있다.

1. Jedis

특징

  • 초기 Redis Java 클라이언트 중 하나로, 간단하고 직관적인 API 제공.
  • Redis 명령어 대부분을 지원하며, 설정이 간단함.
  • 싱글스레드 기반으로 설계됨.
  • Jedis는 Redis 클라이언트를 위한 핵심적인 기능만 포함하며, 추가적인 추상화나 고급 기능(분산 락, 세마포어 등)은 제공하지 않음
  • 다른 클라이언트(예: Redisson)에 비해 의존성이 적고, 실행 크기가 작음, 가벼움

장점

  • 사용법이 단순하고 직관적이어서 빠르게 배울 수 있음.
  • Redis와의 네이티브 한 연동 및 모든 명령어를 제공.
  • 간단한 애플리케이션에서 적합.

단점

  • Thread-Safe 하지 않음: 멀티스레드 환경에서 사용하려면 JedisPool을 사용해야 함.
  • 싱글스레드 기반이라 멀티스레드 환경에서는 성능이 저하될 수 있음.

추천 사용 시나리오

  • 단일 스레드 기반 애플리케이션.
  • Redis를 단순 캐싱 또는 데이터 저장소로 사용하는 경우.

2. Lettuce

특징

  • 비동기 및 동기 API를 모두 제공하며, Reactive Streams(Flux/Mono)도 지원.
  • 기본적으로 비동기적으로 실행되고, 결과는 CompletionStage 또는 Future를 통해 반환됨
  • Netty를 기반으로 한 non blocking I/O 모델 사용.
  • Thread-Safe: 단일 커넥션을 여러 스레드에서 공유 가능.

장점

  • 멀티스레드 환경에서 효율적이며 Thread-Safe.
  • 비동기 작업에 유리하며 고성능 제공.
  • Reactive 프로그래밍 환경과의 호환성이 뛰어남.
  • 클러스터와 Sentinel 환경 지원.

단점

  • API가 Jedis보다 다소 복잡할 수 있음.
  • 초심자에게는 학습 곡선이 약간 높음.

추천 사용 시나리오

  • 비동기 작업이 많은 고성능 애플리케이션.
  • 멀티스레드 환경.
  • 클러스터 또는 Sentinel 기반 Redis 설정.
  • Reactive 프로그래밍(Spring WebFlux 등)을 사용하는 경우.

3. Redisson

특징

  • Redis를 기반으로 한 고급 분산 기능 제공(분산 락, 분산 캐시 등).
  • Redis를 Java의 분산 데이터 구조와 유사하게 다룰 수 있는 API 제공.
  • Thread-Safe 하며, 다양한 고급 기능이 포함됨.

장점

  • 분산 락, 분산 세마포어, RMap, RList 등 고급 데이터 구조 지원.
  • 클러스터, Sentinel, 레플리카 환경에서 유연하게 동작.
  • Redis 클라이언트를 단순한 캐싱 도구 이상으로 활용 가능.

단점

  • 다른 클라이언트보다 무겁고 약간의 추가 오버헤드 발생.
  • Jedis나 Lettuce에 비해 더 많은 메모리 사용 가능.

추천 사용 시나리오

  • 분산 락, 세마포어, 큐와 같은 고급 분산 기능이 필요한 환경.
  • Redis를 데이터베이스 이상의 목적으로 사용하는 경우.
  • 복잡한 클러스터 환경.

 

jedis 사용

<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.8.4.RELEASE</version>
</dependency>

<!-- Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
 public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    log.debug("redisKey: {}, ttl: {}", redisKey, lockSecond);
    try {
      acquireLock(redisKey, lockSecond);
      return logic.get();
    } finally {
      releaseLock(redisKey);
    }
  }

  private void acquireLock(String redisKey, long lockSecond) {
    final RedisCallback<Boolean> redisCallback = connection -> {
      byte[] key = redisTemplate.getStringSerializer().serialize(redisKey);
      byte[] value = redisTemplate.getStringSerializer().serialize("locked");
      return connection.setNX(key, value) && connection.expire(key, lockSecond);
    };

    boolean success = redisTemplate.execute(redisCallback);
    if (!success) {
      throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
    }
  }

  private void releaseLock(String redisKey) {
    redisTemplate.delete(redisKey);
  }

위처럼 분산락을 jedis를 사용하여 개발하면 락을 얻고 ttl을 주고 해제하는 과정을 수동으로 개발해야 한다.

redisson으로 수정

redisson을 사용하면 아래와 같이 라이브러리에서 제공하는 기능을 사용하여 간소화하게 짤 수 있다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    RLock lock = redissonClient.getLock(redisKey); // Redis 분산 락 생성
    log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

    try {
        // 락을 시도하고 성공할 경우 논리 실행
        if (lock.tryLock(0, lockSecond, TimeUnit.SECONDS)) { 
            return logic.get();
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new ApiLockException("락을 획득하는 동안 인터럽트가 발생했습니다. key: " + redisKey, e);
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock(); // 락 해제
            log.debug("Lock released for redisKey: {}", redisKey);
        }
    }
}

lettuce...?

호옥시 lettuce로 바꾸면 어떻게 될까 궁금해서 chat gpt에게 물어봤다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisCommands<String, String> commands = connection.sync();
        log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

        if (acquireLock(commands, redisKey, lockSecond)) {
            try {
                return logic.get();
            } finally {
                releaseLock(commands, redisKey);
            }
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    }
}

private boolean acquireLock(RedisCommands<String, String> commands, String redisKey, long lockSecond) {
    String result = commands.set(redisKey, "locked", io.lettuce.core.SetArgs.Builder.nx().ex(lockSecond));
    return "OK".equals(result); // Redis SET NX PX 결과 확인
}

private void releaseLock(RedisCommands<String, String> commands, String redisKey) {
    commands.del(redisKey); // 락 해제
    log.debug("Lock released for redisKey: {}", redisKey);
}

lettuce는 동기 동작을 위해 .sync()를 꼭 넣어야 하고(비동기는 .async()) 나머지는 jedis와 비슷하게 직접(setnx) 락을 설정하고 ttl도 설정해야 한다.

그리고 lettuce의 StatefulRedisConnection를 사용하는 경우 close 해줘야 한다. AutoCloseable를 implement 했기 때문에 try-with-resources 사용 가능하다.

 

  • 연결 풀을 사용할 경우에는, 연결이 자동으로 풀로 반환되며 그 자체로 명시적인 종료가 필요하지 않지만, StatefulRedisConnection은 연결 풀을 사용하지 않고, 각 연결을 명시적으로 관리하는 방식.
  • 연결 풀을 사용하면 연결을 가져오고 반환하는 방식으로 관리되기 때문에, 풀에서 자동으로 연결을 회수하고 재사용할 수. 하지만 StatefulRedisConnection은 풀을 사용하지 않기 때문에 사용자가 직접 연결을 닫고 관리해야 합니다.

 

shutdown을 수동으로 해야하나..?

참고로 레디스 연결 객체인 RedisClient와 RedisConnectionFactory는 프로그램 종료 시 shutdown이 되어야 하는데, 빈으로 등록되어 있는 경우 Spring Boot에서는 자동으로 연결 종료가 처리된다. 이렇게 되면 명시적인 shutdown() 호출은 필요하지 않는다. Spring Boot는 빈 관리 및 라이프사이클을 자동으로 처리하기 때문에, 애플리케이션 종료 시 리소스를 자동으로 정리한다.

하지만 Spring Framework에서는 명시적인 shutdown() 호출이 필요할 수 있다. 왜냐하면 Spring Framework에서는 자동으로 리소스를 관리하는 기능이 내장되어 있지 않기 때문...?

  • hmmmmmm?? 진짠지 모르겠음.. 레거시에서도 수동으로 shutdown 한건 본적이 없음

 


분산락을 위해 보통 @AOP로 만들 수도 있지만 위와 같은 고차함수 방식을 더 선호한다.

아래와 같은 단점이 있기에..

  • public에만 사용가능
  • 값 넘길 때 함수 argument 첫번째 값 주의 필요
  • @Around( "@within(com.annotation.. <-- 와 같이 문자열로 관리하는 것에 대한 부담
  • 락의 범위가 넓어질 수 있음

 

고차함수 분산락을 사용할 때는 아래와 같이 하면 된다.

final String redisKey = redisKey("invenChangeExpire", String.valueOf(sno));

return this.apiLockService.lockProcess(redisKey, () -> {
	... 로직
    });
728x90
반응형
반응형

환경: java17

 

자바11부터 로컬 변수에 대한 타입 추론이 가능해졌다. 즉 아래와 같이 구체적인 타입을 선언하지 않고 var로 선언이 가능해졌다.

var items = (List<SinyutnoriRanking>) chunk.getItems();

그리고 자바 17을 사용중이다. 그런데 아래와 같은 경고창을 만난다.

Unchecked cast: 'java.util.List<capture<? extends xx.Ranking>>' to 'java.util.List<xx.Ranking>'

위 경고는 컴파일러가 타입 추론을 못해서 발생하는데, 자바 17이면 당연히 자바 11의 내용을 알고 있기에 컴파일러 단에서 타입추론이 되는거 아니야? 하는 생각이 들었다.

제네릭과 타입 추론의 한계

Java 11에서 var를 도입하면서 로컬 변수의 타입 추론이 가능해졌지만, 제네릭 타입의 안전성을 보장하기 위해 여전히 unchecked 경고가 발생할 수 있다.

  • 제네릭 타입을 사용할 때, Java 컴파일러는 타입 소거(Type Erasure)를 사용하여 런타임에 타입 정보를 제거한다. 이로 인해, 컴파일러가 타입 안전성을 완전히 보장할 수 없는 경우 경고를 발생시킨다.
  • 이런 경우 var를 사용하더라도 타입 캐스팅이 명시적이든 암시적이든 타입 안전성을 보장할 수 없으므로 @SuppressWarnings("unchecked")가 필요하다.
List<String> list = (List<String>) new ArrayList(); // Unchecked cast warning

var list = (List<String>) new ArrayList(); // 경고 발생

 

@SuppressWarnings("unchecked")

타입 안정성을 보장할 수 없는 상황에서 컴파일러 경고를 무시하도록 명시적으로 @SuppressWarnings("unchecked")를 추가한다. 이는 컴파일러에게 "나는 이 경고를 알고 있으며, 이 코드가 안전하다는 것을 보장할 수 있다"라는 신호를 주는 것이다.

 

참고로 최신 버전의 IntelliJ IDEA나 다른 IDE를 사용해도, 컴파일러 경고 자체는 여전히 발생할 수 있다는 점..!

728x90
반응형
반응형

P6Spy: Java 기반 애플리케이션에서 SQL 쿼리의 로깅 및 모니터링을 위한 프레임워크

P6Spy 주요 기능:

  • SQL 로깅: 애플리케이션에서 실행되는 모든 SQL 쿼리, 매개변수, 실행 시간을 기록합니다.
  • 성능 모니터링: 각 쿼리의 실행 시간을 보여주어 느린 쿼리를 식별하는 데 도움을 줍니다.
  • 동적 필터링: SELECT, UPDATE 등의 특정 쿼리 유형을 필터링하여 로그에서 제외할 수 있습니다.
  • 간편한 사용: 표준 JDBC 드라이버 대신 P6Spy 드라이버를 사용하여 SQL 쿼리를 감시하고 로깅합니다.

동작 방식:

  1. 스파이 드라이버: P6SpyDriver가 실제 JDBC 드라이버를 감싸고 모든 SQL 쿼리를 가로챕니다.
  2. 로그 기록: 쿼리를 콘솔 또는 파일로 기록하며, 로그 포맷은 커스터마이징 가능합니다.
  3. 분석: 로그를 통해 느린 쿼리를 찾아 성능을 최적화하거나, SQL 관련 문제를 디버깅할 수 있습니다.

 

implementation("p6spy:p6spy:3.9.1")
spring.datasource.url=jdbc:p6spy:mysql://10.162.5.x:3306/your_database_name
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver

with springboot 3.2.5

################################################################################################
# P6Spy Options File                                                                           #
# See documentation for detailed instructions                                                  #
# https://p6spy.readthedocs.io/en/latest/configandusage.html#configuration-and-usage           #
################################################################################################
appender=com.p6spy.engine.spy.appender.Slf4JLogger
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=| %(executionTime) ms | %(sql)
databaseDialectDateFormat=yyyy-MM-dd'T'HH:mm:ss
databaseDialectTimestampFormat=yyyy-MM-dd'T'HH:mm:ss

resources/spy.properties 에 위와 같은 내용 작성

import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.P6SpyOptions;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import jakarta.annotation.PostConstruct;
import java.util.Locale;
import org.hibernate.engine.jdbc.internal.FormatStyle;
import org.springframework.context.annotation.Configuration;

@Configuration
public class P6SpyConfig implements MessageFormattingStrategy {

  @PostConstruct
  public void setLogMessageFormat() {
    P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName());
  }

  @Override
  public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
    sql = formatSql(category, sql);
    return String.format("[%s] | %d ms | %s", category, elapsed, formatSql(category, sql));
  }

  private String formatSql(String category, String sql) {
    if (sql != null && !sql.trim().isEmpty() && Category.STATEMENT.getName().equals(category)) {
      String trimmedSQL = sql.trim().toLowerCase(Locale.ROOT);
      if (trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment")) {
        sql = FormatStyle.DDL.getFormatter().format(sql);
      } else {
        sql = FormatStyle.BASIC.getFormatter().format(sql);
      }
      return sql;
    }
    return sql;
  }

}

자바로도 설정 가능

 

1. P6SpyConfig 클래스

  • 동적 설정: P6Spy의 동작을 코드 기반으로 설정하고, Spring의 컨텍스트에서 동작하는 방식입니다.
  • 장점:
    • 코드에서 직접 포맷을 제어할 수 있어 더 유연한 설정이 가능합니다.
    • Spring Boot의 DI(의존성 주입) 및 기타 설정들과 쉽게 연동됩니다.
    • 복잡한 포맷이나 특정 로깅 로직을 구현하기 더 편리합니다.
  • 단점: P6Spy의 설정을 코드에서 관리해야 하므로 설정을 변경할 때 코드 수정이 필요합니다.

2. spy.properties 파일

  • 정적 설정: P6Spy의 설정을 별도의 설정 파일로 관리하는 방식입니다.
  • 장점:
    • 코드 수정 없이 spy.properties 파일에서 간단하게 설정을 변경할 수 있습니다.
    • 개발 환경이나 운영 환경에서 설정을 쉽게 바꿀 수 있습니다.
  • 단점:
    • 포맷이 비교적 제한적입니다. 커스텀 포맷이 필요한 경우 코드보다 덜 유연합니다.
    • 복잡한 로직을 설정 파일에 구현하기 어렵습니다.

둘 다 설정해야 할까?

  • 필요에 따라 둘 다 사용할 수 있지만, 일반적으로는 둘 중 하나만 사용합니다.
  • P6SpyConfig 클래스 사용: 코드 기반 설정을 선호하거나, Spring 환경에서 동적으로 설정을 관리하고 싶다면 이 클래스를 설정합니다.
  • spy.properties 파일 사용: 간단한 설정 변경을 원하거나, 설정을 코드와 분리하여 운영 환경에서 쉽게 관리하고 싶다면 spy.properties 파일만 설정합니다.
728x90
반응형

'개발 > sql' 카테고리의 다른 글

2 Phase Lock & mysql -> MVCC  (3) 2024.11.06
[분산] mysql 네임드락  (1) 2024.11.01
[mysql] delete, drop, truncate  (0) 2024.10.02
[mysql] basic functions  (0) 2024.09.09
[mysql] collation이란  (0) 2024.06.10
반응형

멀티 스레드 환경을 지원하기 위해 사용

1. Runnable 인터페이스

특징

  • Runnable 인터페이스는 Java 1.0부터 존재하는 기본적인 인터페이스로, 단일 메서드 run()을 제공합니다.
  • 반환값이 없으며, 예외를 던질 수 없습니다.
@FunctionalInterface
public interface Runnable {
    void run();
}
public class RunnableExample {
    public static void main(String[] args) {
        // Runnable 구현체 생성
        Runnable runnableTask = () -> {
            System.out.println("Runnable Task is running...");
        };

        // 스레드에 Runnable 전달하여 실행
        Thread thread = new Thread(runnableTask);
        thread.start();
    }
}

장점

  • 간단한 구조로, 스레드에서 수행할 작업을 정의하기 쉽습니다.
  • 예외를 명시적으로 처리할 필요 없이 간단하게 작업을 정의할 수 있습니다.

단점

  • 반환값을 제공하지 않으므로, 작업 수행 결과를 받을 수 없습니다.
  • run() 메서드는 체크된 예외(Checked Exception)를 던질 수 없으므로, 예외 처리가 필요한 경우 내부적으로 처리해야 합니다.

2. Callable 인터페이스

특징

  • Callable 인터페이스는 Java 5에서 java.util.concurrent 패키지와 함께 도입된 인터페이스로, 단일 메서드 call()을 제공합니다.
  • 반환값을 가지며, 예외를 던질 수 있습니다.
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        // Callable 구현체 생성
        Callable<String> callableTask = () -> {
            return "Callable Task Completed";
        };

        // ExecutorService를 사용하여 Callable 실행
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(callableTask);

        try {
            // 작업 완료 후 결과 가져오기
            String result = future.get();
            System.out.println("Callable Result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
        }
    }
}

 

장점

  • call() 메서드는 작업 수행 후 결과를 반환할 수 있습니다.
  • 체크된 예외를 던질 수 있어, 예외 처리가 더 유연합니다.
  • Future와 같은 API를 통해 비동기 작업의 결과를 받을 수 있습니다.

단점

  • Runnable에 비해 구조가 약간 복잡하며, ExecutorService와 Future 같은 추가적인 클래스와 함께 사용해야 하는 경우가 많습니다.

언제 Runnable과 Callable을 사용해야 하는가?

  • Runnable을 사용할 때:
    • 작업 수행의 결과가 필요 없고, 단순히 백그라운드 작업이나 이벤트 처리를 할 때 적합합니다.
    • 예외를 명시적으로 처리할 필요가 없을 때 사용합니다.
    • 예: 이벤트 핸들링, 타이머 작업, 단순 스레드 실행.
  • Callable을 사용할 때:
    • 작업 수행의 결과를 반환해야 할 때 적합합니다.
    • 작업 중 예외 처리가 필요하고, 호출한 쪽에서 이를 확인할 필요가 있을 때 사용합니다.
    • 예: 데이터베이스 쿼리, 복잡한 계산 작업, 외부 시스템 호출.
  • Runnable과 Callable 모두 ExecutorService를 통해 실행할 수 있습니다. Runnable을 사용하면 결과가 없는 Future<?>를 반환하고, Callable을 사용하면 작업 결과를 담은 Future<V>를 반환합니다.
ExecutorService executor = Executors.newFixedThreadPool(2);

// Runnable 예제
Runnable runnableTask = () -> System.out.println("Runnable Task Running");
Future<?> runnableFuture = executor.submit(runnableTask); // 결과가 없음

// Callable 예제
Callable<Integer> callableTask = () -> {
    return 123;
};
Future<Integer> callableFuture = executor.submit(callableTask); // 결과가 있음

executor.shutdown();

 

executor service.submit는 멀티스레드 시작!!

ExecutorService 종료 관련

ExecutorService는 작업을 스레드 풀에서 관리하고 실행하는 인터페이스로, 사용이 끝나면 반드시 종료(shutdown)해줘야 합니다. 그렇지 않으면 애플리케이션이 종료되지 않고 백그라운드에서 스레드가 계속 실행될 수 있습니다.

ExecutorService의 종료 필요성

ExecutorService는 기본적으로 백그라운드 스레드 풀을 관리합니다. 따라서 다음과 같은 이유로 사용이 끝난 후 반드시 종료해야 합니다:

  1. 리소스 해제:
    • 스레드 풀에 의해 사용되는 스레드와 기타 리소스를 해제하여 메모리 누수를 방지합니다.
  2. 정상적인 애플리케이션 종료:
    • 스레드 풀이 종료되지 않으면 JVM이 종료되지 않고 계속 대기 상태에 있을 수 있습니다.
  3. 명시적 종료 호출:
    • executor.shutdown()을 호출하여 스레드 풀을 정상적으로 종료합니다. 이 메서드는 더 이상 새로운 작업을 수락하지 않고, 기존에 제출된 작업이 완료될 때까지 기다립니다.
    • executor.shutdownNow()를 호출하면 모든 작업을 중지하고, 실행 중인 작업을 즉시 종료하려고 시도합니다.
    • ExecutorService는 AutoCloseable을 구현하지 않기 때문에 try-with-resources 구문을 직접 사용할 수 없습니다.
public class ExecutorServiceExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        try {
            executor.submit(() -> System.out.println("Task 1"));
            executor.submit(() -> System.out.println("Task 2"));
        } finally {
            // ExecutorService 종료
            executor.shutdown();
            try {
                if (!executor.awaitTermination(60, java.util.concurrent.TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
}

위 코드에서는 shutdown() 메서드를 호출하여 새로운 작업을 수락하지 않도록 하고, awaitTermination()을 사용하여 스레드 풀이 완전히 종료될 때까지 대기합니다. awaitTermination()은 주어진 시간 동안 스레드 풀이 종료될 때까지 기다리며, 그 시간이 지나도 종료되지 않으면 shutdownNow()를 호출하여 강제로 종료를 시도합니다.

728x90
반응형
반응형

기본 Xms Xmx

 java -jar -Dspring.profiles.active=real xxx.jar

로 실행하고 있는 프로세스가 있다. 띄울 때 최소/최대 힙 사이즈를 안 줘서 기본값으로 어떻게 들고 있는지 궁금했다.

java -XX:+PrintFlagsFinal -version | grep -E "InitialHeapSize|MaxHeapSize"
   size_t InitialHeapSize                          = 62914560      
   size_t MaxHeapSize                              = 994050048

위 명령어를 사용하면 현재 JVM의 기본 힙 설정을 알 수 있다.

각각은 바이트 단위이다. 따라서 좀 더 이해하기 쉽게 바꿔보면

  1. 바이트(Byte) -> 킬로바이트(Kilobyte):
    • 1KB = 1024B
    • 62914560 ÷ 1024 = 61440 KB
  2. 킬로바이트(Kilobyte) -> 메가바이트(Megabyte):
    • 1MB = 1024KB
    • 61440 ÷ 1024 = 60 MB

따라서, initialHeapSize가 62914560이라는 값은 60MB를 잡고 있다는 뜻이다.

또한 최대 값을 계산해보면

  1. 바이트(Byte) -> 킬로바이트(Kilobyte):
    • 1KB = 1024B
    • 994050048 ÷ 1024 = 970752 KB
  2. 킬로바이트(Kilobyte) -> 메가바이트(Megabyte):
    • 1MB = 1024KB
    • 970752 ÷ 1024 = 948 MB

따라서, maxHeapSize가 994050048 바이트라는 값은 약 948MB를 최대 값으로 설정했다는 것이다.

 

지금 메모리 현황 보기

free -h

              total        used        free      shared  buff/cache   available
Mem:           3.7G        933M        976M        137M        1.8G        2.3G
Swap:          2.0G         13M        2.0G

 

gc 관련 모니터링(jstat) 권한없어도 가능

jstat -gc <PID> <interval> <count>
jstat -gc 12345 1000 10  # 12345 PID의 JVM에 대해 1초 간격으로 10번 GC 정보를 출력

//
S0C    S1C    S0U    S1U      EC       EU        OC        OU      MC       MU     CCSC     CCSU       YGC    FGC
1024.0 1024.0   0.0   0.0   8192.0   1024.0   20480.0    8192.0    512.0    488.0   64.0     62.0       3      1
  • S0C/S1C: Survivor space 0/1의 용량.
  • EC: Eden 영역의 용량.
  • OC: Old 영역의 용량.
  • YGC/FGC: Young/Full GC의 발생 횟수.

 

OOM이 터질 때 자동으로 덤프를 뜨게 하는 옵션을 주려면 아래와 같은 옵션을 자바에 추가한다.

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/경로/heapdump.hprof

 

이미 뭔가 데드락이나 무한루프에 빠진 것 같다면 확인하기

각 스레드의 상태현재 실행 중인 코드를 볼 수 있음(권한 필요)

jstack <PID>
"main" #1 prio=5 os_prio=0 tid=0x00000000023f6000 nid=0x2c runnable [0x0000000002a1e000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.Thread.sleep(Native Method)
        at Example.main(Example.java:5)

 

현재 돌고 있는 프로세스의 덤프 뜨는 법

jmap이나 jcmd 명령어 사용(권한 필요)

sudo jmap -dump:format=b,file=/경로/heapdump.hprof <PID>
-- or 
jcmd <PID> GC.heap_dump <경로>


// /proc/3272/root폴더에 권한이 없을 경우
Exception in thread "main" com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file /proc/3272/root/tmp/.java_pid3272: target process 3272 doesn't respond within 10500ms or HotSpot VM not loaded
        at jdk.attach/sun.tools.attach.VirtualMachineImpl.<init>(VirtualMachineImpl.java:100)
        at jdk.attach/sun.tools.attach.AttachProviderImpl.attachVirtualMachine(AttachProviderImpl.java:58)
        at jdk.attach/com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:207)
        at jdk.jcmd/sun.tools.jmap.JMap.executeCommandForPid(JMap.java:128)
        at jdk.jcmd/sun.tools.jmap.JMap.dump(JMap.java:208)
        at jdk.jcmd/sun.tools.jmap.JMap.main(JMap.java:114)

 

MAT(Eclipse Memory Analyzer Tool)

hprof파일을 얻었으면 분석 프로그램인 eclipse MAT 프로그램이 필요하다.

다운로드하고 hprof파일을 열어준다. hprof 파일의 용량이 클 수록 오래걸린다.

 

분석 보고서 이해하기

  • Overview (개요): 메모리 상태, 누수 가능성, 가장 큰 객체 등을 요약하여 보여줌
  • Dominator Tree: 힙 메모리의 최상위 점유자를 트리 구조로 보여주며 메모리 점유 비율이 큰 객체를 쉽게 파악 가능
  • Histogram: 클래스별로 객체 수와 메모리 점유량
  • Top Consumers: 메모리 사용량이 큰 객체 그룹

힙 분석 예시

  1. 메모리 누수 확인:
    • Leak Suspects Report를 사용하면 누수 가능성이 있는 객체를 분석하여 보여줌
    • "Path to GC Root" 기능을 사용해 메모리에서 해제되지 않은 객체의 참조 경로를 추적할 수 있음
  2. Dominator Tree 분석:
    • Dominator Tree를 통해 메모리를 가장 많이 차지하는 객체 파악
    • with outgoing references를 사용하여 참조 중인 객체들을 확인
  3. Histogram 분석:
    • 클래스별로 객체 수와 메모리 점유율을 확인하여 특정 클래스가 메모리를 많이 사용하는지 파악
    • 특정 클래스에서 메모리를 많이 사용하는 객체가 있다면, 이를 "List Objects -> with incoming references"로 추적 가능

 

728x90
반응형
반응형

try with resources

  • from java7; enhanced in java9
  • try 블락 안에 열린 리소스를 예외 여부와 상관없이 자동으로 닫아주는 기법
    • stream, db, network.. 등

 

예시

try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    // Exception handling
}

: BufferedReader, FileReader 와 같은 리소스의 close함수를 호출하지 않아도 자동으로 닫아줌

 

어떤 클래스들이 자동으로 닫히나?

AutoClosable interface 혹은 이를 확장한 AutoCloaseable interface를 구현한 리소스

public interface AutoCloseable {
    void close() throws Exception;
}
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

이를 구현한 예시로는

  • 스트림: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter
  • reader/writer: InputStreamReader, OutputStreamWriter
  • sql connection: java.sql.Connection, java.sql.ResultSet, java.sql.Statement
  • 소켓: java.net.Socket
  • 채널: java.nio.channels > FileChannel, SocketChannel, ServerSocketChannel
  • zip: java.util.zip.ZipFile

 

728x90
반응형
반응형

객체를 ordering 하는 방법에 대해 알아본다.

 

Comparable interface

  • 객체의 natural ordering 에 대해 정의할 수 있음
  • 클래스 안에서 클래스끼리 비교할 때 override해두면 Collections.sort, Arrays.sort 등에서 사용됨
  • 하나뿐인 아래 함수를 상속받고 구현하면 됨, 구현해야지만 사용할 수 있음(not functional interface)
public int compareTo(T o);
  • 해당 함수로 현재 객체(this)와 다른 객체(o)를 비교할 수 있으며 반환 값은 아래와 같음
    • this < o : -1    정방향
    • this == o : 0
    • this > o : 1     역방향
  • 참고로 String 객체는 해당 interface의 함수를 이미 구현하고 있어서 별도의 설정을 하지 않아도 알파벳 순 정렬을 할 수 있다.

String.java

import java.util.*;

@Getter
@ToString
@AllArgsConstructor
public class Person implements Comparable<Person> { ///
    private String name;
    private int age;

    @Override
    public int compareTo(Person other) { ///
        return Integer.compare(this.age, other.age); // Natural order by age
    }



    // Main method for testing
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 35)
        );

        // Sort using the natural order defined by Comparable
        // 안줘도 자동으로 있는거 사용
        Collections.sort(people);

        System.out.println("Sorted by age (natural order): " + people);
    }
}

 

Comparator Interface

  • Comparator is intended to be used for defining external comparison logic rather than internal natural ordering. However, you can create a Comparator for Person as a separate class, an inner class, or even as a static field in the Person class to provide custom sorting criteria.
  • 객체 자체의 natural ordering 이 없거나, 좀 더 복잡한 정렬 방법이 있을 경우 사용(flexibility)
  • 아래 함수를 상속받고 구현하면 됨(functional interface) ; 람다로 사용가능
int compare(T o1, T o2);
  • int를 반환하도록 되어있는데, 비교 값은 아래와 같다.
    • 앞 < 뒤 : -1    정방향
    • 앞 == 뒤 : 0
    • 앞 > 뒤 : 1     역방햐
  • String 객체에 역시 이미 정의 되어 있다.

String.java

기본함수

 

  • Comparator<T> reversed():
    • Returns a comparator that reverses the order of this comparator.
  • Comparator<T> thenComparing(Comparator<? super T> other):
    • Returns a comparator that first compares using this comparator, and if the comparison is equal, uses the provided comparator. 앞에 비교한 결과가 같으면 추가 사용!

 

import java.util.*;

@Getter
@ToString
@AllArgsConstructor
public class Person { //여기서 comparator implement못하고 별도의 클래스로 빼야 함
    private String name;
    private int age;


    // Main method for testing
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 35)
        );

        // Sort by name
        // 별도로 만들어서 사용 , 람다 사용가능
        Comparator<Person> nameComparator = Comparator.comparing(Person::getName);
        Collections.sort(people, nameComparator);
        System.out.println("Sorted by name: " + people);

        // Sort by age in reverse order
        Comparator<Person> ageComparator = Comparator.comparingInt(Person::getAge).reversed();
        Collections.sort(people, ageComparator);
        System.out.println("Sorted by age (reverse order): " + people);
    }
}
//or 아래처럼 함수로 빼고 new AgeComparator() 삽입
   // Comparator for sorting by age
    public static class AgeComparator implements Comparator<Person> {
        @Override
        public int compare(Person p1, Person p2) {
            return Integer.compare(p1.getAge(), p2.getAge());
        }
    }

 

어떤 것을 사용해야 하나

The Comparable interface is a good choice to use for defining the default ordering, or in other words, if it’s the main way of comparing objects.

기본적으로는 Comparable 을 사용하고 아래와 같은 경우 Comparator을 사용

  • 파일이 수정 불가하여 interface를 구현할 수 없는 경우 Comparator 사용
  • 비교 로직을 분리해서 관리해야하는 경우 Comparator 사용
  • 비교 전략이 다양해서 Comparable 로 부족할 경우 Comparator 사용

 


참고

https://www.baeldung.com/java-comparator-comparable

728x90
반응형
반응형
  1. Synchronization: Use the synchronized keyword to ensure that only one thread can access a critical section of code at a time. This helps prevent race conditions where multiple threads might try to modify shared data simultaneously.
  2. Locks: Java provides various lock implementations like ReentrantLock, ReadWriteLock, and StampedLock which offer more flexibility and functionality compared to intrinsic locks (synchronized). They llow finer control over locking mechanisms and can help avoid deadlock situations.
  3. Thread-safe data structures: Instead of using standard collections like ArrayList or HashMap, consider using their thread-safe counterparts from the java.util.concurrent package, such as ConcurrentHashMap, CopyOnWriteArrayList, and ConcurrentLinkedQueue. These data structures are designed to be safely accessed by multiple threads without external synchronization.
  4. Atomic variables: Java's java.util.concurrent.atomic package provides atomic variables like AtomicInteger, AtomicLong, etc., which ensure that read-modify-write operations are performed atomically without the need for explicit locking.
  5. Immutable objects: Immutable objects are inherently thread-safe because their state cannot be modified once created. If an object's state needs to be shared among multiple threads, consider making it immutable.

멀티 스레드 환경에서 안전하게 코딩하는 기본적인 다섯가지 방법에 대해 살펴본다. 자바 기준

1. synchronized 키워드 사용(직접적인 락은 아니지만 락과 같은 것;  monitor lock)

  • 함수 자체 그리고 함수 안에서도 블락으로 지정하여 사용 가능
  • allowing only one thread to execute at any given time.
  • The lock behind the synchronized methods and blocks is a reentrant. This means the current thread can acquire the same synchronized lock over and over again while holding it(https://www.baeldung.com/java-synchronized

2. 명시적으로 lock interface를 구현한 구현체를 사용 

lock()/tryLock() 함수로 락을 걸고 unlock()함수로 반드시 락을 해제해야한다.(아니면 데드락..)

Condition 클래스를 통해 락에 대한 상세한 조절이 가능

  • ReentrantLock implements Lock
    • synchronized와 같은 방법으로 동시성과 메모리를 핸들링하지만 더 섬세한 사용이 가능하다.
  • ReentrantReadWriteLock implements ReadWriteLock
    • Read Lock – 쓰기락이 없거나 쓰기락을 요청하는 스레드가 없다면, 멀티 스레드가 락 소유 가능
    • Write Lock – 쓰기/읽기 락 모두가 없는 경우 반드시 하나의 스레드만 락을 소유한다.
  • StampedLock(자바8 도입, 읽기/쓰기 락 모두 제공)
    • 락을 걸면 long 타입의 stamp를 반환함 해당 값으로 락을 해제하거나 확인 가능
    • optimistic locking임(수정사항이 별로 없다는 가정하에 읽기에 개방적)
    • https://www.baeldung.com/java-concurrent-locks

3. thread safe 한 자료구조 사용

  • java.util.concurrent 패키지 참고(from java 5)
    • ConcurrentHashMap, ConcurentLinkedQueue, ConcurrentLinkedDeque 등
  1. ConcurrentHashMap: This class is a thread-safe implementation of the Map interface. It allows multiple threads to read and modify the map concurrently without blocking each other. It achieves this by dividing the map into segments, each of which is independently locked.
  2. CopyOnWriteArrayList: This class is a thread-safe variant of ArrayList. It creates a new copy of the underlying array every time it is modified, ensuring that iterators won't throw ConcurrentModificationException. This makes it suitable for scenarios where reads are far more frequent than writes.
  3. ConcurrentLinkedQueue: This class is a thread-safe implementation of the Queue interface. It is designed for use in concurrent environments where multiple threads may concurrently add or remove elements from the queue. It uses non-blocking algorithms to ensure thread safety.
  4. BlockingQueue: This is an interface that represents a thread-safe queue with blocking operations. Implementations like LinkedBlockingQueue and ArrayBlockingQueue provide blocking methods like put() and take() which wait until the queue is non-empty or non-full before proceeding.
  5. ConcurrentSkipListMap and ConcurrentSkipListSet: These classes provide thread-safe implementations of sorted maps and sets, respectively. They are based on skip-list data structures and support concurrent access and updates.

4. thread safe 한 변수 사용

  • java.util.concurrent.atomic 패키지 참고(from java 5)
    • AtomicInteger, AtomocLong, AtomicBoolean 등
    • 스레드 끼리 동기화 필요한 변수에 사용(상태 공유 등); 관련 성능 향상 시
    • 락으로 인한 오버헤드나 컨테스트 스위칭 비용 감소
    • non blocking 상황에서(스레드 별 독립적인 작업 시)

5. immutable object 불변 객체 사용

  • String, Integer, Long, Double 등 과 같은 wrapper 클래스 사용
  • private final로 선언
  • 생성자 이용하여 initialize
  • setter 함수나 수정가능 함수 제공하지 않기

6. volatile 

  • 변수를 Main Memory에 저장하겠다고 명시하는 것
  • 각 스레드는 메인 메모리로 부터 값을 복사해 CPU 캐시에 저장하여 작업하는데 volatile은 CPU 캐시 사용 막고 메모리에 접근해서 실제 값을 읽어오도록 설정하여 데이터 불일치를 막음
  • 자원의 가시성: 메인 메모리에 저장된 실제 자원의 값을 볼 수 있는 것
  • 멀티쓰레드 환경에서 하나의 쓰레드만 read&write하고 나머지 쓰레드가 read하는 상황에 사용
    • Read : CPU cache에 저장된 값 X , 메인 메모리에서 읽음
    • Write : 메인 메모리에 작성
    • CPU Cache보다 메인 메모리가 비용이 더 큼(성능 주의)
  • 가장 최신 값 보장

what to choose

synchronized

  • 여러 쓰레드가 write하는 상황에 적합
  • 가시성 문제해결 : synchronized블락 진입 전/후에 메인 메모리와 CPU 캐시 메모리의 값을 동기화 하여 문제 없도록 처리

volatile

  • 하나의 쓰레드만 read&write하고 나머지 쓰레드가 read하는 상황에 적합
  • 가시성 문제해결 : CPU 캐시 사용 막음 → 메모리에 접근해서 실제 값을 읽어오게 함
    • Read : CPU cache에 저장된 값 X , 메인 메모리에서 읽음
    • Write : 메인 메모리에 작성

AtomicIntger

  • 여러 쓰레드가 read&write를 병행
  • 가시성 문제해결: CAS알고리즘
    • 현재 쓰레드에 저장된 값과 메인 메모리에 저장된 값을 비교하여
      • 일치할 경우 새로운 값으로 교체
      • 불일치할 경우 실패하고 재시도

로컬 변수도 스레드 경합?

함수 안에서도 싱글스레드가 자동으로 보장되지는 않습니다. 하지만 현재 메서드의 경우:

  1. 지역 변수: StringBuilder sb는 메서드 내부의 지역 변수로, 각 호출마다 새로 생성됩니다
  2. 스택 메모리: 지역 변수는 각 스레드의 스택에 저장되어 다른 스레드와 공유되지 않습니다
  3. 메서드 범위: 이 변수는 메서드 실행이 끝나면 소멸됩니다

따라서 여러 스레드가 동시에 serialize 메서드를 호출해도, 각각 독립적인 StringBuilder 인스턴스를 사용하므로 StringBuilder를 사용하는 것이 안전하고 성능상 유리합니다.

728x90
반응형

+ Recent posts