
타입 소거(Generic Type Erasure)와 형 확장(Primitive Type Wrapping)은
자바에서 제네릭을 사용할 때 중요한 개념입니다.
이들은 자바 컴파일러와 JVM이 어떻게 타입을 처리하는지에 대한 이해를 돕습니다.
아래는 이들에 대한 설명과 예시입니다.
public class CacheManager<T> {
private Map<String, T> cache = new HashMap<>();
public void put(String key, T value) {
cache.put(key, value);
}
public T get(String key) {
return cache.get(key);
}
// 문제가 될 수 있는 코드
public boolean isInstance(Object obj) {
// 컴파일 오류! T의 타입 정보가 런타임에 소거됨
return obj instanceof T;
}
}
public class CacheManager<T> {
private final Class<T> type;
public CacheManager(Class<T> type) {
this.type = type;
}
public boolean isInstance(Object obj) {
return type.isInstance(obj);
}
}
// 사용 예시
CacheManager<String> stringCache = new CacheManager<>(String.class);
형 확장이 성능에 미치는 영향을 실제 코드로 살펴보겠습니다
public class PrimitivePerformance {
public static void main(String[] args) {
long start = System.nanoTime();
// 기본형 사용
int sum1 = 0;
for (int i = 0; i < 10_000_000; i++) {
sum1 += i;
}
long middle = System.nanoTime();
// 래퍼 클래스 사용
Integer sum2 = 0;
for (Integer i = 0; i < 10_000_000; i++) {
sum2 += i; // 오토박싱/언박싱 발생
}
long end = System.nanoTime();
System.out.printf("기본형 처리 시간: %d ns%n", middle - start);
System.out.printf("래퍼클래스 처리 시간: %d ns%n", end - middle);
}
}
제네릭은 컴파일 타임에만 타입 체크가 이루어지며, 런타임에서는 모든 제네릭 타입 파라미터가 Object로 처리됩니다.
예를 들어, List은 컴파일 타임에 String 타입으로 동작하지만, 런타임에서는 Object로 변환됩니다.
배열은 런타임에 타입을 필요로 하므로 제네릭 객체를 배열로 만들 수 없습니다.
예를 들어 List[] lists는 불가능합니다.
자바에서 내재적 락은 Object 클래스에서 제공하는 모니터(monitor)로, 멀티스레딩 환경에서 스레드 간의 동기화를 처리하는 데 사용됩니다.
다음은 제품 재고 관리 시스템의 코드입니다.
public class InventoryManager {
private static class Product {
private final String id;
private int stock;
private final int threshold;
public Product(String id, int stock, int threshold) {
this.id = id;
this.stock = stock;
this.threshold = threshold;
}
}
private final Map<String, Product> products = new HashMap<>();
private final Object orderLock = new Object();
private final Object restockLock = new Object();
public boolean processOrder(String productId, int quantity) {
synchronized (orderLock) {
Product product = products.get(productId);
if (product == null || product.stock < quantity) {
return false;
}
synchronized (restockLock) {
product.stock -= quantity;
if (product.stock < product.threshold) {
requestRestock(productId);
}
}
return true;
}
}
private void requestRestock(String productId) {
// 재고 보충 요청 로직
}
}
public class BankTransfer {
private static class Account {
private final String id;
private double balance;
public Account(String id, double balance) {
this.id = id;
this.balance = balance;
}
}
public boolean transfer(Account from, Account to, double amount) {
// 데드락 방지를 위해 항상 낮은 ID의 계좌부터 락 획득
Account firstLock = from.id.compareTo(to.id) < 0 ? from : to;
Account secondLock = from.id.compareTo(to.id) < 0 ? to : from;
synchronized (firstLock) {
synchronized (secondLock) {
if (from.balance < amount) {
return false;
}
from.balance -= amount;
to.balance += amount;
return true;
}
}
}
}
자바에서 Atomic 변수와 Compare-And-Swap(CAS) 메커니즘을 이용해 동기화 없이 안전한 값 변경을 할 수 있습니다.
이들은 java.util.concurrent.atomic 패키지에서 제공됩니다.
public class HighPerformanceCounter {
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failCount = new AtomicLong(0);
private final AtomicLongArray hourlyStats = new AtomicLongArray(24);
public void recordSuccess() {
successCount.incrementAndGet();
int hour = LocalDateTime.now().getHour();
hourlyStats.incrementAndGet(hour);
}
public void recordFailure() {
failCount.incrementAndGet();
}
public double getSuccessRate() {
long success = successCount.get();
long total = success + failCount.get();
return total == 0 ? 0 : (double) success / total;
}
}
public class LockFreeStack<T> {
private static class Node<T> {
final T value;
Node<T> next;
Node(T value) {
this.value = value;
}
}
private final AtomicReference<Node<T>> head = new AtomicReference<>();
public void push(T value) {
Node<T> newNode = new Node<>(value);
while (true) {
Node<T> oldHead = head.get();
newNode.next = oldHead;
if (head.compareAndSet(oldHead, newNode)) {
return;
}
}
}
public T pop() {
while (true) {
Node<T> oldHead = head.get();
if (oldHead == null) {
return null;
}
Node<T> newHead = oldHead.next;
if (head.compareAndSet(oldHead, newHead)) {
return oldHead.value;
}
}
}
}
자바에서 ReentrantLock과 ReentrantReadWriteLock은 내재적 락보다 더 세밀한 락 제어를 제공합니다.
이들은 volatile, CAS, 대기 큐를 기반으로 락을 관리합니다.
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final T[] items;
private int putIndex, takeIndex, count;
@SuppressWarnings("unchecked")
public BoundedBuffer(int capacity) {
items = (T[]) new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = item;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
T item = items[takeIndex];
items[takeIndex] = null;
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
public class CacheWithStats<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final Map<K, Long> accessStats = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public V get(K key) {
lock.readLock().lock();
try {
V value = cache.get(key);
if (value != null) {
// 통계 업데이트를 위해 쓰기 락으로 업그레이드
lock.readLock().unlock();
lock.writeLock().lock();
try {
accessStats.merge(key, 1L, Long::sum);
lock.readLock().lock();
} finally {
lock.writeLock().unlock();
}
}
return value;
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
lock.writeLock().lock();
try {
cache.put(key, value);
accessStats.put(key, 0L);
} finally {
lock.writeLock().unlock();
}
}
}
public class OrderProcessor {
private final ExecutorService executor;
public OrderProcessor(int threadPoolSize) {
this.executor = Executors.newFixedThreadPool(threadPoolSize);
}
public CompletableFuture<OrderResult> processOrder(Order order) {
return CompletableFuture.supplyAsync(() -> {
// 재고 확인
return checkInventory(order);
}, executor).thenCompose(inventoryOk -> {
if (!inventoryOk) {
throw new IllegalStateException("재고 부족");
}
// 결제 처리
return processPayment(order);
}).thenCompose(payment -> {
// 배송 처리
return arrangeShipping(order, payment);
}).thenApply(shipping -> {
// 주문 결과 생성
return new OrderResult(order, shipping);
}).exceptionally(ex -> {
// 에러 처리
logError(order, ex);
return new OrderResult(order, OrderStatus.FAILED);
});
}
// 각각의 비즈니스 로직 메소드들...
private CompletableFuture<Boolean> checkInventory(Order order) {
return CompletableFuture.supplyAsync(() -> {
// 재고 확인 로직
return true;
}, executor);
}
private CompletableFuture<Payment> processPayment(Order order) {
return CompletableFuture.supplyAsync(() -> {
// 결제 처리 로직
return new Payment();
}, executor);
}
private CompletableFuture<Shipping> arrangeShipping(Order order, Payment payment) {
return CompletableFuture.supplyAsync(() -> {
// 배송 처리 로직
return new Shipping();
}, executor);
}
}
public class AsyncEventBus<T> {
private final ExecutorService executor;
private final Map<Class<?>, List<EventHandler<T>>> handlers = new ConcurrentHashMap<>();
public AsyncEventBus(int threadPoolSize) {
this.executor = Executors.newFixedThreadPool(threadPoolSize);
}
public <E extends T> void register(Class<E> eventType, EventHandler<E> handler) {
handlers.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add((EventHandler<T>) handler);
}
public <E extends T> CompletableFuture<Void> publish(E event) {
List<EventHandler<T>> eventHandlers = handlers.getOrDefault(event.getClass(), Collections.emptyList());
List<CompletableFuture<Void>> futures = eventHandlers.stream()
.map(handler -> CompletableFuture.runAsync(() -> handler.handle(event), executor))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
@FunctionalInterface
public interface EventHandler<T> {
void handle(T event);
}
}
단순히 API를 사용하는 것을 넘어 그 내부 동작 원리를 이해하는 것이 얼마나 중요한지였습니다. 특히 다음과 같은 상황에서 이러한 이해가 큰 도움이 되었습니다.
이 학습 과정을 통해 단순히 코드를 작성하는 것을 넘어, 자바 언어의 깊이 있는 이해가 실제 문제 해결에 얼마나 큰 도움이 되는지 깨달았습니다.
특히 기본기를 다시 다지면서, 이전에는 당연하게 여겼던 개념들의 내부 동작 원리를 이해하게 되었고, 이는 더 나은 설계 결정을 내리는 데 큰 도움이 되었습니다.
앞으로도 이러한 깊이 있는 학습을 통해 더 견고하고 효율적인 시스템을 만드는 개발자가 되고자 합니다.