본문 바로가기

멋쟁이사자처럼_부트캠프/Java

[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 10일차 내부 클래스, 익명 개체, 자주 사용되는 클래스, 제네릭

내부 클래스(Inner Class)

 내부 클래스(Inner Class)는 다른 클래스 내부에 정의된 클래스를 의미. 자바에서 내부 클래스는 외부 클래스와 강하게 연관된 객체를 생성하거나, 외부 클래스의 멤버에 쉽게 접근하기 위해 사용. 내부 클래스는 일반적으로 외부 클래스와 함께 동작해야 하는 경우에 적합하다.

 

내부 클래스의 종류

  1. 인스턴스 클래스 (Instance Inner Class)
    • 외부 클래스의 인스턴스와 연결되어 있으며, 외부 클래스의 멤버(필드, 메서드)에 쉽게 접근 가능.
    • 주로 외부 클래스 객체의 특정 작업을 도와주는 용도로 사용.
  2. 정적 클래스 (Static Nested Class)
    • 정적(static)으로 선언된 내부 클래스.
    • 외부 클래스의 인스턴스 없이 독립적으로 사용 가능.
    • 외부 클래스의 정적 멤버에만 접근 가능.
  3. 지역 클래스 (Local Inner Class)
    • 메서드나 블록 내부에 정의된 클래스.
    • 해당 메서드나 블록 내에서만 사용 가능.
  4. 익명 클래스 (Anonymous Inner Class)
    • 이름이 없는 일회용 클래스.
    • 주로 인터페이스나 추상 클래스를 구현하거나 상속받아 특정 기능을 간단히 정의할 때 사용.

내부 클래스 사용법

1. 인스턴스 클래스

class Outer {
    private String message = "Hello from Outer";

    class Inner {
        void displayMessage() {
            System.out.println(message); // 외부 클래스의 멤버 접근
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner(); // 외부 클래스 객체로 내부 클래스 객체 생성
        inner.displayMessage();
    }
}

 

2. 정적 클래스

class Outer {
    static String message = "Hello from Outer";

    static class Inner {
        void displayMessage() {
            System.out.println(message); // 정적 멤버 접근
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Outer.Inner inner = new Outer.Inner(); // 외부 클래스 객체 없이 생성 가능
        inner.displayMessage();
    }
}

 

3. 지역 클래스(로컬 내부 클래스)

class Outer {
    void display() {
        class LocalInner {
            void show() {
                System.out.println("Inside Local Inner Class");
            }
        }
        LocalInner localInner = new LocalInner();
        localInner.show();
    }
}

public class Main {
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.display();
    }
}

 

4. 익명 클래스

interface Greet {
    void sayHello();
}

public class Main {
    public static void main(String[] args) {
        Greet greet = new Greet() { // 익명 클래스
            @Override
            public void sayHello() {
                System.out.println("Hello from Anonymous Inner Class");
            }
        };
        greet.sayHello();
    }
}

 

내부 클래스의 장점

  1. 캡슐화 강화:
    • 외부 클래스와 강하게 결합된 클래스의 구현을 내부 클래스로 정의하여 캡슐화 수준을 높임.
  2. 외부 클래스 멤버 접근 용이:
    • 내부 클래스는 외부 클래스의 모든 멤버(정적/비정적 포함)에 접근 가능.
  3. 가독성 향상:
    • 외부 클래스와 관련된 로직을 한 곳에 그룹화하여 코드 가독성과 유지보수성을 향상.
  4. 독립적 사용 가능 (정적 클래스):
    • static으로 선언된 내부 클래스는 외부 클래스와 독립적으로 사용 가능.

내부 클래스의 단점

  1. 코드 복잡성 증가:
    • 중첩된 구조로 인해 코드가 복잡해질 수 있음.
  2. 남용 주의:
    • 간단히 외부 클래스와 강하게 결합된 경우가 아니면 사용하지 않는 것이 좋음.

내부 클래스의 활용 예

  1. UI 이벤트 처리:
    • GUI 프로그래밍에서 버튼 클릭 이벤트 같은 이벤트 핸들러로 익명 클래스 사용.
  2. 컬렉션 프레임워크:
    • 자바 컬렉션 프레임워크에서 내부 클래스를 활용하여 Iterator 구현.
  3. 캡슐화:
    • 외부 클래스에 강하게 의존적인 클래스들을 내부 클래스로 정의하여 캡슐화 수준 향상.
  4. 유틸리티 메서드:
    • 외부 클래스의 일부 메서드 로직을 내부 클래스로 분리하여 관리.

정리

내부 클래스는 외부 클래스와 긴밀한 관계를 가지며 특정 작업을 처리하기 위해 유용하게 사용됨. 적절히 사용하면 코드 가독성과 재사용성을 높이지만, 불필요하게 남용하면 코드가 복잡해질 수 있으므로 신중하게 사용하는 것이 중요함.

 

 


 

익명 객체

익명 객체(Anonymous Object)는 클래스 이름이 없이 생성된 객체를 의미함. 주로 익명 내부 클래스와 함께 사용되며, 한 번만 사용하거나 특정 기능을 단순히 구현하기 위해 정의될 때 사용됨.

익명 객체는 재사용이 필요 없고 간단한 작업을 처리할 때 유용하며, 이름 없는 일회성 객체로 간결하고 읽기 쉬운 코드를 작성할 수 있음.

 

익명 객체와 익명 내부 클래스

익명 객체는 익명 내부 클래스의 결과물로 생성되는 경우가 많음. 익명 내부 클래스는 클래스 정의와 동시에 객체를 생성하므로, 별도의 이름을 갖지 않음.

 

예시

interface Greet {
    void sayHello();
}

public class Main {
    public static void main(String[] args) {
        // 익명 객체 생성 (익명 내부 클래스 활용)
        Greet greet = new Greet() { // 이름 없는 클래스
            @Override
            public void sayHello() {
                System.out.println("Hello from Anonymous Object!");
            }
        };

        // 객체 호출
        greet.sayHello();
    }
}

 

익명 객체의 주요 특징

  1. 클래스 이름 없음:
    • 익명 내부 클래스 정의와 함께 객체를 생성.
    • 클래스 정의는 한 번 사용하고 더 이상 사용되지 않음.
  2. 단일 인스턴스:
    • 익명 객체는 일반적으로 한 번만 사용되며, 재사용되지 않음.
  3. 간결한 코드:
    • 불필요한 클래스 선언 없이 특정 기능을 간단히 구현 가능.
  4. 인터페이스나 추상 클래스 구현:
    • 주로 인터페이스를 구현하거나 추상 클래스를 확장하여 동작 정의.
  5. 제한된 범위:
    • 익명 객체는 해당 생성된 블록 내에서만 사용 가능.

익명 객체의 활용 예

1. 이벤트 핸들러

익명 객체는 GUI 프로그래밍에서 버튼 클릭 이벤트와 같은 일회성 이벤트 핸들러에 자주 사용됨.

import javax.swing.*;

public class Main {
    public static void main(String[] args) {
        JButton button = new JButton("Click Me");

        // 익명 객체를 사용하여 이벤트 처리
        button.addActionListener(e -> System.out.println("Button Clicked!"));

        JFrame frame = new JFrame();
        frame.add(button);
        frame.setSize(300, 200);
        frame.setVisible(true);
    }
}

 

2. 스레드 처리

익명 객체를 사용하여 간단히 스레드를 정의하고 실행할 수 있음.

public class Main {
    public static void main(String[] args) {
        // 익명 객체로 Runnable 구현
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread is running...");
            }
        });

        thread.start();
    }
}

 

3. 컬렉션 정렬

익명 객체를 사용하여 Comparator를 구현해 간단히 정렬을 수행할 수 있음.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("John", "Alice", "Bob");

        // 익명 객체로 Comparator 구현
        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.compareTo(o2);
            }
        });

        System.out.println(names);
    }
}

 

익명 객체의 장점

  1. 코드 간결성:
    • 클래스 정의와 객체 생성을 동시에 처리하여 코드가 더 간결해짐.
  2. 일회성 작업에 적합:
    • 재사용이 필요 없는 간단한 작업에 적합.
  3. 추가 파일 필요 없음:
    • 별도의 클래스 파일 정의가 필요하지 않아 관리가 용이.

익명 객체의 단점

  1. 디버깅 어려움:
    • 익명 객체의 클래스 이름이 없기 때문에 디버깅이 복잡할 수 있음.
  2. 가독성 저하:
    • 지나치게 사용하면 코드의 구조가 복잡해질 수 있음.
  3. 재사용 불가:
    • 익명 객체는 한 번만 사용 가능하므로, 동일한 동작이 반복적으로 필요하면 별도의 클래스 정의가 더 적합.

정리

익명 객체는 한 번만 사용되며, 간단한 작업이나 인터페이스 구현에 유용함. 하지만 남용하면 코드의 유지보수성과 가독성이 떨어질 수 있으므로, 적절한 경우에만 사용해야 함.

 

 


자주 사용하는 클래스

1. Object 클래스

  • 설명:
    Object는 Java 클래스의 최상위 클래스이며, 모든 클래스는 암묵적으로 Object를 상속받음.
    Java 객체의 공통된 기능(메서드)을 정의하는 기반 클래스.
  • 주요 메서드:
    • toString(): 객체의 문자열 표현을 반환.
    • equals(Object obj): 객체 비교를 위한 메서드.
    • hashCode(): 객체의 해시 코드를 반환.
    • clone(): 객체 복사.
    • getClass(): 객체의 런타임 클래스를 반환.

2. String 클래스

  • 설명:
    문자열을 처리하기 위한 불변(immutable) 클래스. 문자열을 수정할 때 새로운 객체를 생성함.
  • 특징:
    • 변경 불가(immutable): 한 번 생성된 문자열은 변경되지 않음.
    • 값이 동일하면 String Pool에서 같은 객체를 공유.
  • 주요 메서드:
    • length(): 문자열 길이 반환.
    • charAt(int index): 특정 인덱스의 문자 반환.
    • substring(int beginIndex, int endIndex): 부분 문자열 반환.
    • concat(String str): 문자열 결합.
    • equals(Object obj): 문자열 내용 비교.

3. StringBuilder 및 StringBuffer 클래스

  • 설명:
    가변(mutable) 문자열을 처리하기 위한 클래스.
    • StringBuilder: 싱글 스레드 환경에서 빠르게 동작.
    • StringBuffer: 멀티스레드 환경에서 동기화를 지원하여 안전하게 사용 가능.
  • 차이점:
    • StringBuilder는 동기화를 지원하지 않아 더 빠름.
    • StringBuffer는 동기화를 지원하므로 멀티스레드 환경에서 적합.
  • 주요 메서드:
    • append(String str): 문자열 추가.
    • insert(int offset, String str): 특정 위치에 문자열 삽입.
    • delete(int start, int end): 특정 범위의 문자열 삭제.
    • reverse(): 문자열 뒤집기.
    • toString(): String으로 변환.

4. Math 클래스

  • 설명:
    수학 연산을 위한 정적 메서드들을 제공하는 유틸리티 클래스.
  • 주요 메서드:
    • abs(int a): 절댓값.
    • max(int a, int b) / min(int a, int b): 최대값 / 최소값.
    • pow(double a, double b): 거듭제곱 계산.
    • sqrt(double a): 제곱근 계산.
    • random(): 0.0 이상 1.0 미만의 난수를 반환.
    • ceil(double a) / floor(double a): 올림 / 내림.

5. Throwable 클래스

  • 설명:
    예외 처리 계층의 최상위 클래스.
    • 모든 예외(Exception)와 에러(Error)의 부모 클래스.
  • 주요 서브클래스:
    • Exception: 일반적인 예외. 프로그램에서 복구 가능.
      • 예: IOException, SQLException, NullPointerException.
    • Error: 심각한 시스템 오류. 복구 불가능.
      • 예: OutOfMemoryError, StackOverflowError.
  • 주요 메서드:
    • getMessage(): 예외 메시지 반환.
    • printStackTrace(): 예외의 스택 트레이스를 출력.
    • getCause(): 예외의 원인 반환.

 

 

웹 프로그래밍에서 자주 사용하는 Java 클래스


1. HTTP 요청 및 응답 처리

  • HttpServletRequest
    클라이언트 요청 정보를 담는 객체. URL, 파라미터, 헤더 등을 가져올 수 있음.
    예: request.getParameter("name");
  • HttpServletResponse
    서버에서 클라이언트로 응답을 보내는 객체. HTTP 상태 코드 설정, 응답 데이터 작성 등이 가능.
    예: response.setStatus(HttpServletResponse.SC_OK);

2. 세션 및 쿠키 관리

  • HttpSession
    클라이언트와 서버 간 세션 관리를 위한 객체. 사용자 정보를 유지하는 데 사용.
    예: session.setAttribute("user", user);
  • Cookie
    클라이언트 브라우저에 저장되는 데이터 객체. 로그인 상태 유지 등에 사용.
    예: Cookie cookie = new Cookie("sessionId", "12345");

3. 데이터베이스 연결

  • DriverManager
    JDBC 드라이버를 통해 데이터베이스 연결 관리.
    예: Connection conn = DriverManager.getConnection(url, username, password);
  • PreparedStatement
    SQL 쿼리를 실행하기 위한 객체. 파라미터화된 쿼리 지원.
    예: PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
  • ResultSet
    SQL 쿼리 실행 결과를 담는 객체.
    예: while (rs.next()) { String name = rs.getString("name"); }

4. 파일 및 IO 처리

  • File
    파일이나 디렉토리를 생성, 삭제, 읽기, 쓰기 관리.
    예: File file = new File("example.txt");
  • BufferedReader / BufferedWriter
    파일 또는 소켓으로부터 데이터를 읽거나 쓰는 데 사용.
    예: BufferedReader br = new BufferedReader(new FileReader(file));
  • InputStream / OutputStream
    바이너리 데이터를 처리하는 데 사용. 파일 업로드/다운로드 등.
    예: InputStream in = new FileInputStream("image.jpg");

5. JSON 및 데이터 포맷 처리

  • ObjectMapper (Jackson 라이브러리)
    JSON 데이터를 Java 객체로 변환하거나 역으로 변환.
    예: ObjectMapper mapper = new ObjectMapper();
  • Gson (Google GSON 라이브러리)
    JSON 데이터를 Java 객체로 변환하거나 역으로 변환.
    예: Gson gson = new Gson();

6. 날짜와 시간 처리

  • LocalDate / LocalTime / LocalDateTime
    Java 8 이상에서 제공하는 날짜 및 시간 처리 클래스.
    예: LocalDate.now();
  • SimpleDateFormat
    날짜와 시간을 특정 형식으로 변환.
    예: SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

7. 컬렉션 및 데이터 처리

  • ArrayList / HashMap
    데이터를 저장하고 조작하는 데 자주 사용되는 클래스.
    예:
    java
    코드 복사
    List<String> list = new ArrayList<>(); Map<String, String> map = new HashMap<>();

8. 네트워크 및 소켓 통신

  • HttpURLConnection
    HTTP 요청 및 응답 처리를 위한 클래스.
    예: HttpURLConnection conn = (HttpURLConnection) url.openConnection();
  • Socket / ServerSocket
    네트워크 소켓 통신을 구현할 때 사용.
    예: Socket socket = new Socket("localhost", 8080);

9. 웹 프레임워크 관련 클래스

(Spring과 같은 프레임워크를 사용하는 경우)

  • @Controller / @RestController
    HTTP 요청을 처리하는 클래스.
  • @Service
    비즈니스 로직 처리를 위한 클래스.
  • @Repository
    데이터 액세스 레이어 클래스.

제네릭

제네릭(Generics)은 Java에서 데이터 타입을 일반화(generalize)하여 클래스, 인터페이스, 메서드를 정의할 수 있는 기능.
컴파일 시 타입 안정성을 보장하고, 코드 재사용성을 높이기 위해 도입됨.

1. 컴파일러 수준에서의 타입 안정성 제공

  • 제네릭은 Java 5에서 도입되었으며, 컴파일러가 타입을 체크하고 코드의 타입 안정성을 보장하기 위해 만들어짐.
  • 제네릭은 "타입 소거(Type Erasure)"라는 개념을 기반으로 동작. 컴파일 시점에만 타입을 체크하고, 런타임에는 실제 타입 정보가 제거됨.

2. Type Erasure (타입 소거)

  • 제네릭 코드가 컴파일되면, 컴파일러는 제네릭 타입을 제거하고 일반적인 타입(Object)으로 대체함.
  • 필요 시, 타입 캐스팅 코드가 자동으로 삽입되어 런타임 호환성을 유지함.

3. 제네릭 도입 이전의 문제점

  • 컬렉션 등에서 데이터 타입을 명시하지 않으면, Object 타입으로 처리해야 했고, 명시적 캐스팅이 필수였음.
  • 이로 인해 런타임 오류 가능성이 증가.
List list = new ArrayList();
list.add("Hello");
Integer num = (Integer) list.get(0); // 런타임 오류 발생

 

제네릭의 주요 특징

  1. 타입 안정성 제공
    • 제네릭을 사용하면 컴파일 타임에 데이터 타입을 검사할 수 있어 런타임 오류를 줄임.
    • 예: 잘못된 타입을 추가하거나 사용할 경우 컴파일 에러 발생.
  2. 형 변환 불필요
    • 제네릭을 사용하면 객체를 가져올 때 명시적으로 형 변환(casting)을 할 필요가 없음.
  3. 코드 재사용성 증가
    • 다양한 데이터 타입에 대해 동작하는 코드를 작성할 수 있음.

제네릭 사용 예시

1. 제네릭 클래스

// 제네릭 클래스 정의
public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

// 제네릭 클래스 사용
public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setItem("Hello");
        System.out.println("String: " + stringBox.getItem());

        Box<Integer> intBox = new Box<>();
        intBox.setItem(123);
        System.out.println("Integer: " + intBox.getItem());
    }
}

 

2. 제네릭 메서드

// 제네릭 메서드 정의
public class Utils {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

// 제네릭 메서드 사용
public class Main {
    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"A", "B", "C"};

        Utils.printArray(intArray);  // 출력: 1 2 3 4 5
        Utils.printArray(strArray); // 출력: A B C
    }
}

 

3. 제네릭 인터페이스

// 제네릭 인터페이스 정의
public interface Pair<K, V> {
    K getKey();
    V getValue();
}

// 제네릭 인터페이스 구현
public class KeyValue<K, V> implements Pair<K, V> {
    private K key;
    private V value;

    public KeyValue(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

// 제네릭 인터페이스 사용
public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new KeyValue<>("Age", 25);
        System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
    }
}

 

 

제네릭에서 와일드카드 사용

  1. ? (와일드카드): 제네릭 타입의 한정되지 않은 표현.
    • 사용 예: List<?>, List<? extends Number>, List<? super Integer>.
  2. 와일드카드 종류:
    • ?: 모든 타입 허용.
    • ? extends T: 타입 T와 그 하위 클래스만 허용.
    • ? super T: 타입 T와 그 상위 클래스만 허용.
public class WildcardExample {
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }

    public static void main(String[] args) {
        List<String> stringList = List.of("A", "B", "C");
        List<Integer> intList = List.of(1, 2, 3);

        printList(stringList); // 출력: A B C
        printList(intList);    // 출력: 1 2 3
    }
}

 

제네릭의 이점

  1. 컴파일 타임 타입 체크
    잘못된 타입의 데이터가 사용되지 않도록 사전에 방지 가능.
  2. 타입 변환 감소
    제네릭을 사용하면 명시적인 형 변환 필요 없음.
  3. 코드 재사용성 향상
    다양한 데이터 타입을 처리하는 범용 코드를 작성 가능.

웹 프로그래밍에서 제네릭의 사용처

웹 프로그래밍에서는 다양한 계층(프론트엔드와 백엔드 간의 데이터 교환, 데이터베이스 작업 등)에서 제네릭을 활용함.

1. 데이터 전송 객체(DTO)

  • 서버와 클라이언트 간 데이터를 전송할 때, 제네릭으로 타입 안정성을 보장하며 다양한 데이터 타입 처리 가능.
public class ApiResponse<T> {
    private String status;
    private T data;

    public ApiResponse(String status, T data) {
        this.status = status;
        this.data = data;
    }

    public String getStatus() {
        return status;
    }

    public T getData() {
        return data;
    }
}

// 사용 예
ApiResponse<String> response = new ApiResponse<>("success", "User created");

 

2. 데이터베이스 작업

  • ORM (Object-Relational Mapping) 도구(JPA, Hibernate 등)에서 제네릭을 사용하여 데이터 타입과 엔터티 간 매핑을 유연하게 처리.
public interface JpaRepository<T, ID> {
    T findById(ID id);
    List<T> findAll();
}

// 사용 예
JpaRepository<User, Long> userRepository;

 

3. 컬렉션과 데이터 관리

  • 웹 애플리케이션에서 데이터를 저장하거나 관리하는 데 사용하는 List, Map, Set 등의 컬렉션에서 제네릭을 사용하여 타입 안정성 제공.
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");

 

4. REST API 요청/응답 처리

  • 제네릭을 활용하여 다양한 타입의 요청과 응답을 처리하는 메서드를 작성 가능.
public class RestService<T> {
    public T sendRequest(String url, T payload) {
        // 요청 처리 로직
        return payload;
    }
}

 

 

5. 커스텀 유틸리티 클래스

  • 웹 프로그래밍에서 제네릭 기반의 유틸리티 클래스를 만들어 코드 재사용성을 높임.
public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

// 사용 예
Pair<String, Integer> userAge = new Pair<>("Alice", 30);

 

6. JSON 파싱 및 데이터 직렬화

  • JSON 데이터 파싱 라이브러리(예: Jackson, Gson)에서 제네릭을 활용하여 다양한 데이터 타입 처리.
public <T> T parseJson(String json, Class<T> clazz) {
    return new Gson().fromJson(json, clazz);
}

 

요약

  • 제네릭은 타입 안정성과 코드 재사용성을 보장하기 위해 만들어졌으며, 타입 소거(Type Erasure)라는 원리를 기반으로 동작.
  • 웹 프로그래밍에서는 DTO, 데이터베이스 작업, 컬렉션 관리, REST API 처리, 유틸리티 클래스 작성 등 다양한 곳에서 제네릭이 사용됨.
  • 제네릭을 활용하면 코드의 유지보수성과 안정성이 크게 향상됨.

 

제네릭은 Java의 타입 안정성과 코드 재사용성을 극대화하기 위한 강력한 도구로, 특히 컬렉션과 유틸리티 클래스에서 자주 활용됨.