🗂️ 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(문자열)을 저장하고 관리하는 클래스이다.
이들의 차이는 변경 가능 여부(Mutability)와 동기화(Synchronization)에 있다.
🔹 String (불변, Immutable)
문자열이 변하지 않는다면 String 사용!
String str = "Hello";
str += " World"; // 새로운 객체 생성
🔹 StringBuilder (가변, Mutable, 비동기)
문자열이 자주 변경된다면 StringBuilder(단일 스레드) 사용!
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 기존 객체 수정
비동기(non-synchronized)
: 여러 개의 쓰레드가 동시에 접근할 수 있음
→ 빠르지만 안전하지 않을 수 있음
🔹 StringBuffer (가변, Mutable, 동기화)
문자열이 자주 변경되면서 멀티스레드 환경이라면 StringBuffer 사용!
StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World"); // 기존 객체 수정
StringBuilder보다 속도가 느림동기(synchronized)
: 여러 개의 쓰레드(멀티스레드 환경)가 하나의 메서드나 리소스에
동시에 접근하지 못하도록 막는 것
→ 안전하지만 느림
다시 말해서, String 클래스는 StringBuffer 클래스나 StringBuilder 클래스와 다르게
리터럴을 통해 생성되면 그 인스턴스의 메모리 공간은 절대 변하지 않는다.😊
Exception과 Error는 모두 Throwable 클래스를 상속받지만, 발생 원인과 처리 방식이 다르다.
예외(Exception)
- 프로그램 실행 중 예외적인 상황 발생
- 개발자가 예상하고 처리할 수 있는 문제
try-catch로 예외 처리 가능- ex)
NullPointerException,IOException,SQLException
에러(Error)
- 시스템 레벨에서 발생하는 치명적인 오류
- JVM 실행 중 발생하는 심각한 문제
- 일반적으로 복구 불가능 (프로그램 종료)
- ex)
OutOfMemoryError,StackOverflowError
Exception 클래스는 다양한 하위 클래스를 가지며,
주로 Checked Exception과 Unchecked 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 |
|---|---|---|
| 예외 처리 강제 여부 | <try-catch> 또는 <throws> 필수 | 예외 처리가 강제되지 않음 |
| 상속 클래스 | <Exception> (단, <RuntimeException> 제외) | <RuntimeException> 및 그 하위 클래스 |
| 발생 시점 | 컴파일 시점 (예외 처리하지 않으면 컴파일 에러) | 런타임(실행 중) |
| 발생 원인 | 파일, 네트워크, DB 등 외부 환경 문제 | <null> 참조, 잘못된 배열 인덱스 등 코드 로직 문제 |
| 예제 클래스 | <IOException>, <SQLException>, <InterruptedException> | <NullPointerException>, <ArrayIndexOutOfBoundsException>, <ArithmeticException> |
✔ Checked Exception은 반드시 처리해야 하고,
Unchecked Exception은 코드에서 방어하는 것이 핵심 !
throw
- 예외를 직접 발생시키고 메서드 내부에서 사용한다.
throw된 예외는 반드시try-catch로 처리하거나throws로 위임해야 한다.- ex)
throw new NullPointerException();
throws
- 메서드에서 예외가 발생할 가능성을 선언하며, 메서드 선언부에서 사용한다.
throws만 선언하면 예외가 발생하지 않는다. (실제 예외는throw로 발생)- ex)
public void myMethod() throws IOException { }
✔ throw는 예외를 던지는 행위, throws는 예외를 선언하는 역할 !
finally 블록의 역할
finally 블록은 예외 발생 여부와 상관없이 반드시 실행되는 코드 블록이다.
주로 자원 해제(파일, DB 연결 닫기), 정리 작업(로그 기록, 임시 데이터 삭제) 등에 사용된다.
finally 기본 구조
try {
// 예외 발생 가능 코드
} catch (Exception e) {
// 예외 처리 코드
} finally {
// 항상 실행되는 코드 (자원 해제 등)
}
finally의 주요 사용 사례

✔ finally를 활용하면 자원 누수를 방지하고 안정적인 코드 작성 가능 !
| 구분 | Throwable | Exception |
|---|---|---|
| 역할 | 예외(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 등을 혼합 저장 가능
- 장점: 유연하게 응답 데이터를 구성할 수 있음
다양한 타입을 저장할 수 있으므로 코드 재사용성이 높아짐
▶️ 결론적으로, 제네릭을 활용하여 "안전하고 효율적인 코드" 를 작성하기 위해 사용했다 !
익명 함수(anonymous function)를 의미하며, 함수를 간결하게 표현할 수 있는 기능이다.
콜백 함수나 일회성 함수 구현 시 유용하며,
Java에서는 주로 함수형 인터페이스와 함께 사용한다.
또한 가독성을 높이고 불필요한 보일러플레이트 코드(익명 클래스 등)를 줄이는 데 유용합니다.
✍🏻 람다 표현식 문법 (Java 기준)
(parameters) -> { body }
✍🏻 람다를 사용하는 이유
1. 코드를 간결하게 작성 가능
2. 익명 함수 형태로 사용하여 불필요한 코드 제거
3. 함수형 프로그래밍 스타일 지원
4. 콜백(callback) 함수 구현에 유용
Java 8에서 도입된 기능으로,
데이터 컬렉션(배열, 리스트 등)을 처리하는 강력한 기능을 제공한다.
스트림을 사용하면 데이터를 필터링, 변환, 집계 등의 작업을
함수형 스타일로 간결하게 처리할 수 있다.
스트림의 주요 특징
Java는 원래 객체지향 프로그래밍(OOP) 언어로 시작했지만
함수형 프로그래밍(Functional Programming, FP)의 장점을 도입할 필요성이 커졌다.
특히 코드의 가독성, 유지보수성, 병렬 처리의 효율성을 높이기 위해
람다(Lambda)와 스트림(Stream)이 Java 8에서 추가되었다!
람다는 불필요한 코드 작성을 줄이고,
함수형 프로그래밍 스타일을 Java에 도입하기 위해 등장
스트림은 데이터를 효과적으로 처리하고, 반복문을 제거하여
코드의 가독성을 높이며, 병렬 처리를 쉽게 할 수 있도록 도입
람다가 생겨난 이유
// 기존 방식 (익명 클래스 사용)
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello, World!");
}
}).start();
🔹 문제점
- 불필요한 코드가 많음 (익명 클래스, @Override 등)
- 간단한 로직도 복잡하게 작성해야 함
- 코드 가독성이 떨어짐
람다 도입 후
// 람다 표현식 사용
new Thread(() -> System.out.println("Hello, World!")).start();
🔹 장점
- 코드가 짧고 간결해짐
- 익명 클래스보다 가독성이 좋아짐
- 함수형 인터페이스와 쉽게 결합 가능
스트림이 생겨난 이유
// 리스트에서 짝수만 골라내는 코드
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 → 자동 메서드 생성)
@Override를 사용하면 오버라이딩 여부를 체크하여 실수 방지@Getter(Lombok)를 사용하면 Getter 메서드를 자동 생성@Autowired로 의존성 주입을 간단히 처리✅ 즉, 어노테이션을 사용하면 코드가 간결해지고 유지보수가 쉬워지며,
프레임워크와의 연동이 편리해진다 !
리플렉션(Reflection)
실행 시간(Run-time)에 클래스, 메서드, 필드 등의 정보를 분석하고 조작할 수 있는 기능
즉, 컴파일 시점이 아니라 런타임에 객체의 구조를 동적으로 다룰 수 있도록 하는 기능이다.
리플렉션이 필요한 이유
컴파일 타임에 알 수 없는 클래스, 메서드, 필드 등을 동적으로 사용 가능
- 예: JSON 파싱, 프레임워크에서 동적 객체 생성
어노테이션 기반 기능을 구현하기 위해 필요
- 예: Spring의 @Autowired, JUnit의 @Test 등이 동작하는 원리
객체 정보를 출력하거나 디버깅할 때 유용
- 예: toString() 자동 생성, ORM에서 필드 조회
리플렉션의 단점
private 필드도 강제로 수정할 수 있어 보안 문제 발생 가능직접적으로 리플렉션을 활용한 적은 없지만,
Spring 내부에서 @GetMapping, @RequestMapping 등의 어노테이션을 처리할 때
내부적으로 리플렉션을 사용했다.
🔹 @GetMapping("/genre/{genre}")이 붙은 genre() 메서드를 Spring이 실행하는 과정
PerformanceController 클래스를 스캔@Controller가 붙은 클래스를 찾아서 Bean으로 등록@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)을 통해 콘솔에 문자열을 출력하는 기능을 함
- 성능 면에서는 최적화되지 않은 방식이므로, 반복적인 사용 시 성능 저하 초래(느림)
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)를 사용하여 멀티스레드 성능을 최적화할 것 🚀