본문 바로가기
Java & Spring

자바 정리 (3)

by WhoamixZerOne 2024. 2. 19.

이 글은 자바의 정석 3판을 정리한 내용입니다.
자바의 정석 3판

 

✔ 예외처리(Exception Handling)

1. 프로그램 오류

  • 컴파일 에러(Compile-Time Error) : 컴파일할 때 발생하는 에러
  • 런타임 에러(Runtime Error) : 실행할 때 발생하는 에러
  • 논리적 에러(Logical Error) : 작성 의도와 다르게 동작

Java의 런타임 에러

에러(error)는 어쩔 수 없지만, 예외(exception)는 처리하자.

  • 에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

예외 처리의 정의와 목적(Exception Handling)

  • 정의 : 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
  • 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행 상태를 유지하는 것

 

2. 예외 클래스의 계층 구조

예외의 종류 checked, unchecked 예외

  • checked 예외 : 컴파일러가 예외 처리 여부를 체크(예외 처리 필수)
  • unchecked 예외 : 컴파일러가 예외 처리 여부를 체크 안 함(예외 처리 선택)

checked 예외는 Exception 클래스와 그 자식들에 해당

unchecked 예외는 RuntimeException 클래스와 그 자식들에 해당

 

3. 멀티 catch 블록

JDK1.7부터 여러 catch블록을 '|' 기호를 이용해서 하나의 catch 블록으로 합칠 수 있다.

try {
    ...
} catch (ExceptionA e) {
    e.printStackTrace();
} catch (ExceptionB e) {
    e.printStackTrace();
}
// 위의 코드를 아래의 멀티 catch 블록으로 변경 가능
try {
    ...
} catch (ExceptionA | ExceptionB e) {
    e.printStackTrace();
}

단, 멀티 catch 블록으로 연결된 예외 클래스가 부모와 자식의 관계에 있다면 컴파일 에러가 발생한다.

부모와 자식 관계에 있다면 부모 클래스만 써주는 것과 똑같기 때문이다.(다형성!)

 

4. 메서드에 예외 선언하기

예외를 처리하는 방법

  1. try-catch문
  2. 예외 선언하기(예외 떠넘기기, 던지기) : 메서드가 호출 시 발생 가능한 예외를 호출하는 쪽에 알리는 것
// method()를 호출한 쪽에서 선언된 예외를 try-catch문으로 처리해야 한다
// 그래서 예외 떠넘기기 혹은 던지기라고도 한다
void method() throws Exception1, Exception2 { }

 

5. 자동 자원 반환(try-with-resources문)

JDK1.7부터 'try-with-resources문'이라는 try-catch문의 변형이 새로 추가되었다.

입출력(I/O), 소켓(Socket) 등 사용한 후에 꼭 닫아 줘야 하는 것들이 있다.

그래야 사용했던 자원(resources)이 반환되기 때문이다.

try {
    fis = new FileInputStream("score.dat");
    dis = new DataInputStream(fis);
    ...
} catch (IOException ie) {
    ie.printStackTrace();
} finally {
    try {
        if (dis != null) {
            dis.close();
        }
    } catch (IOException ie) {
        ie.printStackTrace();
    }
}

위의 코드처럼 finally 블록에서 try-catch문을 추가해서 close()에서 발생할 수 있는 예외를 처리해야 한다.

하지만 코드가 복잡해져서 별로 보기에 좋지 않다. 그래서 개선하기 위해서 'try-with-resources'문이 추가된 것이다.

// 괄호()안에 두 문장 이상 넣을 경우 ';'로 구분한다
try (FileInputStream fis = new FileInputStream("score.dat");
    DataInputStream dis = new DataInputStream(fis)) {
    ...
} catch (IOException ie) {
    ie.printStackTrace();
}

위의 코드처럼 좀 더 간편하게 구현할 수 있다.

괄호() 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블록을 벗어나는 순간 자동적으로 close()가 호출된다. 그다음에 catch 블록 또는 finally 블록이 수행된다.

 

단, 자동으로 객체의 close()가 호출될 수 있으려면 클래스가 'AutoCloseable' 인터페이스를 구현한 것이어야만 한다.

public interface AutoCloseable {
    void close() throws Exception;
}

 

✔ 오토박싱 & 언박싱(Autoboxing & Unboxing)

int → Integer (오토박싱) : 기본형을 Integer 자료형으로 감싸준다.(wrapper)

int ← Integer (언박싱) : Integer 자료형을 기본형으로 변환한다.

 

컴파일러가 기본형의 값을 객체로 자동변환하는 것을 오토박싱, 그 반대는 언박싱이다.

ex) 컴파일 전과 후의 코드

// 컴파일 전
int i = 5;
Integer iObj = new Integer(7); // new Integer()는 deprecate. Integer.valueOf(); 사용
int sum = i + iObj;  // 에러. 기본형과 참조형 간의 덧셈 불가(JDK1.5 이전)
// 컴파일 후
int i = 5;
Integer iObj = new Integer(7);
int sum = i + iObj.intValue();

 

ex) ArrayList<Integer>

ArrayList<Integer> list = new ArrayList<>();
list.add(10);  // 오토박싱. 10 → new Integer(10);
int value = list.get(0);  // 언박싱. new Integer(10) → 10

위의 코드에서 'ArrayList'의 타입을 'Integer'로 지정했기 때문에 원래는 'Integer'타입으로 넣어줘야 하지만 오토박싱으로 기본형 값을 넣어줘도 된다. 마찬가지로 값을 반환할 때 언박싱이 이루어진다.

 

✔ 컬렉션 프레임웍(Collections framework)

1. 컬렉션(Collection)

  • 여러 객체(데이터)를 모아 놓은 것을 의미
  • 표준화, 정형화된 체계적인 프로그래밍 방식
  • 컬렉션(다수의 객체)을 다루기 위한 표준화된 프로그래밍 방식

 

2. 컬렉션 프레임웍의 핵심 인터페이스

  • List
  • Set
  • Map
인터페이스 특     징
List 순서가 있는 데이터의 집합, 데이터의 중복을 허용한다.
구현클래스 : ArrayList, LinkedList, Stack, Vector 등
Set 순서를 유지하지 않는 데이터의 집합, 데이터의 중복을 허용하지 않는다.
구현클래스 : HashSet, TreeSet 등
Map 키(key)와 값(value)의 쌍(pair)으로 이루어진 데이터의 집합
순서는 유지되지 않으며, 키는 중복을 허용하지 않고, 값은 중복을 허용한다.
구현 클래스 : HashMap, TreeMap, Hashtable, Properties 등

 

3. 컬렉션 클래스 정리 & 요약

컬렉션 클래스간의 관계

 

✔ 지네릭스(Generics)

  • 컴파일 시 타입을 체크해 주는 기능
  • 객체의 타입 안전성을 높이고 형변환의 번거로움을 줄여줌

 

1. 타입 변수

지네릭 클래스를 작성할 때, Object 타입 대신 타입 변수(E)를 선언해서 사용

public class ArrayList extedns AbstractList {
    private transient Object[] elementData;
    public boolean add(Object o) {}
    public Object get(int index) {}
}
// 위의 코드를 지네릭 클래스로 변경
public class ArrayList<E> extends AbstractList<E> {
    private transient E[] elementData;
    public boolean add(E o) {}
    public E get(int index) {}
}
class Box {
    Object item;
    
    void setItem(Object item) {
        this.item = item;
    }
    Object getItem() {
        return item;
    }
}
// 위의 코드를 지네릭 클래스로 변경
class Box<T> {  // 지네릭 타입 T를 선언
    T item;
    
    void setItem(T item) {
        this.item = item;
    }
    T getItem() {
        return item;
    }
}
  • Box : 원시타입(raw type)
  • T : 타입 변수(type variable), 타입 매개변수. '임의의 참조형 타입'을 의미
  • Box<T> : 지네릭 클래스
Box<String> b = new Box<String>();
  • Box<String>, new Box<String> : 지네릭 타입 호출
  • <String> : 대입된 타입(매개변수화 된 타입, parameterized type)

참조변수와 생성자의 대입된 타입은 일치해야 한다.

ArrayList<Tv> tvs = new ArrayList<Tv>();  // 일치 OK
ArrayList<Product> products = new ArrayList<Tv>();  // 불일치 Error

 

2. 지네릭의 제한

모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다.

그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 'new T[10]'과 같이 배열을 생성하는 것은 안 된다는 뜻이다.

class Box<T> {
    T[] itemArr;  // OK T타입의 배열을 위한 참조변수
    ...
    T[] toArray() {
        T[] tmpArr = new T[itemArr.length];  // Error 지네릭 배열 생성 불가
        ...
        return tmpArr;
    }
}

지네릭 배열을 생성할 수 없는 것은 new 연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 위의 코드에 정의된 Box<T>클래스를 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다.

instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.

 

3. 제한된 지네릭 클래스

'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> {  // Fruit의 자식만 타입으로 지정 가능
    ArrayList<T> list = new ArrayList<T>();
    ...
}

FruitBox<Apple> appleBox = new FruitBox<Apple>();  // OK
FruitBox<Toy> toyBox = new FruitBox<Toy>();  // Error Toy는 자식이 아님

인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends'를 사용한다. 'implements'를 사용하지 않는다는 점에 주의한다.

interface Eatable {}
class FruitBox<T extedns Eatable> {}

클래스 Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면 '&' 기호로 연결한다.

class FruitBox<T extends Fruit & Eatable> {}

 

4. 와일드카드 <?>

하나의 참조변수로 대입된 타입(매개변수화 된 타입)이 다른 객체를 참조 가능하다.

메서드의 매개변수에 와일드카드 사용 가능하다.

class Juicer {
    static Juice makeJuice(FruitBox<Fruit> box) {
        ...
    }
}

FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
Juicer.makeJuice(fruitBox);  // OK FruitBox<Fruit>
Juicer.makeJuice(appleBox);  // Error FruitBox<Apple>

static 메서드에서는 타입 매개변수 T를 매개변수에 사용할 수 없으므로 특정 타입을 지정해줘야 한다.

'FruitBox<Fruit>'로 고정해 놓으면, 'FruitBox<Apple>'타입의 객체는 사용할 수 없으므로 여러 가지 타입의 매개변수를 갖는 함수를 만들 수밖에 없다.

 

하지만 오버로딩하면, 컴파일 에러가 발생한다. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.

지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해 버리기 때문이다.

이럴 때 사용하기 위해 고안된 것이 와일드카드이다. 와일드카드는 '?' 기호로 표현하고, 어떠한 타입도 될 수 있다.

  • <? extends T> : 와일드카드의 상한 제한(upper bound). T와 그 자식들만 가능
  • <? super T> : 와일드카드의 하한 제한(lower bound). T와 그 부모들만 가능
  • <?> : 제한 없음. 모든 타입이 가능. <? extends Object>와 동일

와일드카드에는 '&'를 사용할 수 없다. '<? extends T & E>'와 같이 할 수 없다.

위의 코드를 와일드카드를 사용해서 문제를 해결하면 아래와 같다.

class Juicer {
    static Juice makeJuice(FruitBox<? extends Fruit> box) {
        ...
    }
}

FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
Juicer.makeJuice(fruitBox);  // OK FruitBox<Fruit>
Juicer.makeJuice(appleBox);  // OK FruitBox<Apple>

 

5. 지네릭 메서드

지네릭 타입이 선언된 메서드(타입 변수는 메서드 내에서만 유효)

static <T> void sort(List<T> list, Comparator<? super T> c) {}

 

클래스의 타입 매개변수<T>와 메서드의 타입 매개변수<T>는 별개

// 클래스 T와 메서드 반환 타입 앞의 T는 타입 문자가 일치하지만
// 다른 타입변수로 사용
class FruitBox<T> {  // 지네릭 클래스
    ...
    static <T> void sort(List<T> list, Comparator<? super T> c) {}  // 지네릭 메서드
}

 

메서드를 호출할 때마다 타입을 대입해야 한다.(대부분 생략 가능)

System.out.println(Juicer.<Fruit>makeJuice(fruitBox));  // <Fruit> 생략 가능

 

메서드를 호출할 때 타입을 생략하지 않을 때는 클래스 이름 생략 불가

System.out.println(<Fruit>makeJuice(fruitBox)); // Error 클래스 이름 생략 불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // OK

 

지네릭 메서드와 와일드카드 매개변수 차이

  • 지네릭 메서드 : 메서드를 호출할 때마다 다른 지네릭 타입을 대입할 수 있게 한 것
  • 와일드카드 매개변수 : 하나의 참조변수로 서로 다른 타입이 대입된 여러 지네릭 객체를 다루기 위한 것

✔ 열거형(enum)

  • 관련된 상수들을 같이 묶어 놓은 것(그룹)
  • Java는 타입에 안전한 열거형을 제공(값 & 타입 둘 다 체크)
  • 일반 클래스처럼 사용 가능(인스턴스 생성 불가)
  • 열거형 생성자는 묵시적으로 private이므로, 외부에서 객체 생성 불가

1. 열거형의 정의와 사용

열거형을 정의하는 방법

enum 열거형 이름 { 상수명1, 상수명2, ... }

enum Direction {
    EAST(1),
    SOUTH(-2),
    WEST(-1),
    NORTH(2);
    
    private final int value;  // 정수를 저장할 인스턴스 변수(필드)를 추가
    
    Direction(int value) {  // 생성자를 추가. private 생략 가능
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
}

 

enum 활용에 대한 더 자세한 내용은 아래의 우아한 기술 블로그 참조
우아한 기술 블로그 enum

 

✔ 애너테이션(annotation)

주석처럼 프로그래밍 언어에 영향을 미치지 않으며, 유용한 정보를 제공한다.

 

@FunctionalInterface

  • 함수형 인터페이스에 붙이면, 컴파일러가 올바르게 작성했는지 체크
  • 함수형 인터페이스에는 하나의 추상메서드만 가져야 한다는 제약이 있다

1. 메타 애너테이션

  • 메타 애너테이션은 애너테이션을 만들 때 사용
  • 메타 애너테이션은 '애너테이션을 위한 애너테이션'이다
애너테이션 설 명
@Target 애너테이션이 적용가능한 대상을 지정하는데 사용한다.
@Documented 애너테이션 정보가 javadoc으로 작성된 문서에 포함되게 한다.
@Inherited 애니테이션이 자식 클래스에 상속되도록 한다.
@Retention 애너테이션이 유지되는 범위를 지정하는데 사용한다.
@Repeatable 애너테이션을 반복해서 적용할 수 있게 한다.(JDK1.8)

 

@Target

애너테이션을 정의할 때, 적용대상 지정에 사용한다.

대상 타입 의 미
ANNOTATION_TYPE 애너테이션
CONSTRUCTOR 생성자
FIELD 필드(멤버변수, enum상수)
LOCAL_VARIABLE 지역변수
METHOD 메서드
PACKAGE 패키지
PARAMETER 매개변수
TYPE 타입(클래스, 인터페이스, enum)
TYPE_PARAMETER 타입 매개변수(JDK1.8)
TYPE_USE 타입이 사용되는 모든 곳(JDK1.8)
import static java.lang.annotation.ElementType.*;

@Target({FIELD, TYPE, TYPE_USE})  // 적용대상이 FIELD, TYPE, TYPE_USE
public @interface MyAnnotation {}  // MyAnnotation을 정의

@MyAnnotation  // 적용대상이 TYPE인 경우
class MyClass {
    @MyAnnotation  // 적용대상이 FIELD인 경우
    int i;
    
    @MyAnnotation  // 적용대상이 TYPE_USE인 경우
    MyClass mc;
}
// FIELD는 기본형에, TYPE_USE는 참조형에 사용된다는 점에 주의

 

@Retention

애너테이션이 유지(retention)되는 기간을 지정하는 데 사용한다.

유지 정책 의 미
SOURCE 소스 파일에만 존재. 클래스파일에는 존재하지 않음.
CLASS 클래스 파일에 존재. 실행시에 사용불가. 기본값
RUNTIME 클래스 파일에 존재. 실행시에 사용 가능

 

2. 애너테이션 타입 정의하기

@Retention(RetentionPolicy.RUNTIME)  // 실행 시에 사용가능하도록 지정
@interface TestInfo {
    int count() default 1;
    String testedBy();
    String[] testTools() default "JUnit";
    TestType testType() default TestType.FIRST;  // enum TestType
    DateTime testDate();  // 자신이 아닌 다른 애너테이션(@DateTime)을 포함할 수 있다
}
@Retention(RetentionPolicy.RUNTIME)
@interface DateTime {
    String yymmdd();
    String hhmmss();
}
enum TestType { FIRST, FINAL }

@TestInfo(testedBy="aaa", testDate=@DateTime(yymmdd="160101", hhmmss="235959"))
class AnnotationMain {
    ...
}

 

애너테이션 요소의 규칙

애너테이션의 요소를 선언할 때, 아래의 규칙을 반드시 지켜야 한다.

  • 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용
  • 괄호() 안에 매개변수를 선언할 수 없다
  • 예외를 선언할 수 없다
  • 요소를 타입 매개변수(지네릭)로 정의할 수 없다

✔ 레코드 클래스(record class)

레코드는 Java 14 버전에서 Preview로 나왔고 Java 16 버전에서 표준화되었다.

레코드는 불변(Immutable) 데이터 객체를 쉽게 표현할 수 있는 새로운 종류의 클래스이며, 데이터를 저장하고 접근하는 method 등을 자동으로 생성해 주기 때문에 코드의 간결성과 가독성을 향상한다.

 

1. 레코드 특징

  • 불변성(Immutable) : 레코드는 생성된 후에 데이터를 수정할 수 없다. 데이터의 불변성을 보장하며, 불변 객체는 예측 가능하고 안정적인 동작을 촉진
    • 필드는 private final로 선언
  • 자동 생성 메서드 : 컴파일러가 자동으로 생성해 주는 몇 가지 메서드를 포함한다
    • 생성자(public constructor)
    • 접근자 메서드(getter)
    • equals(), hashCode(), toString()
  • 기본 생성자는 제공하지 않으므로 필요한 경우 직접 생성

 

2. 레코드 사용 방법

기존 불변 객체

public class Person {
    private final String name;
    private final int age;    
    // constructor 생성
    // 멤버변수 별 getter 생성
    // equals 메서드 생성
    // hashCode 메서드 생성
    // toString 메서드 생성
}

불변 객체를 생성하기 위해서는 boilerplate field와 method 등 많은 코드를 작성해야 한다. 비록 IDE에서 간편하게 작성하도록 기능을 제공하고 있다고 해도 코드가 길어져서 가독성이 떨어질 수 있고, 여러 데이터 클래스를 만들어야 하면 귀찮다.

이 같은 작업을 레코드 클래스를 사용하면 훨씬 간결하고 가독성이 좋은 코드로 구현할 수 있다.

 

레코드 불변 객체

public record Person(String name, int age) {
    // 추가적인 메서드나 static변수, 새로운 생성자를 정의할 수 있다
}

레코드 클래스를 사용하면 필드 정의, 생성자, equals(), hashCode(), toString(), getter 메서드를 자동으로 생성하기 때문에 정말 간결해졌다. 이러한 기능은 주로 데이터 전달 객체(DTO)나 불변성을 유지해야 하는 데이터 클래스에 유용하다.

public class RecordMain {
    public static void main(String[] args) {
        Person person = new Person("John", 25);
        
        // getter는 '필드명()'으로 사용
        System.out.println(person.name());  // John
        System.out.println(person.age());  // 25
    }
}

 

3. 레코드 제한

  • 레코드는 암묵적으로 final 클래스(상속 불가)
  • abstract 선언 불가
  • 클래스를 상속받을 수 없지만, 인터페이스 구현은 가능

 

 

 

🔗 Reference

'Java & Spring' 카테고리의 다른 글

자바 정리 (4)  (4) 2024.02.28
자바 정리 (2)  (1) 2024.02.06
자바 정리 (1)  (0) 2024.01.22
Java 버전 관리 도구  (0) 2023.11.18
[Spring] 인터셉터(Interceptor) 적용  (0) 2022.04.17

댓글