[카카오테크 부트캠프] 5/15 TIL: 객체지향 / Generic / Collection / 예외처리 / Thread

주영진·2026년 5월 17일

1. Class Explosion

상속은 '~IS A' 관계일 때, 개념이 나뉘어 분류가 필요할 때 쓴다.

Coffee → Americano, Latte 식으로 구성하면 커피 종류가 너무 많아지는 Class Explosion이 발생할 수 있다. 이럴 땐 속성으로 해결하는 게 낫다.

public class Coffee {
    private String name;
    private int shot;
    private int temp;
    private boolean hasMilk;
}

기준 정리:

  • 무기 → 검, 활 ✅ 좋은 상속 관계
  • 아메리카노 → 아이스 아메리카노 ❌ 속성으로 처리하는 게 나음

2. 객체지향(OOP)

절차지향은 덩어리가 커진 다음에 고치기 너무 힘들다. 객체지향은 실생활의 모든 것을 '객체'로 보고, 잘 만든 부품을 조합해서 사용한다.

핵심 장점:
1. 재사용성 — 잘 만든 부품을 다른 곳에서도 쓸 수 있다
2. 유지보수성 — 오류 범위가 특정되기 때문에 수정이 간편하다
3. 대규모 협업 — 역할 분리가 가능하다

2-1. 다형성

하나의 타입으로 여러 객체를 다룰 수 있도록 하는 것. 같은 메서드인데 다른 결과가 나온다. 코드 수정을 최소화하면서 다양한 기능을 유연하게 확장할 수 있다.

오버로딩(Overloading)
하나의 클래스 안에 같은 이름의 메서드를 매개변수의 개수나 타입을 다르게 정의하는 것. 코드 일관성과 가독성이 올라가고, 협업 시 재사용성도 높아진다. 주로 생성자에 많이 사용한다.

오버라이딩(Overriding)
부모 클래스의 메서드를 자식 클래스에서 재정의해서 동작을 바꾸는 것. 리턴 타입은 유지해야 한다.

업캐스팅(Upcasting)
클래스가 구현한 상위 인터페이스나 부모 클래스 타입으로 참조하는 것. 다양한 객체를 동일한 타입으로 다룰 수 있게 된다.

Vehicle vehicle = new Bus(); // Bus → Vehicle로 자동 형변환
vehicle.excel();             // Bus의 오버라이딩된 excel() 실행

코드의 일관성을 유지하면서 다양한 구현체를 유연하게 교체하는 설계가 가능해진다.

다운캐스팅(Downcasting)
부모 클래스 타입으로 참조된 객체를 다시 자식 클래스 타입으로 변환하는 것. 자식 클래스에만 존재하는 메서드나 필드에 접근하고 싶을 때 쓴다.

Vehicle vehicle = new Bus();  // 업캐스팅
Bus bus = (Bus) vehicle;      // 다운캐스팅
bus.excel();                  // Bus 고유 기능 호출 가능

instanceof로 검사 후 사용하는 걸 권장한다.

2-2. 캡슐화

객체의 내부 상태를 숨기고, 검증된 통로(메서드)로만 접근하도록 하는 것.

없으면?

  • 데이터 보호가 안 된다. (ex. 은행 잔액에 음수가 들어와도 막을 방법이 없음)
  • 유효성 검사도 안 된다.
  • 내부 구현 은닉이 불가하다.
    즉, 내가 만든 객체는 내가 허용한 형태로만 작업을 해야 한다.

구현:

  • private으로 감추고
  • getter로 외부에서 읽고
  • setter로 외부에서 값을 설정한다

2-3. 인터페이스(Interface)

구현 세부사항 없이 클래스가 구현해야 할 메서드의 집합만을 정의한 설계 명세서다. implements 키워드를 사용한다.

장점:

  • 유연성 — 인터페이스만 만들어두면 사용자는 내부 동작을 신경 쓰지 않아도 된다
  • 병렬 개발 — 설계자는 구현자들이 요구사항을 지켰는지만 확인하면 된다
  • mock, test double, stub 사용이 훨씬 쉬워진다
    Interface vs 상속:
구분Interface상속
의미CAN DO (~를 할 수 있다)IS A (~이다)
구현다중 구현 가능단일 상속만 가능
구조수평적, 기능적 확장수직적, 계층적 구조

공통점은 상속으로 묶고, 차이점은 인터페이스로 분리한다.


3. Generic

데이터 타입을 클래스나 메서드가 사용할 때 나중에 지정할 수 있도록 하는 타입이다. 자바는 타입에 엄격하기 때문에, 타입 간 오류를 줄이고자 등장했다.

public class Garage<T> {
    private T car;
    public Garage(T car) { this.car = car; }
    public T getCar() { return this.car; }
}
 
Garage<String> garage1 = new Garage<>("전기차");
Garage<Integer> garage2 = new Garage<>(2025);

언제 사용?

  • 공통 API 응답 규격 만들 때
  • Collection 프레임워크: List<T>, Map<K,V>, Set
    Generic은 타입이 너무 자유로울 수 있다는 문제가 있어서, 현업에서는 아래처럼 타입을 제한해서 쓰는 경우가 많다.
<T extends 부모클래스>
// T에 들어올 수 있는 클래스는 반드시 부모클래스이거나 그 자식이어야 함

그냥 Generic 그대로 쓰는 경우는 많지 않다.


4. Collection

여러 개의 데이터를 쉽고 효과적으로 처리할 수 있도록 자바에서 미리 만들어준 표준화된 자료구조 세트다.

4-1. List

순서가 있는 배열. 데이터 넣은 순서가 유지되고 중복이 허용된다. List 자체는 인터페이스라 직접 객체 생성은 불가하고, ArrayList, LinkedList, Vector 같은 구현 클래스를 활용한다.

실무 예시: 게시판 게시물 목록, 채팅 메시지 이력

4-2. Set

중복 없는 집합. 순서는 보장되지 않는다.

실무 예시: 방문자 IP, 특정 글의 좋아요 누른 유저 ID

HashSet은 내부적으로 Hash Table을 사용해 빠른 검색과 조작을 지원한다. Hashcode 결과값을 기반으로 한 위치(slot)에 데이터를 저장한다.

4-3. Map

key와 value가 한 쌍인 자료구조.

  • key 중복 허용 X
  • value 중복 허용 O
  • Hash 기반이라 키 검색 속도가 빠르다
    실무 예시: 유저 로그인 정보 (동명이인이 있어도 key가 다르면 구분 가능)

셋 다 인터페이스인 이유 → 다형성

List<String> list = new ArrayList<>();
List<String> list2 = new LinkedList<>();
// ArrayList로 선언했다가도 LinkedList로 갈아끼울 수 있다

5. 예외 처리

5-1. throw vs throws

  • throw — 예외를 발생시킨다. 조건이 맞으면 그 자리에서 바로 던진다.
  • throws — '예외를 던질 수 있다'는 약속. 호출하는 쪽에서 처리해야 한다.
  • 보통 둘을 혼용한다.
public void setSpeed(int speed) throws IllegalArgumentException {
    if (speed < 0) {
        throw new IllegalArgumentException("음수 불가");
    }
}

5-2. try-with-resources

try 괄호 안에 자원을 선언하면, 블록이 끝날 때 자동으로 close()를 호출해주는 문법이다. 실제로 'with' 키워드를 쓰는 건 아니다.

try (Scanner sc = new Scanner(System.in)) {
    // 사용
} // 예외가 터져도 sc.close() 자동 호출

파일, DB 연결, 스트림 같은 자원은 안 닫으면 메모리 릭이 발생한다. Scanner, BufferedReader, FileWriter 등 자바 표준 입출력 클래스는 모두 AutoCloseable 인터페이스를 구현하고 있다. 자원을 여러 개 선언하면 역순으로 close() 된다.

5-3. Custom Exception

자바의 표준 예외로 표현하기 어려운 상황을 개발자가 직접 이름 붙여 만든 예외 클래스다. 이름 자체가 의미를 담으면 로그 분석도, 예외 처리 분기도 명확해진다.

// Unchecked (주로 이걸 씀) — 처리 강제 안 함
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}
 
// Checked — 호출하는 쪽에서 반드시 처리해야 함
public class FileFormatInvalidException extends Exception {
    public FileFormatInvalidException(String message) {
        super(message);
    }
}

6. Thread

하나의 프로그램 내에서 동시에 실행될 수 있는 독립적 실행 흐름 단위. Thread는 클래스다.

"빠른 실행"이 목적이 아니라 "동시에 수행하기 위해서" 쓴다.
ex. 고화질 영상을 다운받으면서 동시에 채팅 보내기

class MyThread extends Thread {
    @Override
    public void run() { ... }
}
new MyThread().start(); // start()로 실행 — run() 직접 호출하면 멀티스레드 안 됨
메서드역할
start()새 스레드 생성 후 실행
run()실행 코드 정의 (직접 호출 금지)
sleep(ms)현재 스레드 일시 정지
join()해당 스레드 끝날 때까지 대기
interrupt()스레드에 중단 요청

주의사항:

  1. 스레드 내부 에러는 메인 프로그램에 알려지지 않고 조용히 종료된다. → run() 내부에 try-catch 필수.
  2. 공유 변수 값이 다른 스레드에 즉시 안 보일 수 있다. 각 스레드가 CPU cache에 변수를 복사해서 사용하기 때문이다. → volatile 키워드로 해결.
private static volatile boolean stop = false; // 모든 스레드에 즉시 반영

6-1. Runnable

스레드가 실행할 작업을 정의하기 위한 표준 인터페이스. '실행 흐름'과 '제어'를 분리할 수 있다.

// Thread 상속 — 작업과 실행 주체가 한 곳에 묶임
class MyThread extends Thread {
    @Override
    public void run() { ... }
}
 
// Runnable 구현 — 작업과 실행 주체가 분리됨 (실무에서 선호)
class MyTask implements Runnable {
    @Override
    public void run() { ... }
}
new Thread(new MyTask()).start();

Runnable을 쓰는 이유:

  • 자바는 단일 상속만 가능하기 때문에 Thread를 상속하면 다른 클래스 상속이 불가능하다
  • 작업 로직과 스레드 제어의 분리 (Decoupling)
  • ExecutorService 같은 스레드 풀 도구들이 전부 Runnable 기반으로 동작한다
    람다식으로 간결하게도 쓸 수 있다:
new Thread(() -> System.out.println("작업 실행")).start();
구분RunnableThread
역할무엇을 할지 (작업 정의)누가 실행할지 (실행 주체)
형태인터페이스클래스
상속 제약없음다른 클래스 상속 불가

실무에서는 직접 new Thread를 잘 안 하고 ExecutorService를 사용한다.

6-2. ExecutorService

자바에서 스레드를 직접 만들지 않고, 스레드 풀을 통해 작업을 안전하게 병렬 처리하는 실행 관리 인터페이스다.

Thread를 계속 생성하지 못하는 이유:

  • new Thread()는 생성 비용이 높다. 스레드는 OS가 관리하는 무거운 자원이다.
  • OOM(Out of Memory) 위험 — 모든 작업마다 스레드를 만들면 자원이 너무 많이 든다.
  • 명시적으로 스레드를 종료해줘야 해서 관리가 어렵다.
// Thread 직접 생성 — 대량 작업 시 위험
new Thread(() -> System.out.println("작업")).start();
 
// ExecutorService — 스레드 풀에서 재사용. 안정적
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(() -> System.out.println("작업"));
executor.shutdown();

풀 생성 방법 4가지:

메서드특징쓰는 시점
newFixedThreadPool(n)스레드 수 고정처리량이 일정할 때
newCachedThreadPool()필요한 만큼 생성, 유휴 시 제거작업량이 불규칙할 때
newSingleThreadExecutor()스레드 1개, 순차 실행순서가 중요할 때
newScheduledThreadPool(n)지연·주기 실행예약 작업, 배치

구성:

  • Task Submitters (작업 요청)
  • Blocking Queue (대기 공간)
  • Thread Pool (스레드들이 Queue에서 작업을 꺼내 실행)
    작업 제출 — execute vs submit:
// execute — 결과 없음. 단순 작업(로그, 알림 등)에 사용
executor.execute(() -> System.out.println("실행"));
 
// submit — Future 반환, 결과·예외 추적 가능
Future<String> future = executor.submit(() -> "완료");
String result = future.get(); // 작업 끝날 때까지 대기 후 결과 반환

Callable은 작업 후 결과를 반환받고 싶을 때 사용한다.

장점: 자원 보호 / 재사용으로 인한 성능 향상 / 관리 편함

현업에서는 @Async 어노테이션을 쓰면 자동으로 ExecutorService가 켜진다.

종료는 반드시 해야 한다. 안 하면 JVM이 꺼지지 않는다.

executor.shutdown();     // 남은 작업 처리 후 종료 (일반적)
executor.shutdownNow();  // 대기 작업 취소, 즉시 강제 종료

주요 설정값:

설정의미
corePoolSize항상 유지되는 최소 스레드 수
maximumPoolSize생성 가능한 최대 스레드 수
keepAliveTimecore 초과 스레드가 유휴 상태일 때 유지되는 시간

6-3. Synchronized

여러 스레드가 동시에 공유 자원에 접근하지 못하도록 일시적으로 접근을 하나로 제한하는 동기화 키워드다.

임계 영역(Critical Section): 공유 자원에 접근하는 코드의 일부분. 여러 스레드가 동시에 실행되어서는 안 되는 영역이다.

임계 영역의 조건:
1. 공유 자원 — 여러 스레드가 동시에 접근할 수 있는 변수여야 한다. 힙 영역의 변수여야 한다.
2. 수정 가능성 — 단순히 읽는 게 아니라 누군가 값을 바꿔야 한다.
3. 동시 접근 가능성 — 실제로 여러 스레드가 그 코드를 동시에 실행할 가능성이 있어야 한다.
스레드가 임계 영역에 진입하면 Lock을 획득해 다른 스레드는 접근하지 못하도록 막는다. Lock을 가진 스레드만 진입 가능하고, 나머지는 대기한다. 대기 조건도 걸 수 있다.

사용 형태:

형태락 기준동기화 범위
synchronized 인스턴스 메서드this메서드 전체
synchronized static 메서드클래스 객체메서드 전체
synchronized (this)this블록 일부
synchronized (lock)지정 객체블록 일부

문제점:

  • 대기하고 있는 스레드의 우선순위 파악이 어렵다
  • 기다리는 조건이 단일 하나라서 무한 대기가 이루어질 수 있다
    주의점:
  • 동기화 범위가 넓을수록 하나의 스레드만 실행 → 병렬 처리 이점이 사라져 성능 저하
  • Deadlock (여러 락 동시 사용) 주의
profile
'개발사(社)' (주)영진

0개의 댓글