
실행 중인 프로그램의 인스턴스로, 운영체제로부터 CPU, RAM, 파일 핸들 등의 자원을 받아 독립적으로 실행되는 작업 단위
스레드는 프로세스 내부에서 실제 작업을 수행하는 실행 흐름 단위
하나의 프로세스는 하나 이상의 스레드(멀티스레드)를 가질 수 있고,
이 스레드들은 프로세스의 자원(Heap, Method Area 등)을 공유하면서 동시에 병렬 작업을 수행할 수 있다.
함수 호출, 지역변수 등이 저장되는 공간
LIFO 구조(Last In First Out)의 메모리 공간
스레드마다 따로 존재하고 (main 스레드의 Stack, 다른 스레드의 Stack 따로)
빠르지만 공간이 제한됨 → StackOverflowError 발생 가능
new로 만든 객체가 저장되는 공간
여러 스레드가 접근 가능
직접 해제하지 않고 GC(Garbage Collector)가 자동으로 회수함
느리지만 유연함 (동적 메모리)
먼저 도착한 프로세스가 먼저 실행
구현이 가장 간단
Convoy Effect (느린 프로세스가 전체 흐름을 막음), 평균 대기 시간 증가 가능
각 프로세스에 정해진 시간(time quantum)을 주고, 시간 다 되면 강제로 다음 프로세스로 넘김
응답 시간 좋음 (대화형 시스템에 적합), 굶는 프로세스 없음
컨텍스트 스위칭 오버헤드 발생
시간 할당이 너무 짧으면 비효율적
실행 시간이 가장 짧은 작업을 먼저 실행
이론적으로 최소 평균 대기 시간 제공
실행 시간을 미리 알아야 함 (현실적으론 어렵다)
Starvation(기아 현상) 발생 가능: 긴 작업은 계속 밀림
2588 곱셈 (빈 칸에 들어갈 수 구하기)
사용자 입력 예외 처리 (NumberFormatException)
ArrayIndexOutOfBoundsException 처리 방법
try-catch-finally 구조 연습
잘못된 인덱스, null 처리 등 예외 테스트 추가
assertThrows() 문법 도입
TodoServiceTest에 remove 예외 테스트 추가
private, getter, setter 왜 쓰는가?
Service vs Entity 역할
isValidIndex() 같은 구조적 코드의 의미 복습
세 자리 수 × 세 자리 수 문제
(1), (2) 위치에 들어갈 세 자리 자연수가 주어질 때 (3), (4), (5), (6) 위치에 들어갈 값을 구하는 프로그램
import java.util.Scanner;
public class multiply2588 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt();
int b = sc.nextInt();
int c = (a * (b % 10));
int d = (a * ((b / 10) % 10));
int e = (a * (b / 100));
System.out.println(c + "\n" + d + "\n" + e + "\n" + (a * b));
}
}
import java.util.Scanner;
public class Multiply2588 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int a = sc.nextInt(); // 곱하는 수
String b = sc.next(); // 곱해지는 수 (문자열로 입력)
int ones = b.charAt(2) - '0'; // 1의 자리
int tens = b.charAt(1) - '0'; // 10의 자리
int hundreds = b.charAt(0) - '0'; // 100의 자리
System.out.println(a * ones);
System.out.println(a * tens);
System.out.println(a * hundreds);
System.out.println(a * Integer.parseInt(b));
}
}
2번째 방식은 문자열로 입력받은 뒤에 charAt()으로 문자를 꺼내고, - '0'을 이용해 정수로 변환해서 처리
'숫자' - '0' → 아스키 코드 연산
Character.getNumericValue()
숫자뿐만 아니라 특수 문자도 숫자로 바꿀 수 있음
Integer.parseInt(String)
문자열 전체를 정수로 바꿈 (단, 문자열이 숫자일 때만 가능)
NumberFormatException이란?
👉 정수를 입력해야 하는데 문자열을 입력해서 발생하는 오류
→ try - catch - finally 사용
예외 처리를 하려고 try 해보고, 안 되면 catch로 가서 처리!
처리 여부와 관계없이 finally는 무조건 실행되는 구조 💫
import java.util.Scanner;
public class InputExample {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in); // 사용자 입력을 받는 Scanner
System.out.print("숫자를 입력하세요: ");
try {
String input = sc.nextLine(); // 문자열로 입력 받음
int number = Integer.parseInt(input); // 정수로 변환 시도
System.out.println("입력한 숫자: " + number);
} catch (NumberFormatException e) {
// ⚠️ 문자를 입력했다면 여기로 옴
System.out.println("⚠️ 숫자가 아닌 값을 입력했습니다! 숫자만 입력해주세요.");
} finally {
// 예외 발생하든 안 하든 무조건 실행됨
System.out.println("📌 입력 처리 종료");
}
}
}
👉 배열의 인덱스 범위를 넘어갔을 때 발생하는 오류 ⚠️
public class SafeArrayAccess {
public static void main(String[] args) {
String[] fruits = {"사과", "바나나", "체리"};
try {
System.out.println("선택한 과일: " + fruits[3]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("⚠️ 존재하지 않는 인덱스에 접근했습니다! 유효한 범위를 확인해주세요.");
} finally {
System.out.println("✅ 배열 접근 시도 완료");
}
}
}
인덱스가 유효한지 먼저 체크하는 로직이 더 좋음!
int i = 3;
if (i >= 0 && i < fruits.length) {
System.out.println(fruits[i]);
} else {
System.out.println("⚠️ 인덱스 범위를 벗어났습니다!");
}
무조건 예방 로직을 쓰는 게 우선이지만,
📌 상황에 따라 try-catch를 써야 할 때가 있음!
정상적인 흐름 제어로 쓰면 안 됨
남발 시 코드 성능 저하 & 가독성 낮아짐 👎
"일부러 부딪혀보고 예외 처리"는 좋은 습관 아님
사용자의 입력이 예측 불가능할 때
파일이 존재하지 않을 수도 있을 때
네트워크/DB 연결처럼 실패 확률이 있는 작업일 때
try {
// 예외가 발생할 가능성이 있는 코드
} catch (예외타입 변수명) {
// 예외가 발생했을 때 실행할 코드
} finally {
// 예외 발생 여부와 관계없이 항상 실행되는 코드 (선택사항)
}
이렇게 catch를 복수로 사용할 수 있음
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // 인덱스 오류
int x = Integer.parseInt("abc"); // 포맷 오류
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("⚠️ 배열 인덱스 범위 초과!");
} catch (NumberFormatException e) {
System.out.println("⚠️ 정수로 변환할 수 없는 값!");
} finally {
System.out.println("📌 예외 처리 완료 후 종료");
}
finally는 있어도 되고 없어도 되지만...
자바에서는 자원을 사용하고 나서 반드시 닫아줘야 함
닫지 않으면 메모리 누수 or 파일 저장 실패 가능성 있음
sc.close(), connection.close() 등
try (Scanner sc = new Scanner(System.in)) {
int num = sc.nextInt();
} catch (Exception e) {
System.out.println("예외 발생!");
}
예외가 발생하는 경우도 테스트가 필요하다! 🧪
왜? 🤔
예외가 발생해야 하는 상황 예를 들면...
public class TodoService {
private List<String> todos = new ArrayList<>();
public String getTodo(int index) {
return todos.get(index); // 여기서 잘못된 인덱스면 예외 발생!
}
}
이 상황에서 index의 범위를 넘어버리면
IndexOutOfBoundsException를 발생시킨다
→ 그때 예외가 발생하지 않으면 시스템이 멈춰버리기 때문
그럼 예외는 누가 발생시켜주냐?
Java에서 자동으로 발생시켜줌!
그럼에도 불구하고 테스트가 반드시 필요한 이유는
예외는 "발생하는 것"보다 "예상대로 발생하는 것"이 중요함 ✅
미래에 코드가 변경돼도, 예외가 계속 잘 발생하는지 보장해야 함 🛠️
테스트는 "동작 결과"가 아니라 "행동 방식"을 보장함
→ 정상 케이스만 보장하면 반쪽짜리 테스트임
→ 비유하자면... "안전벨트가 고장 났을 때 비상등이 켜지는가!" 와 같음
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TodoServiceTest {
@Test
void testInvalidIndexThrowsException() {
TodoService service = new TodoService();
assertThrows(IndexOutOfBoundsException.class, () -> {
service.getTodo(0); // 아무것도 없으니 에러 나야 정상
});
}
}
assertThrows()란?
특정 예외가 잘 발생하는지 확인하는 메서드 ✅
assertThrows(예외클래스.class, () -> {
// 예외가 발생해야 하는 코드
});
@Test
void testNullPointer() {
String str = null;
assertThrows(NullPointerException.class, () -> {
str.length(); // null에서 메서드 호출 → 예외 발생
});
}
@Test
public void testInvalidIndexThrowsException() {
assertThrows(IndexOutOfBoundsException.class, () -> {
service.remove(0);
});
}
public void remove(int id){
if (isValidIndex(id)) {
todos.remove(id);
System.out.println("삭제되었습니다.");
} else {
System.out.println("❌ 잘못된 번호입니다.");
}
}
이미 예외 처리 구조를 짜놔서 IndexOutOfBoundsException가 발생 안 함
이 예외처리를 추가하려면
else {
throw new IndexOutOfBoundsException("잘못된 인덱스입니다: " + index);
}
으로 바꾸면 되지만...
사용자에겐 부드럽게 메시지로 출력하는 게 더 좋다고 판단 😊
✅ 해결 방법: 테스트 전용 메서드 추가!
public void removeStrict(int index) {
if (!isValidIndex(index)) {
throw new IndexOutOfBoundsException("삭제할 수 없는 인덱스입니다: " + index);
}
todos.remove(index);
}
@Test
public void testInvalidIndexThrowsException() {
assertThrows(IndexOutOfBoundsException.class, () -> {
service.removeStrict(0);
});
}
데이터와 기능을 하나로 묶고, 외부에 필요한 기능만 접근하게 하는 것
private을 사용해 외부에서 직접적인 접근을 막음
불필요한 내부 구조를 숨기고, 필요한 정보만 노출
getter를 통해 값은 노출하지만, 읽기 전용으로 제공 가능
복잡한 내부 동작은 감추고, 필요한 기능만 보여주는 것
예: 리스트가 어떻게 저장되는지가 아니라, "할 일 추가", "완료 처리" 기능만 노출
같은 메서드지만 상황에 따라 다양한 동작을 수행할 수 있음
Entity 데이터를 표현하는 명사 역할 클래스 Todo → String task, boolean done
Service 로직을 수행하는 동사 역할 클래스 TodoService → 할 일 추가, 삭제, 완료 처리 등
private boolean isValidIndex(int index) {
return index >= 0 && index < todos.size();
}
코드 재사용성 향상
가독성 좋아짐
안정성 확보
전체적으로 구조적인 코드 스타일을 만드는 핵심
백준을 하면서 처음으로 좀 막혔다…
어라..? 이거 어떻게 해야할까? 라는 생각이 들어서 고민을 좀 했다
답은 맞았지만 이게 최선일까 어떻게 해야할까?라는 고민이 더 들었다
나중엔 풀지 못하는 문제도 나오겠지?
그래도 이렇게 성장하고 잇는거니까
오늘 예외테스트를 왜하나 라는 생각을 했는데
예외가 발생하지않으면 시스템이 멈춘단다;;
아주 무서운 거였네…
예외 테스트 공부를하고
현재 토이 프로젝트에 적용도 해보고
점점 성장을 하고있다 …!!
오늘도 고생했다 나!
내일도 화이팅! 💪🔥🌱