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

 

코드는 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

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 메서드)을 활용하여 객체 생성을 직관적으로 처리 가능
객체의 불변성을 유지할 수 있음
빌더 패턴을 활용하면 더 유연하고 유지보수하기 좋은 코드를 작성할 수 있습니다. 
 

 

 

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

List.of() 메서드는 Java 9부터 도입되었습니다. 따라서 이 코드는 Java 9 이상에서 사용 가능합니다.

List.of()를 사용하면 불변(immutable) 리스트를 생성할 수 있습니다.

 

불변 객체의 특징 :

  • 안전성: 여러 스레드에서 동시에 접근해도 객체 상태가 변하지 않으므로 동기화가 필요 없습니다.
  • 간단한 사용: 객체가 변경되지 않으므로 예상 가능한 동작을 보장합니다.
//따라서 불변 객체는 요소를 추가, 삭제, 수정할 수 없다. 

List<String> allColors = List.of("Red", "Blue", "Black");

allColors.add("Green");  // UnsupportedOperationException 발생
allColors.remove("Blue");  // UnsupportedOperationException 발생
allColors.set(0, "Yellow");  // UnsupportedOperationException 발생

 

 

변경 가능한 리스트로 사용하려면?

List.of()로 생성된 리스트는 불변이므로, 변경이 필요하다면 ArrayList와 같은 가변 리스트를 사용해야 합니다.

List<String> allColors = new ArrayList<>(List.of("Red", "Blue", "Black"));
allColors.add("Green");  // 이제 요소 추가 가능
allColors.set(0, "Yellow");  // 요소 변경 가능
System.out.println(allColors);  // [Yellow, Blue, Black, Green]

 

출력 형식 :

List<String> colors = List.of("Red", "Blue", "Green");

// for-each 루프를 사용한 순회
for (String color : colors) {
    System.out.println(color);
}

// Iterator를 사용한 순회
Iterator<String> iterator = colors.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

// 람다 
colors.forEach(s-> System.out.println(s));

 

Java 9 이전?

Java 9 이전에는 List.of() 메서드가 없으므로, 리스트를 생성하려면 다른 방법을 사용해야 합니다. 예를 들어, Arrays.asList()를 사용할 수 있습니다.

//Arrays.asList()로 생성된 리스트는 고정 크기 리스트이기 때문에 요소를 추가하거나 제거할 수 없음.
List<String> allColors = Arrays.asList("Red", "Blue", "Black");

 // 요소 수정 가능
 allColors.set(0, "Yellow");  // 0번 인덱스의 "Red"를 "Yellow"로 변경
 // 요소 추가/삭제 불가능
 allColors.add("Purple");  // UnsupportedOperationException 발생
 allColors.remove("Blue"); // UnsupportedOperationException 발생
        
//만약 변경 가능한 리스트가 필요하다면, ArrayList 사용한다. 
List<String> allColors = new ArrayList<>(Arrays.asList("Red", "Blue", "Black"));

// 새로운 요소 추가, 수정, 삭제
allColors.add("Green");
allColors.set(1,"Green");
allColors.remove(1);

 

List.of()와 Arrays.asList() 차이점

  • Arrays.asList(): 리스트의 크기는 고정되지만, 요소는 변경 가능합니다. 다만 요소를 추가하거나 삭제하는 것은 불가능합니다. 이 리스트는 배열과 연결된 리스트입니다.
  • List.of(): 리스트의 크기와 요소가 모두 불변입니다. 요소를 추가, 삭제, 수정할 수 없으며, 이 리스트는 불변 객체로서, 상태 변경이 없습니다.

 

결론 : 

 

Arrays.asList()로 생성된 리스트는 불변 리스트가 아니며, 고정 크기 리스트입니다. 요소의 값을 변경할 수 있지만, 요소를 추가하거나 삭제할 수 없습니다. 반면, List.of()로 생성된 리스트는 불변이고, 요소를 변경할 수 없습니다.

 

public class Main {

	    // 특정 인덱스의 값을 반환하는 메서드
	    static int getElement(int[] arr, int index) {
	        if (index < 0 || index >= arr.length) {
	            throw new IllegalArgumentException("인덱스가 배열의 범위를 벗어났습니다.");
	        }
	        return arr[index];
	    }

	    public static void main(String[] args) {
	        // 배열 초기화
	        int[] array = {10, 20, 30, 40, 50};
	        try {
	            // 잘못된 인덱스 예시
	            int index = 6; // 범위를 벗어나는 인덱스
	            int result = getElement(array, index);

	            System.out.println("인덱스 " + index + "의 값은: " + result);
	        } catch (IllegalArgumentException e) {
	            System.out.println("오류: " + e.getMessage());
	        }
	    }
	}

'Java' 카테고리의 다른 글

Java List.of() 객체(Apple)에 담긴 값 선택(filter)하여 꺼내기  (0) 2025.01.27
Java List.of()  (1) 2025.01.26
JDBC, JPA, MyBatis, jOOQ: 데이터베이스 기술 비교  (0) 2024.08.10
Java 람다  (0) 2024.08.10
JAVA SOLID 개념  (0) 2024.07.17

1. JDBC (Java Database Connectivity)

JDBC는 Java 애플리케이션이 데이터베이스와 연결할 수 있게 해주는 Java의 표준 API입니다. JDBC는 SQL 쿼리를 실행하고, 결과를 처리하며, 데이터베이스 트랜잭션을 관리하는 데 필요한 기본적인 기능들을 제공합니다.

  • 특징:
    • 직접 제어: SQL 쿼리를 직접 작성하고 실행할 수 있습니다.
    • 저수준 API: 데이터베이스와의 연결, 쿼리 실행, 결과 처리 등 모든 작업을 수동으로 처리해야 합니다.
    • 유연성: 복잡한 쿼리와 데이터베이스의 세부적인 설정을 직접 조작할 수 있습니다.

2. JPA (Java Persistence API)

JPA는 Java 애플리케이션에서 객체-관계 매핑(ORM)을 지원하는 표준 API입니다. JPA를 사용하면 데이터베이스 테이블을 Java 객체로 매핑하고, 객체 지향적인 방식으로 데이터베이스 작업을 수행할 수 있습니다.

  • 특징:
    • 고급 추상화: SQL을 직접 작성할 필요 없이 객체 지향적으로 데이터베이스를 조작할 수 있습니다.
    • ORM 프레임워크: Hibernate, EclipseLink 등과 함께 사용됩니다.
    • JPQL (Java Persistence Query Language): 객체 지향 쿼리 언어를 사용하여 데이터를 조회합니다.
    • 자동화된 관리: 엔티티의 생명주기(저장, 수정, 삭제 등)를 자동으로 관리합니다.

3. MyBatis

MyBatis는 SQL 쿼리를 직접 작성하면서도 매핑을 통해 객체와 결과를 쉽게 연결할 수 있도록 도와주는 프레임워크입니다. JPA보다는 더 직접적이고 세밀한 제어를 제공하지만, JDBC보다는 높은 수준의 추상화를 제공합니다.

  • 특징:
    • SQL 직접 작성: 쿼리를 XML 파일이나 애너테이션으로 정의합니다.
    • 매핑: SQL 쿼리 결과를 Java 객체와 매핑할 수 있습니다.
    • 유연성: 복잡한 SQL 쿼리와 매핑 규칙을 세밀하게 조정할 수 있습니다.
    • 쿼리 재사용: SQL 쿼리를 재사용하고, 동적 쿼리 생성 기능을 제공합니다.

4. jOOQ (Java Object Oriented Querying)

jOOQ는 SQL을 Java 코드에서 직접 작성하고 실행할 수 있게 해주는 라이브러리입니다. jOOQ는 SQL 문법을 Java에서 타입 안전하게 사용할 수 있도록 도와줍니다.

  • 특징:
    • 타입 안전: SQL 쿼리를 Java 코드로 작성할 때 타입 안전성을 보장합니다.
    • SQL을 직접 작성: SQL 문을 Java 코드에서 직접 작성하고 실행할 수 있습니다.
    • 쿼리 빌더: SQL 쿼리를 코드에서 체이닝 방식으로 빌드할 수 있습니다.
    • 정적 검증: 컴파일 타임에 SQL 구문을 검증하여 오류를 줄입니다.

이 네 가지 기술은 데이터베이스와 상호작용하는 방식에서 차이를 보이며, 각 기술의 선택은 애플리케이션의 요구사항과 개발자의 선호에 따라 다를 수 있습니다.

 

5. 각 기술에 대한 문법을 간단히 설명

5.1. JDBC (Java Database Connectivity)

JDBC는 Java 코드에서 직접 SQL 쿼리를 작성하고 실행합니다. 기본적인 사용법은 다음과 같습니다

public class JdbcExample {
    public static void main(String[] args) {
        try {
            // 데이터베이스 연결
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
            // SQL 쿼리 실행
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
            // 결과 처리
            while (resultSet.next()) {
                System.out.println(resultSet.getString("username"));
            }
            // 연결 종료
            resultSet.close();
            statement.close();
            connection.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

5.2. JPA (Java Persistence API)

JPA는 객체-관계 매핑을 통해 SQL 쿼리 없이 객체 지향적으로 데이터베이스 작업을 처리합니다. 예를 들어, 엔티티 클래스를 정의하고, 리포지토리 클래스를 사용하여 CRUD 작업을 수행합니다.

//엔티티 클래스:
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    private String username;

    // Getters and setters
}

//리포지토리 클래스:
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

//서비스 클래스:
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User getUserByUsername(String username) {
        return userRepository.findByUsername(username);
    }
}

 

5.3. MyBatis

MyBatis는 SQL 쿼리를 XML 파일 또는 애너테이션으로 정의하고, 매퍼 인터페이스를 통해 쿼리를 호출합니다.

## XML 매퍼 파일:
<mapper namespace="com.example.UserMapper">
    <select id="getUserById" parameterType="int" resultType="com.example.User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

// 매퍼 인터페이스:

public interface UserMapper {
    User getUserById(int id);
}

// 실행파일
public class MyBatisExample {
    public static void main(String[] args) {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.getUserById(1);
            System.out.println(user.getUsername());
        }
    }
}

 

4. jOOQ

jOOQ는 Java 코드에서 SQL 쿼리를 타입 안전하게 작성하고 실행합니다.

import org.jooq.DSLContext;
import org.jooq.impl.DSL;
import org.jooq.impl.DefaultDSLContext;

public class JooqExample {
    public static void main(String[] args) {
        DSLContext create = DSL.using(configuration());

        // SQL 쿼리 작성 및 실행
        Result<Record> result = create.select().from(USERS).fetch();

        // 결과 처리
        for (Record r : result) {
            System.out.println(r.getValue(USERS.USERNAME));
        }
    }
}

 

종합 추천

  • 성능 최적화와 직접 제어가 중요하다면 JDBC를 고려할 수 있습니다.
  • 객체 지향적 접근자동화된 데이터베이스 작업이 필요하다면 JPA가 적합합니다.
  • SQL 쿼리와 매핑의 유연성이 필요하다면 MyBatis를 고려할 수 있습니다.
  • 타입 안전성과 정적 검증이 중요하다면 jOOQ를 추천합니다.

 

 

 

'Java' 카테고리의 다른 글

Java List.of()  (1) 2025.01.26
JAVA 배열 index값 반환 메서드  (2) 2025.01.25
Java 람다  (0) 2024.08.10
JAVA SOLID 개념  (0) 2024.07.17
JAVA 파일 읽기 및 효율적인 I/O 처리 방법  (0) 2024.06.15

+ Recent posts