Java 핵심 개념과 예외 처리

웃음인·2025년 3월 17일

Java

목록 보기
29/37
post-thumbnail

📌 문자열, 예외, 제네릭

String literal과 new String(””)의 차이

🗂️ new String(””)

  • new 연산자를 사용하여 String을 생성하게 되면
    Heap 메모리에 새 객체 생성(String Pool과 별개)
  • == 비교 시 false (새로운 객체)

🗂️ String literal

  • 리터럴(literal)을 사용하여 String을 생성하게 되면
    String constant pool(문자열 상수 풀)이라는 영역에 생성
  • constant pool에 같은 값이 존재한다면
    생성되는 객체는 이미 존재하고 있는 값을 참조 (메모리 절약)
  • == 비교 시 true (같은 객체 참조)

  • literal로 생성한 str2와 str3은 동일한 객체를 바라보지만,
    new String()으로 생성한 str1은 다른 메모리 주소의 객체를 바라본다.

  • String literal로 생성한 객체는 "String constant pool"에 들어간다.

  • String literal 로 생성한 객체가 이미 "String constant pool"영역에 존재한다면,
    해당 객체는 이미 생성되어 있는 String constant pool의 reference를 참조한다.

  • new 연산자로 생성한 String 객체는 같은 값이 String pool에 존재하더라도,
    Heap영역에 별도로 객체를 생성한다.

가능하면 String 리터럴을 사용하는 것이 메모리를 절약하는 좋은 습관! 😊

String, StringBuilder, StringBuffer의 차이점

이 클래스들의 공통점은 모두 String(문자열)을 저장하고 관리하는 클래스이다.
이들의 차이는 변경 가능 여부(Mutability)와 동기화(Synchronization)에 있다.

🔹 String (불변, Immutable)
     문자열이 변하지 않는다면 String 사용!

String str = "Hello";
str += " World";  // 새로운 객체 생성
  • 불변(Immutable): 문자열 변경 시 새로운 객체 생성 → 메모리 낭비 발생 가능
  • 멀티스레드 안전(Thread-Safe): 여러 스레드에서 안전하게 사용 가능하지만 성능이 낮음

🔹 StringBuilder (가변, Mutable, 비동기)
     문자열이 자주 변경된다면 StringBuilder(단일 스레드) 사용!

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // 기존 객체 수정
  • 가변(Mutable): 기존 객체를 수정하여 메모리 낭비 최소화
  • 비동기(Non-Synchronized): 멀티스레드 환경에서 안전하지 않지만,
                                       단일 스레드에서 성능이 좋음
비동기(non-synchronized)
: 여러 개의 쓰레드가 동시에 접근할 수 있음
  → 빠르지만 안전하지 않을 수 있음

🔹 StringBuffer (가변, Mutable, 동기화)
     문자열이 자주 변경되면서 멀티스레드 환경이라면 StringBuffer 사용!

StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World");  // 기존 객체 수정
  • 가변(Mutable): 기존 객체를 수정하여 메모리 효율적
  • 동기화(Synchronized): 멀티스레드 환경에서 안전하지만 StringBuilder보다 속도가 느림
동기(synchronized)
: 여러 개의 쓰레드(멀티스레드 환경)가 하나의 메서드나 리소스에
  동시에 접근하지 못하도록 막는 것
  → 안전하지만 느림

다시 말해서, String 클래스는 StringBuffer 클래스나 StringBuilder 클래스와 다르게
리터럴을 통해 생성되면 그 인스턴스의 메모리 공간은 절대 변하지 않는다.😊

예외(Exception)와 에러(Error)의 차이

ExceptionError는 모두 Throwable 클래스를 상속받지만, 발생 원인과 처리 방식이 다르다.

예외(Exception)

  • 프로그램 실행 중 예외적인 상황 발생
  • 개발자가 예상하고 처리할 수 있는 문제
  • try-catch로 예외 처리 가능
  • ex) NullPointerException, IOException, SQLException

에러(Error)

  • 시스템 레벨에서 발생하는 치명적인 오류
  • JVM 실행 중 발생하는 심각한 문제
  • 일반적으로 복구 불가능 (프로그램 종료)
  • ex) OutOfMemoryError, StackOverflowError

Exception 클래스의 예시

Exception 클래스는 다양한 하위 클래스를 가지며,
주로 Checked ExceptionUnchecked Exception으로 나뉩니다.

1️⃣ Checked Exception (컴파일러가 예외 처리를 강제)
     반드시 try-catch 또는 throws로 처리해야 함!

예외 클래스설명
<IOException>입출력 작업 중 오류 발생 (파일, 네트워크 등)
<SQLException>데이터베이스 관련 오류
<FileNotFoundException>존재하지 않는 파일을 읽으려 할 때
<InterruptedException>쓰레드 실행 중 인터럽트 발생
<ClassNotFoundException>특정 클래스를 찾을 수 없을 때

2️⃣ Unchecked Exception (런타임 예외, 예외 처리가 강제되지 않음)
     RuntimeException을 상속받으며, 개발자가 코드에서 직접 방어해야 함!

예외 클래스설명
<NullPointerException>null 값을 참조할 때
<ArrayIndexOutOfBoundsException>배열 인덱스 초과 접근
<ArithmeticException>숫자 연산 오류 (ex. 0으로 나누기)
<IllegalArgumentException>메서드에 잘못된 인수 전달
<NumberFormatException>문자열을 숫자로 변환할 때 실패

💡 Checked Exception
: 파일, 네트워크, DB 작업과 같이 예측 가능한 오류 (컴파일러가 예외 처리 강제)

💡 Unchecked Exception
: NullPointerException, IndexOutOfBoundsException처럼 개발자가 방어해야 하는 오류

Checked Exception과 Unchecked Exception의 차이

  • Checked Exception
    : 컴파일러가 예외 처리를 강제 → 외부 리소스 관련 (파일, DB, 네트워크 등)
  • Unchecked Exception
    : 예외 처리가 필수 아님 → 주로 개발자의 실수로 발생 (코드 논리 오류)
구분Checked ExceptionUnchecked Exception
예외 처리 강제 여부<try-catch> 또는 <throws> 필수예외 처리가 강제되지 않음
상속 클래스<Exception> (단, <RuntimeException> 제외)<RuntimeException> 및 그 하위 클래스
발생 시점컴파일 시점 (예외 처리하지 않으면 컴파일 에러)런타임(실행 중)
발생 원인파일, 네트워크, DB 등 외부 환경 문제<null> 참조, 잘못된 배열 인덱스 등 코드 로직 문제
예제 클래스<IOException>, <SQLException>, <InterruptedException><NullPointerException>, <ArrayIndexOutOfBoundsException>, <ArithmeticException>

✔ Checked Exception은 반드시 처리해야 하고,
   Unchecked Exception은 코드에서 방어하는 것이 핵심 !

throw와 throws의 차이

throw

  • 예외를 직접 발생시키고 메서드 내부에서 사용한다.
  • throw된 예외는 반드시 try-catch로 처리하거나 throws로 위임해야 한다.
  • ex) throw new NullPointerException();

throws

  • 메서드에서 예외가 발생할 가능성을 선언하며, 메서드 선언부에서 사용한다.
  • throws만 선언하면 예외가 발생하지 않는다. (실제 예외는 throw로 발생)
  • ex) public void myMethod() throws IOException { }

✔ throw는 예외를 던지는 행위, throws는 예외를 선언하는 역할 !

try~catch~finally 구문에서 finally이 어떤 역할을 하는지

finally 블록의 역할
finally 블록은 예외 발생 여부와 상관없이 반드시 실행되는 코드 블록이다.
주로 자원 해제(파일, DB 연결 닫기), 정리 작업(로그 기록, 임시 데이터 삭제) 등에 사용된다.

finally 기본 구조

try {
    // 예외 발생 가능 코드
} catch (Exception e) {
    // 예외 처리 코드
} finally {
    // 항상 실행되는 코드 (자원 해제 등)
}

finally의 주요 사용 사례

  • finally 블록은 try 실행 후 반드시 실행됨
  • 예외 발생 여부와 상관없이 실행
  • 파일, DB, 네트워크 연결 해제 등의 정리 작업에 활용

✔ finally를 활용하면 자원 누수를 방지하고 안정적인 코드 작성 가능 !

Throwable과 Exception의 차이

  • Throwable → Exception과 Error를 포함하는 최상위 클래스 (모든 예외를 포괄)
  • Exception → Throwable의 하위 클래스, 일반적인 예외 처리에 사용
구분ThrowableException
역할예외(Exception)와 오류(Error)의 최상위 클래스예외 처리 전용 클래스
하위 클래스<Exception>, <Error><IOException>, <RuntimeException>
사용 목적모든 예외와 오류를 포괄프로그램에서 예외 상황을 처리
예외 처리 가능 여부<catch(Throwable t)>로 모든 예외와 오류 처리 가능 (비추천)<catch(Exception e)>로 일반적인 예외만 처리

✔ Throwable을 직접 사용하는 경우는 거의 없고,
예외 처리는 Exception을 활용하는 것이 일반적 !

제네릭이란 무엇이고, 왜 사용하는지

제네릭(Generic)이란?
제네릭은 데이터 타입을 일반화하여 코드의 재사용성을 높이고
타입 안정성을 유지할 수 있도록 해주는 기능입니다.
Java, C++, C# 등의 언어에서 많이 사용됩니다.

왜 사용하는가?
1. 코드의 재사용성 증가
 -  같은 로직을 다양한 데이터 타입에 적용할 수 있음.

2. 타입 안정성 보장
 -  컴파일 시 타입을 체크하여 타입 오류를 방지.

3. 캐스팅(형변환) 최소화
 -  불필요한 형변환이 줄어들어 성능과 가독성이 향상됨.

제네릭을 사용한 경험

타입 안정성 유지, 불필요한 형 변환 제거, 코드 재사용성 향상의 목적으로 사용했다.

1. 리스트(List)에 사용

public List<Review> getReviewsByPerformanceId(@PathVariable("mt20id") String mt20id)

 -  이유: List는 다양한 타입을 저장할 수 있지만,
    List<Review>를 사용하면 리뷰(Review) 객체만 저장 가능하도록 타입을 제한

 -  장점: 타입 안정성이 확보되어 캐스팅 불필요, 런타임 에러 방지
    ex) 반환되는 데이터가 List<Object>라면 형 변환이 필요하지만,
         List<Review>로 지정하면 형 변환 없이 바로 사용 가능

2. 맵(Map) 사용

public Map<String, List<ScheduleInfo>> getScheduleWithAvailableSeats(String mt20id)

 -  Map<String, List<ScheduleInfo>> : 공연 스케줄과 잔여석 정보를 매핑

 -  이유: String → 공연 날짜 등의 키값을 의미
            List<ScheduleInfo> → 해당 날짜에 대한 공연 스케줄 목록 저장

 -  장점: 키(String)와 값(List<ScheduleInfo>)의 타입이 명확하여 코드의 가독성 향상
            불필요한 캐스팅이 없어 코드가 간결해지고 안정적

3. 응답 데이터 처리

public Map<String, Object> insertReview(@SessionAttribute("loginMember") Member loginMember, 
                                        @RequestBody Review review)

 -  Map<String, Object> : JSON 응답을 클라이언트에게 전달할 때 사용

 -  이유: 응답 데이터는 성공 여부("success")와 메시지("message") 같은
            다양한 타입의 데이터를 포함할 수 있음
            Object를 사용하여 String, Boolean, Integer 등을 혼합 저장 가능

 -  장점: 유연하게 응답 데이터를 구성할 수 있음
            다양한 타입을 저장할 수 있으므로 코드 재사용성이 높아짐

▶️ 결론적으로, 제네릭을 활용하여 "안전하고 효율적인 코드" 를 작성하기 위해 사용했다 !


📌 람다, 스트림, 어노테이션, 리플렉션

람다(Lambda)

익명 함수(anonymous function)를 의미하며, 함수를 간결하게 표현할 수 있는 기능이다.
콜백 함수나 일회성 함수 구현 시 유용하며,
Java에서는 주로 함수형 인터페이스와 함께 사용한다.
또한 가독성을 높이고 불필요한 보일러플레이트 코드(익명 클래스 등)를 줄이는 데 유용합니다.

✍🏻 람다 표현식 문법 (Java 기준)

(parameters) -> { body }
  • parameters : 입력 매개변수
  • -> : 람다 화살표
  • body : 함수 본문 (return 문이 필요한 경우 {} 사용)

✍🏻 람다를 사용하는 이유
 1.  코드를 간결하게 작성 가능
 2.  익명 함수 형태로 사용하여 불필요한 코드 제거
 3.  함수형 프로그래밍 스타일 지원
 4.  콜백(callback) 함수 구현에 유용

스트림

Java 8에서 도입된 기능으로,
데이터 컬렉션(배열, 리스트 등)을 처리하는 강력한 기능을 제공한다.

스트림을 사용하면 데이터를 필터링, 변환, 집계 등의 작업을
함수형 스타일로 간결하게 처리할 수 있다.

스트림의 주요 특징

  • 연속적인 데이터 처리 가능 → 여러 연산을 조합하여 파이프라인 형태로 처리
  • 원본 데이터 변경 없음 → 스트림은 데이터의 복사본을 사용하므로 원본 컬렉션이 변경되지 않음
  • 내부 반복 사용 → for 루프 없이 내부적으로 반복을 처리하여 코드가 간결해짐
  • 병렬 처리 가능 → parallelStream()을 사용하면 멀티스레드를 활용하여 성능 최적화 가능

람다와 스트림은 왜 생겨났을까?

Java는 원래 객체지향 프로그래밍(OOP) 언어로 시작했지만
함수형 프로그래밍(Functional Programming, FP)의 장점을 도입할 필요성이 커졌다.
특히 코드의 가독성, 유지보수성, 병렬 처리의 효율성을 높이기 위해
람다(Lambda)와 스트림(Stream)이 Java 8에서 추가되었다!

람다는 불필요한 코드 작성을 줄이고,
함수형 프로그래밍 스타일을 Java에 도입하기 위해 등장


스트림은 데이터를 효과적으로 처리하고, 반복문을 제거하여
코드의 가독성을 높이며, 병렬 처리를 쉽게 할 수 있도록 도입

람다가 생겨난 이유

  • Java 8 이전에는 익명 클래스를 사용해 함수형 스타일의
    코드를 작성해야 했기 때문에 코드가 길고 가독성이 떨어졌다.
// 기존 방식 (익명 클래스 사용)
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, World!");
    }
}).start();

🔹 문제점
  -  불필요한 코드가 많음 (익명 클래스, @Override 등)
  -  간단한 로직도 복잡하게 작성해야 함
  -  코드 가독성이 떨어짐


람다 도입 후

// 람다 표현식 사용
new Thread(() -> System.out.println("Hello, World!")).start();

🔹 장점
  -  코드가 짧고 간결해짐
  -  익명 클래스보다 가독성이 좋아짐
  -  함수형 인터페이스와 쉽게 결합 가능


스트림이 생겨난 이유

  • Java 8 이전에는 데이터를 처리할 때 for 또는 while 반복문을 사용해야 했습니다.
// 리스트에서 짝수만 골라내는 코드
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = new ArrayList<>();

for (Integer num : numbers) {
    if (num % 2 == 0) {
        evenNumbers.add(num);
    }
}

System.out.println(evenNumbers); // [2, 4, 6]

🔹 문제점
  -  for 루프를 사용하면 코드가 길고 가독성이 떨어짐
  -  컬렉션을 조작하는 로직이 명령형 스타일(Imperative Style)이라 복잡함
  -  멀티스레드 병렬 처리 지원이 어려움


스트림 도입 후 (함수형 스타일)

List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0)
                                   .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

🔹 장점

   -  코드가 짧고 가독성이 뛰어남
  -  데이터 흐름을 쉽게 표현 가능
  -  내부 반복 처리 → for 루프 없이 자동 처리
  -  병렬 처리(Parallel Stream) 지원으로 성능 향상 가능

자바에서 어노테이션이란?

📌 어노테이션(Annotation)이란?
코드에 추가하는 메타데이터로 컴파일러, 실행 환경, 또는
프레임워크가 특정 기능을 수행할 수 있도록 도와주는 역할을 한다.

✅ 어노테이션의 특징
@ 기호로 시작 (예: @Override, @Deprecated)
컴파일러에게 정보 제공 (예: @Override → 오버라이딩 여부 검사)
실행 시 특정 동작 수행 (예: Spring의 @Autowired → 의존성 자동 주입)
반복적인 코드 감소 (예: Lombok의 @Getter, @Setter → 자동 메서드 생성)

어노테이션을 왜 사용할까?

  • 어노테이션이 필요한 이유
  1. 코드의 가독성과 유지보수성 향상
    -  코드만 보고도 추가적인 정보를 쉽게 이해할 수 있음
  2. 컴파일러에게 추가적인 정보 제공
    -  예: @Override를 사용하면 오버라이딩 여부를 체크하여 실수 방지
  3. 반복적인 코드 감소
    -  예: @Getter(Lombok)를 사용하면 Getter 메서드를 자동 생성
  4. 프레임워크와의 연동 용이
    -  예: 스프링에서는 @Autowired로 의존성 주입을 간단히 처리

✅ 즉, 어노테이션을 사용하면 코드가 간결해지고 유지보수가 쉬워지며,
     프레임워크와의 연동이 편리해진다 !

어노테이션은 리플렉션으로 동작, 그렇다면 리플렉션은 무엇일까?

리플렉션(Reflection)
실행 시간(Run-time)에 클래스, 메서드, 필드 등의 정보를 분석하고 조작할 수 있는 기능
즉, 컴파일 시점이 아니라 런타임에 객체의 구조를 동적으로 다룰 수 있도록 하는 기능이다.

리플렉션이 필요한 이유

  • 컴파일 타임에 알 수 없는 클래스, 메서드, 필드 등을 동적으로 사용 가능
    -  예: JSON 파싱, 프레임워크에서 동적 객체 생성

  • 어노테이션 기반 기능을 구현하기 위해 필요
    -  예: Spring의 @Autowired, JUnit의 @Test 등이 동작하는 원리

  • 객체 정보를 출력하거나 디버깅할 때 유용
    -  예: toString() 자동 생성, ORM에서 필드 조회

리플렉션의 단점

  • 성능 저하: 리플렉션은 일반적인 메서드 호출보다 속도가 느림
  • 보안 위험: private 필드도 강제로 수정할 수 있어 보안 문제 발생 가능
  • 컴파일 시 오류 발견 불가: 동적으로 호출되므로 컴파일 시점에 에러를 찾을 수 없음

리플렉션을 활용해서 어노테이션의 메타 데이터를
가져오는 등의 로직을 실제로 구현해 보신 적이 있는지?

직접적으로 리플렉션을 활용한 적은 없지만,
Spring 내부에서 @GetMapping, @RequestMapping 등의 어노테이션을 처리할 때
내부적으로 리플렉션을 사용했다.

🔹 @GetMapping("/genre/{genre}")이 붙은 genre() 메서드를 Spring이 실행하는 과정

  1. Spring이 PerformanceController 클래스를 스캔
       -  @Controller가 붙은 클래스를 찾아서 Bean으로 등록
  2. 클래스 내 메서드를 스캔하면서 @GetMapping 어노테이션이 있는지 확인
for (Method method : PerformanceController.class.getDeclaredMethods()) {
    if (method.isAnnotationPresent(GetMapping.class)) {
        // 경로 정보를 읽어서 매핑
        GetMapping annotation = method.getAnnotation(GetMapping.class);
        System.out.println("URL: " + annotation.value());
    }
}

📌 심화 질문

System.out.println 클래스는 성능이 좋지 않다고 하는데
이유가 무엇일까?

🏷️ System.out.println

  • 표준 출력 스트림(System.out)을 통해 콘솔에 문자열을 출력하는 기능을 함
  • 성능 면에서는 최적화되지 않은 방식이므로, 반복적인 사용 시 성능 저하 초래(느림)
  • 동기화(Synchronization)로 인한 성능 저하
    System.out.println은 내부적으로 PrintStream 클래스를 사용하며,
    이 클래스의 메서드는 synchronized 키워드가 적용되어 있다.
    즉, 멀티스레드 환경에서 동시에 여러 개의 출력 요청이 들어와도
    한 번에 하나씩 처리해야 한다.
public void println(String x) {
    synchronized (this) { // 동기화 블록
        print(x);
        newLine();
    }
}

🔹 문제점
  -  멀티스레드 환경에서는 성능이 크게 저하됨
  -  여러 스레드가 동시에 println을 호출하면 블로킹(Block) 발생

  • I/O 연산(입출력 연산)이 CPU보다 훨씬 느림
    출력하는 과정에서 표준 출력 스트림(System.out)은 OS의 콘솔과 상호작용해야 한다
    -  콘솔 출력은 단순한 연산이 아니라, OS와의 입출력 작업이 포함되므로 성능이 낮음
    -  특히, 파일 또는 네트워크에 데이터를 기록하는 것보다도 속도가 느릴 수 있음

  • 즉각적인 출력(Flush)로 인한 비효율성
    System.out.println은 출력할 때마다 즉시 flush()를 수행하여 버퍼를 비운다.

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
        flush();  // 즉시 버퍼 비우기 (비효율적)
    }
}

🔹 문제점
  -  매번 데이터를 즉시 출력하므로 버퍼링을 활용한 최적화가 불가능
  -  많은 데이터를 출력하면 불필요한 I/O 연산이 증가하여 성능이 저하됨

🚨 System.out.println은 느린 동기화 방식, 즉각적인 flush(),
     느린 I/O 연산 때문에 성능이 좋지 않음

✅ 반복 출력이 많을 경우 BufferedWriter나 StringBuilder를 사용하면 성능이 향상됨
✅ 로그 기록에는 Logger(예: Log4j, SLF4J)를 사용하여 멀티스레드 성능을 최적화할 것 🚀

0개의 댓글