일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 인프런
- 자바 입출력 스트림
- 실전 자바 고급 1편
- java socket
- LIST
- filewriter filereader
- 도커 엔진
- 리스트
- Thread
- java
- 쓰레드
- 김영한
- Java IO
- 알고리즘
- Docker
- Kubernetes
- 시작하세요 도커 & 쿠버네티스
- 컨테이너
- 자바 io 보조스트림
- java network
- container
- 도커
- 자료구조
- 스레드
- 동시성
- Collection
- 멀티 쓰레드
- 쿠버네티스
- 스레드 제어와 생명 주기
- 자바
- Today
- Total
쌩로그
인프런 - 김영한 - 실전 자바 중급 1편 요약 및 정리 본문
목록
- 개요
- 본론
2-1. 정적 의존관계 vs 동적 의존관계
2-2. 동일성 vs 동등성
2-3. 불변 객체의 값을 변경할 때의 메서드 이름 : with
2-4. 자바 9 이후 String 클래스의 최적화
2-5. 문자열의 변경이 필요할 땐 StringBuilder 를 사용하자.
2-6. Java의 Wrapper 클래스
2-7. 유지보수 vs 최적화
2-8. Class 클래스
2-9. System 클래스
2-10. Enum
2-11. 시간과 날짜
2-12. 중첩 클래스
2-13. 지역 클래스
2-14. 익명 클래스
2-15. 예외 처리 - 다음으로
1. 개요
해당 포스팅은 김영한의 실전 자바 중급 1편
을 학습하고 정리한 포스팅이다.
기본편과 마찬가지로 참고로 정리만 필요한 부분만 정리했다.
학습 레포 : https://github.com/SsangSoo/inflearn-holyeye-java-mid1
2. 본론
2-1. 정적 의존관계 vs 동적 의존관계
정적 의존관계
- 컴파일 시점에 확인할 수 있는 의존관계이다.
- 클래스간의 관계를 의미한다.
동적 의존관계
- 런타임에 확인할 수 있는 의존관계이다
2-2. 동일성 vs 동등성
동일성
- 완전히 같은 것
- 메모리 참조값이 같으면 동일하다.
동등성
- 물리적으로는 다른 객체 즉 참조 값이 다르지만, 논리적으로 정의한 같은 값을 가지면 동등하다.
(단어 정리에 가깝다.)
2-3. 불변 객체의 값을 변경할 때의 메서드 이름 : with
위의 말처럼 불변 객체의 값을 변경이 필요로 할 때 기존 객체는 놔두고, 새로운 객체를 만들어서 반환하게 되는데, 이때 with
이라는 이름의 메서드를 자주 사용한다.
아래는 예시다.
package lang.immutable.test;
public class ImmutableMyDate {
private final int year;
private final int month;
private final int day;
public ImmutableMyDate withYear(int newYear) {
return new ImmutableMyDate(newYear, month, day);
}
public ImmutableMyDate withMonth(int newMonth) {
return new ImmutableMyDate(year, newMonth, day);
}
public ImmutableMyDate withDay(int newDay) {
return new ImmutableMyDate(year, month, newDay);
}
public ImmutableMyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
@Override
public String toString() {
return year + "-" + month + "-" + day;
}
}
2-4. 자바 9 이후 String 클래스의 최적화
private final char[] value; // 자바 9 이전
private final byte[] value; // 자바 9 이후
String 클래스는 char[]
을 이용했다. 그런데 char
는 2byte를 사용한다.char
배열을 이용하며 2byte 씩을 사용했지만, 사실 숫자나 영어는 1byte를 사용한다.
따라서 숫자나 영어를 사용할 때는 1byte를,
그렇지 않은 경우 2byte를 사용하도록 최적화되었다.
2-5. 문자열의 변경이 필요할 땐 StringBuilder 를 사용하자.
아래는 String에 +
을 더하는 코드다. 대략 6500ms 가 걸린다.
public class LoopStringMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String result = "";
for(int i = 0; i < 100000; i++) {
result += "Hello Java ";
}
long endTime = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
아래는 StringBuilder를 통해서 반복문 내에서 문자를 더하는 과정이다.
public class LoopStringBuilderMain {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for(int i = 0; i < 100000; i++) {
sb.append("Hello Java ");
}
long endTime = System.currentTimeMillis();
String result = sb.toString();
System.out.println("result = " + result);
System.out.println("time = " + (endTime - startTime) + "ms");
}
}
결과는 약 7ms 정도 차이난다.
1000배 가까운 차이다....
반복문의 횟수가 별로 얼마 되지 않는다면 String
을 사용해도 상관없겠지만, 몇 천, 몇 만이라면 StringBuiler
를 한 번 고려해보자.
참고로 StringBuilder
는 싱글 스레드 환경에서 StringBuffer
는 멀티스레드 상황에서 사용하면 된다.
2-6. Java의 Wrapper 클래스
package lang.wrapper;
public class WrapperClassMain {
public static void main(String[] args) {
Integer newInteger = new Integer(10); //미래에 삭제 예정, 대신에 valueOf()를 사용
// Integer newInteger = Integer.valueOf(10); //미래에 삭제 예정, 대신에 valueOf()를 사용
Integer integerObj = Integer.valueOf(10); //-128 ~ 127 자주 사용하는 숫자 값 재사용, 불변
Long longObj = Long.valueOf(100);
Double doubleObj = Double.valueOf(10.5);
System.out.println("newInteger = " + newInteger);
System.out.println("integerObj = " + integerObj);
System.out.println("longObj = " + longObj);
System.out.println("doubleObj = " + doubleObj);
System.out.println("내부 값 읽기");
int intValue = integerObj.intValue();
System.out.println("intValue = " + intValue);
long longValue = longObj.longValue();
System.out.println("longValue = " + longValue);
System.out.println("비교");
System.out.println("==: " + (newInteger == integerObj)); // Integer.valueOf(x) x의 값이 -128~127이면 이 범위의 값들은 cashing 되어있기 때문에 true System.out.println("equals: " + (newInteger.equals(integerObj)));
}
}
new XXX(value)
는 내부적으로Deprecated
로 선언되어있다.XXX.valueOf()
을 사용하면 된다.
- 래퍼 클래스의
toString()
은 내부적으로new String()
을 호출한다. ==
비교는 서로 다른 참조르 바라보기때문에, false를 반환하지만,-128~127
의 범위에서valueOf()
메서드를 사용하면 자바에서 해당 범위값은 캐싱되어 있기 때문에 true를 반환한다.
참고
System.out.println();
에서 해당 파라미터가 null
이라면 내부적으로 "null"
이라는 문자열을 출력해준다.
기본형과 래퍼클래스의 연산 성능 비교
package lang.wrapper;
public class WrapperVsPrimitive {
public static void main(String[] args) {
int iterations = 1_000_000_000; // 반복 횟수 설정, 10억
long startTime, endTime;
// 기본형 long 사용
long sumPrimitive = 0;
startTime = System.currentTimeMillis();
for(int i = 0; i < iterations; i++) {
sumPrimitive += i;
}
endTime = System.currentTimeMillis();
System.out.println("sumPrimitive = " + sumPrimitive);
System.out.println("기본 자료형 long 실행 시간: " + (endTime - startTime) + "ms");
// 래퍼 클래스 Long 사용
Long sumWrapper = 0L;
startTime = System.currentTimeMillis();
for(int i = 0; i < iterations; i++) {
sumWrapper += i; // 오토 박싱 발생
}
endTime = System.currentTimeMillis();
System.out.println("sumWrapper = " + sumWrapper);
System.out.println("래퍼 클래스 Long 실행 시간: " + (endTime - startTime) + "ms");
}
}
- 결과
sumPrimitive = 499999999500000000
기본 자료형 long 실행 시간: 302ms
sumWrapper = 499999999500000000
래퍼 클래스 Long 실행 시간: 3391ms
래퍼 클래스가 기본형보다 더 많은 기능을 제공하지만, 자바가 기본형을 게속 제공하는 이유가 있다.
(다만, 위의 값은 반복문이 10억번을 돌아서 그렇지만, 10000까지는 차이가 없다. )
경우에 따라 최적화가 필요할 때 래퍼클래스를 기본형으로 변경해서 사용하자.
2-7. 유지보수 vs 최적화
코드를 작성하다보면 래퍼클래스를 사용해서 유지보수 측면에서 좋은 쪽으로 가져갈지 기본형을 사용해서 최적화를 가져갈지 기로에 많이 서게 된다.
하지만 요즘은 컴퓨터의 성능이 너무 좋아졌기 때문에 추후 사람이 읽기 쉬운 코드로 유지보수 측면으로 이점을 가져가는 것이 더욱 좋다.
- 최적화를 한다고 해도 전체 애플리케이션 성능관점에서는 불필요한 최적화일 가능성도 있다.
- 애플리케이션 메모리의 연산보다 네트워크 호출 비용이 더욱 더 많다.
- 차라리 네트워크 호출을 한 번 더 줄이는 것이 좋다.
- 먼저는 유지보수를 가져가고 추후 최적화가 정말 필요할 때 최적화를 가져가는 방향으로 하는 것이 좋다.
결론: 쓸데없는 최적화를 하지말자.
2-8. Class 클래스
- 자바에서
Class
클래스는 클래스의 정보(메타데이터)를 다루는데 사용된다. Class
클래스를 통해 개발자는 실행 중인 자바 애플리케이션 내에서 필요한 클래스의 속성과 메소드에 대한 정보를 조회하고 조작할 수 있다.
아래는 String 클래스의 정보들을 얻은 것이다.
public class ClassMetaMain {
public static void main(String[] args) throws Exception {
//Class 조회
Class clazz = String.class; // 1. 클래스에서 조회
// Class clazz1 = new String().getClass(); // 2. 인스턴스에서 조회
// Class clazz2 = Class.forName("java.lang.String"); // 3. 문자열로 조회
// 모든 필드 출력
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field.getType() + " " + field.getName());
}
// 모든 메서드 출력
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
// 상위 클래스 정보 출력
System.out.println("Superclass: " + clazz.getSuperclass().getName());
// 인터페이스 정보 출력
Class[] interfaces = clazz.getInterfaces();
for (Class i : interfaces) {
System.out.println("Interface = " + i.getName());
}
}
}
2-9. System 클래스
package lang.system;
import java.util.Arrays;
public class SystemMain {
public static void main(String[] args) {
// 현재 시간(밀리초)를 가져온다.
long currentTimeMillis = System.currentTimeMillis();
System.out.println("currentTimeMillis = " + currentTimeMillis);
// 현재 시간(나노초)을 가져온다.
long currentTimeNano = System.nanoTime();
System.out.println("currentTimeNano = " + currentTimeNano);
// 환경 변수를 읽는다. // 운영체제가 사용
System.out.println("getenv = " + System.getenv());
// 시스템 속성을 읽는다. // 자바가 사용
System.out.println("properties = " + System.getProperties());
System.out.println("Java version = " + System.getProperty("java.version"));
// 배열을 고속으로 복사한다.
char[] originalArray = {'h','e','l', 'l', 'o'};
char[] copiedArray = new char[5];
System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length); // 깊은 복사
// 배열 출력
System.out.println("copiedArray = " + copiedArray);
System.out.println("Array.toString = " + Arrays.toString(copiedArray));
// 프로그램 종료
System.exit(0); // 가급적 사용하지 않기 -> 프로그램이 의도치 않게 종료됨
System.out.println("hello"); // 출력 X -> 프로그램이 추가되었기 때문
}
}
2-10. Enum
타입 안전 열거형 패턴
장점
- 타입 안정성 향상
- 정해진 객체만 사용할 수 있기 때문에 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.
- 데이터 일관성
- 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.
- 제한된 인스턴스 생성
- 클래스는 사전에 정의된 몇개의 인스턴스만 생성하고, 외부에서느 이 인스턴스들만 사용할 수 있도록 한다.
- 타입 안정성
- 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다.
단점
- 이 패턴을 다음과 같이 구현하려면 많은 코드를 작성해야 한다.
- private 생성자를 추가하는 등 유의해야 하는 부분들도 있다.
- 예시 코드
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() {}
}
이 단점을 극복하고자 나온 것이 Enum
이다.
자바는 타입 안전 열거형 패턴 을 매우 편리하게 사용할 수 있는 dufrjgud(Enum Type)을 제공한다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
위의 코드는 아래의 코드와 거의 같다.(완전히 같다는 건 아니다.)
public class Grade extends Enum {
public static final Grade BASIC = new Grade();
public static final Grade GOLD = new Grade();
public static final Grade DIAMOND = new Grade();
}
- 열거형도 클래스이다.
- 열거형은 자동으로
java.lang.Enum
을 상속받는다.- 찾아보니
Enum
은 추상클래스이다.
- 찾아보니
- 외부에서 임의로 생성할 수 없다.
Enum vs Enum을 클래스로 구현한 결과
enum
을 사용한 결과와 class
를 enum처럼 구현하여 확인하느 결과는 같다.
먼저 Enum을 사용하지 않은 결과이다.
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() {}
}
public class ClassRefMain {
public static void main(String[] args) {
System.out.println("class BASIC = " + ClassGrade.BASIC.getClass());
System.out.println("class GOLD = " + ClassGrade.GOLD.getClass());
System.out.println("class DIAMOND = " + ClassGrade.DIAMOND.getClass());
System.out.println("ref BASIC = " + ClassGrade.BASIC);
System.out.println("ref GOLD = " + ClassGrade.GOLD);
System.out.println("ref DIAMOND = " + ClassGrade.DIAMOND);
}
}
// 실행 결과
class BASIC = class enumeration.ex2.ClassGrade
class GOLD = class enumeration.ex2.ClassGrade
class DIAMOND = class enumeration.ex2.ClassGrade
ref BASIC = enumeration.ex2.ClassGrade@5b480cf9
ref GOLD = enumeration.ex2.ClassGrade@6f496d9f
ref DIAMOND = enumeration.ex2.ClassGrade@723279cf
아래는 Enum을 사용한 결과다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
public class EnumRefMain {
public static void main(String[] args) {
System.out.println("class BASIC = " + Grade.BASIC.getClass());
System.out.println("class GOLD = " + Grade.GOLD.getClass());
System.out.println("class DIAMOND = " + Grade.DIAMOND.getClass());
System.out.println("ref BASIC = " + refValue(Grade.BASIC));
System.out.println("ref GOLD = " + refValue(Grade.GOLD));
System.out.println("ref DIAMOND = " + refValue(Grade.DIAMOND));
}
private static String refValue(Object grade) {
return Integer.toHexString(System.identityHashCode(grade));
}
}
// 실행 결과
class BASIC = class enumeration.ex3.Grade
class GOLD = class enumeration.ex3.Grade
class DIAMOND = class enumeration.ex3.Grade
ref BASIC = 5b480cf9
ref GOLD = 6f496d9f
ref DIAMOND = 723279cf
참고로 열거형은 toString()
을 재정의하기 때문에 참조값을 직접 확인할 수 없어서 private static String refValue(Object grade)
메서드를 이용해서 확인할 수 있도록 했다.
참고로 열거형은 swith
문에 사용할 수 있는 장점도 있다.
Enum의 장점
- 타입 안정성 향상
- 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다.
- 이런 경우 컴파일 오류가 발생한다.
- 간결성 및 일관성
- 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
- 확장성
- 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.
Enum의 주요메서드
public class EnumMethodMain {
public static void main(String[] args) {
// 모든 ENUM 반환
Grade[] values = Grade.values();
System.out.println("values = " + Arrays.toString(values));
for (Grade value : values) {
System.out.println("value.name = " + value.name()); // ENUM 그대로 이름 반환 //BASIC, GOLD, DIAMOND
// 순서 -> 가급적 사용하지 않는 것이 좋다. :
// 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경된다.
// ordinal의 값이 데이터베이스나 파일에 저장되어있으면.. 어질어질하다.
System.out.println("value.ordinal = " + value.ordinal());
}
// String -> ENUM 변환
String input = "GOLD";
Grade gold = Grade.valueOf(input);
System.out.println("gold = " + gold); // toString() 오버라이딩 가능
// String -> ENUM 변환, 잘못된 문자면 IllegalArgumentException 발생
// String wrongInput = "GOLDD";
// Grade wrongGold = Grade.valueOf(wrongInput);
// System.out.println("wrongGold = " + wrongGold);
}
}
ordinal
은 가급적사용하지 않는 것이 좋다.- 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경된다.
- 만약 ordinal의 값이 데이터베이스나 파일에 저장되어있으면 데이터의 정합성이 다 깨질 것이다.
- 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경된다.
2-11. 시간과 날짜
일광 절약 시간제(Daylight Saving Time, DTS)
- 3월에서 10월은 태양이 일찍 뜨는데, 시간도 이에 맞추어 1시간 앞당기거나, 늦추는 제도를 일광 절약 시간제 또는 썸머타임이라고 한다.
ZonedDateTime
- 일광 절약 시간제가 적용된다.
offset
(ex: 우리나라는 UTC + 9:00)과 타임존(Asia/Seoul
) 정보를 포함한다.
OffsetDateTime
- 일광 절약 시간제가 적용되지 않는다.
offset
만 포함하고, 타임존 정보는 포함하지 않는다.
Period, Duration
- 시간의 간격을 표현하는데 사용된다.
- EX)
- 앞으로 1년은 더 공부해야 한다.
- 시험기간이 3주 남았다.
- 이 라면은 3분안안 더 끓여야 한다.
- EX)
- Period
- 두 날짜 사이의 간격을 년, 월, 일 단위로 나타낸다.
- Duration
- 두 시간 사이의 간격을 시, 분, 초(나노초) 단위로 나타낸다.
LocalDateTime
Local
이 붙은 것처럼 세계 시간대를 고려하지 않고, 타임존이 적용되지 않기 때문이다.- 특정 날짜의 지역과 시간만 고려할 때 사용한다.
LocalDate
,LocalTime
도 마찬가지다.
ZonedDateTime
ZonedDateTime
은LocalDateTime
에 시간대 정보인ZoneId
가 합쳐진 것이다.
OffsetDateTime
OffsetDateTime
은LocalDateTime
에 UTC 오프셋 정보인ZoneOffset
이 합쳐진 것이다.ZonedDateTime
에서ZonedId
가 빠졌다.
ZonedDateTime vs OffsetDateTime
ZonedDateTime
- 구체적인 지역 시간대를 다룰 때 사용하며, 일광 절약 시간을 자동으로 처리할 수 있다.
- 사용자 지정 시간대에 따른 시간 계산이 필요할 떄 적합하다.
OffsetDateITme
- UTC와의 시간 차이만을 나타낼 떄 사용하며, 지역 시간대의 복잡성을 고혀하지 않는다.
- 시간대 변환 없이 로그를 기록하고, 데이터를 저장하고 처리할 떄 적합하다.
글로벌 서비스를 하지 않으면 깊게 파기보다는 알아뒀다가 추후에 필요할 때 깊이 있게 학습해서 사용하면 된다.
Instant
Instant
는 UTC(Universal Time Coordinated : 협정 세계시)를 기준으로 하는 시간의 한 지점을 나타낸다.- 날짜와 시간을 나노초 정밀도로 표현한다.
- 1970년 1월 1일 0시 0분 0초를 기준으로 경과한 시간으로 계산한다.
Instant
내부에는 초 데이터만 들어있다.(나노초 포함)- 날자와 시간을 계산에 사용할 때는 적합하지 않다.
// 실제 java.time.Instant
public final class Instant
implements Temporal, TemporalAdjuster, Comparable<Instant>, Serializable{
private final long seconds;
private final int nanos;
}
- UTC 기준 1970년 1월 1일 0시 0분 0초라면
second
에 0이 들어간다. - UTC 기준 1970년 1월 1일 0시 0분 1초라면
second
에 1이 들어간다. - UTC 기준 1970년 1월 1일 0시 1분 0초라면
second
에 60이 들어간다. - 참고로 UTC를 기준으로 하기 떄문에
LocalDateTime
은 사용할 수 없다.ZonedDateTime
을 사용해야 한다.
Epoch 시간(참고)
Epoch
뜻 : 어떤 중요한 사건이 발생한 시점을 기준으로 삼는 시작점을 뜻한다.- Epoch time(에포크 시간), 또는 Unixtimestamp는 컴퓨터 시스템에서 시간을 나타내는 방법 중 하나다.
- 1970년 1월 1일 00:00:00 UTC부터 현재까지 경과된 시간을 초 단위로 표현한 것이다.
- Unix 시간은 1970년 1월 1일 이후로 경과한 전체 초의 수로 시간대에 영향을 받지 않는 절대적인 시간 표현 방식이다.
Instant
는 Epoch 시간을 다루는 클래스다.
장점
- 시간대 독립성
Instant
는 UTC를 기준으로 하므로 시간대에 영향을 받지 않는다.- 이는 전 세계 어디서나 동일한 시점을 가리키는데 유용하다.
- 고정된 기준점
- 모든
Instant
는 1970년 1월 1일 UTC를 기준으로 하기 때문에, 시간 계산 및 비교가 명확하고 일관된다.
- 모든
단점
- 사용자 친화적이지 않다.
- 기계적인 시간 처리에는 적합하지만, 사람이 읽고 이해하기에는 직관적이지 않다.
- 시간대 정보 부재
- 시간대 정보가 포함되어 있지 않아 특정 지역의 날짜와 시간을 변환하려면 추가적인 작업이 필요하다.
사용 예시
- 전세계적인 시간 기준 필요시
- 시간대 변환 없이 시간 계산 필요시
- 데이터 저장 및 교환
Duration. Period
Period
- 두 시간 사이의 간격을 년, 월, 일 단위로 나타낸다.
Duration
- 두 날짜 사이의 간격을 시, 분, 초(나노초) 단위로 나타낸다.
날짜 관련 인터페이스

시간의 단위와 시간 필드

시간의 단위 - TemporalUnit, ChronoUnit
TemporalUnit
인터페이스는 날짜와 시간을 측정하는 단위를 나타내며, 주로 사용되는 구현체는java.time.temporal.ChronoUnit
열거형으로 구현되어 있다.ChronoUnit
은 다양한 시간 단위를 제공한다.- 여기서
Unit
이라는 뜻을 번역하면 단위이다. 따라서 시간의 단위 하나하나를 나타낸다.
- 여기서
단위



참고 : DateTimeFormatter 패턴 공식 사이트
https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#pattern
2-12. 중첩 클래스
중첩 클래스 종류

- 정적 중첩 클래스는
static
이 붙는다. - 내부 클래스는
static
이 붙지 않는다.
- 중첩 클래스를 정의하는 위치는 변수의 선언 위치와 같다.
- 정적 중첩 클래스 => 정적 변수(클래스 변수)
- 내부 클래스 => 인스턴스 변수
- 지역 클래스 =>지역 변수
중첩(Nested) vs 내부(Inner)
- 중첩은 나의 안에 있지만 내 것이 아닌 것
- 큰 나무 상자 안에 전혀 다른 작은 나무 상자가 있다.
- 내부는 나의 내부에서 나를 구성하는 요소
- 나의 심장은 나의 내부에서 나를 구성하는 요소다.
클래스 안에 있는 클래스는 다 중첩 클래스이다.
세부적으로 나누기 위해 중첩 클래스와 내부 클래스로 나눈다.
내부 클래스의 생성
- 클래스 내부에
static
이 붙은 중첩 내부 클래스는 외부 클래스의 인스턴스 생성 없이 따로 생성가능하다. - 클래스 내부에
static
이 붙지 않은 내부 클래스는 외부 클래스의 인스턴스 생성을 해야만 생성 가능하다.
내부 클래스의 장점
- 구조적으로 해당 클래스에만 사용하는 클래스라고 논리적으로 그훕화를 할 수 있다.
- 캡슐화가 가능하다.
같은 이름의 바깥 변수 접근 방법
public class ShadowingMain {
public int value = 1;
class Inner {
public int value = 2;
void go() {
int value = 3;
System.out.println("value = " + value); // 3
System.out.println("this.value = " + this.value); // 2
System.out.println("ShadowingMain.value = " + ShadowingMain.this.value); // 1
}
}
public static void main(String[] args) {
ShadowingMain main = new ShadowingMain();
Inner inner = main.new Inner();
inner.go();
}
}
위에처럼 사용하면 된다.
그러나 차라리 그냥 아예 명확하게 다른 이름으로 주는 것이 좋다.
2-13. 지역 클래스
- 지역 클래스는 지역 변수 처럼 접근 제어자를 사용할 수 없다.
변수들의 생명주기
- 클래스 변수
- 메서드 영역에 존재
- 자바가 클래스를 읽어들이는 순갑부터 프로그램 종료까지 존재
- 인스턴스 변수
- 인스턴스 변수는 본인이 소속된 인스턴스가 GC 되기까지 존재
- 지역 변수
- 메서드 호출이 끝나면 사라짐(스택 영역)
- 매개변수도 지역변수의 한 종류
지역 변수 캡처
public class LocalOuter {
private int outInstanceVar = 3;
public Printer process(int paramVar) {
int localVar = 1; // 지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.
class LocalPrinter implements Printer {
int value = 0;
@Override
public void print() {
System.out.println("value = " + value);
// 인스턴스는 지역 변수보다 더 오래 살아남는다.
System.out.println("locarVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("outInstanceVar = " + outInstanceVar);
}
}
return new LocalPrinter();
//printer.print();를 여기서 실행하지 않고 Printer 인스턴스만 반환한다.
}
public static void main(String[] args) {
LocalOuterV3 localOuter = new LocalOuterV3();
Printer printer = localOuter.process(2);
//printer.print()를 나중에 실행한다. process()의 스택 프레임이 사라진 이후에 실행
printer.print();
// 추가
System.out.println("필드 확인");
Field[] fields = printer.getClass().getDeclaredFields();
for (Field field : fields) {
System.out.println("field = " + field);
}
}
}
실행결과
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
필드 확인
// 인스턴스 변수
field = int nested.local.LocalOuterV3$1LocalPrinter.value
// 캡처 변수
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$localVar
field = final int nested.local.LocalOuterV3$1LocalPrinter.val$paramVar
// 바깥 클래스 참조
field = final nested.local.LocalOuterV3 nested.local.LocalOuterV3$1LocalPrinter.this$0
- 지역 변수는 메서드 호출이 끝나면 사라진다.
- main 메서드의 실행결과를 보면 process라는 메서드가 종료된 이후에도 지역 변수인
localVar
와paramVar
가 출력되고 있다. 즉, 값이 남아있단 것이다.
value = 0
localVar = 1
paramVar = 2
outInstanceVar = 3
printer
객체는 main 메서드가 살아있는 한 GC가 참조를 제거하지 않는다.- 따라서 외부 클래스의 인스턴스 변수인
OutInstanceVar
와LocalPrinter
객체의value
는 접근이 가능하다.
- 따라서 외부 클래스의 인스턴스 변수인
- 문제는
localVar
와paramVar
가 살아있다.
그 이유는 바로 지역 변수 캡처 덕분이다.
- 지역 클래스의 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 생성한 인스턴스에 함께 넣어둔다. 이 과정을
Capture
라고 한다.- 인스턴스를 생성할 때 지역 클래스가 접근하는 지역변수를 확인한다.
- 필요한 지역 변수를 복사해서 보관해둔다.
- 복사한 지역변수를 인스턴스에 포함한다.
- 복사한 지역 변수를 포함해서 인스턴스 생성이 완료된다.
- 복사한 지역 변수를 인스턴스를 통해 접근할 수 있다.
- 위의 예시로 든
localVar
와paramVar
는 스택 영역으로 접근하는 것이 아니라, 인스턴스에서 접근하는 것이다.
- 모든 지역 변수를 캡처하는 것이 아니라, 접근이 필요한 지역 변수만 캡처한다.
- 캡처한 지역 변수의 생명주기는 인스턴스 생명주기와 같다.
- 지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안 된다.
final
로 선언하거나 사실상final
이어야 한다.- 위의 예시에서
localVar
를 중간에 바꾸면 컴파일 오류가 발생한다.
지역 클래스가 접근하는 지역 변수는 왜 final
또는 사실상 final
이어야 할까? 왜 중간에 값이 변하면 안 될까?
...
public Printer process(int paramVar) {
int localVar = 1; // 지역 변수는 스택 프레임이 종료되는 순간 함께 제거된다.
class LocalPrinter implements Printer {
int value = 0;
@Override
public void print() {
System.out.println("value = " + value);
// 인스턴스는 지역 변수보다 더 오래 살아남는다.
System.out.println("localVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("outInstanceVar = " + outInstanceVar);
}
}
LocalPrinter printer = new LocalPrinter();
// 만약 localVar의 값을 변경한다면? 다시 캡처해야 하나?
// localVar = 10; // paramVar = 20; return printer;
}
...
Printer printer = new LocalPrinter();LocalPrinter
를 생성하는 시점에 지역 변수인 localVar
, paramVar
를 캡처한다.
이후에 지역 변수의 값을 변경하면 스택 영역에 존재하는 지역 변수의 값과 인스턴스에 캡처한 캡처 변수의 값이 서로 달라지는 문제가 발생한다. (이를 동기화 문제라고 한다)
- 캡처 변수의 값을 변경하지 못하는 이유
- 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경해야 한다.
- 반대로 인스턴스에 있는 캡처 변수의 값을 변경하면 해당 지역 변수의 값도 다시 변경해야 하낟.
- 개발자 입장에서 보면 예상하지 못한 곳에서 값이 변경될 수 있다.
- 이는 디버깅을 어렵게 한다.
- 지역 변수의 값과 인스턴스에 있는 캡처 변수의 값을 서로 동기화 해야 하는데, 멀티 쓰레드 상황에서 이런 동기화는 매우 어렵고, 성능에 나쁜 영향을 줄 수 있다.
2-14. 익명 클래스
- 익명 클래스는 클래스의 이름을 생략하고, 클래스의 선언과 생성을 한 번에 처리할 수 있다.
- 인터페이스의 구현체를 이름 없이 만들어서 바로 new로 생성한다.
- 정확히는 인터페이스를 구현한 익명 클래스를 생성하는 것이다.
package nested.anonymous;
import nested.local.Printer;
public class AnonymousOuter {
private int outInstanceVar = 3;
public void process(int paramVar) {
int localVar = 1;
Printer printer = new Printer() {
int value = 0;
@Override
public void print() {
System.out.println("value = " + value);
System.out.println("locarVar = " + localVar);
System.out.println("paramVar = " + paramVar);
System.out.println("outInstanceVar = " + outInstanceVar);
}
};
printer.print();
}
public static void main(String[] args) {
AnonymousOuter localOuter = new AnonymousOuter();
localOuter.process(2);
}
}
익명 클래스의 특징
- 이름 없는 지역 클래스를 선언하면서 동시에 생성한다.
- 익명 클래스는 부모 클래스를 상속 받거나, 또는 인터페이스를 구현해야 한다.
- 익명 클래스를 사용할 때는 상위 클래스나 인터페이스가 필요하다.
- 익명 클래스는 인스턴스를 단 한번만 생성할 수 있다.
- 여러 번 생성이 필요하다면 지역 클래스를 선언하고 사용하면 된다.
- 일회성으로 사용되거나 간단한 코드를 구현할 때 사용한다.
변하는 부분과 변하지 않는 부분
- 변하는 부분은 코드 내부에 그대로 유지하고,
- 변하지 않는 부분은 외부에서 전달받도록 한다.
- 람다 혹은 데이터 등이 있다.
2-15. 예외 처리

Object
- 자바에서 기본형을 제외한 모든 것은 객체다. 예외도 객체이다.
- 모든 객체의 최상위 부모는
Object
이므로 예외의 최상위 부모도Object
이다.
Throwable
- 최상위 예외이다.
- 하위에
Exception
과Error
가 있다.Error
- 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구가 불가능한 시스템 예외
- 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안된다.
Exception
- 체크 예외 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
Exception
과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. 단RuntimeException
은 예외로 한다.
RuntimeException
- 언체크 예외, 런타임 예외 컴파일러가 체크 하지 않는 언체크 예외이다.
RuntimeException
과 그 자식 예외는 모두 언체크 예외이다.RuntimeException
의 이름을 따라서RuntimeException
과 그 하위 언체크 예외를 런타임 예외라고 많이 부른다.
체크 예외 vs 언체크 예외(런타임 예외)
- 체크 예외는 발생한 예외를 개발자가 명시적으로 처리해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
- 언체크 예외는 개발자가 발생한 예외를 명시적으로 처리하지 않아도 된다.
주의
상속 관계에서 부모 타입은 자식을 담을 수 있다.
이 개념이 예외 처리에도 적용되는데, 상위 예외를 catch
로 잡으면 그 하위 예외까지 함께 잡는다.
따라서 애플리케이션 로직에서는 Throwable
예외를 잡으면 안 되는데, 앞서 이야기한 잡으면 안되는 Error
예외도 함께 잡을 수 있기 때문이다.
애플리케이션 로직은 이런 이유로 Exception
부터 필요한 예외로 생각하고 잡으면 된다.
예외는 폭탄 돌리기와 같다.
- 예외가 발생하면 잡아서 처리하거나, 처리할 수 없으면 밖으로 던져야한다


예외에 대해서는 2가지 기본 규칙을 기억하면된다.
- 예외는 잡아서 처리하거나 밖으로 던져야 한다. 2
- . 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리할 수 있다.
예를 들어서 Exception
을 catch
로 잡으면 그 하위 예외들도 모두 잡을 수 있다.
예를 들어서 Exception
을 throws
로 던지면 그 하위 예외들도 모두 던질 수 있다.
참고로 예외를 처리하지 못하고 계속 던지다보면 자바 main()
밖으로 예외를 던지게 되고, 예외 로그를 출력하면서 시스템이 종료된다.
(체크)예외를 던질 때
- 메서드 선언부에 예외를 던질 때는 선언한 예외의 자식 예외들도 다 더질 수 있다.
- 반면 호출한 메서드로부터 예외를 받고 처리할 때는 호출한 메서드에서 던지는 예외를 받아야 한다.
예를 들어, call()
이란 메서드에서 Exception
을 상속받은 ChildException
가 터지고, Exception
을 던진다고 가정했을 때
call()
을 호출한 메서드는 ChildException
이 아니라, call()
메서드에서 던지고 있는 Exception
을 처리하도록 해야한다.
// Excpetion을 throws 함.
public call() throws Exception {
// 자식 예외 던진
throw new ChildException("ex);
}
...
...
// call()을 호출하는데 call() 에서 Exception 을 던지므로 Exception 을 받아 처리해야 한다.
public callReceived() throws Exception {
// 예외 발생
call();
}
// 혹은
public callReceived() {
try {
call();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
- 반면 언체크 예외는 throws를 생략할 경우 자동으로 예외를 던진다.
정상 로직과 에러 처리 로직의 혼재
네트워크의 서비스를 다음과 같이 구현했다고 했을 때,
메세지를 요청보낼 때, 연결 실패, 혹은 전송 실패할 경우를 가정해서 작성한 코드이다.
package exception.ex1;
public class NetworkServiceV1_3 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV1 client = new NetworkClientV1(address);
client.initError(data); // 추가
String connectResult = client.connect();
if(isError(connectResult)) {
System.out.println("[네트워크 오류 발생] 오류 코드: " + connectResult);
} else {
String sendResult = client.send(data);
if(isError(sendResult)) {
System.out.println("[네트워크 오류 발생] 오류 코드: " + sendResult);
}
}
client.disconnect();
}
private static boolean isError(String connectResult) {
return !connectResult.equals("success");
}
}
- 위의 코드는 정상 처리 로직과 예외 처리 로직이 혼재되어있다.
- 아래의 코드는 정상 로직과 에외 처리 로직을 분리한 최종적인 코드이다.
package exception.ex2;
public class NetworkServiceV2_5 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV2 client = new NetworkClientV2(address);
client.initError(data); // 추가
try {
client.connect();
client.send(data);
} catch (NetworkClientExceptionV2 e) {
System.out.println("[오류] 코드: " + e.getErrorCode() + ", 메시지: " + e.getMessage());
} finally {
client.disconnect();
}
}
}
try
에서 정상 흐름을,catch
안에 예외 처리 흐름을 분리해서 작성하면 된다.- 외부 환경과의 연결 해제를 위해서
finally
키워드를 사용했다.- 자원 해체 코드에 사용된다.
finally
try
를 시작하기만 하면,finally
는 반드시 호출된다.try
,cath
안에서 잡을 수 없는 예외가 발생해도finally
는 반드시 호출된다.
예외의 계층화
- 아래의 그림과 같이 예외를 단순히 오류 코드로 분류하는 것이 아니라, 예외를 계층화해서 다양하게 만들면 더 세밀하게 예외를 처리할 수 있다.

NetworkClientExceptionV3
는 네트워크에 관련된 Exception이다.ConnectExceptionV3
은 연결 실패시 던지는 예외다.SendExceptionV3
는 데이터 전송 실패시 던지는 예외이다.
package exception.ex3;
import exception.ex3.exception.ConnectExceptionV3;
import exception.ex3.exception.SendExceptionV3;
public class NetworkClientV3 {
private final String address;
public boolean connectError;
public boolean sendError;
public NetworkClientV3(String address) {
this.address = address;
}
public String connect() throws ConnectExceptionV3 {
if(connectError) {
throw new ConnectExceptionV3(address, address + " 서버 연결 실패");
}
System.out.println(address + " 서버 연결 성공");
return "success";
}
public String send(String data) throws SendExceptionV3 {
if(sendError) {
throw new SendExceptionV3(data, address + " 서버에 데이터 전송 실패 " + data);
}
System.out.println(address + " 서버에 데이터 전송 " + data);
return "success";
}
public void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
public void initError(String data) {
if(data.contains("error1")) {
connectError = true;
}
if(data.contains("error2")) {
sendError = true;
}
}
}
- 위의 코드는 외부서비스와 통신하는 Client 의 경우에 대한 코드를 작성한 것이다.
- 위와 같이 오류 코드가 아니라, 세밀하게 예외를 던지도록 해놓으면 예외 그 자체로 어떤 문제로 인해 예외가 발생했는지 코드로 확인할 수 있다.
그런데, 만약 네트워크완 관련된 예외가 더 많이 있어 더욱 더 세밀하게 나눠야 할 수도 있다.
그 때마다 catch
문을 쓰게 되면 그것도 참 번거로운 일이다....
이 때는 어떻게 해야할까?
정말 명확한 부분은 catch
문에서 우선순위로 잡도록 하고, 나머지 부분은 예외의 부모계층으로 잡도록 한다.
package exception.ex3;
import exception.ex3.exception.ConnectExceptionV3;
import exception.ex3.exception.NetworkClientExceptionV3;
import exception.ex3.exception.SendExceptionV3;
public class NetworkServiceV3_2 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV3 client = new NetworkClientV3(address);
client.initError(data); // 추가
try {
client.connect();
client.send(data);
} catch (ConnectExceptionV3 e) {
System.out.println("[연결 오류] 주소: " + e.getAddress() + ", 메세지: " + e.getMessage());
} catch (NetworkClientExceptionV3 e) {
System.out.println("[네트워크 오류] 메시지: " + e.getMessage());
} catch (Exception e) {
System.out.println("[알 수 없는 오류] 메시지: " + e.getMessage());
} finally {
client.disconnect();
}
}
}
이러한 방식으로 하도록 한다.
NetworkClientExceptionV3
는 ConnectExceptionV3
의 부모 예외이다.
위는 연결 오류에 대한 예외를 명확히 해야하기 떄문에, ConnectExceptionV3
에 대한 예외 처리는 세밀하게 받도록 했고, 그외의 네트워크 오류는 NetworkClientExceptionV3
를 catch하게 했다.
혹은 다음과 같은 방식으로 할 수 있는데, 반드시 부모 예외가 같아야 한다.
package exception.ex3;
import exception.ex3.exception.ConnectExceptionV3;
import exception.ex3.exception.SendExceptionV3;
public class NetworkServiceV3_2 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV3 client = new NetworkClientV3(address);
client.initError(data); // 추가
try {
client.connect();
client.send(data);
} catch (ConnectExceptionV3 | SendExceptionV3 e) {
System.out.println("[연결 또는 전송 오류] 메세지: " + e.getMessage());
} finally {
client.disconnect();
}
}
}
ConnectExceptionV3
과SendExceptionV3
의 부모 예외는 같다.- 참고로 e에서는 공통적으로 가지고 있는 메서드만 호출할 수 있다.
실무 예외 처리 방안
(해당 concept은 그냥 PPT에서 복붙한다.)
처리할 수 없는 예외
예를 들어서 상대 네트워크 서버에 문제가 발생해서 통신이 불가능하거나, 데이터베이스 서버에 문제가 발생해서 접속이 안되면, 애플리케이션에서 연결 오류, 데이터베이스 접속 실패와 같은 예외가 발생한다.
이렇게 시스템 오류 때문에 발생한 예외들은 대부분 예외를 잡아도 해결할 수 있는 것이 거의 없다.
예외를 잡아서 다시 호출을 시도해도 같은 오류가 반복될 뿐이다.
이런 경우 고객에게는 "현재 시스템에 문제가 있습니다."라는 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다.
그리고 내부 개발자가 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두어야 한다.
체크 예외의 부담
체크 예외는 개발자가 실수로 놓칠 수 있는 예외들을 컴파일러가 체크해주기 때문에 오래전부터 많이 사용되었다.
그런데 앞서 설명한 것 처럼 처리할 수 없는 예외가 많아지고, 또 프로그램이 점점 복잡해지면서 체크 예외를 사용하는 것이 점점 더 부담스러워졌다.
체크 예외 사용 시나리오
체크 예외를 사용하게 되면 어떤 문제가 발생하는지 가상의 시나리오를 보자.

- 실무에서는 수 많은 라이브러리를 사용하고, 또 다양한 외부 시스템과 연동한다.
- 사용하는 각각의 클래스들이 자신만의 예외를 모두 체크 예외로 만들어서 전달한다고 가정하자.

- 이 경우
Service
는 호출하는 곳에서 던지는 체크 예외들을 처리해야 한다. - 만약 처리할 수 없다면 밖으로 던져야 한다.
모든 체크 예외를 잡아서 처리하는 예시
try {
} catch (NetworkException) {...}
} catch (DatabaseException) {...}
} catch (XxxException) {...}
그런데 앞서 설명했듯이 상대 네트워크 서버가 내려갔거나, 데이터베이스 서버에 문제가 발생한 경우
Service
에서 예외를 잡아도 복구할 수 없다.Service
에서는 어차피 본인이 처리할 수 없는 예외들이기 때문에 밖으로 던지는 것이 더 나은 결정이다.모든 체크 예외를 던지는 예시
class Service { void sendMessage(String data) throws NetworkException, DatabaseException, ... { ... } }
이렇게 모든 체크예외를 하나씩 다 밖으로 던져야한다.
라이브러리가 늘어날 수 록 다루어야 하는 예외도 더 많아진다. 개발자 입장에서 이것은 상당히 번거로운 일이 된다.
문제는 여기서 끝이 아니다. 만약 중간에 Facade
라는 클래스가 있다고 가정해보자.

- 이 경우
Facade
클래스에서도 이런 예외들을 복구할 수 없다.Facade
클래스도 예외를 밖으로 던져야 한다. - 결국 중간에 모든 클래스에서 예외를 계속 밖으로 던지는 지저분한 코드가 만들어진다.
throws
로 발견한 모든 예외를 다 밖으로 던지는 것이다.
class Facade {
void send() throws NetworkException, DatabaseException, ...
}
class Service {
void sendMessage(String data) throws NetworkException, DatabaseException, ... }
throws Exception
개발자는 본인이 다룰 수 없는 수 많은 체크 예외 지옥에 빠지게 된다.
결국 다음과 같이 최악의 수를 두게 된다.
class Facade {
void send() throws Exception
}
class Service {
void sendMessage(String data) throws Exception
}
Exception
은 애플리케이션에서 일반적으로 다루는 모든 예외의 부모이다. 따라서 이렇게 한 줄만 넣으면 모든 예외를 다 던질 수 있다.
이렇게 하면 Exception
은 물론이고 그 하위 타입인 NetworkException
, DatabaseException
도 함께 던지게 된다.
그리고 이후에 예외가 추가되더라도 throws Exception
은 변경하지 않고 그대로 유지할 수 있다.
코드가 깔끔해지는 것 같지만 이 방법에는 치명적인 문제가 있다.
throws Exception의 문제
Exception
은 최상위 타입이므로 모든 체크 예외를 다 밖으로 던지는 문제가 발생한다.
결과적으로 체크 예외의 최상위 타입인 Exception
을 던지게 되면 다른 체크 예외를 체크할 수 있는 기능이 무효화 되고, 중요한 체크 예외를 다 놓치게 된다.
중간에 중요한 체크 예외가 발생해도 컴파일러는 Exception
을 던지기 때 문에 문법에 맞다고 판단해서 컴파일 오류가 발생하지 않는다.
이렇게 하면 모든 예외를 다 던지기 때문에 체크 예외를 의도한 대로 사용하는 것이 아니다.
따라서 꼭 필요한 경우가 아니면 이렇게 Exception
자체를 밖으로 던지는 것은 좋지 않은 방법이다.
문제 정리
지금까지 알아본 체크 예외를 사용할 때 발생하는 문제들은 다음과 같다.
- 처리할 수 없는 예외: 예외를 잡아서 복구할 수 있는 예외보다 복구할 수 없는 예외가 더 많다.
- 체크 예외의 부담: 처리할 수 없는 예외는 밖으로 던져야 한다. 체크 예외이므로
throws
에 던질 대상을 일일이 명시해야 한다.
사실 Service
를 개발하는 개발자 입장에서 수 많은 라이브러리에서 쏟아지는 모든 예외를 다 다루고 싶지는 않을 것 이다.
특히 본인이 해결할 수 도 없는 모든 예외를 다 다루고 싶지는 않을 것이다.
본인이 해결할 수 있는 예외만 잡아서 처리하고, 본인이 해결할 수 없는 예외는 신경쓰지 않는 것이 더 나은 선택일 수 있다.
언체크(런타임) 예외 사용 사나리오

- 이번에는
Service
에서 호출하는 클래스들이 언체크(런타임) 예외를 전달한다고 가정해보자. NetworkException
,DatabaseException
은 잡아도 복구할 수 없다. 언체크 예외이므로 이런 경우 무시하면 된다.
언체크 예외를 던지는 예시
class Service {
void sendMessage(String data) {
...
}
}
- 언체크 예외이므로
throws
를 선언하지 않아도 된다. - 사용하는 라이브러리가 늘어나서 언체크 예외가 늘어도 본인이 필요한 예외만 잡으면 되고,
throws
를 늘리지 않아도 된다.
일부 언체크 예외를 잡아서 처리하는 예시
try {
} catch (XxxException) {...}
- 앞서 설명했듯이 상대 네트워크 서버가 내려갔거나, 데이터베이스 서버에 문제가 발생한 경우
Service
에서 예 외를 잡아도 복구할 수 없다. Service
에서는 어차피 본인이 처리할 수 없는 예외들이기 때문에 밖으로 던지는 것이 더 나은 결정이다.- 언체크 예외는 잡지 않으면
throws
선언이 없어도 자동으로 밖으로 던진다. - 만약 일부 언체크 예외를 잡아서 처리할 수 있다면 잡아서 처리하면 된다.
예외 공통 처리
이렇게 처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통으로 처리할 수 있는 곳을 만들어서 한 곳에서 해결하면 된다.
어차피 해결할 수 없는 예외들이기 때문에 이런 경우 고객에게는 현재 시스템에 문제가 있습니다.
라고 오류 메시지를 보여주고, 만약 웹이라면 오류 페이지를 보여주면 된다.
그리고 내부 개발자가 지금의 문제 상황을 빠르게 인지할 수 있도록, 오류에 대한 로그를 남겨두면 된다.
이런 부분은 공통 처리가 가능하다.
위의 concept
을 네트워크 관련 코드로 구현한 것이 아래와 같다.
// NetworkException
package exception.ex4.exception;
public class NetworkClientExceptionV4 extends RuntimeException {
public NetworkClientExceptionV4(String message) {
super(message);
}
}
// 연결 실패 예외
package exception.ex4.exception;
public class ConnectExceptionV4 extends NetworkClientExceptionV4 {
private final String address;
public ConnectExceptionV4(String address, String message) {
super(message);
this.address = address;
}
public String getAddress() {
return address;
}
}
// 전송 실패 예외
package exception.ex4.exception;
public class SendExceptionV4 extends NetworkClientExceptionV4 {
private final String sendData;
public SendExceptionV4(String message, String sendData) {
super(message);
this.sendData = sendData;
}
public String getSendData() {
return sendData;
}
}
- 언체크 예외인
RuntimeException
을 상속받았고, ConnectExceptionV4
와SendExceptionV4
는NetworkClientExceptionV4
을 상속받았다.
// 네트워크 클라이언트
package exception.ex4;
import exception.ex4.exception.ConnectExceptionV4;
import exception.ex4.exception.SendExceptionV4;
public class NetworkClientV4 {
private final String address;
public boolean connectError;
public boolean sendError;
public NetworkClientV4(String address) {
this.address = address;
}
public String connect() {
if(connectError) {
throw new ConnectExceptionV4(address, address + " 서버 연결 실패");
}
System.out.println(address + " 서버 연결 성공");
return "success";
}
public String send(String data) {
if(sendError) {
throw new SendExceptionV4(data, address + " 서버에 데이터 전송 실패 " + data);
}
System.out.println(address + " 서버에 데이터 전송 " + data);
return "success";
}
public void disconnect() {
System.out.println(address + " 서버 연결 해제");
}
public void initError(String data) {
if(data.contains("error1")) {
connectError = true;
}
if(data.contains("error2")) {
sendError = true;
}
}
}
- 각각의 메서드에서 검증을 거친 후 예외 처리가 되도록 했다.
- if 문으로 검증을하고, 예외를 처리한다.
package exception.ex4;
public class NetworkServiceV4 {
public void sendMessage(String data) {
String address = "http://example.com";
NetworkClientV4 client = new NetworkClientV4(address);
client.initError(data); // 추가
try {
client.connect();
client.send(data);
} finally {
client.disconnect();
}
}
}
- 언체크 예외를 물려받았으므로,
catch
문을 작성하지 않아도 된다. - 작성하지 않더라도 예외가 발생하면 호출한 곳으로 던지기 때문이다.
- 코드도 깔끔해졌다.
해당 코드를 사용하는 main
메서드를 보자.
package exception.ex4;
import exception.ex4.exception.SendExceptionV4;
import java.util.Scanner;
public class MainV4 {
public static void main(String[] args) {
NetworkServiceV4 networkService = new NetworkServiceV4();
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.print("전송할 문자: ");
String input = scanner.nextLine();
if(input.equals("exit")) {
break;
}
try {
networkService.sendMessage(input);
} catch (Exception e) {
exceptionHandler(e);
}
System.out.println();
}
System.out.println("프로그램을 정상 종료합니다.");
}
// 공통 예외 처리
private static void exceptionHandler(Exception e) {
// 공통 처리
System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생햇습니다.");
System.out.println("==개발자용 디버깅 메시지==");
e.printStackTrace(System.out); // 스택 트레이스 출력
//e.printStackTrace();
// 필요하면 예외 별로 별도의 추가 처리 가능
if(e instanceof SendExceptionV4 sendEx) {
System.out.println("[전송 오류] 전송 데이터 : " + sendEx.getSendData());
}
}
}
- 위의
try-catch
문을 통해서Exception
으로 한번에 받아서 처리하도록 했다. - 참고로
e.printStackTrace(System.out)
와e.printStackTrace()
은 출력이 다르다.- 중요한 건 아니다. 왜냐하면 스프링에서는
Slf4j
를 사용하기 때문이다.
- 중요한 건 아니다. 왜냐하면 스프링에서는
try-with-resoureces
try
에서 자원을 획득하고,finally
에서 자원을 해체하는 구문이 자주 사용되므로 java에서 편의를 제공하기 위해서resources
구문을 제공하게 되었다.AutoCloseable
인터페이스를 구현하면try
가 끝나는 시점에close()
가 자동으로 호출된다.
package java.lang
public interface AutoCloseable {
void close() throws Exception;
}
그리고 try-with-resource
구문을 사용하면 된다.
try (Resource resource = new Resource()) {
// 리소스를 사용하는 코드
}
아래는 구현 코드다.
public class NetworkClientV5 implements AutoCloseable { // 인터페이스 구현
...
@Override
public void close() {
System.out.println("NetworkClientV5.close");
disconnect();
}
}
AutoCloseable
을 구현했다.try
가 끝나면 자동으로 호출된다.- 여기선 예외를 던지지 않기 때문에 인터페이스의 메서드에 있는
throws Exception
은 제거했다.
아래는 사용하는 코드다.
package exception.ex4;
public class NetworkServiceV5 {
public void sendMessage(String data) {
String address = "http://example.com";
try (NetworkClientV5 client = new NetworkClientV5(address)) {
client.initError(data); // 추가
client.connect();
client.send(data);
} catch (Exception e) {
System.out.println("[예외 확인] : " + e.getMessage());
throw e;
}
}
}
finally
가 없이try
구문 끝에서NetworkClientV5
의close()
가 알아서 자동으로 호출된다.
그리고 위의 main
코드에서 NetworkServiceV4
를 NetworkServiceV5
로 바꾸서 실행해보자.
public class MainV4 {
public static void main(String[] args) {
// NetworkServiceV4 networkService = new NetworkServiceV4();
NetworkServiceV5 networkService = new NetworkServiceV5(); // <-V5
...
}
}
아래는 실행결과다.
전송할 문자: hello
http://example.com 서버 연결 성공
http://example.com 서버에 데이터 전송 hello
NetworkClientV5.close
http://example.com 서버 연결 해제
전송할 문자: error1
NetworkClientV5.close ///////// try 구문을 나가면서 close 수행
http://example.com 서버 연결 해제
[예외 확인] : http://example.com 서버 연결 실패
사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생햇습니다.
==개발자용 디버깅 메시지==
exception.ex4.exception.ConnectExceptionV4: http://example.com 서버 연결 실패
at exception.ex4.NetworkClientV5.connect(NetworkClientV5.java:17)
at exception.ex4.NetworkServiceV5.sendMessage(NetworkServiceV5.java:10)
at exception.ex4.MainV4.main(MainV4.java:20)
전송할 문자: error2
http://example.com 서버 연결 성공
NetworkClientV5.close ///////// try 구문을 나가면서 close 수행
http://example.com 서버 연결 해제
[예외 확인] : http://example.com 서버에 데이터 전송 실패 error2
사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생햇습니다.
==개발자용 디버깅 메시지==
exception.ex4.exception.SendExceptionV4: http://example.com 서버에 데이터 전송 실패 error2
at exception.ex4.NetworkClientV5.send(NetworkClientV5.java:25)
at exception.ex4.NetworkServiceV5.sendMessage(NetworkServiceV5.java:11)
at exception.ex4.MainV4.main(MainV4.java:20)
[전송 오류] 전송 데이터 : error2
try
가 끝나자마자 바로 AuthCloseable
을 구현한 close()
가 해체되었음을 확인할 수 있다.
try with resources 장점
- 리소스 누수 방지
- 모든 시소스가 제대로 닫히도록 보장한다.
- 실수로
finally
블록을 적지 않거나,finally
블럭 안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있다.
- 코드 간결성 및 가독성 향상
- 명시적인
close()
호출이 필요 없어 코드가 더 간결하고 읽기 쉬워진다.
- 명시적인
- 스코프 범위 한정
- 예를 들어 리소스로 사용되는
client
변수의 스코프가try
블럭 안으로 한정된다. - 코드 유지보수가 더 쉬워진다.
- 예를 들어 리소스로 사용되는
- 조금 더 빠른 자원 해제
- 기존에는
try -> catch -> finally
로catch
이후에 자원을 반납했다.Try with resources
구분은try
블럭이 끝나면 즉시close()
를 호출한다.
- 기존에는
3. 다음으로
- 호흡이 길었던 강의였다.
- 자바지만, 이 순간에도 스프링이 보였다.
- 예외 코드 조차도 생각이 담길 수 있음을 느낀다.
- 불변 객체에 대해 제대로 알아간다.
- 얘는 리턴을 할 때 새로운 객체를 반환한다.
- Object, 래퍼 클래스, time 패캐지의 클래스 를 해부했다.
Object는 정리를 안 했다. 머리 속에 이미 있는 내용이라..
Enum
에 대해 제대로 알고간다.- 내부 클래스도 제대로 알고 간다.
- 지금 내 수준에선 클래스내에 중첩 클래스보단, Enum을 자주 쓸 거 같다.
- 최근에 스프링으로
@ConfigurationProperies
를 사용하는데,prefix
에서 yml의 값들을 가져올 때, 중첩클래스를 사용하는 것을 봤다. - 그리고 내부 클래스의 지역 내부 클래스의 지역 변수 캡처도 이전에 공부한 부분에서 더욱 더 깊이 한층 더 잘 알아가는 것 같다.
- 예외처리도 컨셉은 대부분 아는 부분이지만, 실무적인 측면에서는 중구난방으로 사용하고 있었다. 실무에서 어떤식으로 사용하게 되는지 알아보았다.
- 그리고 예외 처리도 정상 로직, 예외 로직, 자원 해제 로직을 나누는 부분 또한 인상적이었다.
- 추후 프로젝트 혹은 사용할 코드에 한번 적용해봐야겠다.
3. 다음으로
- 현재 영한님 자바 강의 중급 2편까지 지속되는 스터디에 참여했다. 중급 2편은 스터디 진도에 맞춰서 진행할 예정이다.
- 도커 + 쿠버네티스를 책으로 공부할 예정이다.
- 강의는 듣고 있던 DataBase 기초를 다 듣고
(얼마 안 남았다), OOO 에 대한 강의를 들을 예정이다.(무엇인지는 비밀 (물어봐도 안 말해줄거임 할 떄 공개할거임.))
'Language > JAVA' 카테고리의 다른 글
김영한의 실전 자바 - 중급 2편 - Sec 03. 제네릭 - Generic2 (0) | 2025.01.05 |
---|---|
김영한의 실전 자바 - 중급 2편 - Sec 02. 제네릭 - Generic1 (4) | 2025.01.01 |
인프런 - 김영한 - 실전 자바 기본편 요약 및 정리 (2) | 2024.10.31 |
Java - Scope (2) | 2023.12.11 |
TIL - 자바의정석 1~9장까지 흩어보기 (1) | 2023.08.14 |