Java의 ThreadPoolExecutor를 활용하여 Singleton 패턴 기반의 커스텀 스레드 풀을 구현하고, 여러 개의 작업을 스레드 풀에서 실행하는 예제입니다. 

 

CustomThreadPool 클래스는 Java의 ThreadPoolExecutor를 활용하여 최소/최대 스레드 개수, 대기 큐 크기 및 스레드 유지 시간을 설정한 후, 단일 인스턴스로 관리하는 Singleton 패턴을 적용한 스레드 풀입니다.

 

● 실행 흐름 :

  1. 최대 2개의 스레드가 즉시 실행됩니다.
  2. 이후 최대 5개의 작업이 대기 큐에 쌓입니다.
  3. 대기 큐가 가득 차면 새로운 스레드를 생성하여 실행합니다. (최대 10개)
  4. 10개의 작업이 순차적으로 실행된 후 스레드 풀이 종료됩니다.
import java.util.concurrent.*;

public enum CustomThreadPool {
    INSTANCE;

    private static final int CORE_POOL_SIZE = 2;   // 최소 스레드 개수
    private static final int MAX_POOL_SIZE = 10;   // 최대 스레드 개수
    private static final int QUEUE_CAPACITY = 5;   // 대기 큐 크기
    private static final long KEEP_ALIVE_TIME = 60L; // 유휴 스레드 유지 시간 (초)

    private final ThreadPoolExecutor executor;

    // enum에서 생성자를 사용하여 인스턴스를 초기화
    CustomThreadPool() {
        this.executor = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAX_POOL_SIZE,
            KEEP_ALIVE_TIME, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(QUEUE_CAPACITY),
            new ThreadPoolExecutor.AbortPolicy() // 큐가 가득 차면 예외 발생
        );
    }

    // Singleton 인스턴스를 가져오는 방법: enum을 통해 인스턴스를 직접 접근
    public static CustomThreadPool getInstance() {
        return INSTANCE;
    }

    // 작업 실행 메서드
    public void executeTask(Runnable task) {
        executor.execute(task);
        System.out.println("Active Threads: " + executor.getActiveCount() +
                           ", Queue Size: " + executor.getQueue().size());
    }

    // 안전한 종료
    public void shutdown() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

public class MainApp {
    public static void main(String[] args) {
        CustomThreadPool pool = CustomThreadPool.getInstance();

        for (int i = 1; i <= 10; i++) {
            final int taskNumber = i;
            pool.executeTask(() -> {
                System.out.println(Thread.currentThread().getName() + " - Task " + taskNumber);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        pool.shutdown(); // 모든 작업이 끝나면 스레드 풀 종료
    }
}

 

● 출력결과 : 

pool-1-thread-1 - Task 1
pool-1-thread-2 - Task 2
Active Threads: 2, Queue Size: 5
Active Threads: 2, Queue Size: 5
...
pool-1-thread-3 - Task 3
pool-1-thread-4 - Task 4
...
pool-1-thread-10 - Task 10

 

 

● 핵심정의 :  

  1. Singleton 패턴 적용
  2. ThreadPoolExecutor 활용 (최소/최대 스레드 개수, 대기 큐 설정)
  3. 안전한 종료 메커니즘 제공
  4. 다중 작업을 효율적으로 처리 가능

이러한 커스텀 스레드 풀은 대량의 작업을 병렬 처리하는 서버 애플리케이션, 백그라운드 작업, 데이터 처리 시스템 등에 활용될 수 있습니다.

 

 


 

● 단순히 고정된 스레드 풀을 사용하려는 목적 : 

 public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        for (int i = 1; i <= 5; i++) {
            final int taskNumber = i;
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " - Task " + taskNumber);
                try {
                    Thread.sleep(1000); // 작업이 1초 걸린다고 가정
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        pool.shutdown();
    }

 출력결과 : 

pool-1-thread-1 - Task 1
pool-1-thread-2 - Task 2
pool-1-thread-2 - Task 3
pool-1-thread-1 - Task 4
pool-1-thread-1 - Task 5

 

단순히 고정된 스레드 풀을 사용하려는 목적이라면 ExecutorService pool = Executors.newFixedThreadPool(5); 방식이 훨씬 간단하고 효율적입니다.

  1. 간결한 코드 : 스레드 풀을 사용하는 데 필요한 설정이 매우 직관적이고 간단합니다. 풀의 크기만 지정하면 되므로 복잡한 설정 없이 바로 사용할 수 있습니다.
  2. 자동 관리 : Executors.newFixedThreadPool()을 사용하면 스레드 풀의 크기와 관리를 자동으로 처리해줍니다. 즉, 풀에 들어갈 스레드를 자동으로 생성하고, 작업 큐를 관리하며, 스레드의 유휴 시간과 종료 등을 관리합니다.
  3. 효율성 : 기본적으로 필요한 스레드 수만큼만 스레드가 생성되므로 불필요한 스레드를 생성하지 않고, 시스템 자원을 효율적으로 사용할 수 있습니다.
  4. 스레드 관리에 대한 신경을 덜어줌 : ExecutorService는 스레드 풀의 관리 및 스케줄링을 자동으로 처리해주기 때문에 개발자가 스레드 관리에 대해 신경 쓸 필요가 없습니다. 단순히 작업을 제출하면 풀에서 처리해줍니다.

'Java' 카테고리의 다른 글

Java Singleton  (2) 2025.03.01
Java 스레드 실행 및 안전한 종료 방법  (2) 2025.02.16
JAVA Index 기반 문자열 변형 및 랜덤 셔플러  (2) 2025.02.15
비트 연산  (2) 2025.01.31
Java 빌더 패턴 (Builder Pattern)  (0) 2025.01.29

Java 싱글톤(Singleton) 패턴 : 객체를 하나만 생성하고, 그 객체를 어디서든 접근할 수 있도록 하는 디자인 패턴입니다. 이 패턴은 특정 클래스의 인스턴스가 하나만 생성되도록 보장하며, 이 인스턴스에 대한 전역적인 접근점을 제공합니다.

 

 

  • 싱글턴 패턴을 사용하지 않으면:
    1. 매번 새 인스턴스가 생성됩니다.
    2. 클래스의 생성자가 매번 호출되어 새로운 객체가 생성됩니다.

 

public class MyClass {
    public MyClass() {
        System.out.println("MyClass constructor called!");
    }

    public void doSomething() {
        System.out.println("Doing something...");
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        MyClass obj1 = new MyClass();  // 첫 번째 인스턴스 생성
        obj1.doSomething();

        MyClass obj2 = new MyClass();  // 두 번째 인스턴스 생성
        obj2.doSomething();
    }
}
  • 출력결과  : 
MyClass constructor called! //생성자 1회 호출
Doing something...
MyClass constructor called! //생성자 2회 호출
Doing something...

  • 싱글톤 패턴의 주요 특징 :
    1. 유일한 인스턴스: 클래스의 인스턴스가 하나만 존재하도록 보장합니다.
    2. 전역 접근: 애플리케이션 어디에서든지 이 인스턴스에 접근할 수 있도록 합니다.
    3. 인스턴스 제어: 인스턴스 생성이 제한되므로 불필요한 리소스 사용을 방지할 수 있습니다.

 

  • 싱글톤 예제 1번  :  Double-Checked Locking 방식  성능 최적화를 위해 흔히 사용하는 방법 중 하나는 Double-Checked Locking입니다. 이 방법에서는 인스턴스가 이미 생성된 후에는 동기화를 피하는 방식으로, 성능을 최적화할 수 있습니다.
public class Singleton {
    private static volatile Singleton instance;
    private final String name;
    private final int age;

    // private 생성자, 값을 초기화하는 역할
    private Singleton(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getInstance 메서드는 인스턴스를 한 번만 초기화하고 이후에는 같은 인스턴스를 반환
    public static Singleton getInstance(String name, int age) {
        if (instance == null) {  // 첫 번째 체크 (동기화 없이)
            synchronized (Singleton.class) {
                if (instance == null) {  // 두 번째 체크 (동기화된 상태에서)
                    // 첫 번째 인스턴스 생성 시 name과 age를 설정
                    instance = new Singleton(name, age);
                }
            }
        }
        return instance;
    }

    // Getter 메서드
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // 이 메서드는 인스턴스의 값을 변경할 수 없게 만들어서 불변성 유지
    public static void resetInstance(String name, int age) {
        throw new UnsupportedOperationException("Singleton instance cannot be reset");
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        // 첫 번째 인스턴스 생성 (name과 age 값 설정)
        Singleton singleton = Singleton.getInstance("John", 30);
        System.out.println("Name: " + singleton.getName());
        System.out.println("Age: " + singleton.getAge());

        // 동일한 인스턴스를 반환, 다른 값으로 초기화 불가능
        Singleton singleton2 = Singleton.getInstance("Jane", 25);
        System.out.println("Name: " + singleton2.getName()); // 기존 값 출력 ("John")
        System.out.println("Age: " + singleton2.getAge());   // 기존 값 출력 (30)

        // resetInstance() 메서드를 호출하여 재설정 불가
        // singleton.resetInstance("New Name", 40); // 실행 시 예외 발생
    }
}

 

  • 싱글턴 클래스 요소 :
    1. 단일 인스턴스 보장: getInstance 메서드는 항상 동일한 인스턴스를 반환합니다. instance가 null인 경우에만 새로운 인스턴스를 생성하고, 그 후에는 기존의 인스턴스를 반환합니다.
    2. 멀티스레드 안전성: synchronized 키워드를 사용하여 멀티스레드 환경에서 동기화를 처리하고, 두 번째 체크를 통해 불필요한 동기화 작업을 피하고 성능을 최적화합니다.
    3. 불변 객체: name과 age 값은 객체가 한 번 생성되면 변경할 수 없습니다. 이는 객체의 불변성을 보장하는 중요한 요소입니다.

 

  • 싱글톤 예제 2번  :    
    • enum은 자바에서 기본적으로 Serializability, Thread-Safety, Instance Control을 보장해주기 때문에, 이를 활용하여 Singleton 패턴을 구현하면 추가적인 코드 없이 간단하게 안전한 싱글턴 객체를 생성할 수 있습니다. enum 사용한 싱글턴 패턴은 구현이 간단하면서도, 멀티스레드 환경과 직렬화 문제를 자연스럽게 해결하는 강력한 방법입니다. 따라서 enum을 이용한 싱글턴 구현은 가장 안전하고 바람직한 방식입니다.
 enum Singleton {
    UNIQUE_INSTANCE("John", 30);  // name과 age 값을 전달

    private final String name;
    private final int age;

    // 생성자
    Singleton(String name, int age) {
        this.name = name;
        this.age = age;
        System.out.println("Singleton instance created!");
    }

    // 싱글턴 객체에서 사용할 메소드들
    public void doSomething() {
        System.out.println("Doing something...");
    }

    // 값들을 반환하는 메소드
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

public class SingletonTest {
    public static void main(String[] args) {
        Singleton single = Singleton.UNIQUE_INSTANCE;
        single.doSomething();  // 첫 번째 호출
        System.out.println("Name: " + single.getName() + ", Age: " + single.getAge());

        Singleton single2 = Singleton.UNIQUE_INSTANCE;
        single2.doSomething(); // 두 번째 호출
        System.out.println("Name: " + single2.getName() + ", Age: " + single2.getAge());
    }
}
  • 출력결과  : 
Singleton instance created! //생성자 1번만 호출 
Doing something...
Name: John, Age: 30
Doing something...
Name: John, Age: 30

 

이런 현상은 문자 인코딩 문제로 발생합니다. STS에서 문서 인코딩을 UTF-8로 변경하면 해결될 수 있습니다.

인코딩 오류

 

해결방법 : 

  • Window 메뉴를 클릭합니다.
  • Preferences를 선택합니다.
  • General -> Workspace를 선택합니다.
  • Text file encoding에서 Other를 선택하고 UTF-8로 변경합니다.
  • [Apply and Close]버튼을 클릭하여 저장합니다. 

 

'Eclipse tool' 카테고리의 다른 글

스프링 STS에서 SSH 설정하여 import  (0) 2025.02.15
이클립스 자동 줄바꿈 현상 없애기  (0) 2024.02.28

코드는 Runnable 인터페이스를 구현한 MyRunnable 클래스를 생성하고, 이를 실행하는 Thread를 생성하여 동작시킨 후 안전하게 종료하는 방식의 예제입니다.

class MyRunnable implements Runnable {
    /** 
      * volatile 키워드의 역할:
      * volatile을 사용하면 running 변수가 메모리 캐시가 아닌 메인 메모리에서 읽고 쓰도록 보장됩니다.
      * 따라서 한 스레드에서 변경한 running 값을 다른 스레드에서 즉시 감지할 수 있습니다.
      * 이를 통해 while (running) 조건이 변경 사항을 즉시 반영하여 루프가 빠르게 종료될 수 있습니다.
      *    
      * volatile: 변수의 가시성을 보장하지만 원자성은 보장하지 않음.
      * synchronized: 코드 블록에 동기화를 적용해 원자성을 보장하고 경쟁 상태를 방지
      */
    private volatile boolean running = true;

    public void run() {
        while (running) {
            System.out.println("Thread 실행 중...");
            try {
                Thread.sleep(500); // 0.5초 대기
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        System.out.println("Thread 종료됨.");
    }

    public void stopThread() {
        running = false;
    }
}

public class Main{
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable); // Thread 객체 생성
        thread.start();  // 스레드 시작

        Thread.sleep(2000); // 2초 동안 메인 스레드 대기
        System.out.println("스레드 종료 요청...");
        myRunnable.stopThread(); // 안전하게 종료 요청

        thread.join(); // thread가 종료될 때까지 main 스레드 대기
        System.out.println("메인 스레드 종료.");
    }
}

 

왜 thread.stop()을 사용하지 않고 stopThread()메소드를 만들어 사용할까?

 

 1. 강제 종료로 인한 리소스 정리 문제

  • Thread.stop()은 스레드를 즉시 종료시키므로, 스레드가 사용 중인 리소스 (파일, 네트워크 연결 등)를 정리할 기회를 주지 않습니다.
  • 예를 들어, 파일을 쓰고 있는 도중에 stop()이 호출되면, 파일이 손상될 수 있습니다.

 2. 락(잠금) 해제 문제

  • stop()을 호출하면 스레드가 즉시 종료되므로, 해당 스레드가 점유하고 있던 락(lock)이 즉시 해제됩니다.
  • 이로 인해 다른 스레드가 해당 리소스에 접근할 때 데이터 무결성 문제가 발생할 수 있습니다.
  • 예를 들어, 하나의 스레드가 synchronized 블록 안에서 실행 중인데 stop()이 호출되면, 해당 블록이 비정상적으로 종료되면서 공유 데이터가 손상될 수 있습니다.

 3. ThreadDeath 예외 발생

  • stop()을 호출하면 스레드는 ThreadDeath 예외를 발생시키며 종료됩니다.
  • 이 예외를 잡아 처리하면 스레드가 완전히 종료되지 않고 계속 실행될 수도 있습니다.
  • 예기치 않은 예외로 인해 프로그램이 불안정해질 수 있습니다.

메소드를 만드는 대신 interrupt() 활용 : 

 

스레드를 interrupt()를 이용해 종료하는 것도 좋은 방법입니다. thread.interrupt()를 호출하면 sleep() 상태에서 InterruptedException이 발생하여 catch 블록으로 빠지고, 루프가 종료됩니다.

 

public class Main{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyRunnable());
        thread.start();

        Thread.sleep(2000); // 2초 대기
        System.out.println("스레드 종료 요청...");
        thread.interrupt();  // 인터럽트 요청

        thread.join();
        System.out.println("메인 스레드 종료.");
    }
}

 

 

 

마지막에 선언된 thread.join();의 역할은? 

 

thread.join();은 현재 실행 중인 메인 스레드(main thread)가 thread 스레드가 종료될 때까지 기다리도록 하는 메서드입니다. Java 프로그램에서 여러 개의 스레드를 실행하면, 메인 스레드는 독립적으로 실행됩니다. 즉, 메인 스레드가 먼저 종료되더라도 다른 스레드는 계속 실행될 수 있습니다. 하지만, 특정한 스레드가 완전히 종료된 후에만 다음 작업을 진행하고 싶다면 join()을 사용해야 합니다.

1️⃣ thread.start();로 새 스레드를 실행
2️⃣ main() 스레드는 Thread.sleep(2000);을 실행하며 2초 동안 대기
3️⃣ myRunnable.stopThread();를 호출하여 thread 스레드가 종료하도록 요청
4️⃣ thread.join();을 실행하여 thread가 종료될 때까지 메인 스레드가 대기
5️⃣ thread가 종료된 후 "메인 스레드 종료." 출력

💡 즉, join()을 사용하면 특정 스레드가 종료된 후에 다음 코드가 실행되도록 보장됩니다.

 

핵심:

  • volatile을 사용해 running 변수를 안전하게 공유.
  • stopThread()메서드 활요하여 스레드 종료 요청 / interrupt()로 스레드 종료 요청.
  • join()으로 메인 스레드가 종료될 때까지 대기.

 

'Java' 카테고리의 다른 글

Java ThreadPoolExecutor를 활용한 Custom스레드 풀 구현  (1) 2025.03.08
Java Singleton  (2) 2025.03.01
JAVA Index 기반 문자열 변형 및 랜덤 셔플러  (2) 2025.02.15
비트 연산  (2) 2025.01.31
Java 빌더 패턴 (Builder Pattern)  (0) 2025.01.29

Spring Tool Suite (STS)에서 Git 및 SSH 설정하기에 관련된 명령어를 도표 형식으로 정리한 것입니다.

1. Git 설치 Git이 설치되지 않았다면 Git을 다운로드하고 설치합니다. Git 공식 사이트에서 다운로드 Git 공식 사이트
2. Git 초기 설정 사용자 이름과 이메일을 설정합니다. git config --global user.name "Your Name"
    git config --global user.email "your.email@example.com"
3. SSH 키 생성 SSH 키를 생성합니다. ssh-keygen -t rsa -b 4096 -C "your.email@example.com"
4. SSH 키 추가 생성한 SSH 키를 GitHub 또는 GitLab 계정에 추가합니다. cat ~/.ssh/id_rsa.pub
5. SSH 에이전트 시작 SSH 에이전트를 시작하고 키를 추가합니다. eval $(ssh-agent -s)
    ssh-add ~/.ssh/id_rsa
6. SSH 키 확인 추가된 SSH 키를 확인합니다. ssh-add -l
7. SSH 키 파일 권한 설정 SSH 키 파일의 권한을 설정합니다. chmod 600 ~/.ssh/id_rsa
8. PEM 형식 키 변환 (필요시) PEM 형식의 키를 OpenSSH 형식으로 변환합니다. ssh-keygen -p -m PEM -f ~/.ssh/id_rsa
9. GitHub의 SSH 호스트 추가 GitHub의 SSH 호스트를 known_hosts 파일에 추가합니다. ssh-keyscan github.com >> ~/.ssh/known_hosts
10. 원격 저장소 클론 GitHub 또는 GitLab의 원격 저장소를 클론합니다. git clone git@github.com:username/repository.git

표는 STS에서 Git 및 SSH 설정을 위한 명령어들을 각 단계별로 정리한 것입니다. 필요한 작업에 맞게 명령어를 사용하시면 됩니다.

 

1. 도표 4. SSH STS에 등록 및 SSH 공개 키 GitHub에 등록하기 :

 

  1). SSH STS에 등록 : 

    Window > Preferences > General > Network Connections : SSH2 > [Key Mangement] 탭 > [Load Existing Key...]버튼 >

    id_rsa 파일 선택 > [Apply and Close],  err발생시 도표 7, 8실행

 

   2). SSH 공개 키 GitHub에 등록하기 : 

  • GitHub에 로그인합니다.
  • 오른쪽 상단의 프로필 아이콘을 클릭하고, Settings를 선택합니다.
  • 좌측 메뉴에서 SSH and GPG keys를 클릭합니다.
  • New SSH key 버튼을 클릭합니다.
  • Title에 키의 이름을 입력합니다. (예: "My_key")
  • Key 필드에 방금 복사한 SSH 공개 키를 붙여넣기 합니다.
  • Add SSH key 버튼을 클릭하여 키를 추가합니다.

2. STS에서 Git 리포지토리 클론 : 

  • SSH 연결이 설정되었으면, STS에서 Git 리포지토리를 SSH로 클론할 수 있습니다.
  • File > Import > Git > Projects from Git을 선택합니다.
  • Clone URI를 선택한 후, Repository URI에 SSH URL(git@github.com:your_username/your_repository.git)을 입력합니다.
  • Protocol : 자동 입력(git) 따로 설정하지 않습니다
  • Authentication : 자동 입력(git) 따로 설정하지 않습니다.  
  • next 버튼을 클릭하여 등록 진행을 합니다.  

 

class는 문자열의 인덱스를 활용하여 변형 및 무작위 재배열하는 기능을 수행합니다.

  1. 역순 변환 → 문자열의 인덱스를 뒤에서부터 읽어 리스트에 저장
for (int i = input.length() - 1; i >= 0; i--) 에서 length() -1을 하는 이유? 
------------------------------------------------------------------------------------------
문자열:  " A  B  C  D  E "
인덱스:   0  1  2  3  4    ← (0부터 시작)
길이:     5               ← (문자 개수)

즉, length - 1이 적용되는 이유는 0부터 시작하는 인덱스 체계에서 마지막 요소를 가리키기 위해서입니다.
  1. 인덱스 기반 랜덤 셔플 → 특정 seed 값을 사용하여 인덱스 순서를 무작위로 섞음
  2. HashMap을 활용한 저장  → 변형된 데이터를 seed 값과 함께 저장
public class ShuffledStringProcessor {

    /**
     * 주어진 문자열을 역순으로 변환하여 리스트에 저장합니다.
     *
     * @param input 변환할 문자열
     * @return 역순으로 변환된 문자 리스트
     */
    static ArrayList<Character> reverseStringToList(String input) {
        ArrayList<Character> reversedList = new ArrayList<>();
        for (int i = input.length() - 1; i >= 0; i--) {
            reversedList.add(input.charAt(i));
        }
        return reversedList;
    }

    /**
     * 주어진 seed 값을 사용하여 리스트의 요소를 랜덤하게 섞습니다.
     *
     * @param list  랜덤하게 섞을 문자 리스트
     * @param seed  난수 생성의 기준이 되는 seed 값
     */
    static void shuffleListWithSeed(ArrayList<Character> list, int seed) {
        Random random = new Random(seed);
        Collections.shuffle(list, random);
    }

    /**
     * 주어진 문자열을 역순 변환 후, 특정 seed 값에 따라 순서를 무작위로 변경하여 출력합니다.
     *
     * @param input 변환할 문자열
     * @param seed  난수 생성의 기준이 되는 seed 값
     */
    static void processAndPrintShuffledString(String input, int seed) {
        ArrayList<Character> reversedList = reverseStringToList(input);
        shuffleListWithSeed(reversedList, seed);

        // 결과를 HashMap에 저장
        HashMap<Integer, List<Character>> shuffledMap = new HashMap<>();
        shuffledMap.put(seed, reversedList);

        // 출력
        System.out.println("Seed 값: " + seed);
        System.out.println("변경된 출력: " + reversedList);
        System.out.println("HashMap: " + shuffledMap);
    }

    public static void main(String[] args) {
        String inputString = "AbCdEfGHi";

        processAndPrintShuffledString(inputString, 10); // seed 값 10
        System.out.println("------------------------");
        processAndPrintShuffledString(inputString, 20); // seed 값 20

        System.out.println("------------동일성 여부 체크-----------");
        processAndPrintShuffledString(inputString, 30); // seed 값 30
        processAndPrintShuffledString(inputString, 30); // seed 값 30
    }
}

 

이 클래스는 문자열을 인덱스 기반으로 변형(역순)한 후, seed 값에 따라 일관된 랜덤 순서를 적용하는 기능을 제공합니다.
이를 통해 같은 seed 값이면 항상 같은 결과를 얻을 수 있어, 난수 기반 섞기에서 재현성을 보장할 수 있습니다.

'Java' 카테고리의 다른 글

Java Singleton  (2) 2025.03.01
Java 스레드 실행 및 안전한 종료 방법  (2) 2025.02.16
비트 연산  (2) 2025.01.31
Java 빌더 패턴 (Builder Pattern)  (0) 2025.01.29
Java List.of() 객체(Apple)에 담긴 값 선택(filter)하여 꺼내기  (0) 2025.01.27

비트 논리 연산자는 이진수 비트(bit) 수준에서 연산을 수행하는 연산자입니다. 자바를 비롯한 대부분의 프로그래밍 언어에서 이러한 연산자들은 비트 단위로 AND, OR, XOR, NOT 등의 연산을 수행합니다.

 

1. 비트 AND 연산자 (&)

비트 AND 연산자는 두 비트가 모두 1일 때만 1을 반환하고, 나머지 경우에는 0을 반환합니다.

작동 원리:

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 1 = 0
  • 0 & 0 = 0
int a = 5;   // 5는 이진수로 0101
int b = 3;   // 3은 이진수로 0011
int result = a & b;  // result는 1이 됩니다 (0001)

 

2. 비트 OR 연산자 (|)

비트 OR 연산자는 두 비트 중 하나라도 1이면 1을 반환하고, 둘 다 0일 때만 0을 반환합니다.

작동 원리:

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 1 = 1
  • 0 | 0 = 0
int a = 5;   // 5는 이진수로 0101
int b = 3;   // 3은 이진수로 0011
int result = a | b;  // result는 7이 됩니다 (0111)

 

3. 비트 XOR 연산자 (^)

비트 XOR 연산자는 두 비트가 서로 다를 때 1을 반환하고, 같을 때는 0을 반환합니다.

작동 원리:

  • 1 ^ 1 = 0
  • 1 ^ 0 = 1
  • 0 ^ 1 = 1
  • 0 ^ 0 = 0
int a = 5;   // 5는 이진수로 0101
int b = 3;   // 3은 이진수로 0011
int result = a ^ b;  // result는 6이 됩니다 (0110)

 

4. 비트 NOT 연산자 (~)

비트 NOT 연산자는 단항 연산자로, 각 비트를 반전시킵니다. 1은 0으로, 0은 1로 바뀝니다.

작동 원리:

  • ~1 = 0
  • ~0 = 1
int a = 5;   // 5는 이진수로 0101
int result = ~a;  // result는 -6이 됩니다 (이진수로 1010, 즉 2의 보수)

 

설명:

  • 자바에서는 부호 있는 정수형을 사용하기 때문에, 비트 NOT 연산을 수행하면 2의 보수(컴퓨터에서 음수를 표현하는 방식)로 변환됩니다.
  • 예를 들어, 5의 이진수는 32비트 정수에서 00000000 00000000 00000000 00000101이며, NOT 연산 후 11111111 11111111 11111111 11111010이 되어 -6이 됩니다.

5. 비트 연산자의 활용

  • 마스크 처리: 특정 비트만 변경하거나 확인할 때 사용됩니다.
    • 예: 특정 플래그를 켜거나 끌 때
  • 비트 플래그: 여러 상태나 옵션을 하나의 변수로 관리할 때 비트 연산을 사용합니다.
  • 암호화 및 해싱: XOR 연산은 간단한 암호화 알고리즘에서 사용되기도 합니다.

비트 연산자는 효율적이고 빠르기 때문에, 저수준 프로그래밍이나 성능이 중요한 상황에서 유용하게 사용될 수 있습니다.

빌더 패턴(Builder Pattern)은 복잡한 객체의 생성을 단순화하고, 가독성을 높이며, 불변성을 유지하는 데 유용한 디자인 패턴입니다. 이 패턴을 사용하면 객체의 속성을 단계적으로 설정할 수 있어 가독성이 좋고 유지보수가 용이합니다.
 

빌더 패턴이 필요한 이유

1. 생성자 오버로딩 문제 :

객체를 생성할 때 생성자 오버로딩을 활용하면 다양한 매개변수 조합을 처리할 수 있습니다. 하지만 매개변수 개수가 많아질수록 생성자 오버로딩이 복잡해지고 유지보수가 어려워집니다.
 

1) 생성자 오버로딩(Constructor Overloading)이란? : 
클래스 내에서 같은 이름의 생성자를 여러 개 정의하는 것을 의미합니다.
즉, 매개변수의 개수나 타입이 다르게 여러 개의 생성자를 만드는 기법입니다.
생성자 오버로딩은 객체를 다양한 방법으로 초기화할 수 있어 유연성을 제공하지만,
매개변수의 개수가 많아질 경우 코드 가독성과 유지보수성이 떨어지는 문제가 발생할 수 있습니다.

public class User {
    private final String name;
    private final int age;

	// 생성자 1: 이름만 설정 (age 기본값 0)
    public User(String name) {//매개변수 name 
        this.name = name;
        this.age = 0; 
    }
	
    //필드가 많아질수록 경우의 수가 기하급수적으로 증가하여 생성자 관리가 어려워짐
    //public User(String name, int age, String email, String address) { ... }
    //public User(String name, int age, String email) { ... }
    //public User(String name, String email) { ... }
    //public User(String name, int age) { ... }

    public void displayInfo() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

 

2. 가독성 및 유지보수

 
생성자의 매개변수가 많으면 어떤 값이 어떤 속성을 의미하는지 알아보기 어렵습니다.

public class Main {
    public static void main(String[] args) {
    //"Alice"는 이름(name)이고 25는 나이(age)이지만, 명확하게 이해하기 어렵습니다.
    User user = new User("Alice", 25);
         user.displayInfo(); // 출력: 이름: Alice, 나이: 25
    }
}

 
빌더 패턴을 사용하면 아래 코드와 같이 가독성이 개선됩니다.

 public static void main(String[] args) {
	//setName()과 setAge()를 사용하여 어떤 값을 설정하는지 명확하게 알 수 있습니다.
	User user = new User.UserBuilder()
            	.setName("Alice")
            	.setAge(25)
            	.build();
                
        user.displayInfo(); // 출력: 이름: Alice, 나이: 25
 }

 

3. 빌더패턴 적용

**빌더 패턴(Builder Pattern)**을 사용하면 생성자 오버로딩 문제를 해결할 수 있습니다.
빌더 패턴을 사용하면 유지보수가 쉬워지고, 가독성이 향상되며, 불변 객체를 만들기 용이합니다.

public class User {
    // **필드 (불변 객체를 위해 final 사용)**
    private final String name;
    private final int age;
	 
    // **🔹 private 생성자** : 외부에서 직접 객체 생성을 막고, 빌더를 통해서만 생성할 수 있도록 제한
    private User(UserBuilder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

	// **🔹 내부 정적 클래스 : UserBuilder (User 객체 생성을 위한 빌더)**
    public static class UserBuilder {
        private String name;
        private int age;
		
        //**🔹 setName() 메서드** : name 필드를 설정하고 현재 빌더 객체(this)를 반환하여 메서드 체이닝 가능
        public UserBuilder setName(String name) { 
            this.name = name;
            return this;
        }

        public UserBuilder setAge(int age) { 
            this.age = age;
            return this;
        }
        
        // **🔹 build() 메서드** : 설정된 값으로 User 객체를 생성하여 반환
        public User build() {
            return new User(this);
        }
    }
    
     public void displayInfo() {
        System.out.println("이름: " + name + ", 나이: " + age);
    }
}

 
Lombok의 @Builder를 사용하면 아래와 같이 객체를 더 간결하게 생성할 수 있습니다.

import lombok.Builder;

@Builder
public class User {
    private final String name;
    private final int age;
}

 
Lombok 사용을 위한 설정( Maven or Gradle) : 
ㄱ.  Maven (pom.xml)
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version> <!-- 최신 버전 확인 -->
    <scope>provided</scope>
</dependency>

ㄴ. Gradle (build.gradle)
dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.28'
    annotationProcessor 'org.projectlombok:lombok:1.18.28'
}


결론 : 

빌더 패턴을 사용하면 가독성이 향상되고 유지보수가 쉬워짐
메서드 체이닝 방식(set 메서드)을 활용하여 객체 생성을 직관적으로 처리 가능
객체의 불변성을 유지할 수 있음
빌더 패턴을 활용하면 더 유연하고 유지보수하기 좋은 코드를 작성할 수 있습니다. 
 

 

 

SQL 쿼리는 각 부서별로 급여 순위를 매기는 쿼리입니다. 각 부서 내에서 급여가 높은 직원 순으로 순위를 계산하고, 급여가 NULL인 직원은 마지막에 배치되도록 처리합니다.

 

SQL 부서의 급여 순위 NULL LAST : 

SELECT 
    department,
    employee_name,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC NULLS LAST) AS rank --설명 ●
FROM 
    employees;

 

  • ROW_NUMBER(): 각 행에 고유한 순차적인 번호를 부여합니다. 동점이 있더라도 건너뛰지 않고 순차적으로 번호를 매깁니다.
  • OVER: 이 함수가 윈도우 함수임을 나타내며, PARTITION BY와 ORDER BY 절을 통해 계산 방식을 지정합니다.
  • PARTITION BY department: department별로 데이터를 그룹화합니다. 즉, 각 부서별로 ROW_NUMBER()가 별도로 계산됩니다.
  • ORDER BY salary DESC: salary를 기준으로 내림차순 정렬합니다. 즉, 급여가 높은 순서대로 번호를 매깁니다.
  • NULLS LAST: NULL 값이 있는 경우, 정렬 시 NULL을 마지막에 배치하도록 합니다. 이는 salary가 NULL인 경우 해당 행이 가장 낮은 순위(가장 나중에)로 처리되도록 합니다.

예시 데이터 

HR John 4000
HR Alice 3500
HR Bob NULL
IT Charlie 5000
IT Eve 4500
IT David 6000

실행 결과

HR John 4000 1
HR Alice 3500 2
HR Bob NULL 3
IT David 6000 1
IT Charlie 5000 2
IT Eve 4500 3

 

 

 

 

Class별 설명 :

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

 

Predicate는 함수형 인터페이스로, test(T t) 메서드를 통해 특정 조건을 만족하는지 여부를 검사합니다. @FunctionalInterface 어노테이션은 이 인터페이스가 함수형 인터페이스임을 명시합니다.

 

public class FilterClass {
    public <T> List<T> filter(List<T> list, Predicate<T> p) {
        List<T> results = new ArrayList<>();
        
        for (T t : list) {
            if (p.test(t)) {
                results.add(t);
            }
        }
        return results;
    }
}

FilterClass는 리스트를 받아 Predicate 조건을 만족하는 요소들만을 필터링하는 역할을 합니다. filter 메서드는 제네릭 타입 <T>를 사용하여 다양한 타입의 리스트를 처리할 수 있습니다.

 

public enum Type {
    RED, BLUE, BLACK, YELLOW;
}

Type은 열거형으로, 사과의 타입을 정의합니다. RED, BLUE, BLACK, YELLOW 네 가지 타입이 있습니다.

 

public class Apple {
    private int weight;
    private Type type;

    public Apple(int weight, Type type) {
        this.weight = weight;
        this.type = type;
    }

    public int getWeight() {
        return weight;
    }

    public Type getType() {
        return type;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public void setType(Type type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "Apple [weight=" + weight + ", type=" + type + "]";
    }
}

Apple 클래스는 사과 객체를 정의합니다. weight와 type 필드를 가지고 있으며, 각각 사과의 무게와 종류를 나타냅니다. 생성자와 게터, 세터 메서드를 통해 사과 객체를 생성하고 조작할 수 있습니다. toString() 메서드는 사과 객체의 문자열 표현을 반환합니다.

 

public class Main {
    public static void main(String[] args) {
        FilterClass Flag = new FilterClass();
        
        List<Apple> inventory = List.of(
            new Apple(100, Type.RED),
            new Apple(120, Type.BLUE),
            new Apple(140, Type.BLACK),
            new Apple(160, Type.YELLOW)
        );
        
        // 색상, 무게 필터 테스트 
        List<Apple> getRedApple = Flag.filter(inventory, (Apple apple) -> Type.RED.equals(apple.getType()));
		List<Apple> getWeight = Flag.filter(inventory, (Apple apple) -> apple.getWeight() == 160);
        System.out.println(getRedApple);
		System.out.println(getWeight);
        
        //FilterClass 다양한 타입 필터 테스트 
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
		List<Integer> evenNumbers = Flag.filter(numbers, (Integer number) -> number % 2 == 0);
		System.out.println(evenNumbers);
        
    }
}

Main 클래스는 프로그램의 시작점입니다. FilterClass 인스턴스를 생성하고, 다양한 사과 객체로 구성된 inventory 리스트를 생성합니다. filter 메서드를 사용하여 타입이 RED인 사과만을 필터링하고, 필터링된 결과를 출력합니다.

필터 출력 값

 

Class별 요약 :

 

  • Predicate<T>: 조건을 검사하는 함수형 인터페이스.
  • FilterClass: 리스트에서 조건을 만족하는 요소를 필터링.
  • Type: 사과의 타입을 정의하는 열거형.
  • Apple: 사과 객체를 정의하고 관리.
  • Main: 프로그램을 실행하고 필터링 작업을 수행.

 

 

 

 

 

참고자료 : 모던자바인액션

 

'Java' 카테고리의 다른 글

비트 연산  (2) 2025.01.31
Java 빌더 패턴 (Builder Pattern)  (0) 2025.01.29
Java List.of()  (1) 2025.01.26
JAVA 배열 index값 반환 메서드  (2) 2025.01.25
JDBC, JPA, MyBatis, jOOQ: 데이터베이스 기술 비교  (0) 2024.08.10

+ Recent posts