oslob.site
☕️
ITJAVA

자바 중급 1편

2025.01.02

1. Object 클래스

java.lang 패키지의 대표적인 클래스들

  • Object : 모든 자바 객체의 부모 클래스
  • String : 문자열
  • Integer , Long , Double : 래퍼 타입, 기본형 데이터 타입을 객체로 만든 것
  • Class : 클래스 메타 정보
  • System : 시스템과 관련된 기본 기능들을 제공

import 생략 가능

java.lang 패키지는 모든 자바 애플리케이션에 자동으로 임포트( import )된다. 따라서 임포트 구문을 사용하지 않아도 된다.

object

묵시적(Implicit) vs 명시적(Explicit)

묵시적: 개발자가 코드에 직접 기술하지 않아도 시스템 또는 컴파일러에 의해 자동으로 수행되는 것을 의미 명시적: 개발자가 코드에 직접 기술해서 작동하는 것을 의미

자바에서 Object 클래스가 최상위 부모 클래스인 이유

  • 공통 기능 제공
  • 다형성의 기본 구현

Object 다형성

Object를 활용한 다형성의 한계

  • Object 는 모든 객체를 대상으로 다형적 참조를 할 수 있다.
    • 쉽게 이야기해서 Object 는 모든 객체의 부모이므로 모든 객체를 담을 수 있다.
  • Object 를 통해 전달 받은 객체를 호출하려면 각 객체에 맞는 다운캐스팅 과정이 필요하다.
    • Object 가 세상의 모든 메서드를 알고 있는 것이 아니다.

Object 는 모든 객체의 부모이므로 모든 객체를 대상으로 다형적 참조를 할 수 있다. 하지만 Object 에는Dog.sound() , Car.move() 와 같은 다른 객체의 메서드가 정의되어 있지 않다. 따라서 메서드 오버라이딩을 활용할 수 없다. 결국 각 객체의 기능을 호출하려면 다운캐스팅을 해야 한다. 참고로 Object 본인이 보유한 toString() 같은 메서드는 당연히 자식 클래스에서 오버라이딩 할 수 있다.

ToString()

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • Object 가 제공하는 toString() 메서드는 기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시 코드)를 16진수로 제공한다.

toString() 오버라이딩

Object.toString() 메서드가 클래스 정보와 참조값을 제공하지만 이 정보만으로는 객체의 상태를 적절히 나타내지 못한다. 그래서 보통 toString() 을 재정의(오버라이딩)해서 보다 유용한 정보를 제공하는 것이 일반적이다.

@Override
public String toString() {
    return "Dog{" +
            "dogName='" + dogName + '\'' +
						", age=" + age +
						'}';
}

toString() 은 기본으로 객체의 참조값을 출력한다. 그런데 toString() 이나 hashCode() 를 재정의하면 객체의 참조값을 출력할 수 없다. 이때는 다음 코드를 사용하면 객체의 참조값을 출력할 수 있다.

 String refValue = Integer.toHexString(System.identityHashCode(dog1));
 System.out.println("refValue = " + refValue);

Object 와 OCP

OCP 원칙

기본편에서 학습한 OCP 원칙을 떠올려보자.

  • Open : 새로운 클래스를 추가하고, toString() 을 오버라이딩해서 기능을 확장할 수 있다.
  • Closed : 새로운 클래스를 추가해도 ObjecttoString() 을 사용하는 클라이언트 코드인 ObjectPrinter 는 변경하지 않아도 된다.

자바 언어는 객체지향 언어 답게 언어 스스로도 객체지향의 특징을 매우 잘 활용한다. 우리가 지금까지 배운 toString() 메서드와 같이, 자바 언어가 기본으로 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩해서 사용할 수 있도록 설계되어 있다.

  • 자바 언어로 개발하면서 필요한 기본적이고 공통적인 기능을 사용할 수 있는 이유이고, 또한 개발자들이 기능들을 재정의해서 개발할 수 있는 이유이다.

Equals() - 1. 동일성과 동등성

  • 동일성 (Identity) : == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
  • 동등성 (Equality) : equals() 메서들르 사용해 두 객체가 논리적으로 동등한지 확인

동일성은 자바 머신 기준이고 메모리의 참조가 기준이므로 물리적이다. 반면 동등성은 보통 사람이 생각하는 논리적인 기준에 맞추어 비교한다.

동등성 비교를 사용하고 싶으면 equals() 메서드를 재정의해야 한다. 그렇지 않으면 Object 는 동일성 비 교를 기본으로 제공한다.

Equals() - 2. 구현

//변경 - 정확한 equals 구현, IDE 자동 생성 @Override
public boolean equals(Object o) {
     if (this == o) return true;
     if (o == null || getClass() != o.getClass()) return false;
     User user = (User) o;
     return Objects.equals(id, user.id);
}

equals() 메서드를 구현할 때 지켜야 하는 규칙

  • 반사성(Reflexivity) : 객체는 자기 자신과 동등해야 한다. ( x.equals(x) 는 항상 true )
  • 대칭성(Symmetry) : 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다.

(x.equals(y)true 이면 y.equals(x)true).

  • 추이성(Transitivity) : 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객체가 세 번째 객체와 동일하다면, 첫번째 객체는 세 번째 객체와도 동일해야 한다.
  • 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, equals() 메소드는 항상 동일한 값을 반환해야한다.
  • null에 대한 비교 : 모든 객체는 null 과 비교했을 때 false 를 반환해야 한다.

2. 불변 객체

기본형과 참조형의 공유

  • 기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
  • 참조형 : 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.

정리 불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있다.

  • 객체의 공유 참조는 막을 수 없다. 그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는사이드 이펙트가 발생한다. 사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 된다.

불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단된다.

  • 불변 객체는 값을 변경할 수 없다. 따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변객체를 생성해야 한다. 이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않는다.

자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이기 때문이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer , LocalDate 등 수 많은 클래스가 불변으로 설계되어 있다.

클래스를 불변으로 설계하는 이유는 더 많다.

  • 캐시 안정성
  • 멀티 쓰레드 안정성
  • 엔티티의 값 타입

3. String 클래스

참고: 자바 9 이후 String 클래스 변경 사항

자바 9부터 String 클래스에서 char[] 대신에 byte[] 을 사용한다.

자바에서 문자 하나를 표현하는 char2byte 를 차지한다. 그런데 영어, 숫자는 보통 1byte 로 표현이 가능하다. 그래서 단순 영어, 숫자로만 표현된 경우 1byte 를 사용하고(정확히는 Latin-1 인코딩의 경우 1byte 사용) , 그렇지 않은 나머지의 경우 2byte 인 UTF-16 인코딩을 사용한다. 따라서 메모리를 더 효율적으로 사용할수 있게 변경되었다.

문자열은 너무 자주 다루어지기 때문에 자바 언어에서 편의상 특별히 + 연산을 제공한다.

String 클래스 - 비교

  • 동일성(Identity) : == 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인
  • 동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 같은지 확인
public static void main(String[] args) {
	String str1 = new String("hello");
	String str2 = new String("hello");
	
	System.out.println("new String() == 비교: " + (str1 == str2));
	System.out.println("new String() equals 비교: " + (str1.equals(str2)));
	
	String str3 = "hello";
	String str4 = "hello";
	
	System.out.println("리터럴 == 비교: " + (str3 == str4));
	System.out.println("리터럴 equals 비교: " + (str3.equals(str4)));
} }

실행결과

new String() == 비교: false
new String() equals 비교: true
리터럴 == 비교: true
리터럴 equals 비교: true

string

  • String str3 = “hello” 와 같이 문자열 리터럴을 사용하는 경우 자바는 메모리 효율성과 성능 최적화를 위

해 문자열 풀을 사용한다.

  • 자바가 실행되는 시점에 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 만들어둔다. 이때 같은 문자열이 있으면 만들지 않는다.
  • String str3 = "hello" 와 같이 문자열 리터럴을 사용하면 문자열 풀에서 "hello" 라는 문자를 가진

String 인스턴스를 찾는다. 그리고 찾은 인스턴스의 참조( x003 )를 반환한다.

  • String str4 = "hello" 의 경우 "hello" 문자열 리터럴을 사용하므로 문자열 풀에서 str3 과 같은

x003 참조를 사용한다.

  • 문자열 풀 덕분에 같은 문자를 사용하는 경우 메모리 사용을 줄이고 문자를 만드는 시간도 줄어들기 때문에 성능도 최적화 할 수 있다.

따라서 문자열 리터럴을 사용하는 경우 같은 참조값을 가지므로 == 비교에 성공한다.

참고 : 풀(Pool)은 자원이 모여있는 곳을 의미한다. 프로그래밍에서 풀(Pool)은 공용 자원을 모아둔 곳을 뜻한다. 여러 곳에서 함께 사용할 수 있는 객체를 필요할 때 마다 생성하고, 제거하는 것은 비효율적이다. 대신에 이렇게 문자열 풀에 필요한 String 인스턴스를 미리 만들어두고 여러곳에서 재사용할 수 있다면 성능과 메모리를 더 최적화 할 수 있다.

참고로 문자열 풀은 힙 영역을 사용한다. 그리고 문자열 풀에서 문자를 찾을 때는 해시 알고리즘을 사용하기 때문에 매우 빠른 속도로 원하는 String 인스턴스를 찾을 수 있다. 해시 알고리즘은 뒤에서 설명한다.

StringBuilder

불변인 String 클래스의 단점은 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성해야 한다는 점이다. 문자를 자주 더하거나 변경해야 하는 상황이라면 더 많은 String 객체를 만들고, GC해야 한다. 결과적으로 컴퓨터의 CPU, 메모리를 자원을 더 많이 사용하게 된다. 그리고 문자열의 크기가 클수록, 문자열을 더 자주 변경할수록 시스템의 자원을 더 많이 소모한다.

이 문제를 해결하는 방법은 단순하다. 바로 불변이 아닌 가변 String 이 존재하면 된다.

가변(Mutable) vs 불변(Immutable):

  • String 은 불변하다. 즉, 한 번 생성되면 그 내용을 변경할 수 없다. 따라서 문자열에 변화를 주려고 할 때마다 새로운 String 객체가 생성되고, 기존 객체는 버려진다. 이 과정에서 메모리와 처리 시간을 더 많이 소모한다.
  • 반면에, StringBuilder 는 가변적이다. 하나의 StringBuilder 객체 안에서 문자열을 추가, 삭제, 수정할 수 있으며, 이때마다 새로운 객체를 생성하지 않는다. 이로 인해 메모리 사용을 줄이고 성능을 향상시킬 수 있다. 단 사이드 이펙트를 주의해야 한다.

String 최적화

문자열 리터럴 최적화

컴파일 전

String helloWorld = "Hello, " + "World!";

컴파일 후

String helloWorld = "Hello, World!";

따라서 런타임에 별도의 문자열 결합 연산을 수행하지 않기 때문에 성능이 향상된다.

최적화 방식

String result = new StringBuilder().append(str1).append(str2).toString();

참고: 자바 9부터는 StringConcatFactory 를 사용해서 최적화를 수행한다.

String 최적화가 어려운 경우

 String result = "";
 for (int i = 0; i < 100000; i++) {
     result = new StringBuilder().append(result).append("Hello Java
 ").toString();
}

반복문의 루프 내부에서는 최적화가 되는 것 처럼 보이지만, 반복 횟수만큼 객체를 생성해야 한다. 반복문 내에서의 문자열 연결은, 런타임에 연결할 문자열의 개수와 내용이 결정된다. 이런 경우, 컴파일러는 얼마나 많은 반복이 일어날지, 각 반복에서 문자열이 어떻게 변할지 예측할 수 없다. 따라서, 이런 상황에서는 최적화가 어렵다.

정리 문자열을 합칠 때 대부분의 경우 최적화가 되므로 + 연산을 사용하면 된다.

StringBuilder를 직접 사용하는 것이 더 좋은 경우

  • 반복문에서 반복해서 문자를 연결할 때
  • 조건문을 통해 동적으로 문자열을 조합할 때
  • 복잡한 문자열의 특정 부분을 변경해야 할 때
  • 매우 긴 대용량 문자열을 다룰 때

참고: StringBuilder vs StringBuffer

StringBuilder 와 똑같은 기능을 수행하는 StringBuffer 클래스도 있다. StringBuffer는 내부에 동기화가 되어 있어서, 멀티 스레드 상황에 안전하지만 동기화 오버헤드로 인해 성능이 느리다. StringBuilder 는 멀티 쓰레드에 상황에 안전하지 않지만 동기화 오버헤드가 없으므로 속도가 빠르다. StringBuffer 와 동기화에 관한 내용은 이후에 멀티 스레드를 학습해야 이해할 수 있다. 지금은 이런 것이 있구나 정도만 알아두면 된다.

메서드 체인닝 - Method Chaining

메서드 체이닝 기법은 코드를 간결하고 읽기 쉽게 만들어준다.

정리 **“만드는 사람이 수고로우면 쓰는 사람이 편하고, 만드는 사람이 편하면 쓰는 사람이 수고롭다”**는 말이 있다. 메서드 체이닝은 구현하는 입장에서는 번거롭지만 사용하는 개발자는 편리해진다. 참고로 자바의 라이브러리와 오픈 소스들은 메서드 체이닝 방식을 종종 사용한다.

4. 래퍼 클래스

기본형의 한계

자바는 객체 지향 언어이다. 그런데 자바 안에 객체가 아닌 것이 있다. 바로 int , double 같은 기본형(PrimitiveType)이다. 기본형은 객체가 아니기 때문에 다음과 같은 한계가 있다.

  • 객체가 아님 : 기본형 데이터는 객체가 아니기 때문에, 객체 지향 프로그래밍의 장점을 살릴 수 없다. 예를 들어 객체는 유용한 메서드를 제공할 수 있는데, 기본형은 객체가 아니므로 메서드를 제공할 수 없다.
    • 추가로 객체 참조가 필요한 컬렉션 프레임워크를 사용할 수 없다. 그리고 제네릭도 사용할 수 없다.
  • null 값을 가질 수 없음 : 기본형 데이터 타입은 null 값을 가질 수 없다. 때로는 데이터가 없음 이라는 상태를 나타내야 할 필요가 있는데, 기본형은 항상 값을 가지기 때문에 이런 표현을 할 수 없다.
public class MyInteger {
    private final int value;
    public MyInteger(int value) {
        this.value = value;
}
    public int getValue() {
        return value;
}
    public int compareTo(int target) {
        if (value < target) {
            return -1;
        } else if (value > target) {
            return 1;
        } else {
return 0; }
}
    @Override
    public String toString() {
return String.valueOf(value); //숫자를 문자로 변경 }
}
  • MyIntegerint value 라는 단순한 기본형 변수를 하나 가지고 있다.
  • 그리고 이 기본형 변수를 편리하게 사용하도록 다양한 메서드를 제공한다.
  • 앞에서 본 compareTo() 메서드를 클래스 내부로 캡슐화 했다.
  • 이 클래스는 불변으로 설계했다.

기본형과 null

기본형은 항상 값을 가져야 한다. 하지만 때로는 데이터가 ‘없음’이라는 상태가 필요할 수 있다.

기본형은항상값이존재해야한다.숫자의경우 0 , -1 같은값이라도항상존재해야한다.반면에객체인참조형은값 이 없다는 null 을 사용할 수 있다. 물론 null 값을 반환하는 경우 잘못하면 NullPointerException 이 발생할 수 있기 때문에 주의해서 사용해야 한다.

래퍼클래스 - 자바 래퍼 클래스

래퍼 클래스는 기본형을 객체로 감싸서 더 편리하게 사용하도록 도와주기 때문에 상당히 유용하다. 쉽게 이야기해서 래퍼 클래스는 기본형의 객체 버전이다.

래퍼 클래스 생성 박싱(Boxing)

  • 기본형을 래퍼 클래스로 변경하는 것을 마치 박스에 물건을 넣은 것 같다고 해서 **박싱(Boxing)**이라 한다.
  • new Integer(10) 은 직접 사용하면 안된다. 작동은 하지만, 향후 자바에서 제거될 예정이다.
  • 대신에 Integer.valueOf(10) 를 사용하면 된다.
    • 내부에서 new Integer(10) 을 사용해서 객체를 생성하고 돌려준다.
  • 추가로 Integer.valueOf() 에는 성능 최적화 기능이 있다. 개발자들이 일반적으로 자주 사용하는 -128 ~ 127 범위의 Integer 클래스를 미리 생성해준다. 해당 범위의 값을 조회하면 미리 생성된 Integer 객체를 반환한다. 해당 범위의 값이 없으면 new Integer() 를 호출한다.
    • 마치 문자열 풀과 비슷하게 자주 사용하는 숫자를 미리 생성해두고 재사용한다.
    • 참고로 이런 최적화 방식은 미래에 더 나은 방식으로 변경될 수 있다.

intValue() - 언박싱(Unboxing)

  • 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 메서드이다.
  • 박스에 들어있는 물건을 꺼내는 것 같다고 해서 **언박싱(Unboxing)**이라 한다.

비교는 equals() 사용

  • 래퍼 클래스는 객체이기 때문에 == 비교를 하면 인스턴스의 참조값을 비교한다.
  • 래퍼 클래스는 내부의 값을 비교하도록 equals() 를 재정의 해두었다. 따라서 값을 비교하려면 equals() 를사용해야 한다.

참고로 래퍼 클래스는 객체를 그대로 출력해도 내부에 있는 값을 문자로 출력하도록 toString() 을 재정의했다.

오토 박싱, 오토 언박싱

Integer boxedValue = value; //오토 박싱(Auto-boxing)
Integer boxedValue = Integer.valueOf(value); //컴파일 단계에서 추가
int unboxedValue = boxedValue; //오토 언박싱(Auto-Unboxing) 
int unboxedValue = boxedValue.intValue(); //컴파일 단계에서 추가

오토 박싱과 오토 언박싱은 컴파일러가 개발자 대신 valueOf , xxxValue() 등의 코드를 추가해주는 기능이다. 덕분에 기본형과 래퍼형을 서로 편리하게 변환할 수 있다. 컴파일 단계에서 추가

래퍼클래스 - 주요 메서드

Integer i1 = Integer.valueOf(10);//숫자, 래퍼 객체 반환
Integer i2 = Integer.valueOf("10");//문자열, 래퍼 객체 반환
int intValue = Integer.parseInt("10");//문자열 전용, 기본형 반환

parseInt() vs valueOf() 원하는 타입에 맞는 메서드를 사용하면 된다.

  • valueOf("10") 는 래퍼 타입을 반환한다.
  • parseInt("10") 는 기본형을 반환한다.
  • Long.parseLong() 처럼 각 타입에 parseXxx() 가 존재한다.

래퍼클래스 - 성능

실행결과

sumPrimitive = 499999999500000000 기본 자료형
long 실행 시간: 318ms
sumWrapper = 499999999500000000
래퍼 클래스 Long 실행 시간: 1454ms
  • 기본형 연산이 래퍼 클래스보다 대략 5배 정도 빠른 것을 확인할 수 있다. 참고로 계산 결과는 시스템 마다 다르다.
  • 기본형은 메모리에서 단순히 그 크기만큼의 공간을 차지한다. 예를 들어 int 는 보통 4바이트의 메모리를 사용한다.
  • 래퍼 클래스의 인스턴스는 내부에 필드로 가지고 있는 기본형의 값 뿐만 아니라 자바에서 객체 자체를 다루는데 필요한 객체 메타데이터를 포함하므로 더 많은 메모리를 사용한다. 자바 버전과 시스템마다 다르지만 대략 8~16바이트의 메모리를 추가로 사용한다.

기본형, 래퍼 클래스 어떤 것을 사용?

  • 이 연산은 10억 번의 연산을 수행했을 때 0.3초와, 1.5초의 차이다.
  • 기본형이든 래퍼 클래스든 이것을 1회로 환산하면 둘다 매우 빠르게 연산이 수행된다.
  • 0.3초 나누기 10억, 1.5초 나누기 10억이다.
  • 일반적인 애플리케이션을 만드는 관점에서 보면 이런 부분을 최적화해도 사막의 모래알 하나 정도의 차이가 날 뿐이다.
  • CPU 연산을 아주 많이 수행하는 특수한 경우이거나, 수만~ 수십만 이상 연속해서 연산을 수행해야 하는 경우라면 기본형을 사용해서 최적화를 고려하자.
  • 그렇지 않은 일반적인 경우라면 코드를 유지보수하기 더 나은 것을 선택하면 된다.

유지보수 vs 최적화

유지보수 vs 최적화를 고려해야 하는 상황이라면 유지보수하기 좋은 코드를 먼저 고민해야 한다. 특히 최신 컴퓨터는 매우 빠르기 때문에 메모리 상에서 발생하는 연산을 몇 번 줄인다고해도 실질적인 도움이 되지 않는 경우가 많다.

  • 코드 변경 없이 성능 최적화를 하면 가장 좋겠지만, 성능 최적화는 대부분 단순함 보다는 복잡함을 요구하고, 더 많은 코드들을 추가로 만들어야 한다. 최적화를 위해 유지보수 해야 하는 코드가 더 늘어나는 것이다. 그런데 진짜 문제는 최적화를 한다고 했지만 전체 애플리케이션의 성능 관점에서 보면 불필요한 최적화를 할 가능성이 있다.
  • 특히 웹 애플리케이션의 경우 메모리 안에서 발생하는 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸린다. 자바 메모리 내부에서 발생하는 연산을 수천번에서 한 번으로 줄이는 것 보다, 네트워크 호출 한 번을 더 줄이는 것이 더 효과적인 경우가 많다.
  • 권장하는 방법은 개발 이후에 성능 테스트를 해보고 정말 문제가 되는 부분을 찾아서 최적화 하는 것이다.

Class 클래스

Class 클래스의 주요 기능은 다음과 같다.

  • 타입 정보 얻기 : 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회할 수 있다.
  • 리플렉션 : 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메서드를 호출하는 등의 작업을 할 수 있다.
  • 동적 로딩과 생성 : Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance()

메서드를 통해 새로운 인스턴스를 생성할 수 있다.

  • 애노테이션 처리 : 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공한다.

리플렉션 reflection Class 를 사용하면 클래스의 메타 정보를 기반으로 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해객체 인스턴스를 생성하거나 메서드를 호출하는 작업을 할 수 있다. 이런 작업을 리플렉션이라 한다. 추가로 애노테이션 정보를 읽어서 특별한 기능을 수행할 수 도 있다. 최신 프레임워크들은 이런 기능을 적극 활용한다. 지금은 Class 가 뭔지, 그리고 대략 어떤 기능들을 제공하는지만 알아두면 충분하다. 지금은 리플렉션을 학습하는 것보다 훨씬 더 중요한 기본기들을 학습해야 한다. 리플렉션은 이후에 별도로 다룬다.

5. 열거형 - ENUM

문자열과 타입 안정성

등급에 문자열을 사용하는 지금의 방식은 다음과 같은 문제가 있다.

  • 타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
  • 데이터 일관성 : “GOLD”, “gold”, “Gold” 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.

String 사용 시 타입 안정성 부족 문제

  • 값의 제한 부족 : String 으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. 예를들어, “Monday”, “Tuesday” 등을 나타내는 데 String 을 사용한다면, 오타(“Munday”)나 잘못된 값(“Funday”)이 입력될 위험이 있다.
  • 컴파일 시 오류 감지 불가 : 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.

문자열 상수를 사용한 덕분에 전체적으로 코드가 더 명확해졌다. 그리고 discount() 에 인자를 전달할 때도 StringGrade 가 제공하는 문자열 상수를 사용하면 된다. 더 좋은 점은 만약 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생한다는 점이다. 따라서 오류를 쉽고 빠르게 찾을 수 있다.

하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수 는 없다. 왜냐하면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 어떤 개발자가 실수로 StringGrade 에 있는 문자열 상수를 사용하지 않고, 다음과 같이 직접 문자열을 사용해도 막을 수 있는 방법이 없다.

타입 안전 열거형 패턴

타입 안전 열거형 패턴 - Type-Safe Enum Pattern 지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴이다.

public class ClassGrade {
     public static final ClassGrade BASIC = new ClassGrade();
     public static final ClassGrade GOLD = new ClassGrade();
     public static final ClassGrade DIAMOND = new ClassGrade();
     
     //private 생성자 추가
     private ClassGrade() {}
}

타입 안전 열거형 패턴 (Type-Safe Enum Pattern)의 장점

  • 타입 안정성 향상 : 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.
  • 데이터 일관성 : 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.

조금 더 자세히

  • 제한된 인스턴스 생성 : 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.
  • 타입 안전성 : 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다. 예를 들어, 특정 메서드가 특정 열거형 타입의 값을 요구한다면, 오직 그 타입의 인스턴스만 전달할 수 있다. 여기서는 메서드의 매개변수로 ClassGrade 를 사용하는 경우, 앞서 열거한 BASIC , GOLD , DIAMOND 만 사용할 수 있다.

단점

이 패턴을 구현하려면 다음과 같이 많은 코드를 작성해야 한다. 그리고 private 생성자를 추가하는 등 유의해야 하는부분들도 있다.

열거형 - Enum Type

자바는 타입 안전 열거형 패턴(Type-Safe Enum Pattern)을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을제공한다. 쉽게 이야기해서 자바의 열거형은 앞서 배운 타입 안전 열거형 패턴을 쉽게 사용할 수 있도록 프로그래밍 언어에서 지원하는 것이다.

“Enumeration”은 일련의 명명된 상수들의 집합을 정의하는 것을 의미하며, 프로그래밍에서는 이러한 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합을 나타낸다.

열거형(ENUM)의 장점

  • 타입 안정성 향상 : 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.
  • 간결성 및 일관성 : 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
  • 확장성 : 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.

열거형 정리

  • 열거형은 java.lang.Enum 를 자동(강제)으로 상속 받는다.
  • 열거형은 이미 java.lang.Enum 을 상속 받았기 때문에 추가로 다른 클래스를 상속을 받을 수 없다.

열거형은 인터페이스를 구현할 수 있다.

  • 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
    • 이 경우 익명 클래스와 같은 방식을 사용한다. 익명 클래스는 뒤에서 다룬다.

열거형 - 리팩토링

public enum Grade {
     BASIC(10), GOLD(20), DIAMOND(30);
     private final int discountPercent;
     Grade(int discountPercent) {
         this.discountPercent = discountPercent;
}
     public int getDiscountPercent() {
         return discountPercent;
}
//추가
public int discount(int price) {
         return price * discountPercent / 100;
     }
}
  • 객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것 보다는, Grade 클래스가 자신의 할인율을 어떻 게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.

7. 중첩 클래스, 내부 클래스

inner

  • 중첩 클래스 : 정적 중첩 클래스 + 내부 클래스 종류 모두 포함
  • 정적 중첩 클래스 : 정적 중첩 클래스를 말함
  • 내부 클래스 : 내부 클래스, 지역 클래스, 익명 클래스를 포함해서 말함
public class Car {
     private String model;
     private int chargeLevel;
     private Engine engine;
     
     public Car(String model, int chargeLevel) {
         this.model = model;
         this.chargeLevel = chargeLevel;
         this.engine = new Engine();
		}
     public void start() {
         engine.start();
					System.out.println(model + " 시작 완료"); 
			}
     private class Engine {
         public void start() {
					System.out.println("충전 레벨 확인: " + chargeLevel);
					System.out.println(model + "의 엔진을 구동합니다."); }
} }

리팩토링 후에는 getModel() , getChargeLevel() 과 같은 메서드를 모두 제거했다. 결과적으로 꼭 필요한 메서 드만 외부에 노출함으로써 Car 의 캡슐화를 더 높일 수 있었다.

중첩 클래스는 언제 사용해야 하나? 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴밀하게 연결되어 있는 특별한 경우에만 사용해야 한다. 외부 여러곳에서 특정 클래스를 사용한다면 중첩 클래스로 사용하면 안된다.

중첩 클래스를 사용하는 이유

  • 논리적 그룹화 : 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화가 된다. 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는 장점도 있다.
  • 캡슐화 : 중첩 클래스는 바깥 클래스의 private 멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있다.

8. 중첩 클래스, 내부 클래스 2

변수의 생명 주기

  • 클래스 변수 : 프로그램 종료 까지, 가장 길다(메서드 영역)
    • 클래스 변수(static 변수)는 메서드 영역에 존재하고, 자바가 클래스 정보를 읽어 들이는 순간부터 프로그램 종료까지 존재한다.
  • 인스턴스 변수 : 인스턴스의 생존 기간(힙 영역)
    • 인스턴스 변수는 본인이 소속된 인스턴스가 GC 되기 전까지 존재한다. 생존 주기가 긴 편이다.
  • 지역 변수 : 메서드 호출이 끝나면 사라짐(스택 영역)
    • 지역 변수는 스택 영역의 스택 프레임 안에 존재한다. 따라서 메서드가 호출 되면 생성되고, 메서드 호출이 종료되면 스택 프레임이 제거되면서 그 안에 있는 지역 변수도 모두 제거된다. 생존 주기가 아주 짧다. 참고로 매개변수도 지역 변수의 한 종류이다.

지역 변수 캡처

지역 클래스는 지역 변수에 접근할 수 있다.

그런데 앞서 본 것 처럼 지역 변수의 생명주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다. 지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는 살아 있지만, 지역 변수는 이미 제거된 상태일 수 있다.

자바는 이런 문제를 해결하기 위해 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이런 과정을 변수 캡처(Capture)라 한다. 캡처라는 단어는 스크린 캡처를 떠올려 보면 바로 이해가 될 것이다. 인스턴스를 생성할 때 필요한 지역 변수를 복사해서 보관해 두는 것이다. 물론 모든 지역 변수를 캡처하는 것이 아니라 접근이 필요한 지역 변수만 캡처한다.

용어 사실상 final 영어로 effectively final이라 한다. 사실상 final 지역 변수는 지역 변수 final 키워드를 사용하지는 않았지만, 값을 변경하지 않는 지역 변수를 뜻한다. final 키워드를 넣지 않았을 뿐이지, 실제로는 final 키워드를 넣은 것 처 럼 중간에 값을 변경하지 않은 지역 변수이다. 따라서 사실상 final 지역 변수는 final 키워드를 넣어도 동일하게 작동해야 한다.

캡처 변수의 값을 변경하지 못하는 이유

  • 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야 한다.
  • 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 한다.
  • 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있다. 이는 디버깅을 어렵게 한다.
  • 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다. 이 부분은 멀티쓰레드를 학습하면 이해할 수 있다.

이 모든 문제는 캡처한 지역 변수의 값이 변하기 때문에 발생한다. 자바는 캡처한 지역 변수의 값을 변하지 못하게 막아서 이런 복잡한 문제들을 근본적으로 차단한다.

익명 클래스

new Printer() {body} 익명 클래스는 클래스의 본문(body)을 정의하면서 동시에 생성한다.

new 다음에 바로 상속 받으면서 구현 할 부모 타입을 입력하면 된다. 이 코드는 마치 인터페이스 Printer 를 생성하는 것 처럼 보인다. 하지만 자바에서 인터페이스를 생성하는 것을 불가능하다. 이 코드는 인터페이스를 생성하는 것이 아니고, Printer 라는 이름의 인터페이스를 구현한 익명 클래스를 생 성하는 것이다. {body} 부분에 Printer 인터페이스를 구현한 코드를 작성하면 된다. 이 부분이 바로 익명 클래스의 본문이 된다. 쉽게 이야기해서 Printer 를 상속(구현) 하면서 바로 생성하는 것이다.

익명 클래스 특징

  • 익명 클래스는 이름 없는 지역 클래스를 선언하면서 동시에 생성한다.
  • 익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 한다. 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요하다.
  • 익명 클래스는 말 그대로 이름이 없다. 이름을 가지지 않으므로, 생성자를 가질 수 없다. (기본 생성자만 사용됨)
  • 익명 클래스는 AnonymousOuter$1 과 같이 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의된다. 익명 클래스가 여러개면 $1 , $2 , $3 으로 숫자가 증가하면서 구분된다.

익명 클래스의 장점

익명 클래스를 사용하면 클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 더 간결해진다. 하지만, 복잡하거나 재사용이 필요한 경우에는 별도의 클래스를 정의하는 것이 좋다.

9. 예외 처리 1 - 이론

참고 : 자바의 경우 GC가 있기 때문에 JVM 메모리에 있는 인스턴스는 자동으로 해제할 수 있다. 하지만 외부 연결과 같은 자바 외부의 자원은 자동으로 해제가 되지 않는다. 따라서 외부 자원을 사용한 후에는 연결을 해제해서 외부 자원을 반드시 반납해야 한다.

예외 계층

exception

  • Object : 자바에서 기본형을 제외한 모든 것은 객체다. 예외도 객체이다. 모든 객체의 최상위 부모는 Object이므로 예외의 최상위 부모도 Object 이다.
  • Throwable : 최상위 예외이다. 하위에 ExceptioError 가 있다.
  • Error : 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구가 불가능한 시스템 예외이다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
  • Exception : 체크 예외
    • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
    • Exception 과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단 RuntimeException 은 예외로 한다.
  • RuntimeException : 언체크 예외, 런타임 예외 컴파일러가 체크 하지 않는 언체크 예외이다.
    • RuntimeException 과 그 자식 예외는 모두 언체크 예외이다.
    • RuntimeException 의 이름을 따라서 RuntimeException 과 그 하위 언체크 예외를 런타임 예외라고 많이 부른다. 여기서도 앞으로는 런타임 예외로 종종 부르겠다.

체크 예외 vs 언체크 예외(런타임 예외) 체크 예외는 발생한 예외를 개발자가 명시적으로 처리해야 한다. 그렇지 않으면 컴파일 오류가 발생한다. 언체크 예외는개발자가 발생한 예외를 명시적으로 처리하지 않아도 된다. 자세한 내용은 조금 뒤에서 코드로 알아보자.

주의 상속 관계에서 부모 타입은 자식을 담을 수 있다. 이 개념이 예외 처리에도 적용되는데, 상위 예외를 catch 로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable 예외를 잡으면 안되는데, 앞서 이야기 한 잡으면 안되는 Error 예외도 함께 잡을 수 있기 때문이다. 애플리케이션 로직은 이런 이유로 Exception 부터 필요한 예외로 생각하고 잡으면 된다.

참고 : 예외를 처리하지 못하고 계속 던지면 어떻게 될까? 자바 main() 밖으로 예외를 던지면 예외 로그를 출력하면서 시스템이 종료된다.

public class Client {
    public void call() throws MyCheckedException {
        throw new MyCheckedException("ex");
    }
}
  • throw 예외 라고 하면 새로운 예외를 발생시킬 수 있다. 예외도 객체이기 때문에 객체를 먼저 new 로 생성하고예외를 발생시켜야 한다.
  • throws 예외 는 발생시킨 예외를 메서드 밖으로 던질 때 사용하는 키워드이다.
  • throw , throws 의 차이에 주의하자

Exception을 상속받은 예외는 체크 예외가 된다.

public class MyCheckedException extends Exception {
     public MyCheckedException(String message) {
              super(message);
     }
}
  • MyCheckedExceptionException 을 상속받았다. Exception 을 상속받으면 체크 예외가 된다.
  • 참고로 RuntimeException 을 상속받으면 언체크 예외가 된다. 이런 규칙은 자바 언어에서 문법으로 정한 것이다.
  • 예외가 제공하는 기본 기능이 있는데, 그 중에 오류 메시지를 보관하는 기능도 있다. 예제에서 보는 것 처럼 생성자를 통해서 해당 기능을 그대로 사용하면 편리하다.

checkExcep

체크 예외의 장단점 체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외 를 필수로 선언해야 한다. 그렇지않으면 컴파일 오류가 발생한다. 이것 때문에 장점과 단점이 동시에 존재한다.

  • 장점 : 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전 장치이다. 이를 통해 개발자는 어떤 체크 예외가 발생하는지 쉽게 파악할 수 있다.
  • 단점 : 하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운일이 된다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야 한다.

체크 예외는 잡아서 직접 처리하거나 또는 밖으로 던지거나 둘중 하나를 개발자가 직접 명시적으로 처리해야한다. 그렇 지 않으면 컴파일 오류가 발생한다.

언체크 예외

  • RuntimeException 과 그 하위 예외는 언체크 예외로 분류된다.
  • 언체크 예외는 말 그대로 컴파일러가 예외를 체크하지 않는다는 뜻이다.
  • 언체크 예외는 체크 예외와 기본적으로 동일하다. 차이가 있다면 예외를 던지는 throws 를 선언하지 않고, 생략할 수 있다. 생략한 경우 자동으로 예외를 던진다.

체크 예외 VS 언체크 예외

  • 체크 예외 : 예외를 잡아서 처리하지 않으면 항상 throws 키워드를 사용해서 던지는 예외를 선언해야 한다.
  • 언체크 예외 : 예외를 잡아서 처리하지 않아도 throws 키워드를 생략할 수 있다.

언체크 예외의 장단점 언체크예외는예외를잡아서처리할수없을때,예외를밖으로던지는 throws 예외 를생략할수있다.이것때문에 장점과 단점이 동시에 존재한다.

  • 장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 throws 예외 를 선언해야 하지만, 언체크 예외는 이 부분을 생략할 수 있다.
  • 단점 : 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다. 반면에 체크 예외는 컴파일러를 통해 예외 누락을잡아준다.

정리 체크 예외와 언체크 예외의 차이는 예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분에 있다. 이 부분을 필수로 선언해야 하는가 생략할 수 있는가의 차이다.

10. 예외 처리 2 - 실습

리소스 반환 문제 - finally

try {
//정상 흐름
} catch { 
//예외 흐름 
} finally {
//반드시 호출해야 하는 마무리 흐름 
}
  • 정상 흐름 → finally
  • 예외 catch → finally
  • 예외 던짐 → finally
    • finally 코드 블럭이 끝나고 나서 이후에 예외가 밖으로 던져짐

finally 블럭은 반드시 호출된다. 따라서 주로 try 에서 사용한 자원을 해제할 때 주로 사용한다.

정리 자바 예외 처리는 try ~ catch ~ finally 구조를 사용해서 처리할 수 있다. 덕분에 다음과 같은 이점이 있다.

  • 정상 흐름과 예외 흐름을 분리해서, 코드를 읽기 쉽게 만든다.
  • 사용한 자원을 항상 반환할 수 있도록 보장해준다.

실무 예외 처리 방안 1 - 설명

처리할 수 없는 예외

상대 서버에 문제가 발생해서 통신이 불가능해지면 해당 서버에서는 해결할 수 없는 문제이다. 이럴 경우 오류에 대한 로그를 남겨두고 웹이라면 클라이언트들을 오류 페이지를 보여주면 된다.

체크 예외의 부담

체크 예외는 개발자가 실수로 놓칠 수 있는 예외들을 컴파일러가 체크해주기 때문에 많이 사용된다.

throws Exception

 class Facade {
     void send() throws Exception
}
 class Service {
     void sendMessage(String data) throws Exception
}

Exception 은 애플리케이션에서 일반적으로 다루는 모든 예외의 부모이다. 따라서 이렇게 한 줄만 넣으면 모든 예외를 다 던질 수 있다. → 치명적인 문제 존재

throws Exception의 문제 Exception 은 최상위 타입이므로 모든 체크 예외를 다 밖으로 던지는 문제가 발생한다. 결과적으로 체크 예외의 최상위 타입인 Exception 을 던지게 되면 다른 체크 예외를 체크할 수 있는 기능이 무효화되고, 중요한 체크 예외를 다 놓치게 된다. 중간에 중요한 체크 예외가 발생해도 컴파일러는 Exception 을 던지기 때문에 문법에 맞다고 판단해서 컴파일 오류가 발생하지 않는다. 이렇게 하면 모든 예외를 다 던지기 때문에 체크 예외를 의도한 대로 사용하는 것이 아니다. 따라서 꼭 필요한 경우가 아니면 이렇게 Exception 자체를 밖으로 던지는 것은 좋지 않은 방법이다.

문제 정리

  • 처리할 수 없는 예외 : 예외를 잡아서 복구할 수 있는 예외보다 복구할 수 없는 예외가 더 많다.
  • 체크 예외의 부담 : 처리할 수 없는 예외는 밖으로 던져야 한다. 체크 예외이므로 throws 에 던질 대상을 일일이명시해야 한다.

언체크 예외 사용시 예외 공통 처리 이렇게 처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통으로 처리할 수 있는 곳을 만들어서 한 곳에서 해결하면 된다. 어차피 해결할 수 없는 예외들이기 때문에 이런 경우 고객에게는 현재 시스템에 문제가 있습니다. 라고 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다. 그리고 내부 개발자가 지금의 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두면 된다. 이런 부분은 공통 처리가 가능하다.

실무 예외 처리 방안2 - 구현

image

  • 언체크 예외이므로 throws 를 사용하지 않는다.

exceptionHandler()

  • 해결할 수 없는 예외가 발생하면 사용자에게는 시스템 내에 알 수 없는 문제가 발생했다고 알리는 것이 좋다.
    • 사용자가 디테일한 오류 코드나 오류 상황까지 모두 이해할 필요는 없다. 예를 들어서 사용자는 데이터베이스 연결이 안되서 오류가 발생한 것인지, 네트워크에 문제가 있어서 오류가 발생한 것인지 알 필요는 없다.
  • 개발자는 빨리 문제를 찾고 디버깅 할 수 있도록 오류 메시지를 남겨두어야 한다.
  • 예외도 객체이므로 필요하면 instanceof 와 같이 예외 객체의 타입을 확인해서 별도의 추가 처리를 할 수 있다.

참고 : 실무에서는 System.out 이나 System.err 을 통해 콘솔에 무언가를 출력하기 보다는, 주로 Slf4J, logback 같은 별도의 로그 라이브러리를 사용해서 콘솔과 특정 파일에 함께 결과를 출력한다. 그런데 e.printStackTrace() 를 직접 호출하면 결과가 콘솔에만 출력된다. 이렇게 되면 서버에서 로그를 확인하기 어렵다. 서버에서는 파일로 로그를 확인해야 한다. 따라서 콘솔에 바로 결과를 출력하는 e.printStackTrace() 는 잘 사용하지 않는다. 대신에 로그 라이브러리를 통해서 예외 스택 트레이스를 출력한다. 지금은 로그 라이브러리라는 것이 있다는 정도만 알아두자. 학습 단계에서는 e.printStackTrace() 를 적극 사용해도 괜찮다.

try-with-resources

이 기능을 사용하려면 먼저 AutoCloseable 인터페이스를 구현해야 한다.

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

이 인터페이스를 구현하면 Try with resources를 사용할 때 try 가 끝나는 시점에 close() 가 자동으로 호출된다.

try (Resource resource = new Resource()) { 
// 리소스를 사용하는 코드
}

Try with resources 장점

  • 리소스 누수 방지 : 모든 리소스가 제대로 닫히도록 보장한다. 실수로 finally블록을 적지 않거나, finally 블럭 안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있다.
  • 코드 간결성 및 가독성 향상 : 명시적인 close() 호출이 필요 없어 코드가 더 간결하고 읽기 쉬워진다.
  • 스코프 범위 한정 : 예를 들어 리소스로 사용되는 client 변수의 스코프가 try 블럭 안으로 한정된다. 따라서코드 유지보수가 더 쉬워진다.
  • 조금 더 빠른 자원 해제 : 기존에는 try→catch→finally로 catch 이후에 자원을 반납했다. Try with resources구분은 try 블럭이 끝나면 즉시 close() 를 호출한다.

정리

처음 자바를 설계할 당시에는 체크 예외가 더 나은 선택이라 생각했다. 그래서 자바가 기본으로 제공하는 기능들에는 체 크 예외가 많다. 그런데 시간이 흐르면서 복구 할 수 없는 예외가 너무 많아졌다. 특히 라이브러리를 점점 더 많이 사용 하면서 처리해야 하는 예외도 더 늘어났다. 라이브러리들이 제공하는 체크 예외를 처리할 수 없을 때마다 throws 에예외를 덕지덕지 붙어야 했다. 그래서 개발자들은 throws Exception 이라는 극단적?인 방법도 자주 사용하게 되었 다. 물론 이 방법은 사용하면 안된다. 모든 예외를 던진다고 선언하는 것인데, 결과적으로 어떤 예외를 잡고 어떤 예외를 던지는지 알 수 없기 때문이다. 체크 예외를 사용한다면 잡을 건 잡고 던질 예외는 명확하게 던지도록 선언해야 한다. 체크 예외의 이런 문제점 때문에 최근 라이브러리들은 대부분 런타임 예외를 기본으로 제공한다.

가장 유명한 스프링이 나 JPA 같은 기술들도 대부분 언체크(런타임) 예외를 사용한다. 런타임 예외도 필요하면 잡을 수 있기 때문에 필요한 경우에는 잡아서 처리하고, 그렇지 않으면 자연스럽게 던지도록 둔다. 그리고 처리할 수 없는 예외는 예외를 공통으로 처리하는 부분을 만들어서 해결하면 된다.

👇 도움이 되셨다면 👇

B

u

y

M

e

A

C

o

f

f

e

e

© Powered by oslob