코드 한 줄의 기록

Java 내부 클래스 완전 정복: 멤버/로컬/익명 클래스 실무 활용법 본문

JAVA

Java 내부 클래스 완전 정복: 멤버/로컬/익명 클래스 실무 활용법

CodeByJin 2025. 10. 11. 08:37
반응형

Java 개발을 하다 보면 클래스 안에 또 다른 클래스를 정의하는 경우를 종종 만나게 됩니다. 이런 구조를 바로 '중첩 클래스'라고 하는데요, 처음 접할 때는 좀 복잡해 보이지만 실제로는 코드를 더 깔끔하고 효율적으로 만들어주는 강력한 기능입니다.
 
오늘은 Java의 내부 클래스 중에서도 특히 자주 사용되는 멤버 내부 클래스, 로컬 클래스, 익명 클래스에 대해 실무에서 어떻게 활용할 수 있는지 함께 알아보겠습니다. 이론만 아는 것보다는 실제 코드 예제와 함께 배우는 것이 더 도움이 될 테니까요!

중첩 클래스란 무엇인가?

중첩 클래스(Nested Class)는 말 그대로 하나의 클래스 내부에 선언된 또 다른 클래스를 의미합니다. 자바에서는 이런 중첩 클래스를 크게 두 가지로 분류할 수 있어요.

  • Static Nested Class (정적 중첩 클래스): static 키워드가 붙은 클래스
  • Inner Class (내부 클래스): static 키워드가 없는 클래스

내부 클래스는 다시 선언 위치에 따라 세 가지로 나뉩니다.

  • 멤버 내부 클래스 (Member Inner Class)
  • 로컬 클래스 (Local Class)
  • 익명 클래스 (Anonymous Class)
class OuterClass {
    // 멤버 내부 클래스
    class MemberInner {
        // ...
    }
    
    // 정적 중첩 클래스
    static class StaticNested {
        // ...
    }
    
    void method() {
        // 로컬 클래스
        class LocalInner {
            // ...
        }
        
        // 익명 클래스
        Runnable r = new Runnable() {
            @Override
            public void run() {
                // ...
            }
        };
    }
}

멤버 내부 클래스 (Member Inner Class)

멤버 내부 클래스는 외부 클래스의 멤버 영역에 선언되는 클래스입니다. 이 클래스의 가장 큰 특징은 외부 클래스의 인스턴스에 종속적이라는 점이에요.
 

멤버 내부 클래스의 특징

1. 외부 클래스 멤버에 자유로운 접근
멤버 내부 클래스는 외부 클래스의 모든 멤버(private 포함)에 접근할 수 있습니다. 이는 강력한 캡슐화를 제공하면서도 내부적으로는 높은 결합도를 유지할 수 있게 해줍니다.

public class BankAccount {
    private String accountNumber;
    private double balance;
    private String ownerName;
    
    public BankAccount(String accountNumber, String ownerName, double initialBalance) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = initialBalance;
    }
    
    // 멤버 내부 클래스
    class Transaction {
        private String type;
        private double amount;
        private String description;
        
        public Transaction(String type, double amount, String description) {
            this.type = type;
            this.amount = amount;
            this.description = description;
        }
        
        public void execute() {
            // 외부 클래스의 private 멤버에 직접 접근 가능
            if ("DEPOSIT".equals(type)) {
                balance += amount;
                System.out.println(ownerName + "님 계좌에 " + amount + "원이 입금되었습니다.");
            } else if ("WITHDRAW".equals(type)) {
                if (balance >= amount) {
                    balance -= amount;
                    System.out.println(ownerName + "님 계좌에서 " + amount + "원이 출금되었습니다.");
                } else {
                    System.out.println("잔액이 부족합니다.");
                }
            }
            System.out.println("현재 잔액: " + balance + "원");
        }
    }
    
    public void processTransaction(String type, double amount, String description) {
        Transaction transaction = new Transaction(type, amount, description);
        transaction.execute();
    }
}

 
2. 객체 생성 방식
멤버 내부 클래스의 인스턴스를 생성하기 위해서는 반드시 외부 클래스의 인스턴스가 먼저 생성되어야 합니다.

// 외부에서 멤버 내부 클래스 인스턴스 생성
BankAccount account = new BankAccount("123-456", "김개발", 50000);
BankAccount.Transaction transaction = account.new Transaction("DEPOSIT", 10000, "월급");

// 외부 클래스 내부에서는 일반 클래스처럼 생성
public void processTransaction(String type, double amount, String description) {
    Transaction transaction = new Transaction(type, amount, description);
    transaction.execute();
}

메모리 관련 주의사항

멤버 내부 클래스는 외부 클래스 인스턴스에 대한 숨겨진 참조를 유지합니다. 이로 인해 메모리 누수가 발생할 수 있어요.

public class OuterClass {
    private List<String> data = new ArrayList<>();
    
    class InnerClass {
        public void doSomething() {
            // 외부 클래스 참조 유지
        }
    }
    
    public InnerClass createInner() {
        return new InnerClass(); // 외부 클래스 참조가 같이 반환됨
    }
}

 

만약 내부 클래스가 외부 클래스 멤버에 접근할 필요가 없다면 static을 붙여서 정적 중첩 클래스로 만드는 것이 좋습니다.

로컬 클래스 (Local Class)

로컬 클래스는 메소드 내부에서 선언되는 클래스입니다. 지역 변수와 마찬가지로 해당 메소드 내에서만 사용할 수 있어요.
 

로컬 클래스의 특징

1. 메소드 스코프 내에서만 존재
로컬 클래스는 선언된 메소드가 실행되는 동안에만 유효합니다.

public class TaskManager {
    private String managerName;
    
    public void executeTask(String taskName, int priority) {
        // 로컬 변수
        final String executionTime = new Date().toString();
        int taskId = generateTaskId();
        
        // 로컬 클래스 선언
        class Task {
            private String name;
            private int taskPriority;
            
            public Task(String name, int priority) {
                this.name = name;
                this.taskPriority = priority;
            }
            
            public void execute() {
                // 외부 클래스의 인스턴스 멤버 접근 가능
                System.out.println("관리자: " + managerName);
                
                // 로컬 변수 접근 (effectively final)
                System.out.println("실행 시간: " + executionTime);
                System.out.println("작업 ID: " + taskId);
                System.out.println("작업명: " + name + ", 우선순위: " + taskPriority);
            }
        }
        
        // 로컬 클래스 사용
        Task task = new Task(taskName, priority);
        task.execute();
    }
    
    private int generateTaskId() {
        return (int)(Math.random() * 10000);
    }
}

 

2. 로컬 변수 캡처 (Variable Capture)
로컬 클래스는 자신을 둘러싸고 있는 메소드의 로컬 변수에 접근할 수 있는데, 이때 해당 변수들은 effectively final이어야 합니다.

public class PriceCalculator {
    public Runnable createCalculationTask(int basePrice, double taxRate) {
        // 로컬 변수들 (effectively final)
        String currency = "KRW";
        int discountAmount = 1000;
        
        // 로컬 클래스
        class CalculationTask implements Runnable {
            @Override
            public void run() {
                // 로컬 변수들이 캡처됨
                double finalPrice = (basePrice - discountAmount) * (1 + taxRate);
                System.out.println("최종 가격: " + finalPrice + " " + currency);
            }
        }
        
        return new CalculationTask();
    }
}

 
3. 실무에서의 활용
로컬 클래스는 주로 메소드 내에서 특정 로직을 캡슐화하고 싶을 때 사용합니다.

public class DataProcessor {
    public void processUserData(List<User> users) {
        final String processingDate = LocalDate.now().toString();
        
        // 사용자 검증을 위한 로컬 클래스
        class UserValidator {
            public boolean isValid(User user) {
                return user.getName() != null && 
                       user.getEmail().contains("@") && 
                       user.getAge() >= 18;
            }
            
            public void logValidation(User user, boolean isValid) {
                String status = isValid ? "VALID" : "INVALID";
                System.out.println(processingDate + " - " + user.getName() + ": " + status);
            }
        }
        
        UserValidator validator = new UserValidator();
        
        List<User> validUsers = users.stream()
            .filter(user -> {
                boolean valid = validator.isValid(user);
                validator.logValidation(user, valid);
                return valid;
            })
            .collect(Collectors.toList());
            
        System.out.println("처리된 유효 사용자 수: " + validUsers.size());
    }
}

익명 클래스 (Anonymous Class)

익명 클래스는 이름이 없는 클래스로, 클래스 정의와 객체 생성을 동시에 수행합니다. 주로 일회성으로 사용할 클래스가 필요할 때 활용해요.
 

익명 클래스의 특징

1. 클래스 선언과 인스턴스 생성을 동시에
익명 클래스는 다음과 같은 구조를 가집니다.

// 인터페이스 구현
new 인터페이스명() {
    // 메소드 구현
};

// 클래스 상속
new 부모클래스명() {
    // 메소드 오버라이드
};

 
실무 활용 예제

GUI 이벤트 처리에서 익명 클래스가 자주 사용됩니다.

public class OrderSystem {
    private List<Order> orders = new ArrayList<>();
    
    public void setupEventHandlers() {
        // 주문 완료 이벤트 핸들러
        EventHandler orderCompleteHandler = new EventHandler() {
            @Override
            public void handle(Event event) {
                Order order = (Order) event.getData();
                System.out.println("주문 완료: " + order.getOrderId());
                sendConfirmationEmail(order);
                updateInventory(order);
            }
        };
        
        // 결제 실패 이벤트 핸들러
        EventHandler paymentFailedHandler = new EventHandler() {
            @Override
            public void handle(Event event) {
                Order order = (Order) event.getData();
                System.out.println("결제 실패: " + order.getOrderId());
                order.setStatus("PAYMENT_FAILED");
                notifyCustomer(order);
            }
        };
        
        EventManager.registerHandler("ORDER_COMPLETE", orderCompleteHandler);
        EventManager.registerHandler("PAYMENT_FAILED", paymentFailedHandler);
    }
    
    // 쓰레드 생성에서의 익명 클래스 활용
    public void processOrdersAsync() {
        Thread processingThread = new Thread() {
            @Override
            public void run() {
                for (Order order : orders) {
                    try {
                        processOrder(order);
                        Thread.sleep(100); // 시뮬레이션
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        };
        
        processingThread.start();
    }
}

 
Comparator에서의 활용

익명 클래스는 정렬 로직을 구현할 때도 매우 유용합니다.

public class ProductManager {
    public void sortProducts(List<Product> products, String criteria) {
        switch (criteria) {
            case "PRICE":
                Collections.sort(products, new Comparator<Product>() {
                    @Override
                    public int compare(Product p1, Product p2) {
                        return Double.compare(p1.getPrice(), p2.getPrice());
                    }
                });
                break;
                
            case "RATING":
                Collections.sort(products, new Comparator<Product>() {
                    @Override
                    public int compare(Product p1, Product p2) {
                        return Double.compare(p2.getRating(), p1.getRating()); // 내림차순
                    }
                });
                break;
                
            case "NAME":
                Collections.sort(products, new Comparator<Product>() {
                    @Override
                    public int compare(Product p1, Product p2) {
                        return p1.getName().compareTo(p2.getName());
                    }
                });
                break;
        }
    }
}

 
익명 클래스 vs 람다 표현식

Java 8 이후로는 함수형 인터페이스의 경우 람다 표현식을 사용할 수 있습니다. 하지만 익명 클래스만 사용할 수 있는 경우도 있어요.

// 추상 클래스 상속 - 익명 클래스만 가능
abstract class AbstractProcessor {
    abstract void process();
    void commonMethod() {
        System.out.println("공통 메소드");
    }
}

AbstractProcessor processor = new AbstractProcessor() {
    @Override
    void process() {
        System.out.println("처리 로직");
        commonMethod(); // 부모 클래스의 메소드 호출 가능
    }
};

// 함수형 인터페이스 - 람다로 간단하게
Runnable task1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("익명 클래스");
    }
};

Runnable task2 = () -> System.out.println("람다 표현식");

내부 클래스 사용 시 고려사항

메모리 누수 방지

비정적 내부 클래스는 외부 클래스에 대한 참조를 유지하므로 메모리 누수에 주의해야 합니다.

public class OuterClass {
    private byte[] largeData = new byte[1024 * 1024]; // 1MB
    
    // 잘못된 예: 외부 클래스 참조로 인한 메모리 누수 위험
    class ProblematicInner {
        public void doSomething() {
            // 외부 클래스 멤버를 사용하지 않음에도 참조 유지
        }
    }
    
    // 올바른 예: static으로 선언하여 외부 참조 제거
    static class BetterInner {
        public void doSomething() {
            // 외부 클래스 멤버가 필요 없다면 static 사용
        }
    }
}

 
적절한 접근 제어자 사용

내부 클래스는 적절한 접근 제어자를 사용하여 캡슐화를 강화할 수 있습니다.

public class DatabaseConnection {
    private String connectionString;
    private boolean isConnected;
    
    // private 내부 클래스로 구현 세부사항 숨김
    private class ConnectionPool {
        private List<Connection> availableConnections;
        private List<Connection> usedConnections;
        
        private ConnectionPool() {
            this.availableConnections = new ArrayList<>();
            this.usedConnections = new ArrayList<>();
        }
        
        private Connection getConnection() {
            // 연결 풀 로직
            return null;
        }
    }
    
    private ConnectionPool pool = new ConnectionPool();
    
    public Connection getConnection() {
        return pool.getConnection();
    }
}

 
성능 고려사항

내부 클래스 사용 시 성능도 고려해야 합니다.

  • 정적 중첩 클래스: 외부 인스턴스 없이 독립적으로 생성 가능하므로 메모리 효율적
  • 멤버 내부 클래스: 외부 인스턴스 참조로 인한 추가 메모리 사용
  • 로컬/익명 클래스: 변수 캡처로 인한 추가 오버헤드

실무에서의 활용 패턴

1. Builder 패턴에서의 정적 중첩 클래스

public class User {
    private final String name;
    private final String email;
    private final int age;
    private final String address;
    
    private User(Builder builder) {
        this.name = builder.name;
        this.email = builder.email;
        this.age = builder.age;
        this.address = builder.address;
    }
    
    public static class Builder {
        private String name;
        private String email;
        private int age;
        private String address;
        
        public Builder setName(String name) {
            this.name = name;
            return this;
        }
        
        public Builder setEmail(String email) {
            this.email = email;
            return this;
        }
        
        public Builder setAge(int age) {
            this.age = age;
            return this;
        }
        
        public Builder setAddress(String address) {
            this.address = address;
            return this;
        }
        
        public User build() {
            return new User(this);
        }
    }
}

// 사용법
User user = new User.Builder()
    .setName("김개발")
    .setEmail("kim@example.com")
    .setAge(30)
    .setAddress("서울시 강남구")
    .build();

 
2. Iterator 패턴에서의 멤버 내부 클래스

public class CustomList<T> {
    private List<T> items = new ArrayList<>();
    
    public void add(T item) {
        items.add(item);
    }
    
    public Iterator<T> iterator() {
        return new CustomIterator();
    }
    
    private class CustomIterator implements Iterator<T> {
        private int currentIndex = 0;
        
        @Override
        public boolean hasNext() {
            return currentIndex < items.size();
        }
        
        @Override
        public T next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return items.get(currentIndex++);
        }
    }
}

 
내부 클래스는 Java 프로그래밍에서 코드의 구조화와 캡슐화를 위한 강력한 도구입니다. 각각의 특성을 잘 이해하고 적절한 상황에서 사용한다면, 더 깔끔하고 유지보수하기 쉬운 코드를 작성할 수 있을 거예요.
 
특히 메모리 누수 방지와 적절한 접근 제어자 사용에 주의하면서, 실무에서 이런 패턴들을 활용해보시길 바랍니다. 처음에는 조금 복잡해 보일 수 있지만, 한 번 익숙해지면 정말 유용한 기능이라는 것을 느끼실 수 있을 거예요!

Java 다형성과 동적 디스패치 완벽 가이드: 개발자가 알아야 할 핵심 개념

Java를 공부하면서 객체지향 프로그래밍의 핵심 개념 중 하나인 다형성(Polymorphism)에 대해 깊이 있게 알아보려고 합니다. 이번 포스팅에서는 다형성의 기본 개념부터 동적 디스패치, 업캐스팅과

byteandbit.tistory.com

반응형