현재 선택한 정보처리 과정

정보처리기사 필기 이론 학습

이론 목록으로 돌아가기

Java static·final·예외·컬렉션·접근 제어

static과 final의 제한 대상, Java 예외의 분류와 처리 흐름, List·Set·Map의 차이, 접근 제한자의 범위를 구분한다. 참조를 바꾸지 못하는 것과 객체 내용을 바꾸지 못하는 것은 다르다.

예상 읽기 19

static과 final의 제한 대상, Java 예외의 분류와 처리 흐름, List·Set·Map의 차이, 접근 제한자의 범위를 구분한다. 참조를 바꾸지 못하는 것과 객체 내용을 바꾸지 못하는 것은 다르다.

그림으로 확인하기

좌우로 이동해 그림을 확인하세요.그림 크게 보기
try에서 반환할 기본형 값이 먼저 결정된다. finally가 지역 변수를 바꿔도 이미 정한 반환값은 그대로다.
try에서 반환할 기본형 값이 먼저 결정된다. finally가 지역 변수를 바꿔도 이미 정한 반환값은 그대로다.

static: 클래스에 연결된 멤버

static 필드와 인스턴스 필드

필드가 static이면 그 필드는 특정 객체 하나에 속하지 않고 클래스에 속한다. 객체가 0개이거나 여러 개여도 로드된 해당 클래스에는 static 필드가 하나 존재한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
class Counter {
    static int total;
    int own;
}
JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
Counter a = new Counter();
Counter b = new Counter();

a.total++;
a.own++;
b.total++;
b.own++;

문법상 객체 참조를 통해 a.total처럼 정적 필드에 접근할 수 있지만, 정적 멤버라는 사실이 흐려지므로 일반적으로 Counter.total처럼 클래스 이름으로 접근한다.

상태는 다음처럼 나뉜다.

대상Counter.totala.ownb.own
초기값000
위 코드 실행 후211

total은 두 객체가 공유하지만 own은 객체마다 별도다. 시험에서 객체 수를 세는 문제는 먼저 필드 선언에 static이 있는지 표시해야 한다.

static 메서드에는 현재 객체가 없다

인스턴스 메서드는 특정 객체를 대상으로 호출되므로 실행 중 this가 현재 객체를 가리킨다. static 메서드는 특정 객체 없이 호출되므로 thissuper를 사용할 수 없다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
class Account {
    static int count;
    int balance;

    static int getCount() {
        return count;
    }

    int getBalance() {
        return balance;
    }
}

다음 형태는 컴파일되지 않는다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
class Account {
    int balance;

    static int invalid() {
        // return balance;  // 인스턴스가 정해지지 않음
        // return this.balance;  // static 문맥에는 this가 없음
        return 0;
    }
}

정적 메서드가 인스턴스 필드나 인스턴스 메서드를 사용하려면 대상 객체를 명시적으로 받아야 한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
static int readBalance(Account account) {
    return account.balance;
}

핵심은 “정적 메서드가 인스턴스 멤버를 절대 사용할 수 없다”가 아니다. 현재 객체를 자동으로 갖지 않으므로 어떤 객체를 사용할지 명시해야 한다는 뜻이다.

정적 초기화

정적 필드 초기화식과 static 초기화 블록은 클래스 초기화 과정에서 실행된다. 같은 클래스 안에서는 일반적으로 소스에 적힌 순서대로 정적 필드 초기화식과 정적 초기화 블록을 처리한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
class Config {
    static int first = init("first", 1);

    static {
        System.out.println("block");
    }

    static int second = init("second", 2);

    static int init(String name, int value) {
        System.out.println(name);
        return value;
    }
}

처음으로 클래스 초기화가 필요한 적극적 사용이 발생하면 위 초기화가 한 번 수행된다. 다만 컴파일 시간 상수의 단순 사용처럼 클래스 초기화를 일으키지 않을 수 있는 예외가 있으므로, 모든 static 필드 참조가 반드시 초기화 블록을 실행한다고 단정하면 안 된다.

static 메서드는 오버라이딩되지 않는다

하위 클래스가 상위 클래스의 같은 시그니처 static 메서드를 선언하면 인스턴스 메서드 오버라이딩이 아니라 정적 메서드 숨김이다. 호출할 메서드는 실제 객체 형이 아니라 컴파일 시점의 형을 기준으로 정해진다. 이 규칙의 자세한 호출 판정은 앞 이론의 오버로딩·오버라이딩과 연결된다.

final: 적용 대상에 따라 달라지는 제한

final 변수는 한 번만 대입한다

지역 변수, 매개변수, 필드에 final을 붙이면 해당 변수는 한 번 대입된 뒤 다른 값을 다시 대입할 수 없다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
final int limit = 3;
// limit = 4;  // 컴파일 오류

선언과 동시에 초기화하지 않은 final 변수를 빈 final 변수(blank final variable) 라고 한다. 인스턴스의 빈 final 필드는 모든 생성 경로에서 한 번 대입되어야 한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
class Member {
    final int id;

    Member(int id) {
        this.id = id;
    }
}

생성자에서 대입한 뒤 다시 바꾸는 것은 허용되지 않는다. static final 빈 필드는 정적 초기화식이나 정적 초기화 블록 등 허용된 초기화 경로에서 정확히 한 번 대입해야 한다.

기본형 final과 참조형 final

기본형 변수는 값 자체를 저장한다. 기본형 final 변수는 저장된 값을 다시 바꿀 수 없다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
final int score = 100;

참조형 변수는 객체를 가리키는 참조값을 저장한다. 참조형 final 변수는 다른 객체를 가리키도록 재대입할 수 없지만, 현재 가리키는 객체의 상태 변경까지 자동으로 막지는 않는다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
import java.util.ArrayList;
import java.util.List;

final List<String> names = new ArrayList<>();
names.add("Kim");        // 가능
names.set(0, "Lee");    // 가능
// names = new ArrayList<>();  // 컴파일 오류

배열도 객체이므로 같은 원리가 적용된다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
final int[] values = {1, 2};
values[0] = 9;           // 가능
// values = new int[]{3, 4};  // 컴파일 오류

따라서 다음 세 문장을 구분해야 한다.

  • final 참조: 변수의 재대입을 막는다.
  • 변경 불가 컬렉션: 컬렉션을 통한 구조 변경 연산을 막는다.
  • 불변 객체: 외부에서 관찰되는 객체 상태가 생성 후 바뀌지 않도록 전체 설계한다.

final 메서드와 final 클래스

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
class Parent {
    final void fixed() { }
}

final 메서드는 하위 클래스에서 오버라이딩할 수 없다. 다만 이름이 같아도 매개변수 목록이 다른 메서드를 새로 오버로딩하는 것은 별개의 문제다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
final class Utility { }

final 클래스는 하위 클래스를 만들 수 없다. 클래스의 모든 객체가 자동으로 불변이라는 뜻은 아니다. final 클래스 안에 변경 가능한 필드와 변경 메서드가 있으면 객체 상태는 바뀔 수 있다.

Java 예외 계층과 검사 규칙

기본 계층

예외와 오류는 Throwable 계층으로 표현된다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Throwable
├─ Error
└─ Exception
   ├─ RuntimeException
   └─ 그 밖의 일반적인 검사 예외

시험에서는 다음처럼 분류한다.

분류대표 관계컴파일 시 처리·선언 의무일반적인 의미
검사 예외Exception의 하위이지만 RuntimeException 계열은 아님있음파일·네트워크 등 호출자가 처리하거나 전달할 수 있는 실패를 API 계약에 나타냄
실행 예외RuntimeException과 그 하위없음잘못된 인자, 널 참조, 잘못된 형 변환 등 실행 중 계약 위반이나 프로그램 오류를 자주 표현
오류Error와 그 하위없음JVM 자원 고갈 등 일반 애플리케이션이 보통 복구 대상으로 삼지 않는 심각한 문제

검사 예외라고 반드시 복구 가능하고, 실행 예외라고 절대 잡으면 안 된다는 뜻은 아니다. 분류의 핵심은 컴파일러의 처리·선언 검사 여부다.

catch (Exception e)Exception 계열을 잡지만 그 형제 계층인 Error까지 잡지는 않는다. Throwable을 잡으면 둘 다 포함될 수 있지만 일반적인 애플리케이션 코드에서 넓게 잡는 것은 신중해야 한다.

throwthrows

throw는 실행문에서 실제 예외 객체 하나를 발생시킨다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
if (amount < 0) {
    throw new IllegalArgumentException("amount");
}

throws는 메서드나 생성자 선언부에서 밖으로 전달될 수 있는 예외 형을 적는다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
static String readText() throws java.io.IOException {
    return "data";
}

검사 예외가 메서드 밖으로 전달될 가능성이 있으면 해당 메서드는 다음 중 하나를 해야 한다.

  1. 적절한 catch에서 처리한다.
  2. throws 절에 그 예외 또는 상위 형을 선언하여 호출자에게 전달한다.

비검사 예외도 문법상 throws에 적을 수 있지만 컴파일러가 선언을 의무화하지 않는다. throws를 적었다고 메서드가 호출될 때마다 반드시 예외가 발생하는 것도 아니다.

try·catch·finally의 제어 흐름

예외가 발생하지 않은 경우

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
try {
    System.out.println("try");
} catch (RuntimeException ex) {
    System.out.println("catch");
} finally {
    System.out.println("finally");
}
System.out.println("after");

예외가 없으면 try → finally → 이후 문장 순서로 진행한다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
try
finally
after

예외가 발생한 경우

try 안에서 예외가 발생하면 남은 문장은 실행하지 않는다. 현재 trycatch를 위에서부터 검사하여 실제 예외 객체와 대입 호환되는 첫 번째 처리기를 선택한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
try {
    System.out.println("start");
    Integer.parseInt("x");
    System.out.println("end");
} catch (NumberFormatException ex) {
    System.out.println("number");
} finally {
    System.out.println("cleanup");
}
CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
start
number
cleanup

end는 실행되지 않는다.

catch 순서

하위 예외 형을 먼저, 넓은 상위 형을 뒤에 둔다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
try {
    // 작업
} catch (java.io.FileNotFoundException ex) {
    // 더 구체적인 처리
} catch (java.io.IOException ex) {
    // 더 넓은 처리
}

반대로 IOException을 먼저 두면 뒤의 FileNotFoundException 처리기는 도달할 수 없으므로 컴파일 오류다.

여러 예외를 같은 방식으로 처리하면 다중 catch를 사용할 수 있다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
try {
    // 작업
} catch (java.io.IOException | java.sql.SQLException ex) {
    System.out.println(ex.getMessage());
}

다중 catch의 대안 형끼리는 상하위 관계일 수 없다. 예를 들어 IOException | FileNotFoundException처럼 한쪽이 다른 쪽의 하위 형이면 허용되지 않는다.

finally는 언제 실행되는가

finallytry 또는 catch가 정상 종료되거나 return, break, continue, 예외 발생 등으로 갑자기 종료될 때에도 일반적으로 실행된다. 그래서 잠금 해제나 임시 상태 복구 같은 정리 작업에 사용할 수 있다.

하지만 “어떤 상황에서도 무조건 실행된다”는 설명은 정확하지 않다. JVM 프로세스가 강제로 종료되거나 가상 머신이 비정상적으로 중단되는 상황에서는 실행이 보장되지 않는다.

finally 안에서 새 예외를 던지거나 return하면 앞서 발생한 예외나 반환값을 가릴 수 있다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
static int dangerous() {
    try {
        return 1;
    } finally {
        return 2;
    }
}

위 메서드의 결과는 2다. 그러나 이런 코드는 원래 흐름과 예외를 숨기므로 사용하지 않는 것이 안전하다.

예외를 잡지 못하면 상위 호출자로 전파된다

현재 메서드에서 일치하는 catch를 찾지 못하면 메서드 실행이 갑자기 끝나고 호출자 쪽으로 예외가 전달된다. 호출 스택을 따라 올라가며 처리기를 찾고, 끝까지 찾지 못하면 해당 스레드의 미처리 예외가 된다.

finally가 있다면 각 스택 프레임을 빠져나가는 과정에서 해당 정리 블록이 수행될 수 있다. 따라서 출력 문제는 “예외 발생 지점 → 현재 catch 탐색 → finally → 호출자 전파” 순서로 추적한다.

Java 컬렉션 계층의 기본 구조

CollectionMap

대표 구조는 다음처럼 이해한다.

CODE코드 영역 안에서 좌우로 이동할 수 있습니다.
Iterable
└─ Collection
   ├─ List
   ├─ Set
   └─ Queue / Deque

Map  ← Collection의 하위가 아닌 별도 인터페이스

Collection<E>는 여러 원소를 한 그룹으로 다루는 계층의 루트 인터페이스다. Map<K, V>은 키와 값의 대응을 다루므로 이 계층에 속하지 않는다.

다만 Map은 내용을 컬렉션 형태로 볼 수 있는 뷰를 제공한다.

  • keySet(): 키의 Set
  • values(): 값의 Collection
  • entrySet(): 키-값 쌍의 Set

값은 중복될 수 있으므로 values()의 반환형은 Set이 아니다.

CollectionCollections

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
java.util.Collection<String> values;
java.util.Collections.sort(list);
  • Collection: 원소 그룹의 공통 연산을 정의하는 인터페이스
  • Collections: 정렬, 역순, 변경 불가 뷰 생성 같은 정적 도우미 메서드를 제공하는 클래스

끝의 s 유무만 다른 별개의 대상이다.

제네릭 원소 형

컬렉션 제네릭에는 기본형을 직접 사용할 수 없다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
// List<int> invalid;  // 컴파일 오류
java.util.List<Integer> valid;

또한 제네릭은 기본적으로 불공변이다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
java.util.List<Integer> integers = new java.util.ArrayList<>();
// java.util.List<Number> numbers = integers;  // 컴파일 오류

IntegerNumber의 하위 타입이어도 List<Integer>List<Number>의 하위 타입이 되는 것은 아니다.

List·Set·Map 선택 기준

핵심 계약 비교

인터페이스핵심 계약중복위치·순서대표 연산
List<E>순서가 있는 원소 열과 정수 인덱스일반적으로 허용있음get, set, add, remove
Set<E>같은 원소를 둘 이상 보관하지 않음허용하지 않음구현체에 따라 다름add, contains, remove
Map<K,V>고유한 키 하나에 값 하나를 대응키 중복 불가, 값 중복 가능구현체에 따라 다름put, get, containsKey, remove

Set.add(value)는 동등한 원소가 이미 있으면 보통 false를 반환하고 구조를 바꾸지 않는다. Map.put(key, value)는 같은 키가 있으면 새 엔트리를 하나 더 만드는 것이 아니라 기존 값을 교체하며 이전 값을 반환할 수 있다.

List

List는 원소의 순서와 위치를 보존하고 인덱스로 접근한다. 같은 값이 여러 번 들어갈 수 있다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
java.util.List<String> list = new java.util.ArrayList<>();
list.add("A");
list.add("B");
list.add("A");
System.out.println(list.get(1));  // B

정수 래퍼를 저장한 목록에서는 remove 오버로드를 주의한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
java.util.List<Integer> numbers =
        new java.util.ArrayList<>(java.util.List.of(10, 20, 30));

numbers.remove(1);                   // 인덱스 1의 값 20 제거
numbers.remove(Integer.valueOf(30)); // 값 30 제거

Set

Set은 동등한 원소를 중복 보관하지 않는다. 일반적인 해시 기반 SetequalshashCode를 이용해 중복 여부를 판단한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
java.util.Set<String> set = new java.util.HashSet<>();
System.out.println(set.add("A"));  // true
System.out.println(set.add("A"));  // false

Set이라는 인터페이스 이름만으로 순회 순서를 단정할 수 없다. 순서 보장 여부는 구현체 계약을 확인해야 한다.

Map

Map은 키를 이용해 값을 저장하고 검색한다.

JAVA코드 영역 안에서 좌우로 이동할 수 있습니다.
java.util.Map<String, Integer> scores = new java.util.HashMap<>();
scores.put("Kim", 80);
scores.put("Lee", 90);
scores.put("Kim", 95);

마지막 putKim 키의 값을 80에서 95로 바꾼다. 키는 하나지만 값 95가 다른 키에도 저장되는 것은 가능하다.

get(key)null을 반환하면 키가 없을 수도 있고, 해당 키에 실제로 null 값이 연결되어 있을 수도 있다. 구현체가 널 값을 허용하는 경우 두 상태를 구분하려면 containsKey(key)도 확인한다.

대표 컬렉션 구현

구현체중심 특성
ArrayList배열 기반 목록, 인덱스 접근에 적합
LinkedList연결 구조 기반 목록, 원소 탐색과 연결 변경 비용을 구분
HashSet·HashMap해시 기반, 반복 순서를 보장하지 않음
LinkedHashSet·LinkedHashMap일반적인 사용에서 삽입 순서 유지
TreeSet·TreeMap정렬 기준에 따라 원소 또는 키의 순서 유지

List는 같은 값을 여러 번 저장할 수 있다. Set는 같은 것으로 판단되는 원소의 중복을 허용하지 않는다. Map은 키마다 하나의 값을 대응시키며, 같은 키에 새 값을 넣으면 보통 기존 값을 교체한다. Map의 값은 중복될 수 있다.

해시 기반 컬렉션에서는 객체의 동등성 판단과 해시값이 함께 사용된다. 같은 것으로 비교되는 객체는 같은 해시값을 가져야 한다. 여기서 내부 버킷 구조나 충돌 처리 구현까지 외울 필요는 없다.

final List<String> list는 list가 다른 객체를 가리키도록 재대입하지 못한다는 뜻이다. 가리키는 목록이 변경 가능하다면 addremove는 가능하다.

접근 제어

다음은 일반적인 최상위 클래스의 멤버 접근 범위를 비교한 것이다.

접근 제한같은 클래스같은 패키지다른 패키지의 하위 클래스그 밖의 클래스
public가능가능가능가능
protected가능가능상속 관계와 접근 방식의 조건 아래 가능불가
생략: 패키지 접근가능가능불가불가
private가능불가불가불가

접근 제한자를 생략하면 패키지 접근이며, default라는 키워드를 붙이는 방식이 아니다. protected는 같은 패키지에서도 사용할 수 있으므로 “자식 클래스만 접근 가능”이라고 설명하면 부정확하다. 다른 패키지의 하위 클래스에서는 임의의 상위 클래스 객체를 통해 모든 protected 멤버에 자유롭게 접근할 수 있는 것은 아니다.

캡슐화는 내부 상태를 감추고 필요한 연산만 공개하여 올바른 상태를 유지하게 하는 것이다. private 필드에 대해 무조건 모든 setter를 공개하는 것이 목적은 아니다. 상위 메서드를 오버라이딩할 때는 접근 범위를 더 좁힐 수 없다.

자동 자원 정리의 기본

try-with-resources는 파일 같은 자원을 try 문에 선언하고 사용 후 자동으로 닫도록 돕는 문법이다. 일반 finally를 이용한 정리와 마찬가지로 성공·실패 경로에서 자원이 남지 않게 하는 데 목적이 있다. 예외 처리와 자원 해제의 역할은 서로 구분한다.

자원 종료 순서와 반환값의 구분

try-with-resources에는 AutoCloseable 계약에 맞는 자원을 사용한다. 여러 자원을 순서대로 초기화해 모두 성공했다면 종료 시에는 선언·초기화의 역순으로 close를 호출한다. 예를 들어 자원 A 다음 B를 열면 B, A 순서로 닫는다. 모든 자원이 초기화되기 전에 실패하면 성공적으로 초기화된 자원만 종료 대상이 된다.

return x는 반환할 값을 먼저 정하고 finally를 거쳐 호출자에게 전달한다. finally에서 지역 변수 x의 값만 바꾸어도 이미 정한 기본형 반환값이 바뀌지는 않는다. 다만 finally가 새로 return하거나 예외를 던지면 앞선 반환·예외를 덮을 수 있으므로, finally에서는 불필요한 제어 흐름 변경을 피한다.