Lucian Log
Blog
Computer Science
Algorithm
DB
Network
OS
General
AI
Blockchain
Concurrency
ETC
Git
Infrastructure
AWS
Docker
Java-Ecosystem
JPA
Java
Spring
JavaScript-Ecosystem
JavaScript
Next.js
React
TypeScript
Python-Ecosystem
Django
FastAPI
Python
SQLAlchemy
Software Engineering
Architecture
Culture
Test
Home
Contact
Copyright © 2024 |
Yankos
Home
>
Java-Ecosystem
> Java
Now Loading ...
Java
자바 애노테이션
애노테이션 (Annotation) 프로그램 실행 중에 읽어서 사용할 수 있는 특별한 주석 내부에서 리플렉션 같은 기술 등을 활용 e.g. Class ,Method ,Field ,Constructor 클래스는 getAnnotation() 제공 참고: 본래 주석은 코드가 아니므로 컴파일 시점에 모두 제거됨 코드에 메모를 달아 놓는 것처럼 코드에 대한 메타데이터를 표현 프로그램 코드가 아니어서 애노테이션이 달린 메서드를 호출해도 영향을 주지 않음 애노테이션 정의 규칙 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface AnnoElement { String value(); int count() default 0; String[] tags() default {}; Class<? extends MyLogger> annoData() default MyLogger.class; //MyLogger data(); // 다른 타입은 적용X } 정의: @interface 키워드 속성: 애노테이션은 속성을 가질 수 있음 요소 이름 메서드 형태로 정의 괄호()를 포함하되 매개변수는 없어야 함 데이터 타입 기본 타입 (int, float, boolean 등) String Class (메타데이터) 또는 인터페이스 (직접 정의한 것이 아닌 Class에 대한 정보) enum 다른 애노테이션 타입 위의 타입들의 배열 앞서 설명한 타입 외에는 정의 불가 (즉, 일반적인 클래스를 사용할 수 없음) 예) Member , User , MyLogger default 값 요소에 default 값을 지정 가능 예: String value() default "기본 값을 적용합니다."; 반환 값 void 반환 타입 사용 불가 예외 예외 선언 불가 메타 애노테이션: 애노테이션을 정의하는데 사용하는 특별한 애노테이션 @Retention 애노테이션의 생존 기간을 지정 종류: RetentionPolicy enum RetentionPolicy.SOURCE (특별한 경우 사용) 소스코드에서만 생존 -> 컴파일 시점에 제거 RetentionPolicy.CLASS (기본값, 그러나 거의 사용 X) 컴파일 후 .class 파일까지 생존 -> 자바 실행 시점에 제거 RetentionPolicy.RUNTIME (대부분 사용) 자바 실행 중에도 생존 런타임에 리플렉션으로 읽을 수 있어 자주 사용됨 @Target 애노테이션을 적용할 수 있는 위치 지정 지정하지 않은 곳에 애노테이션을 적용하면 컴파일 오류 발생 종류: ElementType enum 주로 TYPE, FIELD, METHOD 사용 e.g. 배열로 여러 위치도 적용 가능 @Target({ElementType.METHOD, ElementType.TYPE}) @Documented (보통 함께 사용) 자바 API 문서를 만들 때, 해당 애노테이션이 문서에 포함되어 표현되도록 지정 @Inherited 자식 클래스가 애노테이션을 상속 받을 수 있게 함 애노테이션 사용법 기본 @AnnoElement(value = "data", count = 10, tags = {"t1", "t2"}) public class ElementData1 { } 배열 항목이 하나인 경우 {} 생략 가능 & default 항목은 생략 가능 @AnnoElement(value = "data", tags = "t1") public class ElementData2 { } 입력 요소가 하나인 경우 value 키 생략 가능 (value = "data" 와 동일) @AnnoElement("data") public class ElementData3 { } 애노테이션과 상속 public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); Class<? extends Annotation> annotationType(); //애노테이션 타입 반환 } 자바에서 애노테이션은 특별한 형태의 인터페이스로 간주 모든 애노테이션은 java.lang.annotation.Annotation 인터페이스를 묵시적으로 상속 받음 @interface로 정의하면 자바 컴파일러가 자동으로 Annotation 인터페이스를 확장 애노테이션 정의 public @interface MyCustomAnnotation {} 자바가 자동으로 처리 public interface MyCustomAnnotation extends java.lang.annotation.Annotation {} 다만, 애노테이션 사이에는 상속이라는 개념이 존재 X 애노테이션은 오직 Annotation 인터페이스만 상속 애노테이션은 다른 애노테이션이나 인터페이스를 직접 상속할 수 없음 @Inherited 애노테이션을 적용한 클래스의 자식 클래스도 해당 애노테이션을 부여 받을 수 있음 클래스 상속에서만 작동 (인터페이스 구현에는 적용 X) 자바 기본 애노테이션 @Override 메서드 재정의가 정확하게 잘 되었는지 컴파일러가 체크하는데 사용 @Deprecated 더 이상 사용되지 않는다는 뜻을 표현하며, 적용된 기능은 사용을 권장하지 않음 컴파일 시점에 경고를 나타내지만, 프로그램은 작동 옵션 since : 더 이상 사용하지 않게된 버전 정보 forRemoval : 미래 버전에 코드가 제거될 예정 (더더욱 강력한 경고) 예제 @Deprecated -> 진짜 쓰지마 @Deprecated(since = "2.4", forRemoval = true) -> 진짜진짜 쓰지마 @SuppressWarnings 자바 컴파일러가 문제를 경고하지만, 개발자가 문제를 잘 알고 있으니 경고하지 말라고 지시 왠만하면 사용 X (제네릭 쓰다보면 개발자가 책임지겠다고 쓸 때 정도 있음) 옵션 all: 모든 경고 억제 deprecation: deprecated 코드를 사용할 때 발생하는 경고 억제 unchecked: 제네릭 타입과 관련된 unchecked 경고 억제 serial: Serializable 인터페이스를 구현할 때 serialVersionUID 필드를 선언하지 않은 경우 발생하는 경고 억제 rawtypes: 제네릭 타입이 명시되지 않은(raw) 타입을 사용할 때 발생하는 경고 억제 unused: 사용되지 않는 변수, 메서드, 필드 등을 선언했을 때 발생하는 경고 억제 참고: 애노테이션 기반 검증기 활용 예제 public class Validator { public static void validate(Object obj) throws Exception { Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); // @NotEmpty 어노테이션 검사 if (field.isAnnotationPresent(NotEmpty.class)) { String value = (String) field.get(obj); NotEmpty annotation = field.getAnnotation(NotEmpty.class); if (value == null || value.isEmpty()) { throw new RuntimeException(annotation.message()); } } // @Range 어노테이션 검사 if (field.isAnnotationPresent(Range.class)) { long value = field.getLong(obj); Range annotation = field.getAnnotation(Range.class); if (value < annotation.min() || value > annotation.max()) { throw new RuntimeException(annotation.message()); } } } } } Reference 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
Java-Ecosystem
· 2025-03-25
자바 리플렉션
리플렉션 클래스가 제공하는 다양한 정보(메타 데이터)를 런타임에 동적으로 분석하고 사용하는 기능 e.g. 스프링 프레임워크가 내가 만든 클래스를 대신 생성해주는 경우 메타데이터 종류 클래스 e.g. 클래스 이름, 접근 제어자, 부모 클래스, 구현한 인터페이스 필드 e.g. 필드 이름, 타입, 접근 제어자 런타임에 동적으로 해당 필드 값을 읽거나 수정 가능 메서드 e.g. 메서드 이름, 반환 타입, 매개변수 정보 런타임에 동적으로 메서드 조회 및 호출 가능 생성자 e.g. 매개변수 타입 및 개수 런타임에 동적으로 생성자 조회 및 객체 생성 가능 주의점 리플렉션 코드는 특별한 상황에서 사용 공통 문제를 해결하는 유틸리티, 프레임워크, 라이브러리 개발 테스트 등 일반적인 애플리케이션은 권장 X 무분별한 리플렉션 사용은 코드의 가독성과 안정성이 크게 저하 e.g. private 직접 접근은 객체 지향 원칙을 위반 (캡슐화 및 유지보수성 저하) 클래스 내부 구조나 구현 세부사항이 변경되면 쉽게 깨지거나 버그를 초래 리플렉션은 문자를 활용하므로, 필드 및 메서드 이름 변경 시 컴파일러가 놓침 클래스 메타데이터 클래스의 메타데이터는 Class 클래스로 표현 Class 조회 방법 클래스에서 찾기 클래스명.class e.g. Class<BasicData> basicDataClass1 = BasicData.class; 인스턴스에서 찾기 인스턴스.getClass() e.g. BasicData basicInstance = new BasicData(); Class<? extends BasicData> basicDataClass2 = basicInstance.getClass(); 문자로 찾기 Class.forName(패키지명문자열) e.g. String className = "reflection.data.BasicData"; Class<?> basicDataClass3 = Class.forName(className); 기본 정보 탐색 클래스 이름 경로 포함 이름: basicData.getName() //reflection.data.BasicData 클래스 이름: basicData.getSimpleName() //BasicData 패키지 basicData.getPackage() //package reflection.data 부모 클래스 basicData.getSuperclass() //class java.lang.Object 구현한 인터페이스 basicData.getInterfaces() //[] 조건 판별 basicData.isInterface() //false basicData.isEnum() //false basicData.isAnnotation() //false 수정자 정보 (규칙있는 숫자로 리턴) basicData.getModifiers() //1 참고: 수정자는 접근제어자와 비접근제어자(기타 수정자)로 분류 접근 제어자: public , protected , default ( package-private ), private 비 접근 제어자: static , final , abstract , synchronized , volatile 등 메서드 메타데이터 Method 클래스로 표현 (클래스 메타데이터를 통해 획득 가능) 메서드 메타데이터 조회 getMethod(메서드이름, 매개변수타입) 해당 클래스와 상위 클래스에서 상속된 모든 public 메서드 중 지정 메서드 조회 e.g. String methodName = "hello"; Method method = helloClass.getMethod(methodName, String.class); getDeclaredMethod(메서드이름, 매개변수타입) 해당 클래스에서 선언된 모든 메서드 중 지정 메서드 조회 e.g. String methodName = "hello"; Method method = helloClass.getMethod(methodName, String.class); getMethods() 해당 클래스와 상위 클래스에서 상속된 모든 public 메서드를 반환 e.g. Class<BasicData> helloClass = BasicData.class; Method[] methods = helloClass.getMethods(); getDeclaredMethods() 해당 클래스에서 선언된 모든 메서드를 반환 접근 제어자에 관계 X, 상속된 메서드 포함 X e.g. Class<BasicData> helloClass = BasicData.class; Method[] declaredMethods = helloClass.getDeclaredMethods(); 동적 메서드 호출 메서드 이름을 입력 받으면, 호출 대상 메서드를 동적으로 조회해 호출 가능 getMethod(), getDeclaredMethod()로 메서드 동적 조회 Method 객체의 invoke(인스턴스, 인자1, ...) 로 메서드 호출 e.g. Class<? extends BasicData> helloClass = helloInstance.getClass(); String methodName = "hello"; Method method = helloClass.getDeclaredMethod(methodName, String.class); Object returnValue = method.invoke(helloInstance, "hi"); 필드 메타데이터 Field 클래스로 표현 (클래스 메타데이터를 통해 획득 가능) 필드 조회 getField(필드이름) 해당 클래스와 상위 클래스에서 상속된 모든 public 필드 중 지정 필드 조회 e.g. Field nameField = aClass.getField("name"); getDeclaredField(필드이름) 해당 클래스에서 선언된 모든 필드 중 지정 필드 조회 e.g. Field nameField = aClass.getDeclaredField("name"); getFields() 해당 클래스와 상위 클래스에서 상속된 모든 public 필드를 반환 e.g. Class<BasicData> helloClass = BasicData.class; Field[] fields = helloClass.getFields(); getDeclaredFields() 해당 클래스에서 선언된 모든 필드를 반환 접근 제어자에 관계 X, 상속된 필드 포함 X e.g. Class<BasicData> helloClass = BasicData.class; Field[] declaredFields = helloClass.getDeclaredFields(); 필드 값 변경 setAccessible(true) private 필드에 직접 접근해 변경할 수 있는 기능 e.g. nameField.setAccessible(true) 참고: private 메서드, 생성자에서도 사용 가능 (Method, Constructor) set(인스턴스, 변경값) 필드 값 변경 메서드 e.g. nameField.set(user, "userB") 생성자 메타데이터 Constructor 클래스로 표현 (클래스 메타데이터를 통해 획득 가능) 생성자 조회 getConstructor(매개변수타입) 해당 클래스와 상위 클래스에서 상속된 모든 public 생성자 중 지정 생성자 조회 e.g. Constructor<?> constructor = aClass.getConstructor(String.class); getDeclaredConstructor(매개변수타입) 해당 클래스에서 선언된 모든 생성자 중 지정 생성자 조회 e.g. Constructor<?> constructor = aClass.getDeclaredConstructor(String.class); getConstructors() 해당 클래스와 상위 클래스에서 상속된 모든 public 생성자를 반환 e.g. helloClass.getConstructors(); getDeclaredConstructors() 해당 클래스에서 선언된 모든 생성자를 반환 접근 제어자에 관계 X, 상속된 생성자 포함 X e.g. helloClass.getDeclaredConstructors(); 동적 인스턴스 생성 setAccessible(true) private 생성자에 직접 접근해 호출할 수 있는 기능 e.g. constructor.setAccessible(true) newInstance(인자) 생성자를 호출해 동적으로 객체 생성 e.g. Object instance = constructor.newInstance("hello") Reference 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
Java-Ecosystem
· 2025-03-15
자바 I/O & 네트워크
컴퓨터 데이터 개발하면서 다루는 데이터는 2가지 바이너리 데이터 (byte 기반 - e.g. 010101) 텍스트 데이터 (문자 기반 - e.g. “ABC”) 컴퓨터 메모리 컴퓨터 메모리는 반도체로 만들어짐 (e.g. RAM) 반도체: 트랜지스터의 모임 (수 많은 전구들이 모여 있는 것) 트랜지스터: 아주 작은 전자 스위치 (전구 하나) 전기가 흐르거나 흐르지 않는 두 가지 상태 가짐 -> 0 & 1 이진수 표현 메모리는 단순히 전구를 켜고 끄는 방식으로 작동 -> 컴퓨터는 전구의 상태만 변경 혹은 확인 컴퓨터는 전구들을 켜고 끄는 방식으로 데이터를 기록하고 읽음 현대 컴퓨터 메모리는 초당 수십억 번의 데이터 접근으로 매우 빠름 컴퓨터는 데이터 처리 시 2진수로 변환해 저장 10진수 숫자 -> 간단한 공식 -> 2진수 e.g. 10진수 100 -> 2진수 1100100 문자 -> 문자 집합(Character Set) -> 10진수 -> 간단한 공식 -> 2진수 e.g. “A” -> 65 -> 1000001 단위 1비트(bit): 2가지 상태 표현 1바이트(byte) = 8bit : 256가지 표현 (정보를 처리하는 기본 단위) 음수 표현시 앞의 1비트를 사용 (e.g. 자바의 숫자 타입들) 문자 집합 (Character Set) 사용 전략: 사실상 표준인 UTF-8을 사용하자 문제: 문자는 2진수로 나타낼 수 없음 해결책: 문자 집합 - 컴퓨터 과학자들이 문자에 숫자를 연결시키는 방법을 고안 문자 인코딩: 문자 -> 문자 집합(Character Set) -> 10진수 -> 간단한 공식 -> 2진수 문자 디코딩: 2진수 -> 간단한 공식 -> 10진수 -> 문자 집합(Character Set) -> 문자 문자 집합 종류와 역사 ASCII (American Standard Code for Information Interchange, 1960년도) 각 컴퓨터 회사 간 호환성 문제 해결을 위해 개발 7비트로 128가지 문자 표현 영문 알파벳, 숫자, 키보드 특수문자, 스페이스, 엔터 ISO_8859_1 (= LATIN1 = ISO-LATIN-1, 1980년도) 서유럽 문자를 표현하는 문자 집합 8비트(1byte)로 256가지 문자 표현 ASCII 128가지 + 서유럽 문자, 추가 특수 문자 기존 ASCII와 호환 가능 한글 문자 집합 특징 한글을 표현할 수 있는 문자 집합 16비트(2byte)로 65536가지 문자 표현 기존 ASCII와 호환 가능 ASCII는 1바이트, 한글은 2바이트로 메모리에 저장 한글은 글자가 많아서 1바이트로 표현 불가 EUC-KR (1980년도) 자주 사용하는 한글 표현 ASCII + 자주 사용하는 한글 2350개 + 한국에서 자주 사용하는 기타 글자 MS949 (1990년도) 마이크로소프트가 EUC-KR을 확장해, 한글 11,172자를 모두 표현 e.g. “쀍”, “삡” 등 모든 초성, 중성, 종성 조합 표현 가능 EUC-KR과 호환 가능하고 윈도우 시스템에서 계속 사용됨 전세계 문자 집합 (유니코드) 특징 전세계 문자를 대부분 표현할 수 있는 문자 집합 국제적 호환성을 위해 개발 특정 언어를 위한 문자 집합이 PC에 설치되지 않으면 글자가 깨짐 한 문서 안에 여러 나라 언어 저장 시에도 문제가 됨 UTF-16 (1990년도) - 초반에 인기 16비트(2byte) 기반 기본 다국어는 2byte로 표현 (영어, 유럽, 한국어, 중국어, 일본어) 그 외는 4byte로 표현 (고대문자, 이모지, 중국어 확장 한자) 큰 단점 ASCII 호환 불가 무조건 2바이트로 읽어서 ASCII 영문을 못 읽음 (ASCII 문서가 안열림) 영문의 경우 다른 문자 집합에 비해 2배 메모리 더 사용 웹 문서 80% 이상이 영문 문서라 비효율적 UTF-8 (1990년도) 현대의 사실상 표준 인코딩 기술 8비트(1byte) 기반, 가변 길이 인코딩 1byte: ASCII, 영문, 기본 라틴 문자 2byte: 그리스어, 히브리어 라틴 확장 문자 3byte: 한글, 한자, 일본어 4byte: 이모지, 고대문자등 단점: 일부 언어에서 더 많은 용량 사용 큰 장점 ASCII 호환 저장 공간 및 네트워크 효율성 (ASCII 문자를 1바이트로 사용) 한글이 깨지는 가장 큰 이유 2가지 EUC-KR(MS949)와 UTF-8이 서로 호환되지 않아서 윈도우에서 저장한 것을 UTF-8로 불러오거나 역인 경우 EUC-KR(MS949) 혹은 UTF-8로 인코딩한 한글을 ISO-8891-1로 디코딩할 때 개발 툴 같은 곳에서 ISO-8891-1로 설정되어 있으면, 한글을 저장 및 읽을 때 깨짐 코드 예시 Charset : 문자 집합 클래스 StandardCharsets : 자주 사용하는 문자 집합을 상수로 지정해둠 e.g. StandardCharsets.UTF_8, StandardCharsets.UTF_16BE 참고: UTF-16의 경우, UTF-16BE 사용하자 UTF-16BE & UTF-16LE는 바이트의 순서 차이 String.getBytes(Charset) : 지정한 문자 집합으로 문자 인코딩 참고: 자바의 바이트는 첫 비트로 음양을 표현 예를 들어, EUC-KR의 ‘가’를 2진수로 표현하면 -> (10110000, 10100001) 기본 십진 수 표현 : [176, 161] 자바 바이트로 십진수 표현 : [-80, -95] 즉, 십진수 표현만 다를 뿐 실제 메모리에 저장되는 값은 동일 문자 인코딩 및 디코딩 시 문자 집합이 생략된 경우, 시스템 기본 문자 집합 사용 (보통 UTF-8) I/O (Input/Output) 데이터를 주고 받는 것 현대 컴퓨터는 대부분 byte 단위로 주고 받음 (bit 단위는 너무 작기 때문에) 자바 프로세스는 파일, 네트워크(소켓), 콘솔 등과 byte 단위로 데이터를 주고 받음 스트림(Stream) 데이터를 주고 받는 방식(I/O)을 추상화한 것 파일이든 소켓을 통한 네트워크든 일관된 방식으로 데이터를 주고 받을 수 있음 덕분에 기억할 메서드가 단순화 읽기: read(), readAllBytes() 쓰기: write() 자원해제: close() 분류 입출력 입력 스트림 : 외부 데이터를 자바 프로세스 내부로 가져옴 출력 스트림 : 자바 프로세스 내부 데이터를 외부로 보냄 독립성 기본 스트림 단독 사용 가능한 스트림 File, 메모리, 콘솔등에 직접 접근하는 스트림 e.g. FileInputStream, FileOutputStream, FileReader, FileWriter, ByteArrayInputStream, ByteArrayOutputStream 보조 스트림 단독 사용 불가능한 스트림 (대상 스트림 필수 필요) 기본 스트림에 보조 기능을 제공하는 스트림 e.g. BufferedInputStream, BufferedOutputStream, PrintStream, InputStreamReader, OutputStreamWriter, DataOutputStream, DataInputStream 스트림 유의할 개념 스트림의 모든 데이터는 byte 단위를 사용 문자 역시 byte로 변환이 필요 코드에서 바이트를 표현할 때 10진수로 사용하자 개발자는 코드에서 문자, 문자집합, 10진수까지만 다루면 됨 e.g. A를 바이트로 표현하고 싶으면 65로 쓰자 (2진수 1000001 사용 X) 참고: write()와 read()가 int를 입력 및 반환하는 이유 자바 byte는 부호 있는 8비트(-128~127)라 EOF(End of File) 표현이 어려움 int를 반환하면 0~255로 표현하고 -1을 EOF로 사용할 수 있음 ByteArrayStream은 거의 사용되지 않는다! 메모리에 데이터를 저장하고 읽을 때는 컬렉션이나 배열을 사용 버퍼(Buffer) : 데이터를 모아서 전달하거나 모아서 전달 받는 용도로 사용하는 것 e.g. byte[] buffer = new byte[BUFFER_SIZE]; 버퍼의 크기는 보통 4KB or 8KB 정도 잡는 것이 효율적 (최근엔 16KB도 가끔 보임) 디스크나 파일 시스템의 데이터 기본 읽기 쓰기 단위가 보통 4KB, 8KB이기 때문 즉, 버퍼 크기가 더 커져도 속도가 계속 향상되지 않음 플러시(flush()) : 버퍼가 다 차지 않아도 버퍼에 남아있는 데이터를 전달하는 것 참고: BufferedStream close() 호출 시 내부에서 flush()를 먼저 호출한 후 연결된 스트림의 close() 호출 컴퓨터 간 데이터 교환 형식 사용 전략 JSON을 사용하자 (대부분 충분) 성능 최적화가 매우 중요하다면, Protobuf와 Avro 등을 고려하자 발전 과정 자바 객체 직렬화(Serialization) - 거의 사용하지 않음 메모리에 있는 객체 인스턴스를 바이트 스트림으로 변환해 파일에 저장하거나 네트워크로 전송할 수 있도록 하는 기능 역직렬화(Deserialization)을 통해 원래 객체로 복원 가능 직렬화하려는 클래스는 Serialization 인터페이스를 구현해야 함 장점: 편의성으로 인해 초기 분산 시스템에서 활용 단점: 장애날 확률 높음 호환성 문제 (버전 관리 어려움, 자바 플랫폼 종속성으로 타언어와 호환 불편) 성능 느림, 상대적으로 큰 용량… XML 장점: 텍스트이므로 플랫폼 간 호환성 해결 단점: 복잡성, 무거움 JSON 가볍고 간결, 좋은 호환성 2000년대 후반, 웹 API와 RESTful 서비스가 대중화되며 사실상 표준이 됨 Protobuf, Avro 장점: 더 적은 용량, 더 빠른 성능 (Byte 기반으로 용량과 성능 최적화됨) XML, JSON은 텍스트 기반이라 용량이 상대적으로 큼 숫자도 텍스트로 표현되어 바이트를 더 잡아 먹음 단점: 호환성이 떨어지고, byte 기반이라 사람이 직접 읽기 어려움 스트림 종류 Byte Stream (byte를 다루는 스트림) 특징 바이트로 스트림 입출력 지원 BufferdInputStream, BufferedOutputStream (보조 스트림) 내부에서 단순히 버퍼(byte[] buf) 기능 제공 - 대상 Stream이 필요 byte[] buf가 가득차면 대상 스트림의 write(byte[]) 호출 후 버퍼 비움 byte[] buf가 비어 있으면 버퍼 크기만큼 대상 스트림의 read(byte[]) 호출 후 버퍼에서 읽음 close() 호출 시, 내부에서 플러시하고 연결된 스트림의 close()까지 호출됨 장점: 단순한 코드 유지 가능 단점: 기본 read(), write()에 직접 버퍼 사용 보단 느림 (동기화 락 때문) PrintStream (보조 스트림) System.out의 실체, 데이터 출력 기능 제공 추가 기능인 println() 제공 (콘솔 출력) 콘솔에 출력하듯 파일이나 다른 스트림에 문자, 숫자, boolean 등 출력 가능 e.g. FileOutputStream과 조합하면 콘솔에 출력하듯 파일에 출력 가능 DataInputStream, DataOutputStream (보조 스트림) 자바 데이터 형을 편리하게 입출력 가능 e.g. String, int, double, boolean… 데이터 형에 따라 알맞은 메서드를 사용 e.g. writeUTF(), writeInt(), writeDouble(), writeBoolean()… 데이터를 정확하게 읽을 수 있는 이유 String의 경우 저장 시 2byte를 사용해 문자의 길이도 함께 저장해 둠 2byte -> 65535 길이까지만 가능 e.g. dos.writeUTF("id1"); -> 3id1(2byte(문자 길이) + 3byte(실제 문자 데이터)) -> dis.readUTF()가 글자 길이를 확인하고 해당 길이만큼 읽음 Int는 단순히 4byte를 사용하므로, 4byte로 저장하고 4byte로 읽음 e.g. dos.writeInt(20) -> dis.readInt() e.g. FileOutputStream 조합 -> 파일에 자바 데이터 형을 편리하게 저장 가능 주의점: 저장한 순서대로 읽어야 함 writeUTF(), writeInt()였다면, readUTF(), readInt() 순으로 각 타입마다 그에 맞는 byte 단위로 저장되기 때문 e.g. 문자는 UTF-8 형식 저장, 자바 int는 4byte로 묶어 저장… ObjectInputStream, ObjectOutputStream (보조 스트림, 거의 사용 X) 자바 객체 직렬화 및 역직렬화를 지원 자바 객체 직렬화는 버그를 많이 일으켜서, 거의 사용하지 않음 Character Stream (문자를 다루는 스트림) 특징 문자로 스트림 입출력 지원 내부에서 문자 <-> byte 인코딩 및 디코딩을 대신 처리 따라서, 문자 집합 전달 필수 InputStreamReader, OutputStreamWriter (보조 스트림) InputStreamReader은 반환타입이 int -> char형으로 캐스팅해 사용 EOF인 -1 표현을 위해 int로 반환 FileReader, FileWriter 내부에서 스스로 FileOutputStream, FileInputStream을 생성해 사용 나머지는 InputStreamReader, OutputStreamWriter과 동일 BufferedReader, BufferedWriter (보조 스트림) 버퍼 보조 기능 제공 (Reader, Writer를 생성자에서 전달) BufferedReader는 한 줄 단위로 문자 읽는 기능도 추가 제공 (readLine()) 한 줄 단위로 문자를 읽고 String 반환, EOF에 도달하면 null 반환 코드 예시 FileStream 예시 (메모리, 콘솔도 유사하게 사용) 출력 생성: FileOutputStream fos = new FileOutputStream("temp/hello.dat"); 1바이트 쓰기: fos.write(65); 여러 바이트 한 번에 쓰기: fos.write({65, 66, 67}); 입력 생성: FileInputStream fis = new FileInputStream("temp/hello.dat"); 1바이트 읽기: fis.read(); 여러 바이트 한 번에 읽기 (버퍼 읽기) byte[] buffer = new byte[10]; int readCount = fis.read(buffer, 0, 10); 모든 바이트 한 번에 읽기 byte[] readBytes = fis.readAllBytes(); 파일 및 버퍼 사이즈 설정 예시 public static final int FILE_SIZE = 10 * 1024 * 1024; // 10MB public static final int BUFFER_SIZE = 8192; // 8KB Buffered 스트림 사용 예시 (보조 스트림들은 이와 비슷) 출력 FileOutputStream fos = new FileOutputStream(FILE_NAME); BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER_SIZE); for (int i = 0; i < FILE_SIZE; i++) { bos.write(1); } 입력 FileInputStream fis = new FileInputStream(FILE_NAME); BufferedInputStream bis = new BufferedInputStream(fis, BUFFER_SIZE); while ((data = bis.read()) != -1) { fileSize++; } BufferedReader, BufferedWriter 사용 예시 // 파일에 쓰기 FileWriter fw = new FileWriter(FILE_NAME, UTF_8); BufferedWriter bw = new BufferedWriter(fw, BUFFER_SIZE); bw.write(writeString); bw.close(); // 파일에서 읽기 StringBuilder content = new StringBuilder(); FileReader fr = new FileReader(FILE_NAME, UTF_8); BufferedReader br = new BufferedReader(fr, BUFFER_SIZE); String line; while ((line = br.readLine()) != null) { content.append(line).append("\n"); } br.close(); PrintStream 사용 예시 FileOutputStream fos = new FileOutputStream("temp/print.txt"); PrintStream printStream = new PrintStream(fos); printStream.println("hello java!"); printStream.println(10); printStream.println(true); printStream.close(); DataInputStream, DataOutputStream 사용 예시 FileOutputStream fos = new FileOutputStream("temp/data.dat"); DataOutputStream dos = new DataOutputStream(fos); dos.writeUTF("회원A"); dos.writeInt(20); dos.writeDouble(10.5); dos.writeBoolean(true); dos.close(); FileInputStream fis = new FileInputStream("temp/data.dat"); DataInputStream dis = new DataInputStream(fis); System.out.println(dis.readUTF()); System.out.println(dis.readInt()); System.out.println(dis.readDouble()); System.out.println(dis.readBoolean()); dis.close(); FileInputStream, FileOutputStream은 디렉토리 지정시 해당 디렉토리를 미리 생성해두자. 그렇지 않으면 FileNotFoundException이 발생한다. 스트림 입출력 성능 최적화 핵심 전략 적당한 크기 파일이라면, 한 번에 처리하자 (수십 MB 정도가 안전 범위) 대용량 파일이라면, 버퍼로 처리하자 일반적인 상황에서는 Buffered 스트림으로 처리 성능이 중요하다면 버퍼를 직접 다루자 (read(byte[]), write(byte[])) 버퍼의 이점 버퍼를 사용 -> OS 시스템 콜 & HDD, SSD 읽기 쓰기 작업 횟수 감소 -> 큰 속도 향상 write(), read()는 호출할 때마다 OS 시스템 콜을 통해 입출력 명령을 전달 OS 시스템 콜과 디스크 읽기/쓰기 -> 무거운 작업 하나씩 입출력 VS 버퍼 입출력 VS 한 번에 전체 입출력 하나씩 입출력 e.g. 1Byte씩 10MB 파일(약 1000만번 호출) -> 쓰기: 약 14초 / 읽기: 약 5초 자바 최적화로 인해 실제로는 배치로 나가서 그나마 이정도 버퍼 입출력 -> 대용량 파일 처리에 유리 e.g. 8192Byte(8KB)씩 10MB 파일 -> 쓰기: 약 14ms / 읽기: 5ms 속도 1000배 향상 편리하게 BuffedStream 사용도 가능 -> 쓰기: 약 102ms / 읽기: 약 94ms 쓰기 속도 140배, 읽기 속도 50배 향상 -> 버퍼 직접 사용보단 느림 (동기화 락 때문) 한 번에 전체 입출력 -> 작은 파일 처리에 유리 e.g. -> 쓰기: 약 15ms / 읽기: 약 3ms 버퍼 입출력 예제와 성능이 비슷 한 번에 쓴다고 무작정 빨라지지 않음 디스크나 파일 시스템의 데이터 읽기 쓰기 단위가 보통 4KB, 8KB이기 때문 부분 읽기 VS 전체 읽기 (둘 다 필요) 부분 읽기(버퍼 읽기) 메모리 사용량 제어 가능 -> 대용량 파일 처리에 유리 e.g. read(byte[], offset, lentgh) 전체 읽기 한 번의 호출로 모든 데이터를 읽을 수 있어 편리 -> 작은 파일 처리에 유리 한 번에 많은 메모리 사용으로 OutOfMemoryError 발생을 조심해야 함 e.g. readAllBytes() File, Files 자바에서 파일, 디렉토리를 다룰 때 사용 핵심 전략 Files + Path를 사용하자 성능도 좋고 사용도 편리 File 뿐만아니라 파일 관련 스트림 사용도 Files부터 찾아보고 결정할 것 기본 사용법 예전 방식: File (자바 1.0, 레거시에 많음) public class OldFileMain { public static void main(String[] args) throws IOException { File file = new File("temp/example.txt"); File directory = new File("temp/exampleDir"); // 1. exists(): 파일이나 디렉토리의 존재 여부를 확인 System.out.println("File exists: " + file.exists()); // 2. createNewFile(): 새 파일을 생성 boolean created = file.createNewFile(); System.out.println("File created: " + created); // 3. mkdir(): 새 디렉토리를 생성 boolean dirCreated = directory.mkdir(); System.out.println("Directory created: " + dirCreated); // 4. delete(): 파일이나 디렉토리를 삭제 //boolean deleted = file.delete(); //System.out.println("File deleted: " + deleted); // 5. isFile(): 파일인지 확인 System.out.println("Is file: " + file.isFile()); // 6. isDirectory(): 디렉토리인지 확인 System.out.println("Is directory: " + directory.isDirectory()); // 7. getName(): 파일이나 디렉토리의 이름을 반환 System.out.println("File name: " + file.getName()); // 8. length(): 파일의 크기를 바이트 단위로 반환 System.out.println("File size: " + file.length() + " bytes"); // 9. renameTo(File dest): 파일의 이름을 변경하거나 이동 File newFile = new File("temp/newExample.txt"); boolean renamed = file.renameTo(newFile); System.out.println("File renamed: " + renamed); // 10. lastModified(): 마지막으로 수정된 시간을 반환 long lastModified = newFile.lastModified(); System.out.println("Last modified: " + new Date(lastModified)); } } 대체 방식: Files + Path (자바 1.7) public class NewFilesMain { public static void main(String[] args) throws IOException { Path file = Path.of("temp/example.txt"); Path directory = Path.of("temp/exampleDir"); // 1. exists(): 파일이나 디렉토리의 존재 여부 확인 System.out.println("File exists: " + Files.exists(file)); // 2. createFile(): 새 파일 생성 try { Files.createFile(file); System.out.println("File created"); } catch (FileAlreadyExistsException e) { System.out.println(file + " File already exists"); } // 3. createDirectory(): 새 디렉토리 생성 try { Files.createDirectory(directory); System.out.println("Directory created"); } catch (FileAlreadyExistsException e) { System.out.println(directory + " Directory already exists"); } // 4. delete(): 파일이나 디렉토리 삭제 (주석 해제 시 실행됨) // Files.delete(file); // System.out.println("File deleted"); // 5. isRegularFile(): 일반 파일인지 확인 System.out.println("Is regular file: " + Files.isRegularFile(file)); // 6. isDirectory(): 디렉토리인지 확인 System.out.println("Is directory: " + Files.isDirectory(directory)); // 7. getFileName(): 파일이나 디렉토리의 이름 반환 System.out.println("File name: " + file.getFileName()); // 8. size(): 파일의 크기를 바이트 단위로 반환 System.out.println("File size: " + Files.size(file) + " bytes"); // 9. move(): 파일 이름 변경 또는 이동 Path newFile = Paths.get("temp/newExample.txt"); // Path.of(...)가 더 좋은 방식 Files.move(file, newFile, StandardCopyOption.REPLACE_EXISTING); System.out.println("File moved/renamed"); // 10. getLastModifiedTime(): 마지막 수정 시간 반환 System.out.println("Last modified: " + Files.getLastModifiedTime(newFile)); // 추가: readAttributes(): 파일의 기본 속성 읽기 BasicFileAttributes attrs = Files.readAttributes(newFile, BasicFileAttributes.class); System.out.println("===== Attributes ====="); System.out.println("Creation time: " + attrs.creationTime()); System.out.println("Is directory: " + attrs.isDirectory()); System.out.println("Is regular file: " + attrs.isRegularFile()); System.out.println("Is symbolic link: " + attrs.isSymbolicLink()); System.out.println("Size: " + attrs.size()); } } 파일이나 디렉토리 경로는 Path 활용 static 메서드를 활용해 기능 제공 경로 표시 방법 절대 경로(Absolute path) PC 내 루트 디렉토리부터 시작하는 전체 경로 e.g. 정규 경로와 대비해 둘 다 가능 /Users/yh/study/inflearn/java/java-adv2 /Users/yh/study/inflearn/java/java-adv2/temp/.. 정규 경로(Canonical path) 절대 경로 + 경로 계산이 완료된 것 e.g. 단 하나만 존재 /Users/yh/study/inflearn/java/java-adv2 상대 경로(Relative path) 현재 작업 디렉토리를 기준으로 하는 경로 e.g. 경로 앞에 아무것도 없을 때는 현재 자바 프로젝트 디렉토리부터 시작 java/java-adv2 File에서 사용하기 File file = new File("temp/.."); 상대 경로: file.getPath() 절대 경로: file.getAbsolutePath() 정규 경로: file.getCanonicalPath() 현재 경로에 있는 모든 파일 및 디렉토리 반환: file.listFiles() Files에서 사용하기 Path path = Path.of("temp/.."); 상대 경로: path 절대 경로: path.toAbsolutePath() 정규 경로: path.toRealPath() 현재 경로에 있는 모든 파일 및 디렉토리 반환: Files.list(path) 문자 파일 읽기 (Files) FileReader, FileWriter 스트림 클래스의 기능을 단순한 코드로 대체 가능 메서드 Files.writeString() 파일에 쓰기 e.g. Files.writeString("temp/hello.txt", "abc", UTF_8); Files.readString() 파일에서 모든 문자 읽기 e.g. Files.readString("temp/hello.txt", UTF_8); Files.readAllLines(path) 파일을 한 번에 다 읽고, 라인 단위로 List 에 나누어 저장하고 반환 e.g. Files.readAllLines("temp/hello.txt", UTF_8); Files.lines(path) 파일을 한 줄 단위로 나누어 읽음 (메모리 사용량 최적화 가능) e.g. 1000MB 파일이라면, 1MB 한 줄 불러와 처리하고 다음 줄 호출 후 기존 1MB 데이터를 GC try(Stream<String> lineStream = Files.lines(path, UTF_8)){ lineStream.forEach(line -> System.out.println(line)); } 파일 복사 최적화 (Files.copy()) Path source = Path.of("temp/copy.dat"); Path target = Path.of("temp/copy_new.dat"); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); 자바에 파일 데이터를 불러오지 않고, 운영체제의 파일 복사 기능 사용해 가장 빠름 파일 스트림 사용: 파일(copy.dat) -> 자바(byte) -> 파일(copy_new.dat) Files.copy(): 파일(copy.dat) -> 파일(copy_new.dat) - 한 단계 생략 네트워크 프로그래밍 - 소켓 (Socket) 조각 개념 localhost 현재 사용 중인 컴퓨터 자체를 가리키는 특별한 호스트 이름 루프백 주소라 지칭하는 127.0.0.1 이라는 IP로 매핑됨 127.0.0.1은 컴퓨터가 네트워크 패킷을 네트워크 인터페이스를 통해 외부로 나가지 않고, 자신에게 직접 보낼 수 있도록 함 DNS 탐색 과정 TCP/IP 통신에서는 통신 대상 서버를 찾을 때, 호스트 이름이 아니라 IP 주소가 필요 호스트 이름이 주어졌을 경우, IP 주소를 자동으로 찾음 과정 (InetAddress) 자바는 InetAddress.getByName("호스트명") 메서드 사용 이 과정에서 시스템의 호스트 파일을 먼저 확인 /etc/hosts (리눅스, mac) C:\Windows\System32\drivers\etc\hosts (윈도우,Windows) 호스트 파일에 정의되어 있지 않다면, DNS 서버에 요청해서 IP 주소를 얻음 호스트 파일 예시 127.0.0.1 localhost 255.255.255.255 broadcasthost ::1 localhost Socket 클래스 클라이언트와 서버의 연결에 사용하는 클래스 (TCP 연결, 소켓 객체로 서버와 통신) Socket socket = new Socket("localhost", PORT) InetAddress로 IP 찾기 해당 IP와 포트로 TCP 연결 시도 (성공하면 Socket 객체 반환) 클라이언트와 서버 간의 데이터 통신은 Socket이 제공하는 스트림 사용 DataInputStream input = new DataInputStream(socket.getInputStream()); DataOutputStream output = new DataOutputStream(socket.getOutputStream()); 서버는 서버 소켓(ServerSocket)을 사용해 포트를 열어두어야 함 (TCP 연결) ServerSocket serverSocket = new ServerSocket(PORT); TCP 연결만 지원하는 특별한 소켓 포트를 지정해 서버 소켓을 생성하면, 클라이언트가 포트를 지정해 접속 가능 Socket socket = serverSocket.accept(); 실제 데이터를 주고 받기 위한 Socket 객체 반환 클라이언트의 TCP 연결이 있으면 반환 없으면 연결 정보가 도착할 때까지 대기 (블로킹) 서버는 소켓(Socket) 객체 없이 서버 소켓(ServerSocket)만으로도 TCP 연결이 완료됨 연결 이후에 메시지를 주고 받으려면 Socket 객체 필요 참고: 연결 정보가 있는데 accept() 호출이 없어 서버에는 Socket 객체가 없을 때 클라이언트가 데이터를 보내면 OS TCP 수신 버퍼에서 대기 클라이언트와 서버의 연결 과정 서버가 12345 포트로 서버 소켓을 열어둠 (클라이언트는 이제 12345 포트로 서버 접속 가능) 클라이언트가 12345 포트에 연결 시도 클라이언트 자신의 포트는 보통 생략하고, 이 경우 남아있는 포트 중에 랜덤 할당됨 OS 계층에서 TCP 3 way handshake 발생하고 TCP 연결 완료 서버는 OS backlog queue에 TCP 연결 정보 보관 (자바가 아닌 OS 수준) 연결 정보에는 클라이언트의 IP 및 PORT, 서버의 IP 및 PORT가 모두 있음 서버가 serverSocket.accept()를 호출하면, backlog queue에서 TCP 연결 정보 조회 만약 연결 정보가 없다면, 연결 정보가 생성될 때까지 대기 (블로킹) 해당 정보를 기반으로 Socket 객체 생성 사용한 TCP 연결 정보는 backlog queue에서 제거 여러 클라이언트 접속을 위한 멀티스레드 (Session) 서버 및 네트워크의 기본 베이스이자 거의 다라고 봐도 무방 핵심: 2개의 블로킹의 작업을 해결하기 위해 별도의 스레드를 사용하자 (역할의 분리) main 스레드 작업: accept() (클라이언트와 서버의 연결을 위해 대기) 새로운 연결이 있을 때마다 Session 객체와 별도 스레드 생성 및 실행하는 역할 Session 담당 스레드 작업: readXxx() (클라이언트의 메시지를 받아 처리하기 위해 대기) 자신의 소켓이 연결된 클라이언트와 메시지를 반복해서 주고 받는 역할 한 세션이 하나의 클라이언트 담당 과정 main 스레드는 서버 소켓을 생성하고, 서버 소켓의 accept()를 반복 호출 클라이언트가 서버에 접속하면, accept()가 Socket을 반환 main 스레드는 이 정보를 기반으로 Runnable을 구현한 Session 객체를 만들고, 새 스레드에서 Session 객체를 실행 Session 객체와 Thread-0는 연결된 클라이언트와 메시지를 주고 받음 네트워크 프로그래밍 - 자원 정리 자원 정리 예외 처리 기본 자원 정리 시 try~catch~finally 구문의 문제 2가지 핵심 문제 close() 시점에 실수로 예외를 던지면, 이후 다른 자원을 닫을 수 없는 문제 발생 finally 블럭 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally 에서 발생한 부가 예외로 바뀌어 버림 (핵심 예외가 사라짐) 해결책 1: try~catch~finally + finally 내 자원 정리 코드 try~catch 2가지 핵심 문제 해결 자원 정리에서 발생한 예외는 로그만 남기고 넘어감 4가지 부가 문제 잔존 resource 변수를 선언하면서 동시에 할당할 수 없음( try , finally 코드 블록과 변수 스코프가 다른 문제) catch 이후에 finally 호출, 자원 정리가 조금 늦어짐 개발자가 실수로 close() 를 호출하지 않을 가능성 개발자의 close() 호출순서 실수 (보통 자원을 생성한 순서와 반대로 닫아야 함) 해결책 2: Try with resources 2가지 핵심 문제 + 4가지 부가 문제 모두 해결 리소스 누수 방지: 모든 리소스가 제대로 닫히도록 보장 finally 블록 누락이나 finally 내 자원 해제 코드 누락 문제 예방 코드 간결성 및 가독성 향상 명시적인 close() 호출이 필요 없음 스코프 범위 한정: 코드 유지보수 향상 리소스 변수의 스코프가 try 블럭으로 한정 조금 더 빠른 자원 해제: try 블럭이 끝나면 즉시 close() 를 호출 기존에는 try~catch~finally에서 catch 이후에 자원을 반납 자원 정리 순서: 먼저 선언한 자원을 나중에 정리 핵심 예외 반환 및 부가 예외 포함: try-with-resources 는 핵심 예외를 반환 부가 예외는 핵심 예외안에 Suppressed 로 담아서 반환 개발자는 자원 정리 중 발생한 부가 예외를 e.getSuppressed() 로 활용 e.addSuppressed(ex) : 예외 안에 참고할 예외를 담음 네트워크 클라이언트와 서버에서의 자원 정리 문제: 클라이언트 프로세스 종료 시, OS 단에서 TCP 연결 종료 및 정리 발생 TCP 연결 종료로 인해 서버도 예외가 발생하는데, 이 때 자원 정리 없이 종료되면 문제 서버는 프로세스가 계속 살아 실행되어야 하므로, 외부 자원은 즉각 정리되어야 함 클라이언트는 종료 후 다시 실행해도 되고, 컴퓨터를 자주 재부팅해도 돼서 괜찮음 해결 전략 자원의 사용과 해제를 함께 묶어 처리하는 경우 -> Try with resources Try with resources 적용이 어려운 경우 (자원 해제가 여러 곳에서 진행되는 경우) -> try~catch~finally + finally 내 자원 정리 코드 try~catch e.g. 세션 자원 정리는 클라이언트 종료 시점, 서버 종료 시점 모두 이뤄져야 함 서버의 안정적인 종료 처리 (feat. 셧다운 훅) 서버는 종료할 때 사용하는 세션들도 함께 종료해야 함 필요 작업 모든 세션이 사용하는 자원 닫기 (Socket, InputStream, OutputStream) 서버 소켓(ServerSocket) 닫기 셧다운 훅(Shutdown Hook) 자바는 프로세스 종료 시, 자원 정리나 로그 기록 같은 종료 작업을 마무리하는 기능 제공 shutdown 스레드가 개발자가 만든 shutdownHook 실행 e.g. 서버 종료 시, shutdown 스레드가 모든 세션의 자원을 닫고 서버 소켓 닫음 정상 종료는 셧다운 훅 작동 but, 강제 종료는 셧다운 훅 작동 X 정상 종료 모든 non 데몬 스레드의 실행 완료로 자바 프로세스 정상 종료 사용자가 Ctrl+C를 눌러서 프로그램을 중단 kill 명령 전달 (kill -9 제외) IntelliJ의 stop 버튼 강제 종료 운영체제에서 프로세스를 더 이상 유지할 수 없다고 판단할 때 사용 리눅스/유닉스의 kill -9 나 Windows의 taskkill /F 서버 적용 과정 세션에 자원 정리 기능 추가 클라이언트 연결 종료 및 서버 종료 2곳에서 사용 예정 예외처리: try~catch~finally + finally 내 자원 정리 코드 try~catch 동시성 처리를 적용한 세션 매니저 개발 (SessionManager) 세션 매니저: 생성한 세션을 보관하고 관리하는 객체 동시성 처리 이유: 2곳에서 호출될 수 있음 클라이언트와 연결이 종료됐을 때 서버를 종료할 때 ShutdownHook 클래스를 Runnable을 구현해 개발 주요 코드 sessionManager.closeAll(); serverSocket.close(); 자바 종료시 호출되는 셧다운 훅을 등록 ShutdownHook shutdownHook = new ShutdownHook(serverSocket, sessionManager); Runtime.getRuntime().addShutdownHook(new Thread(shutdownHook, "shutdown")); 타임아웃(Timeout) 핵심: 외부 서버와 통신하는 경우, 반드시 연결 타임아웃과 소켓 타임아웃을 지정하자 타임아웃 서버에서 응답이 없을 때 제한 시간을 설정하는 것 (타임아웃 시간이 지나면 예외 발생) 종류 TCP 연결 타임아웃 네트워크 연결(TCP 연결) 시도 시, 서버에서 응답이 없을 때 제한 시간을 설정 연결이 안되면 고객에게 빠르게 현재 연결에 문제가 있다고 알려주는 것이 더 나은 방법 설정 방법 기본 설정: OS 연결 대기 타임아웃 (서비스 관점에서 너무 김) Windows: 약 21초 Linux: 약 75초에서 180초 사이 예외: java.net.ConnectException: Operation timed out 직접 설정 Socket socket = new Socket(); Socket 객체는 생성 시 IP, PORT를 전달하면 생성자에서 TCP 연결 IP, PORT를 빼고 생성하면, 추가 설정을 한 다음 TCP 연결 시도 가능 socket.connect(new InetSocketAddress("192.168.1.250", 45678), 1000); 타임아웃 설정 후 TCP 연결 시도 예외: java.net.SocketTimeoutException: Connect timed out Read 타임아웃 (소켓 타임아웃) 연결(TCP 연결)이 잘 된 이후, 클라이언트 요청에 서버 응답이 없을 때 제한 시간 설정 서버에 사용자가 폭주해 느려지는 상황 등 설정 방법 Socket socket = new Socket("localhost", 12345); socket.setSoTimeout(3000); 예외: java.net.SocketTimeoutException: Read timed out TCP 연결 종료 핵심: 기본적으로 정상 종료, 강제 종료 모두 자원 정리하고 닫도록 설계 IOException 발생 시 자원을 정리 (네트워크 예외가 많아서 부모 예외로 한 번에 처리) -1, null, EOFException, SocketException 등을 한 번에 처리 정상 종료 TCP 연결 종료 규칙: 서로 FIN 메시지를 보내야 함 (4-way-handshake) socket.close() 호출 시, FIN 패킷을 상대방에게 전달 FIN 패킷을 받은 상대도 항상 socket.close()를 호출해야 함 (지켜야하는 규칙) 흐름 서버가 클라이언트에게 FIN 패킷 보냄 (socket.close()) 패킷을 받으면 클라이언트의 OS에서 FIN에 대한 ACK 패킷 전달 (자동) 클라이언트도 서버에게 FIN 패킷 보냄 (socket.close()) 패킷을 받으면 서버의 OS에서 FIN에 대한 ACK 패킷 전달 (자동) 강제 종료 TCP 연결 중에 문제가 발생하면 RST 패킷이 발생 처음 연결이 거부 당할 때 연결 후 통신 중에 상대가 연결을 끊었을 때 방화벽 같은 곳에서 연결을 강제로 종료할 때 … RST(Reset) 패킷 TCP 연결에 문제가 있다는 뜻 연결 상태를 초기화(리셋)해서 더 이상 현재의 연결을 유지하지 않겠다는 의미 “현재의 세션을 강제로 종료하고, 연결을 무효화하라” 이 패킷을 받은 대상은 바로 연결을 해제해야 함 흐름 서버가 클라이언트에게 FIN 패킷 보냄 (socket.close()) 패킷을 받으면 클라이언트의 OS에서 FIN에 대한 ACK 패킷 전달 (자동) 클라이언트가 종료하지 않고, output.write(1) 를 통해 서버에 메시지를 전달 데이터를 전송하는 PUSH 패킷을 서버에 전달 서버는 기대값인 FIN 패킷이 오지 않아, RST 패킷 전송 (TCP 연결에 문제가 있다 판단) RST 패킷을 받은 클라이언트가 다음 행동을 하면 예외 발생 클라이언트가 read() 시, java.net.SocketException: Connection reset 발생 클라이언트가 write() 시, java.net.SocketException: Broken pipe 발생 주요 네트워크 예외 정리 RST 패킷 예외 java.net.ConnectException: Connection refused 클라이언트가 해당 IP의 서버에 접속은 했으나 연결이 거절됨 서버는 OS 단에서 RST 패킷을 보냄 클라이언트는 연결 시도 중 RST 패킷을 받고 해당 예외를 발생시킴 다음 경우들에서 발생 해당 IP의 서버는 켜져 있지만, 포트가 없을 때 주로 발생 네트워크 방화벽 등에서 무단 연결로 인지하고 연결을 막을 때 java.net.SocketException: Connection reset RST 패킷을 받은 클라이언트가 연결을 바로 종료하지 않고 read() 시 발생 java.net.SocketException: Broken pipe RST 패킷을 받은 클라이언트가 연결을 바로 종료하지 않고 write() 시 발생 java.net.SocketException: Socket is closed 자기 자신의 소켓을 닫은 이후에 read(), write()를 호출할 때 발생 연결 타임아웃 예외: 네트워크 연결을 하기 위해 서버 IP에 연결 패킷을 전달했지만 응답이 없는 경우 java.net.ConnectException: Operation timed out OS 기본 설정에 의한 예외 java.net.SocketTimeoutException: Connect timed out 직접 설정 시 발생하는 예외 다음 경우들에서 발생 IP를 사용하는 서버가 없어서 응답이 없는 경우 해당 서버가 너무 바쁘거나 문제가 있어서 연결 응답 패킷을 보내지 못하는 경우 Read 타임아웃 예외 java.net.SocketTimeoutException: Read timed out 연결이 된 이후, 클라이언트 요청에 서버 응답이 없는 경우 서버에 사용자가 폭주해 느려지는 상황 등 java.net.BindException: Address already in use 지정한 포트를 다른 프로세스가 이미 사용하고 있을 때 발생 해당 프로세스를 종료하면 해결 java.net.UnknownHostException 호스트를 알 수 없음 (존재하지 않는 IP, 도메인 이름) e.g. Socket socket = new Socket("999.999.999.999", 80); e.g. Socket socket = new Socket("google.gogo", 80); 커맨드 패턴 & Null Object 패턴 public class CommandManagerImpl implements CommandManager { public static final String DELIMITER = "\\|"; private final Map<String, Command> commands; private final Command defaultCommand = new DefaultCommand(); public CommandManagerV4(SessionManager sessionManager) { commands = new HashMap<>(); commands.put("/join", new JoinCommand(sessionManager)); commands.put("/message", new MessageCommand(sessionManager)); commands.put("/change", new ChangeCommand(sessionManager)); commands.put("/users", new UsersCommand(sessionManager)); commands.put("/exit", new ExitCommand()); } @Override public void execute(String totalMessage, Session session) throws IOException { String[] args = totalMessage.split(DELIMITER); String key = args[0]; // NullObject Pattern Command command = commands.getOrDefault(key, defaultCommand); command.execute(args, session); } } 불필요한 조건문이 많다면 유용한 디자인 패턴 적용 전략 기능이 어느정도 있는데 향후 확장까지 고려해야 한다면 커맨드 패턴을 도입하자 단순한 if 문 몇 개로 해결된다면, 도입 X (굳이 복잡성을 높이지 말자) Command Pattern 요청을 독립적인 객체로 변환해서 처리하는 방법 장점 분리: 작업을 호출하는 객체와 작업을 수행하는 객체가 분리되어 있어 명확 확장성: 기존 코드 변경 없이 새로운 명령 추가 가능 단점 복잡성 증가: 간단한 작업이어도 여러 클래스를 생성해야 함 Null Object Pattern null인 상황을 객체(Object)로 만들어 처리하는 방법 (객체의 기본 동작을 정의) null 체크를 없애 코드의 간결성을 높임 캐리지 리턴(\r) & 라인 피드(\n) 캐리지 리턴은 옛 타자기의 동작을 표현한 것이다. (커서를 맨 앞으로) 윈도우는 엔터를 표현할 때, 캐리지 리턴 + 라인 피드(\r\n) 로 채택했다. 맥, 리눅스는 엔터를 표현할 때, 라인 피드(\n) 만으로 표현했다. HTTP 공식 스펙에서는 다음 라인을 \r\n로 표현하나 \n만 사용해도 대부분의 웹 브라우저는 문제없이 작동한다. HTTP 서버 HTTP 서버와 서비스 개발을 위한 로직은 명확하게 분리 가능 HTTP 서버는 재사용 가능 개발자는 새로운 HTTP 서비스에 필요한 서블릿만 구현 WAS (Web Application Server) 웹(HTTP)를 기반으로 작동하면서 프로그램의 코드도 실행할 수 있는 서버 웹 서버 역할 + 애플리케이션 프로그램 코드 수행 웹 서버 역할 = 복잡한 네트워크, 멀티스레드, HTTP 메시지 파싱 등을 모두 해결 프로그램 코드 = 서블릿 구현체들 자바 진영에서는 보통 서블릿 기능을 포함하는 서버를 의미 서블릿 (Servlet, 1990년대) public interface Servlet { void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; ... } HTTP 서버에서 실행되는 작은 자바 프로그램 (Server + Applet) WAS 개발에 대한 자바 진영의 표준 많은 회사가 WAS를 개발하는데, 각각의 서버 간 호환성이 전혀 없어서 등장 A사 HTTP 서버를 사용하다 느려서 B사로 바꾸려면, 인터페이스가 달라 수정이 많음 HTTP 서버를 만드는 회사들은 모두 서블릿을 기반으로 기능 제공 Apache Tomcat, Jetty, Undertow, IBM WebSphere… 장점 표준화 덕에 개발자는 jakarta.servlet.Servlet 인터페이스만 구현하면 됨 WAS를 변경해도 구현했던 서블릿을 그대로 사용 가능 참고: URL 인코딩 HTTP 메시지 시작 라인과 헤더의 이름은 항상 ASCII를 사용해야 한다 초기 인터넷 설계 시기에는 ASCII를 사용했음 HTTP 스펙은 보수적으로 호환성을 가장 중요시함 (많은 레거시 시스템과의 호환) URL에 ASCII로 표현할 수 없는 문자가 있다면, 퍼센트 인코딩해 ASCII로 표현 퍼센트(%)인코딩 UTF-8 16진수로 표현한 각각의 바이트 문자 앞에 %(퍼센트)를 붙이는 인코딩 e.g. ‘가’ -> UTF-8 16 진수로 표현 -> [EA, B0, 80] (3byte) -> 퍼센트 삽입 -> %EA%B0%80 서블릿에서 URL 파싱할 때도 적용됨 String encode = URLEncoder.encode("가", UTF_8) //%EA%B0%80 String decode = URLDecoder.decode(encode, UTF_8) //가 데이터 크기로는 비효율적이지만 URL, 헤더 정도는 호환성을 위해 감당 가능 큰 용량은 메시지 바디에서 UTF-8로 처리 가능 웹 애플리케이션 서버 제작 과정 멀티스레드 적용 main 스레드는 소켓 연결만 담당 클라이언트와 요청 처리 작업은 ExecutorService 스레드 풀에 전달 HttpRequest, HttpResponse 객체 적용 HTTP 메시지 파싱 및 생성 역할을 담당 퍼센트 인코딩도 처리 커맨드 패턴 서블릿 if문으로 URL을 처리하고 스태틱 메서드로 서비스 로직을 처리하던 것을 리팩토링 URL : 서블릿 구현체 쌍으로 Map<String, HttpServlet> servletMap 관리 HTTP 서버와 서비스 개발을 위한 로직이 명확하게 분리 분리 예시 HTTP 서버와 관련된 부분 HttpServer, HttpRequestHandler, HttpRequest, HttpResponse HttpServlet, HttpServletManager 공용 서블릿 InternalErrorServlet, NotFoundServlet, DiscardServlet 서비스 개발을 위한 로직 HomeServlet Site1Servlet, Site2Servlet, SearchServlet HTTP 서버는 재사용 가능 서블릿에는 요청을 처리하는 서비스 로직만 구현 Request, Response 객체는 HTTP 메시지 파싱 및 생성 담당하고 서블릿에게 전달 문제점 기능마다 서블릿 클래스가 너무 많아짐 새로 만든 클래스를 URL 경로와 항상 매핑해야 하는 불편함 메타 프로그래밍(리플렉션, 애노테이션)을 통한 극대화 - 보일러플레이트 코드 크게 감소 리플렉션 서블릿 서비스 로직은 새로운 컨트롤러 클래스들에 메서드 단위로 위치하도록 리팩토링 URL과 메서드 이름을 동일하게 함 리플렉션 서블릿 하나를 구현해 기본 서블릿으로 사용 요청이 오면 모든 컨트롤러를 순회 요청 URL 경로와 같은 이름의 컨트롤러 메서드를 리플렉션으로 읽고 호출 method.invoke(controller, request, response); 존재하는 서블릿 ReflectionServlet, HomeServlet, DiscardServlet… 장점 하나의 클래스 내에서 메서드로 기능 처리 가능 (관련 기능 별로 클래스 분류) URL 매핑 작업 제거 (URL 경로의 이름과 같은 이름의 메서드를 찾아 호출) 문제점 요청 URL과 메서드 이름을 다르게 할 수 없음 자바 메서드 이름으로 처리가 어려운 URL 존재 /, /favicon.ico, /add-member 애노테이션 서블릿 컨트롤러에 URL 정보가 담긴 애노테이션 추가 (e.g. @Mapping("/")) 기본 서블릿이 리플렉션으로 애노테이션을 읽도록 리팩토링 요청이 오면 모든 컨트롤러를 순회 요청 URL과 애노테이션 속성값이 같은 메서드를 리플렉션으로 읽고 호출 장점 어떤 요청 URL이든 컨트롤러에서 다른 메서드 이름으로 처리 가능 -> 스프링 프레임워크는 스프링 MVC를 통해 이 과정을 더욱 최적화해 기능을 제공 동적 파리미터 바인딩 (HttpServletRequest, HttpServletRequest…) 요청마다 모든 컨트롤러 조회 -> 처음 서블릿 생성 시점에 PathMap 초기화 … Reference 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
Java-Ecosystem
· 2025-02-28
자바 멀티스레드와 동시성
멀티태스킹 & 멀티프로세싱 프로그램 실행 프로그램을 구성하는 코드를 순서대로 CPU(=프로세서)에서 연산하는 일 초창기 컴퓨터 하나의 CPU 코어에서 한 프로그램 코드를 모두 수행 후 다른 프로그램 코드 실행 e.g. 사용자는 음악 프로그램 끝난 후에야 워드 프로그램 실행 가능해 불편 멀티태스킹 (소프트웨어 관점 - 운영체제) 단일 CPU(단일 CPU 코어)가 여러 작업을 동시에 수행하는 것처럼 보이게 하는 것 e.g. 현대 운영체제에서 여러 애플리케이션이 동시에 실행되는 환경 CPU가 매우 빠르게 두 프로그램의 코드를 번갈아 수행한다면, 사람은 동시에 실행되는 것처럼 느낄 것 현대 CPU는 초당 수십억 번 이상의 연산 수행 대략 0.01초(10ms) 동안 한 프로그램을 수십만 번 연산 하나의 CPU 코어 -> 프로그램 A 코드 수행 (약 10ms) -> 프로그램 B 코드 수행 (약 10ms) -> 프로그램 A의 이전 실행 중인 코드부터 다시 수행 (약 10ms) -> … 멀티프로세싱 (하드웨어 관점) 여러 CPU(여러 CPU 코어)를 사용하여 여러 작업을 동시에 수행하는 것 e.g. 멀티코어 프로세서를 사용하는 현대 컴퓨터 시스템 여러 개의 CPU 코어에서 여러 프로그램이 물리적으로 동시에 실행 코어가 2개여도 2개보다 많은 프로그램 실행 가능 하나의 CPU 코어만 사용하는 시스템보다 동시에 더 많은 작업을 처리 e.g. CPU 코어 2개에서 프로그램 A, B, C 처리 CPU 코어 2개에서 물리적으로 동시에 2개의 프로그램 처리 A, B 실행 (약 10ms) B, C 실행 (약 10ms) … 멀티 태스킹과 멀티프로세싱은 함께 일어날 수 있는 개념 CPU 코어 최근의 일반적인 컴퓨터는 하나의 CPU 안에 여러 개의 코어를 가지는 멀티코어 프로세서를 가진다. 코어는 CPU 안의 실제 연산을 처리하는 장치를 말한다. 과거에는 하나의 CPU 안에 하나의 코어만 들어있었다. 프로세스와 스레드 프로세스 운영체제 안에서 실행 중인 프로그램 실행 환경과 자원을 제공하는 컨테이너 역할 자바 언어와 비유하면 클래스는 프로그램(=코드뭉치, 파일), 인스턴스는 프로세스 메모리 구성 각 프로세스는 독립적인 메모리 공간을 가짐 서로의 메모리에 직접 접근 불가 특정 프로세스에 심각한 문제가 발생해도 다른 프로세스에 영향 X (해당 프로세스만 종료) 구성 코드 섹션: 실행할 프로그램의 코드가 저장되는 부분 데이터 섹션: 전역 변수 및 정적 변수가 저장되는 부분 (위 그림의 기타에 포함) 힙: 동적으로 할당되는 메모리 영역 스택: 메서드(함수) 호출 시 생성되는 지역 변수와 반환 주소의 저장 영역 (스레드에 포함) 하나 이상의 스레드를 반드시 포함 스레드 프로세스 내에서 실행되는 작업 단위 CPU를 사용해 코드를 하나하나 실행 메모리 구성 공유 메모리 한 프로세스 내 여러 스레드들은 프로세스가 제공하는 메모리 공간을 공유 e.g. 코드 섹션, 데이터 섹션, 힙, 스택을 프로세스 안 모든 스레드가 공유 개별 스택 각 스레드는 자신의 스택을 가짐 프로세스보다 생성 및 관리가 단순하고 가벼움 멀티스레드가 필요한 이유 하나의 프로그램도 그 안에서 동시에 여러 작업이 필요하다 e.g. 워드 프로그램 - 프로세스A 스레드1: 문서 편집 스레드2: 자동 저장 스레드3: 맞춤법 검사 유튜브 - 프로세스B 스레드1: 영상 재생 스레드2: 댓글 멀티스레드도 단일 코어 스케줄링 & 멀티 코어 스케줄링 모두 발생 가능 프로그램 실행 프로그램을 실행하면 운영체제는 먼저 디스크에 있는 파일 덩어리인 프로그램을 메모리로 불러와 프로세스를 만든다. 프로그램이 실행된다는 것은 사실 프로세스 안에 있는 코드가 한 줄씩 실행되는 것이다. 코드는 보통 main()부터 시작해서 스레드가 하나씩 순서대로 내려가면서 실행한다. 한 프로세스 안에는 최소 하나의 스레드가 존재한다. 그래야 프로그램이 실행될 수 있다. CPU 스케줄링 운영체제가 CPU에 어떤 프로그램을 얼마만큼 실행할지 결정하는 것 CPU를 최대한 활용할 수 있는 다양한 우선순위와 최적화 기법 사용 e.g. 시분할 기법 (Time Sharing, 시간 공유) 각 프로그램의 실행 시간을 분할해서 마치 동시에 실행되는 것처럼 하는 기법 운영체제는 내부에 스케줄링 큐를 가지고, 각각의 스레드는 스케줄링 큐에서 대기 스레드들이 운영체제한테 내가 실행되어야 한다고 알리면 운영체제는 해당 스레드들을 큐에 넣음 운영체제는 큐에서 대기중인 스레드를 하나씩 꺼내 CPU를 통해 실행 스레드는 CPU 연산을 통해 프로그램 코드를 수행 운영체제는 10ms 정도 후 작업 중인 스레드를 잠시 멈추고 다시 스케줄링 큐에 넣음 스케줄링 큐에서 다음 스레드를 꺼내 CPU를 통해 실행 반복… 단일 스레드: 한 프로세스 내에 하나의 스레드만 존재 멀티 스레드: 한 프로세스 내에 여러 스레드가 존재 컨텍스트 스위칭 (Context Switching) CPU는 컴퓨터에 있는 여러 Process, 여러 Thread 들을 돌아가면서 실행함 컨텍스트 (Context) Process나 Thread가 중단 됐다가 다시 실행될 때 필요한 정보 컨텍스트 스위칭 (Context Switching) 현재 실행 중인 Context를 잠시 중단 및 저장하고 새로운 Context를 로딩 및 실행하는 것 멈춰지는 스레드는 수행 위치와 CPU에서 사용하던 변수 값들을 메모리에 저장 실행하는 스레드는 수행 위치와 CPU에서 사용하던 변수 값들을 메모리에서 CPU로 불러옴 컨텍스트 스위칭 발생 시 CPU Cache가 초기화됨 다른 코드 수행을 위해 Cache를 비우고 새로 메모리를 읽어 Caching함 값을 저장하고 불러오는 과정은 약간의 비용을 발생시킴 실제로 컨텍스트 스위칭 시간은 짧지만, 스레드가 매우 많다면 비용이 커질 수 있음 유력한 발생 시점 Sleep Lock I/O 작업 (Network I/O, File I/O, Console 출력) 시스템 API 호출 혹은 큰 단위 계산을 할 때 컨텍스트 스위칭 발생 가능성 높음 멀티스레드는 대부분 효율적이지만, 컨텍스트 스위칭 과정이 필요하므로 항상 효율적이진 않음 90% 경우는 효율적, 1~3% 경우는 비효율적 예시 CPU 코어가 2개이고 스레드 2개 만들어 연산 2배 빠르게 처리 가능 (효율적) CPU 코어가 1개인데 스레드 2개 만들어 연산 연산 시간 + 컨텍스트 스위칭 시간 (비효율적) 단일 스레드로 연산하는 것이 오히려 효율적 스레드 숫자 최적화 전략 CPU 개수와 스레드 개수 CPU 4개, 스레드 2개 CPU 100% 활용 X, 컨텍스트 스위칭 비용은 감소 컨텍스트 스위칭이 일어날 수는 있지만 거의 없을 것 CPU 4개, 스레드 100개 CPU 100% 활용 O, 컨텍스트 스위칭 비용 증가 CPU 4개, 스레드 4개 최적 상태 (CPU 100% 활용 O, 컨텍스트 스위칭 비용 거의 X) 스레드 개수로 CPU 코어 개수 + 1개가 이상적 (특정 스레드 대기 시 남은 스레드 활용 가능) 스레드 작업 유형 CPU 바운드 작업 CPU 연산 능력을 많이 요구하는 작업 e.g. 복잡한 수학 연산, 데이터 분석, 비디오 인코딩, 과학적 시뮬레이션… I/O 바운드 작업 입출력(I/O) 작업을 많이 요구하는 작업 (대기 시간으로 인해 CPU 유휴 상태 빈번) e.g. DB 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리… 실무 전략 스레드 숫자는 작업 유형에 따라 다르게 설정해야 한다! CPU 바운드 작업: CPU 코어 수 + 1개 CPU를 거의 100% 사용하는 작업이므로 스레드를 CPU 숫자에 최적화 I/O 바운드 작업: CPU 코어 수 보다 많은 스레드 생성 성능 테스트 통해 CPU를 최대한 활용하는 최적의 스레드 개수 찾을 것! 너무 많은 스레드는 컨텍스트 스위칭 비용 증가 웹 애플리케이션 서버 실무는 I/O 바운드 작업이 많음 -> CPU 코어 수 보다 많은 스레드 생성할 것! 사용자 요청 1개 처리 -> 스레드 1개 필요 (CPU 1%) I/O 작업(DB 쿼리 대기 등)을 생각하면 스레드는 CPU를 거의 사용하지 않고 대기 이 경우 CPU 코어가 4개 있다고 해서 스레드도 4개만 만들면 안됨 동시에 4명의 사용자 요청만 처리 -> CPU 4% 사용 -> CPU가 심하게 놀고 있음! 단순 생각해도 100개 스레드 생성 가능 (CPU 100%) 스레드 개수만 늘리면 되는데, 서버 장비를 늘리는 비효율적인 사태가 벌어지기도… 웹 애플리케이션 서버도 상황에 따라 CPU 바운드 작업이 많을 수 있음 이 때는 CPU 코어 수 + 1개 고려 스레드 생성 및 실행 스레드 생성과 메모리 자바는 실행 시점에 main이라는 이름의 스레드를 만들고, 프로그램의 시작점인 main() 메서드 실행 새로운 스레드를 생성 및 시작하면 자바는 스레드를 위한 실행 스택을 할당 start() 메서드 새로운 스레드를 실행 main 스레드는 다른 스레드에게 일을 시작하라고 지시만 하고 바로 빠져나옴 스레드에 이름을 주지 않으면 임의의 이름 부여 (Thread-0, Thread-1…) 메서드를 실행하면 스택 위에 스택 프레임이 쌓임 main 스레드는 main() 메서드 스택 프레임 올리며 시작 새로 만든 스레드는 run() 메서드 스택 프레임 올리며 시작 유의점 반드시 run() 메서드가 아닌 start() 메서드 호출해야 함 start() 호출 O -> 실행 스택 생성되고 별도의 스레드로 작동 start() 호출 X -> run() 호출은 단순 함수 실행, 생성한 스레드도 단순한 객체일 뿐 일반적인 메서드 호출 (main 스레드의 실행 스택 위에서 실행) 스레드 간 실행 순서를 보장하지 않음 -> 이것이 멀티스레드! 스레드는 동시에 실행되므로 스레드 간 실행 순서는 얼마든지 달라질 수 있음 CPU 코어가 2개여서 물리적으로 정말 동시에 실행될 수도 있고 하나의 CPU 코어에 시간을 나누어 실행할 수도 있음 생성 방법 Runnable 인터페이스 구현 (권장) 정의 public class HelloRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + ": run()"); } } 실행 Thread thread = new Thread(new HelloRunnable()); thread.start() 더 유연하고 유지보수하기 좋은 방식 상속이 자유로움 (Thread 상속 방식은 다른 상속이 불가능) 스레드와 작업 코드가 서로 분리되어 가독성 상승 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리가 효율적 Thread 클래스 상속 정의 public class HelloThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + ": run()"); } } 실행 HelloThread thread = new HelloThread(); thread.start() 자바는 스레드도 객체로 다룸 스레드가 실행할 코드를 run() 메서드에 재정의 Thread 주요 메서드 Thread.currentThread(): 해당 코드를 실행하는 스레드 객체 조회 가능 Thread.currentThread().getName(): 실행 중인 스레드의 이름을 조회 Runnable 인터페이스와 체크 예외 자식 클래스가 부모보다 더 넓은 범위의 예외를 던지면, 일관성을 해치고 예상치 못한 런타임 오류를 초래할 수 있다. 따라서, 자바에서는 메서드 재정의 시 다음과 같은 예외 관련 규칙을 적용한다. 부모 메서드가 체크 예외를 던지지 않는 경우, 자식 재정의 메서드도 던질 수 없다. 자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다. 언체크 예외는 강제하지 않으므로 상관없이 던질 수 있다. Runnable 인터페이스의 run() 메서드는 어떤 예외도 던지지 않기 때문에, 개발자는 run() 메서드 재정의시 반드시 try-catch 블록 내에서 체크 예외를 처리해야 한다. 예를 들어, 유틸리티 메서드를 하나 만들어, 내부에서 try-catch로 체크 예외를 잡고 언체크 예외로 변경해 재발생시키는 방법도 있다. this와 스레드 this는 호출된 인스턴스 메서드가 소속된 객체를 가리키는 참조이며, 스택 프레임 내부에 저장된다. 메서드를 호출하는 것은 정확히는 특정 스레드가 어떤 메서드를 호출하는 것이다. 스레드는 메서드 호출을 관리하기 위해 메서드 단위로 스택 프레임을 만든다. 이 때 인스턴스 메서드를 호출하면 어떤 인스턴스 메서드를 호출했는지 기억하기 위해 해당 인스턴스의 참조값을 스택 프레임 내부에 저장해두는데, 이것이 this다. 따라서, 특정 메서드 안에서 this를 호출하면 스택프레임 내의 this 값을 불러서 사용하고 필드 접근시 this를 생략하면 자동으로 this를 참고해 필드에 접근한다. 참고로 인스턴스 메서드는 this가 있지만 클래스 메서드는 this가 없다. 데몬 스레드 (Daemon Thread) 스레드는 2가지 종류로 구분 사용자 스레드 프로그램의 주요 작업 수행 모든 사용자 스레드가 종료되면 JVM도 종료 Main 뿐만 아니라 다른 사용자 스레드까지 모두 종료되어야 자바 종료 (중간에 작업 끊김 X) 데몬 스레드 백그라운드에서 보조적인 작업 수행 모든 사용자 스레드가 종료되면 JVM이 종료되고 데몬 스레드도 자동 종료 (작업 끊김) 데몬 스레드 실행 방법 thread.setDaemon(true) // 데몬 스레드로 설정 (기본값은 false, user 스레드가 기본) thread.start() // 데몬 스레드 여부는 start() 실행 이후에는 변경되지 않음 데몬 컴퓨터 과학에서는 사용자에게 보이지 않으면서 시스템의 백그라운드에서 작업을 수행하는 것을 데몬 스레드, 데몬 프로세스라고 한다. 예를 들어, 사용하지 않는 파일이나 메모리를 정리하는 작업들이 있다. 스레드 기본 정보 스레드 이름 부여 Thread myThread = new Thread(new HelloRunnable(), "myThread"); 스레드 이름이 “myThread” 디버깅, 로깅 목적으로 유용 Thread 클래스 메서드 threadId(): 스레드 고유 식별자 반환 (JVM 내 각 스레드에 대해 유일) getName(): 스레드 이름 반환 (스레드 이름은 중복 가능) getPriority(): 스레드의 우선순위 반환 (1: 가장 낮음 ~ 10: 가장 높음, 기본값: 5) setPriority()로 변경 가능하지만, 실제 실행 순서는 운영체제에 달려있음 getThreadGroup(): 스레드가 속한 그룹을 반환 getState(): 스레드의 현재 상태를 반환 (Thread.State 열거형에 정의된 상수) NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED 부모 스레드 새로운 스레드를 생성하는 스레드를 의미한다. 스레드는 기본적으로 다른 스레드에 의해 생성된다. (main 스레드는 제외) 스레드 그룹 직접적으로 잘 사용하지 않는다. 스레드를 그룹화하여 관리할 수 있는 기능을 제공한다. 스레드 그룹에는 특정 작업을 일괄적으로 적용할 수 있다. (e.g. 일괄 종료, 우선순위 설정) 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속한다. main 스레드는 기본으로 제공되는 main 스레드 그룹에 속한다. 스레드의 생명 주기 NEW 스레드가 생성되었으나 아직 시작되지 않은 상태 Thread 객체는 생성되었지만 start() 메서드가 호출되지 않음 RUNNABLE 스레드가 실행 중이거나 실행될 준비가 된 상태 (=CPU에서 실행될 수 있음) start() 메서드 호출 후 상태 RUNNABLE 상태의 모든 스레드가 동시 실행되지는 않음 (운영체제 스케줄러가 CPU 할당하기 때문) 자바에서는 운영체제 스케줄러에 있든 CPU에서 실제 실행되고 있든 모두 RUNNABLE 상태 (구분 X) TERMINATED 스레드가 실행을 마친 상태 (run() 메서드 정상 종료 혹은 예외 발생 종료) 스레드는 한 번 종료되면 다시 시작할 수 없음 (새로 만들어서 실행해야 함) 일시 중지 상태 BLOCKED 스레드가 동기화 락을 기다리는 상태 synchronized 블록 진입 위해 락 획득 대기할 때 WAITING 스레드가 다른 스레드의 특정 작업 완료를 무기한 기다리는 상태 e.g. wait(), join() 혹은 LockSupport.park() 호출 시 다른 스레드가 notify(), notifyAll() 호출하거나 join()이 완료될 때까지 기다림 TIMED_WAITING 스레드가 다른 스레드의 특정 작업 완료를 일정 시간 동안 기다리는 상태 e.g. sleep(long millis), wait(long timeout), join(long millis) 혹은 LockSupport.parkNanos(ns) 호출 시 주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨우면 이 상태를 벗어남 다른 스레드의 작업 기다리기 - join() 다른 스레드의 작업 완료를 기다려하는 상황에 사용 join(): 무한정 기다릴 때 사용 (WAITING) join(ms): 특정 시간만 기다릴 때 사용 (TIMED_WAITING) 진행 과정 호출 스레드는 WAITING 상태가 됨 대상 스레드가 TERMINATED 상태가 될 때까지 대기 대상 스레드가 TERMINATED 상태가 되면 RUNNABLE 상태가 되어 다음 코드 수행 대상 스레드가 이미 TERMINATED 상태라면 바로 빠져나옴 e.g. 연산을 두 개의 스레드로 나누어 진행하고 완료된 후 결과를 합쳐 사용 thread-1: 1 ~ 50까지 더하기 thread-2: 51 ~ 100까지 더하기 main: 두 스레드의 계산 결과를 받아 합치기 스레드 작업을 중간에 중단하기 다른 스레드의 작업을 중간에 중단하기 인터럽트 (권장) 대기 상태의 스레드(WAITING, TIMED_WAITING…)를 직접 깨워, RUNNABLE 상태로 변경 작업 중단 지시 후, 거의 즉각적으로 인터럽트 발생 인터럽트 상태가 되면 InterruptedException 예외 발생 상태 변화 인터럽트 상태(true) -> InterruptedException -> 인터럽트 상태(false) InterruptedException을 던지는 메서드를 호출하거나 호출 중일 때만 예외가 발생 e.g. Thread.sleep(), join() 일반 코드에서는 예외가 발생하지 않음 관련 메서드 interrupt(): 특정 스레드에 인터럽트 걸기 isInterrupted(): 인터럽트 상태 단순 확인 (인터럽트 상태 변경 X) interrupted(): 인터럽트 상태를 확인 및 상태 변경 스레드가 인터럽트 상태면 true를 반환 및 인터럽트 상태 false로 변경 스레드가 인터럽트 상태가 아니면 false를 반환 (상태 변경 X) 인터럽트 직접 체크 시, interrupted() 사용할 것! (with interrupt()) InterruptedException을 던지는 메서드가 없을 때도 인터럽트 사용 가능 isInterrupted()는 인터럽트 상태가 true로 남겨진 채 유지됨 다른 곳에서도 계속 인터럽트가 발생할 수 있어 위험 방법 예시 Task 주요 코드 (Runnable) while (!Thread.interrupted()) {...} main 스레드가 thread.interrupt() 실행해 work 스레드에 인터럽트 지시 변수 사용하기 방법 예시 Task 주요 코드 (Runnable) volatile boolean runFlag = true; while (runFlag) {...} main 스레드가 runFlag = false;를 실행해 work 스레드에 작업 중단 지시 문제점 work 스레드가 작업 중단 지시에 바로 반응 불가 (반응성이 느림) while 조건문을 읽을 때에서야 인지하므로 루프 내 작업이 길다면, 반응이 더 느려짐 스레드 스스로 작업을 중간에 중단하기 (yield, 양보하기) 현재 스레드가 크게 바쁘지 않다면, 스케줄링 큐에 대기 중인 다른 스레드에게 CPU 실행 기회를 양보 현재 스레드는 다시 스케줄링 큐로 돌아감 (RUNNABLE 상태 유지) CPU 코어 수보다 많은 스레드가 있을 때 의미가 있음 yield는 스케줄러에게 힌트만 줄 뿐, 실행 순서 강제 X 굳이 양보할 필요 없는 상황이면 본인 스레드 계속 실행 (운영체자가 최적화) 구현 예시 while (!Thread.interrupted()) { if (jobQueue.isEmpty()) { Thread.yield(); // 추가 continue; } ... } 최대한 실시간으로 확인 원할 시 yield()가 효율적 만일 좀 더 오래 기다려도 될 것 같아 CPU 사용을 최대한 줄이고 싶다면 sleep()도 괜찮음 참고: sleep()의 단점 복잡한 상태 변화 과정 (RUNNABLE -> TIMED_WAITING -> RUNNABLE) 특정 시간만큼 스레드가 실행되지 않음 (양보할 상황이 아닌데도 휴식) 메모리 가시성 (Memory Visibility) 멀티 스레딩 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제 다른 스레드는 캐시에서Stale Data(오래된 데이터) 읽을 수 있음 CPU와 캐시 메모리 CPU는 처리 성능을 개선하기 위해 캐시 메모리를 사용 (L1, L2, L3 캐시…) 현대 CPU는 코어 단위로 캐시 메모리를 보유 각 스레드가 각자의 캐시 메모리를 바라보고 작업해 서로 값 변경을 감지하지 못함 스레드가 특정 변수 값 사용 시, 점유하는 코어의 캐시 메모리로 값을 불러옴 (From 메인 메모리) 값 변경 시, 캐시 메모리의 값만 변경 (메인 메모리에 즉시 반영 X) 메인 메모리 반영 및 읽기 시점은 알 수 없음! 주로 컨택스트 스위칭이 있을 때 캐시 메모리 함께 갱신 (sleep(), 콘솔 출력…) 그러나 환경마다 다르고 갱신이 일어나지 않을 수도 있음 volatile 성능을 약간 포기하는 대신에, 값 읽기 및 쓰기를 모두 메인 메모리에 직접 접근해 진행 사용 상황 여러 스레드에서 같은 시점에 정확히 같은 데이터를 보는게 중요할 때 사용 캐시 메모리보다 성능이 떨어지므로 꼭 필요한 곳에만 사용! (약 5배 차이, 환경에 따라 다름) Memory Wall 메모리 액세스 속도보다 CPU 처리 속도가 훨씬 빠르기 때문에 발생하는 문제를 말한다. 일반적으로 CPU에 캐시를 두어 속도를 개선한다. (L1, L2, L3) 자바 메모리 모델 (Java Memory Model) 자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지 규정한다. 특히, 멀티스레드 프로그래밍에서 여러 스레드들의 작업 순서를 보장하는 happens-before 관계를 정의한다. 만일 A happens before B 관계가 성립한다면, A 작업의 모든 메모리 변경 사항은 B 작업 시작 전에 메인 메모리에 반영되어 B 작업에서 볼 수 있다. 즉, 다른 스레드 작업의 최신 상태를 참조하는 것(메모리 가시성)을 보장한다. 이 규칙을 따르면, 멀티스레드 프로그래밍 시 예상치 못한 동작을 피할 수 있다. 스레드 시작 및 종료, 인터럽트, 객체 생성 등의 규칙이 있지만 핵심은 volatile 혹은 동기화 기법(synchronized, ReentrantLock)을 사용하면 메모리 가시성 문제가 발생하지 않는다는 점이다. 동시성 문제와 동기화 동시성 문제 멀티 스레드 상황에서 공유 자원에 여러 스레드가 동시에 접근할 때 발생 e.g. 두 개 스레드가 계좌 출금 로직을 실행할 때, 둘 다 검증 로직을 통과해 잔액 없는데도 출금 근본 원인 공유자원에 대한 변경이 있는 연산을 여러 단계로 나누어 사용하는 것 e.g. 출금은 검증 단계와 계산 단계로 나뉘어 있어 원자적이지 않은 연산 여러 단계 없이 원자적인 변경이라면 문제 없음 변경 없이 읽기만 한다면, 이것 역시 문제될 것이 없음 멀티 스레드에서는 공유 자원에 대한 접근을 적절하게 동기화해서 동시성 문제를 예방하는게 중요 동시성 문제가 있다면 메모리 가시성을 해결해도 문제가 지속됨 임계 영역 (Critical Section) 공유 자원 접근 및 수정으로 인해 여러 스레드가 동시에 작업할 때 문제가 생기는 코드 e.g. 임계영역 = 검증 단계 + 계산 단계 동기화 (Synchronization) 공유 자원에 대해 일관성 있고 안전한 접근을 보장하기 위한 메커니즘 임계 영역은 한 번에 하나의 스레드만 접근할 수 있도록 보호해야 함 멀티 스레드 상황에서의 동시성 문제를 해결하기 위해 사용 경합 조건(Race Condition) 해결 두 개 이상의 스레드가 경쟁적으로 동일한 자원을 수정할 때 발생하는 문제 데이터 정합성이 깨짐 데이터 일관성 해결 여러 스레드가 동시에 읽고 쓰는 데이터의 일관성 유지 e.g. 입출금 예제 (1000원 잔액에서 두 개 스레드가 800원 출금 시도) 순차 실행 결과로 -600은 데이터 일관성이 있음 (숫자는 맞으니까) 완전 동시 실행 결과로 200은 데이터 일관성이 깨짐 (아얘 800원 증발) 멀티스레드 환경에서 필수적인 기능이지만, 성능저하 예방을 위해 꼭 필요한 곳에 사용해야 함 동기화 기법 synchronized 모니터 락을 사용해 동기화하는 방법 모니터 락(monitor lock) 모든 객체(인스턴스)가 내부에 가지고 있는 자신만의 락 자바 기본 제공 스레드가 synchronized 메서드에 진입하려면 반드시 모니터 락을 얻어야 함 적용 범위는 인스턴스 단위 한 스레드가 withdraw() 실행 중일 때, 다른 스레드는 withdraw()와 getBalance() 모두 호출 불가 장점 프로그래밍 언어 문법으로 제공 (자바 1.0) 단순하고 편리한 사용 단점 무한정 대기 문제 BLOCKED 상태 스레드는 락 획득까지 무한정 대기 - 타임아웃 or 인터럽트 불가능 e.g. 웹의 경우 요청한 고객의 화면에 계속 요청 중만 뜨고 응답 X 공정성 문제 BLOCKED 상태 스레드들의 락 획득 순서는 보장 X (자바 표준에 정의 X) 최악의 경우 특정 스레드가 너무 오랜기간 락을 획득하지 못할 수 있음 예시 코드 메서드 동기화 public class BankAccountImpl implements BankAccount { private int balance; ... @Override public synchronized boolean withdraw(int amount) { ... } @Override public synchronized int getBalance() { ... } } 클래스 내 모든 메서드에 일일이 키워드 적용하는게 일반적 블록 동기화 (권장) @Override public boolean withdraw(int amount) { log("거래 시작: " + getClass().getSimpleName()); synchronized (this) { ... } log("거래 종료"); return true; } 동기화 구간은 꼭 필요한 코드 블럭만 최소한으로 한정해 설정해야 함 (최적화) 동기화는 여러 스레드가 동시에 실행하지 못하므로 성능이 떨어짐 동시 처리 구간을 늘려서 전체적인 성능을 더 높일 수 있음 괄호 () 안에 들어가는 값은 락을 획득할 인스턴스의 참조 ReentrantLock 자바는 더 유연하고 세밀한 제어를 위한 동시성 문제 해결 라이브러리 패키지 지원 (java.util.concurrent, 자바 1.5) Lock 인터페이스와 ReentrantLock 구현체 지원 (LockSupport 활용) 모니터 락이 아닌 자체적으로 구현한 락 사용 synchronized의 단점 극복 무한 대기 문제 -> LockSupport 이용해 해결 공정성 문제 -> ReentrantLock 공정 모드 옵션으로 해결 비공정 모드 (Non-fair mode) - 디폴트 private final Lock nonFairLock = new ReentrantLock(); 성능 우선: 락을 획득하는 속도가 빠름 선점 가능: 새 스레드가 대기 스레드보다 먼저 락 획득할 수도 있음 기아 현상 가능: 특정 스레드가 계속해서 락 획득 못할 수 있음 비공정 모드도 내부는 큐로 구현되어 있음 대부분의 경우 오래된 스레드 먼저 실행 Race Condition 정말 심할 때 가끔 새치기 스레드 나올 수 있음 공정 모드 (Fair mode) 서비스에서 로직상 반드시 순서가 지켜져야 할 때 사용 (e.g. 선착순) private final Lock fairLock = new ReentrantLock(true); 공정성 보장: 먼저 대기한 스레드가 락을 먼저 획득 기아 현상 방지: 모든 스레드가 언젠가 락 획득할 수 있도록 보장 성능 저하: 락 획득 속도 느려짐 LockSupport 스레드를 WAITING 상태로 변경 (BLOCKED X, 무한 대기 문제 해결) unpark()로 깨울 수 있음 타임아웃 및 인터럽트도 가능해짐! 주요 기능 park(): 스레드를 WAITING 상태로 변경 parkNanos(nanos): 스레드를 TIMED_WAITING 상태로 변경 (지정 나노초) unpark(thread): 스레드를 WAITING, TIME_WAITING -> RUNNABLE 변경 대기 상태의 스레드는 외부 스레드의 도움을 받아야 깨어 날 수 있음 (파라미터) LockSupport 활용은 무한 대기하지 않는 락 기능 개발의 토대 if (!lock.tryLock(10초)) { // 내부에서 parkNanos() 사용 log("[진입 실패] 너무 오래 대기했습니다."); return false; } //임계 영역 시작 ... //임계 영역 종료 lock.unlock() // 내부에서 unpark() 사용 락(lock) 클래스를 만들어 락 획득 및 반납에 따라 스레드 상태 변경 다만, 구현을 위해서는 대기 스레드를 위한 자료구조 및 스레드를 깨우는 우선순위 알고리즘도 필요하므로 복잡 자바는 저수준의 LockSupport를 활용하는 고수준의 ReentrantLock 구현해둠 Lock 인터페이스 public interface Lock { //락 획득 시도, 락 없을 시 WAITING, 인터럽트 반응 X //lock()은 인터럽트 시 잠깐 RUNNABLE 됐다가 강제로 WAITING 상태로 되돌림 void lock(); void lockInterruptibly() throws InterruptedException;//인터럽트O boolean tryLock(); //락 획득 시도, 성공 여부 즉시 반환 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //주어진 시간 동안 락 획득 시도, 이후 성공 여부 반환 void unlock(); //락 반납, 락을 획득한 스레드가 호출해야 함 //락과 결합해 사용하는 Condition 객체 생성 및 반환 //스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록 함 Condition newCondition(); } 예시 코드 1 - 무한정 대기 (lock.lock()) public class BankAccountImpl implements BankAccount { private int balance; private final Lock lock = new ReentrantLock(); ... @Override public boolean withdraw(int amount) { log("거래 시작: " + getClass().getSimpleName()); lock.lock(); // ReentrantLock 이용하여 lock을 걸기 try { ... } finally { lock.unlock(); // ReentrantLock 이용하여 lock 해제 } log("거래 종료"); return true; } @Override public int getBalance() { lock.lock(); // ReentrantLock 이용하여 lock 걸기 try { ... } finally { lock.unlock(); // ReentrantLock 이용하여 lock 해제 } } } 스레드가 락을 획득하지 못하면 WAITING 상태가 되고, 대기 큐에서 관리 (내부에서 LockSupport.park() 호출) 락 반납 시, 대기 큐의 스레드를 하나 깨움 (내부에서 LockSupport.unpark() 호출) 대기 큐에 스레드가 없을 시, 깨우지 않음 깨어난 스레드는 락 획득을 시도 락을 획득하면 대기 큐에서 제거 락을 획득하지 못하면 다시 대기 상태가 되면서 대기 큐에 유지 비공정 모드 락 획득을 시도하는 잠깐 사이에 새 스레드가 락을 먼저 가져갈 수 있음 경쟁: 새로 락을 호출하는 스레드 VS 대기 큐에 있는 스레드 공정 모드 대기 큐에 먼저 대기한 스레드가 락을 가져감 예시 코드 2 - 대기 빠져나오기 (lock.tryLock()) @Override public boolean withdraw(int amount) { log("거래 시작: " + getClass().getSimpleName()); // 대기 없이 획득 여부 바로 판단 if (!lock.tryLock()) { log("[진입 실패] 이미 처리중인 작업이 있습니다."); return false; } // 특정 시간만큼 대기 /** try { if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) { log("[진입 실패] 이미 처리중인 작업이 있습니다."); return false; } } catch (InterruptedException e) { throw new RuntimeException(e); } **/ try { ... } finally { lock.unlock(); // ReentrantLock 이용하여 lock 해제 } log("거래 종료"); return true; } 공유 자원 여러 스레드가 접근하는 자원을 말한다. (e.g. 인스턴스 변수, 클래스 변수, 인스턴스 자체) 사실, 공유 자원은 원자적이지 않은 변경이 문제가 되는 것이므로, final 키워드가 붙은 공유자원은 멀티스레드 상황에 안전한 공유자원이다. 어떤 스레드도 값을 변경할 수 없기 때문이다. 참고로, 지역 변수는 공유 자원이 아니므로 동시성 문제를 전혀 고민하지 않아도 된다. 지역 변수는 각각의 스레드가 가지는 별도의 스택 공간에 저장되어서 다른 스레드와 공유하지 않기 때문이다. BLOCKED VS WAITING (WAITING & TIMED_WAITING) 두 상태 모두 스레드가 실행 스케줄링에 들어가지 않고 대기한다는 점에서 비슷한 상태이다. (CPU가 실행 X) 다만, BLOCKED 상태는 synchronized에서만 사용되며 타임 아웃이나 인터럽트가 불가능하다. 반면에, WAITING 상태는 범용적으로 사용되며 타임아웃이나 인터럽트를 통해 대기 상태를 빠져나올 수 있다. 생산자 소비자 문제 생산자 소비자 문제(producer-consumer problem) 여러 스레드가 동시에 특정 자원을 함께 생산하고 소비하는 상황 멀티스레드에서 자주 등장하는 동시성 문제 = 한정된 버퍼 문제(bounded-buffer problem) 기본 개념 생산자(Producer) 데이터를 생성하는 역할 e.g. 파일에서 데이터를 읽어오거나 네트워크에서 데이터를 받아오는 스레드 소비자(Consumer) 데이터를 사용하는 역할 e.g. 데이터를 처리하거나 저장하는 스레드 버퍼(Buffer) 생산자가 생성한 데이터를 일시적으로 저장하는 공간 e.g. 큐 생산자 소비자 모두 여럿일 수 있음 문제 상황 생산자가 너무 빠를 때 생산자가 데이터를 빠르게 생성해 버퍼가 가득차면, 버퍼에 빈 공간이 생길 때까지 기다려야 함 소비자가 너무 빠를 때 소비자가 데이터를 빠르게 소비해 버퍼가 비면, 버퍼에 새 데이터가 들어올 때까지 기다려야 함 해결책 스레드를 제어할 수 있는 특별한 자료구조 사용 예제: 스레드를 제어하는 큐 만들기 기본 가정: 소비자 스레드와 생산자 스레드는 지속적으로 발생함 (생산자 소비자 구조는 계속 실행) BoundedQueueV1 특징 동기화한 큐 사용 (take(), put() 메서드를 synchronized) 생산자가 자원을 생산할 때, 큐가 가득찼다면 데이터를 버림 소비자가 자원을 소비할 때, 큐가 비었다면 아무일도 안하고 null 반환 문제 생산자가 데이터를 버리는 것이 비효율적 (기다림 X) 소비자가 데이터를 기다리지 않는 것이 비효율적 (기다림 X) BoundedQueueV2 목표: 생산자 혹은 소비자 스레드가 기다리도록 하기 특징 생산자는 큐에 빈 공간이 생길 때까지 기다림 put() 메서드 -> while (queue.size() == max) 생산자 스레드는 반복문을 통해 큐에 빈공간이 생기는지 주기적으로 체크 소비자는 큐에 데이터가 추가될 때까지 기다림 take() 메서드 -> while (queue.isEmpty()) 소비자 스레드는 반복문을 통해 큐에 데이터가 추가되는지 주기적으로 체크 문제 생산자나 소비자 스레드가 락을 가지고 대기하면, 다른 스레드들은 BLOCKED 됨 (synchronized) BoundedQueueV3 목표 임계 영역 안에서 락을 가지고 기다리는 스레드가 락을 다른 스레드에게 양보하도록 하기 Object 클래스를 통한 해결 (wait(), notify(), notifyAll()) synchronized 에서 비롯된 락 획득 후 임계영역 내 무한 대기 문제 해결 모든 객체가 사용 가능 (자바는 멀티스레드를 고려하며 탄생한 언어) 주요 메서드 유의점 모두 synchronized 메서드 및 블록 내에서 호출되어야 함 대기하는 스레드는 스레드 대기 집합에서 대기 Object.wait() 현재 스레드가 가진 락을 반납하고 대기 (WAITING, 스레드 대기 집합) 다른 스레드가 notify(), notifyAll()을 호출할 때까지 대기 유지 Object.notify() 스레드 대기 집합에서 대기 중인 스레드 중 하나를 깨움 대기 집합에서 어떤 스레드가 깨어날지는 예측 불가능 (JVM 스펙 명시 X) 락을 다시 획득할 기회 얻음 깨어난 스레드는 WAITING -> BLOCKED 상태가 됨 깨어난 스레드는 임계 영역 내에 있음 임계 영역 내 코드를 실행하려면 락이 필요 락 획득을 위해 BLOCKED 상태로 대기 Object.notifyAll() 스레드 대기 집합에서 대기 중인 모든 스레드를 깨움 모두 락 획득 기회를 얻음 깨어난 스레드는 WAITING -> BLOCKED 상태가 됨 깨어난 스레드는 임계 영역 내에 있음 임계 영역 내 코드를 실행하려면 락이 필요 락 획득을 위해 BLOCKED 상태로 대기 특징 생산자 - put() 반복문 내에서 wait()으로 락을 반납하고 큐의 빈 공간을 기다림 (WAITING) 자원 생산에 성공하면 notify()로 대기 스레드를 깨우고 종료 소비자 - take() 반복문 내에서 wait()으로 락을 반납하고 큐의 데이터 추가를 기다림 (WAITING) 자원 소비에 성공하면 notify()로 대기 스레드를 깨우고 종료 문제 생산자 소비자 모두 데이터를 정상 생산하고 정상 소비하나… 1) 스레드 대기 집합 하나에 생산자, 소비자 스레드를 함께 관리 2) 깨울 스레드 선택이 불가능 (notify()) 같은 종류의 스레드를 깨울 때 비효율 발생 큐에 데이터가 없는데 소비자가 소비자를 깨우거나 큐가 가득 찼는데 생산자가 생산자를 깨우는 케이스 존재 깨어난 스레드가 CPU 자원만 소모하고 바로 다시 대기 집합에 들어가 비효율 스레드 기아 상태 (thread starvation) 발생 최악의 경우 특정 스레드만 영원히 깨어나지 못할 수 있음 notify()가 어떤 스레드를 깨우는지 자바 스펙에 명기 X 물론 보통은 오래 기다린 스레드가 깨어나도록 구현됨 큐에 데이터가 없는데 소비자 스레드만 계속 깨우거나 큐가 가득 찼는데 생산자 스레드만 계속 깨울 수 있음 notifyAll()을 사용하면 스레드 기아 상태를 막을 수 있으나 비효율은 지속 BoundedQueueV4 목표: 구현을 synchronized에서 ReentrantLock으로 변경 특징 Lock 인터페이스와 ReentrantLock 구현체 사용 private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); ReentrantLock 을 사용하는 스레드가 대기하는 스레드 대기 공간 Lock(ReentrantLock)을 사용하면 스레드 대기 공간을 직접 만들어야 함 변경 포인트 synchronized -> lock.lock() wait() -> condition.await() 지정 condition에 현재 스레드를 대기(WAITING) 상태로 보관 notify() -> condition.signal() 지정 condition에서 대기 중인 스레드를 하나 깨움 Condition은 Queue 구조를 사용하므로 FIFO 순서로 깨움 BoundedQueueV5 목표: 서로 다른 종류의 스레드를 꺠우도록 생산자용, 소비자용으로 스레드 대기 집합을 분리 특징 생산자와 소비자 스레드 대기 집합 분리 (condition) private final Lock lock = new ReentrantLock(); private final Condition producerCond = lock.newCondition(); private final Condition consumerCond = lock.newCondition(); 생산자는 소비자를 깨우고 소비자는 생산자를 깨움 생산자 - put() 큐가 가득 찬 경우: producerCond.await() 데이터 저장한 경우: consumerCond.signal() 소비자 - take() 큐가 빈 경우: consumerCond.await() 데이터를 소비한 경우: producerCond.signal() BlockingQueue 자바는 생산자 소비자 문제 해결을 위해 BlockingQueue 인터페이스와 구현체를 제공 큐가 특정 조건을 만족할 때까지 스레드를 차단할 수 있는 큐 (큐가 가득차거나 비어 있을 때) 실무 멀티스레드는 응답성이 중요하므로, 인터럽트나 타임아웃을 받을 수 있게 설계됨 e.g. 생산자 스레드(서버에 상품을 주문하는 고객)가 고객의 요청을 큐에 넣고 소비자 스레드는 큐에서 주문 요청을 꺼내 처리 선착순 할인 이벤트가 크게 성공해 주문이 폭주하면, 소비가 생산을 따라가지 못하고 큐가 가득 차게 될 수 있음 수 많은 생산자 스레드가 큐 앞에서 대기 (고객도 응답 없이 무한 대기) 너무 오래 기다리지 않고 데이터 추가 포기 및 고객에게 나중에 다시 시도해달라고 응답 보내는게 나은 선택 큐가 가득 찼을 때 생각할 수 있는 4가지 선택 대기 없이 예외 던지기 (Throws Exception) 대기 없이 즉시 false 반환 (Special Value) 대기 (Blocks) - 인터럽트 제공 특정 시간 만큼 대기 (Times Out) - 인터럽트 제공 synchronized와 ReentrantLock의 유사성 생산자 소비자 문제는 5, 60년대 해결된 개념이므로 synchronized와 ReentrantLock은 유사한 모습을 보인다. ReentrantLock이 조금 더 편하게 쓸 수 있게 나왔을 뿐이다. 스레드 대기 집합 (wait set) & 락 대기 집합 자바의 모든 객체 인스턴스는 멀티스레드와 임계 영역을 다루기 위해 모니터 락, 락 대기집합, 스레드 대기 집합 3가지 기본 요소를 가지고 있다. (synchronized 적용 상황) synchronized에서 스레드의 대기는 wait() 대기, 락 획득 대기 2단계가 존재하며, 스레드 대기 집합은 2차 대기소, 락 대기 집합은 1차 대기소라 볼 수 있다. 만일 임의의 스레드들이 동시에 실행되면, 하나의 스레드가 락을 획득하고 나머지 스레드는 1차 대기소에 들어간다. 또한, 특정 스레드가 wait()을 호출하면 2차 대기소로 들어가고 notify()가 호출되면 2차 대기소에서 나와 락 획득을 시도하며, 락이 없을 경우 1차 대기소로 간다. 즉, 2차 대기소에 있는 스레드는 2차, 1차 대기소를 모두 빠져 나와야 임계 영역을 수행할 수 있다. 스레드 대기 집합은 대기 상태에 들어간 스레드를 관리하는 것이다. 예를 들어, synchronized 임계 영역 안에서 Object.wait()을 호출하면, 스레드는 대기(WAITING) 상태에 들어가고 대기 집합 내에서 관리된다. 이후, 다른 스레드가 Object.notify()를 호출하면 대기 집합에서 빠져나간다. (참고로, wait() 호출은 앞에 this를 생략할 수 있다. this는 해당 인스턴스를 뜻한다.) 락 대기 집합은 락을 기다리는 BLOCKED 상태의 스레드들을 관리한다. synchronized를 시작할 때, 락이 없으면 BLOCKED 상태로 락 대기 집합에서 대기한다. ReentrantLock도 마찬가지로 2단계 대기 상태로 동작한다. 다만, 다음의 차이가 있다. 독립적으로 구현된 락, 락 대기 큐, condition(스레드 대기 공간)으로 구성 락 획득 대기 시 WAITING 상태로 대기 condtion.await() 호출 시 스레드 대기 공간에서 대기 (WAITING) 다른 스레드가 condition.signal() 호출 시 스레드 대기 공간 빠져나옴 Doug Lea 동시성 프로그래밍, 멀티스레딩, 병렬 컴퓨팅, 알고리즘 및 데이터 구조 등의 분야에서 많은 업적을 만들었다. 특히, java.util.concurrent 패키지의 주요 설계 및 구현을 주도했다. java.util.concurrent 패키지가 제공하는 동시성 라이브러리는 견고함 및 성능 최적화에 더불어 개발자가 쉽고 편리하게 동시성 문제를 다룰 수 있게 해준다. 이러한 기여는 자바 동시성 프로그래밍을 크게 발전시키고 현대 자바 프로그래밍의 핵심적 부분이 되었다. 이외에도 Queue, Deque 같은 자료구조에서 Doug Lea의 이름을 찾을 수 있다. 동기화와 원자적 연산 (CAS) 원자적 연산 해당 연산이 더 이상 나눌 수 없는 단위로 수행되는 것 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산 원자적 연산은 멀티스레드 상황에서 전혀 문제가 없음 원자적 연산이 아닌 경우, synchronized나 Lock 등을 사용해 안전한 임계 영역 만들어야 함 e.g. i = 1은 원자적 연산 O (대입 연산) i = i + 1, i++은 원자적 연산 X (3단계: i 값 읽기, 더하기 연산, 대입 연산) 원자적 연산 제공 클래스 자바는 각 타입 별로 멀티스레드 상황에 안전하면서 다양한 값 증가, 감소 연산을 제공 AtomicInteger, AtomicLong, AtomicBoolean, AtomicXxx… 원자적 연산 구현 성능 비교 상황: 1000개 스레드를 사용해 값을 0에서 1000으로 증가시키기 성능 비교 BasicInteger (result: 950, 39ms) CPU 캐시를 적극 사용하므로 가장 빠름 멀티스레드에서 사용 불가하지만 단일 스레드 사용 시 효율적 VolatileInteger (result: 961, 455ms) CPU 캐시를 사용하지 않고 메인 메모리 사용해 느려짐 멀티스레드에서 사용 불가 SyncInteger (result: 1000, 625ms) 멀티스레드 상황에서 안전 (synchronized) MyAtomicInteger보다 느림 MyAtomicInteger (result: 1000, 367ms) 멀티스레드 상황에서 안전 (incrementAndGet(), CAS) synchronized, Lock(ReentrantLock) 보다 1.5~2배 빠름 CAS 연산 (Compare-And-Swap, Compare-And-Set) 락을 걸지 않고 원자적인 연산 수행 (락 프리(lock-free) 기법) CPU 하드웨어 차원에서 내리는 특별한 명령 CPU는 잠깐 다른 스레드가 메모리에 write하는 것을 막음 너무 찰나의 시간이므로 락이라 부르진 않음 (성능에 큰 영향 X) 원자적이지 않은 두 과정을 묶어 하나의 원자적 명령으로 만듦 (중간에 다른 스레드 개입 X) 주 메모리에서 값 읽기 기대하는 값이 맞다면 읽은 값을 변경하기 (아니라면 변경 X) 대부분의 현대 CPU가 CAS 연산 명령어 제공 JAVA가 CAS 연산 요청 시 운영체제는 현재 컴퓨터 CPU 종류 확인 (인텔, AMD, MAC…) 그 후 그에 맞는 CAS 연산을 CPU 코어에 명령 자바는 AtomicXxx 클래스에서 CAS 연산 메서드 제공 (compareAndSet()) e.g. compareAndSet(0, 1) 주 메모리 현재 값이 0이라면 1로 변경하고 true 반환 주 메모리 현재 값이 0이 아니라면 변경없이 false 반환 e.g. incrementAndGet() 내부 구현 예시 (CAS 활용) private static int incrementAndGet(AtomicInteger atomicInteger) { int getValue; boolean result; do { getValue = atomicInteger.get(); log("getValue: " + getValue); result = atomicInteger.compareAndSet(getValue, getValue + 1); log("result: " + result); } while (!result); return getValue + 1; } 스레드 충돌이 발생해도 CAS 연산이 성공할 때까지 반복 재시도 덕분에 락 없이 안전한 데이터 변경 가능 작은 단위의 일부 영역에 적용 가능 (락 완전히 대체 X) 락 기반 방식의 문제점 (synchronized, Lock(ReentrantLock)) 락 기반 방식은 직관적이지만 무거움 스레드의 상태 변경으로 CPU 스케줄러에 들어갔다 나왔다 하는 무거운 과정 동반 락 획득 및 반납에 시간 소요 락 획득 및 반납하는 과정의 반복 스핀 락(Spin-Lock) - CAS 활용 락 구현 public class SpinLock { private final AtomicBoolean lock = new AtomicBoolean(false); public void lock() { log("락 획득 시도"); while (!lock.compareAndSet(false, true)) { // 락을 획득할 때 까지 스핀 대기(바쁜 대기) 한다. log("락 획득 실패 - 스핀 대기"); } log("락 획득 완료"); } public void unlock() { lock.set(false); log("락 반납 완료"); } } 스핀 락 락을 획득하기 위해 자원을 소모하면서 반복적으로 확인하는 락 메커니즘 CAS를 사용해서 구현 CAS는 단순한 연산 뿐만 아니라, 가벼운 락 구현에도 사용 가능 락 획득은 원자적이지 않은 임계 영역 락 사용 여부 확인 락의 값 변경 synchronized, Lock 등으로 동기화할 수도 있지만, CAS를 사용하면 원자적 연산 가능 장점 무거운 동기화 작업(락) 없이 아주 가벼운 락을 만들 수 있음 (RUNNABLE 상태를 유지, 빠른 성능 동작) 단점 반복문으로 CPU 자원을 계속 사용하면서 락을 대기 (스핀 대기, 바쁜 대기) 사용 방향 아주 짧은 CPU 연산 수행 시에만 사용해야 효율적 나노 초 단위에서 사용해야 함 e.g. 숫자 값 증가, 자료 구조 데이터 추가 I/O 작업 같이 오래 기다리는 작업에서는 최악 CPU를 계속 사용하며 기다림 e.g. DB 쿼리, 다른 서버 응답 기다리기 보통 I/O 작업은 0.X초 ~ X초까지 걸릴 수 있음 (최소 10ms 이상) 이 경우 일반적인 락을 사용해야 함 동기화 락 방식 VS 락 프리 방식(CAS 활용) 두 방식 모두 안정적인 데이터 변경 보장 동기화 락 방식 비관적 접근법 (pessimistic, 가정: “스레드 충돌이 반드시 일어날 것이다”) 항상 락 획득하고 데이터 접근 다른 스레드의 접근을 막음 스레드를 하나씩 순서대로 돌림 멀티스레드에서 순간적으로 싱글 스레드로 바뀜 장점 하나의 스레드만 리소스에 접근할 수 있으므로 충돌 발생 X 락을 대기하는 스레드는 CPU를 거의 사용 X 단점 락 획득을 위한 대기 시간이 길어질 수 있음 스레드 상태 변경으로 인한 컨텍스트 스위칭 오버헤드 락 프리 방식(CAS 활용) 낙관적 접근법 (optimistic, 가정: “대부분의 경우 충돌이 없을 것이다”) 락을 사용하지 않고 데이터에 바로 접근 어떤 스레드도 멈추지 않음 (10개 스레드면 모두 돌아감) 충돌이 발생하면 그 때 재시도 장점 충돌이 적은 환경에서 높은 성능 발휘 스레드가 블로킹되지 않아(RUNNABLE) 병렬 처리가 더 효율적일 수 있음 단점 충돌이 빈번한 환경이라면 대기 시에도 CPU 자원 계속 소모해 비효율적 실무 사용 전략 기본은 동기화 락을 사용하고 특별한 경우에 CAS를 적용하여 최적화 임계 영역이 필요한 매우 간단한 CPU 연산에만 CAS 연산 사용이 효과적 간단한 CPU 연산은 매우 빨리 처리되므로 충돌이 자주 발생 X 나노 초 단위에서 간단한 연산에서 사용해야 함 e.g. 숫자 값 증가, 자료 구조 데이터 추가 I/O 작업 혹은 몇 초씩 걸리는 복잡한 비즈니스 로직이라면 락 사용 충돌이 어마어마하게 날 것이므로 보통 I/O 작업은 0.X초 ~ X초까지 걸릴 수 있음 (최소 10ms 이상) e.g. DB 쿼리, 다른 서버 응답 기다리기 실무에서 대부분의 애플리케이션은 공유 자원 사용시, 생각보다 충돌하지 않을 가능성이 훨씬 높음 e.g. 주문 수 실시간 카운트 특정 피크 시간에 주문이 100만건 들어오는 서비스 (1시간 100만건이면 우리나라 탑 서비스) 1,000,000 / 60분 = 1분에 16,666건, 1초에 277건 1초 CPU 연산수 고려하면, 100만 건 중 충돌 나는 경우는 넉넉 잡아도 몇 십건 이하일 것 주문 수 증가 같은 단순한 연산은 AtomicInteger 같은 CAS가 더 나은 성능 보임 CAS 연산과 라이브러리 복잡한 동시성 라이브러리들은 CAS 연산을 사용하지만, 개발자가 직접 사용하는 경우는 거의 없다. CAS 연산을 사용하는 라이브러리를 잘 사용하는 정도면 충분하다. (AtomicInteger…) 스레드 충돌 두 스레드가 동시에 실행되면서 문제가 발생하는 상황을 말한다. 스레드 세이프 여러 스레드가 동시에 접근해도 괜찮은 경우를 말한다. 동시성 컬렉션 실무 전략 멀티스레드 환경에서 필요한 동시성 컬렉션을 잘 선택해 사용할 수 있으면 충분 단일 스레드에는 일반 컬렉션 사용, 멀티스레드에는 동시성 컬렉션 사용 (성능 트레이드 오프) 기존 컬렉션 프레임워크 (java.util)는 스레드 세이프 X 원자적 연산 제공 X -> 동시성 문제 및 버그 발생 e.g. ArrayList, LinkedList, HashSet, HashMap… 다만, 성능 트레이드 오프로 인해 처음부터 모든 자료구조에 동기화를 해둘 수는 없음 단일 스레드 환경에서 불필요한 동기화는 성능 저하 발생 e.g. java.util.Vector는 현재 거의 사용 X 대안 1: 기존 컬렉션 프레임워크에 synchronized, Lock을 적용해 임계 영역 만들기 컬렉션을 모두 복사해서 동기화 용으로 새로 구현해야하는데 비효율적 구현 변경 시 2곳에서 변경해야 함 대안 2: 프록시가 대신 동기화 기능 처리 (프록시 패턴) 자료구조의 인터페이스를 구현한 프록시 클래스를 만들어 synchronized 적용해 target 호출 클라이언트 -> SyncProxyList (인터페이스 구현 및 synchronized 적용) -> BasicList 자바는 기본 컬렉션을 스레드 세이프하게 만드는 동기화 프록시 기능 제공 Collections를 통해 다양한 synchronized 동기화 메서드 지원 synchronizedList() synchronizedCollection() synchronizedMap() synchronizedSet() synchronizedNavigableMap() synchronizedNavigableSet() synchronizedSortedMap() synchronizedSortedSet() 장점 기존 코드를 그대로 사용하면서 synchronized만 살짝 추가 가능 예를 들어, SimpleList 인터페이스를 구현한 모든 구현체에 적용 가능 단점 대상 컬렉션 전체에 동기화가 이뤄져 잠금 범위가 넓어짐 동기화 필요 없는 메서드에도 synchronized 적용해야 함 메서드 내 특정 부분에만 정교한 동기화 불가능 (최적화 불가) 대안 3: 자바는 스레드 세이프한 동시성 컬렉션을 제공 (java.util.concurrent, 자바 1.5) 유연하고 성능 최적화된 동기화 전략 사용 일부 메서드에 대해서만 동기화 적용 더욱 정교한 잠금을 통해 성능 최적화 e.g. synchronized , Lock(ReentrantLock), CAS , 분할 잠금 기술(segment lock) 분할 잠금 기술 e.g. 해시맵 -> 버킷마다 락을 분산 다른 버킷에 접근한 스레드는 락 획득을 위해 경쟁하지 않음 같은 버킷 접근해 충돌 시 락 혹은 CAS 기법 적용 종류 (ConcurrentHashMap 가장 많이 사용, 다른 것은 자주 사용 X) List CopyOnWriteArrayList : ArrayList 의 대안 Set CopyOnWriteArraySet : HashSet 의 대안 ConcurrentSkipListSet : TreeSet의 대안 (정렬된 순서 유지, Comparator 사용 가능) Map ConcurrentHashMap : HashMap 의 대안 ConcurrentSkipListMap : TreeMap 의 대안 (정렬된 순서 유지, Comparator 사용 가능) Queue ConcurrentLinkedQueue : 동시성 큐, 비 차단(non-blocking) 큐 BlockingQueue: 동시성 큐, 스레드 차단(blocking) 큐 ArrayBlockingQueue 크기가 고정된 블로킹 큐 공정(fair) 모드를 사용 가능 (사용 시 성능이 저하될 수 있음) LinkedBlockingQueue 크기가 무한하거나 고정된 블로킹 큐 PriorityBlockingQueue 우선순위가 높은 요소를 먼저 처리하는 블로킹 큐 SynchronousQueue 데이터를 저장하지 않는 블로킹 큐 생산자가 데이터를 추가하면 소비자가 그 데이터를 받을 때까지 대기 중간에 큐 없이 생산자, 소비자가 직접 거래 생산자-소비자 간의 직접적인 핸드오프(hand-off) 메커니즘을 제공 DelayQueue 지연된 요소를 처리하는 블로킹 큐 (지정된 지연 시간이 지난 후 소비) 일정 시간이 지난 후 작업을 처리해야 하는 스케줄링 작업에 사용 Deque ConcurrentLinkedDeque : 동시성 덱, 비 차단(non-blocking) 큐 LinkedHashSet, LinkedHashMap의 동시성 컬렉션은 제공 X 필요하다면 Collections.synchronizedXxx() 사용할 것 스레드 풀 (Thread Pool) 스레드 직접 사용의 문제점 스레드 생성 비용으로 인한 성능 문제 스레드 생성 = 스레드 객체 생성 (new Thread()) + 스레드 시작 (thread.start()) new Thread는 단순히 자바 객체만 생성하는 것 thread.start() 호출 시 실제 스레드 생성 (메모리 할당, 시스템 콜, 스케줄링…) 스레드 생성은 매우 무거운 작업 (스레드 하나는 보통 1MB 이상의 메모리 사용) 메모리 할당: 스레드 생성 시 호출 스택을 위한 메모리 공간을 할당해야 함 운영체제 자원 사용: 운영체제 커널 수준에서 시스템 콜을 통해 처리 (CPU와 메모리 소모) 운영체제 스케줄러 설정: 새 스레드를 관리하고 실행 순서 조정 스레드를 재사용하면 효율적일 것 스레드 생성은 단순 자바 객체 생성보다 비교할 수 없을 정도로 큰 작업 아주 가벼운 작업이라면, 작업 실행 시간보다 스레드 생성 시간이 더 오래 걸릴 수 있음 스레드를 재사용하면 처음 생성 후에는 생성 시간 없이 아주 빠르게 작업 수행 가능 스레드 관리 문제 시스템이 버틸 수 있는 최대 스레드 수까지만 스레드를 생성할 수 있게 관리해야 함 서버의 CPU, 메모리 자원이 한정되어 있으므로, 스레드 무한 생성 불가 애플리케이션 종료 시에도 스레드 관리 필요 실행 중 스레드가 남은 작업을 모두 수행한 후 프로그램 종료하도록 관리 급하게 종료해야할 때는 인터럽트를 통해 바로 스레드를 종료하도록 관리 Runnable 인터페이스의 불편함 반환 값이 없음 run() 메서드에 반환 값 X -> 스레드 실행 결과를 직접 받을 수 없음 e.g. 스레드 실행 결과를 멤버 변수에 넣어두고 join()으로 기다린 후 보관 값을 사용 예외 처리 체크 예외를 던질 수 없어 메서드 내부에서 반드시 처리해야 함 스레드 풀 스레드를 생성하고 관리하는 풀 단순히 컬렉션에 스레드를 보관하고 재사용하는 것이지만, 구현은 복잡 스레드 상태 관리 (WAITING, RUNNABLE) 생산자 소비자 문제 (스레드 풀의 스레드가 소비자) Executor 프레임워크 사용시 편리하게 사용 가능 작업 흐름 스레드를 필요한만큼 미리 생성 작업 요청이 오면 이미 만들어진 스레드를 조회해 작업 처리 작업 완료 후, 스레드를 재사용할 수 있도록 스레드 풀에 다시 반납 스레드 풀을 사용하면 스레드 생성 및 관리 문제 해결 재사용을 통해 스레드 생성 시간을 절약 필요한 만큼만 스레드를 만들고 관리 Executor 프레임워크 (스레드 사용 시 실무 권장) 자바 멀티스레드를 쉽고 편리하게 사용하도록 돕는 프레임워크 작업 실행 관리, 스레드 풀 관리, 스레드 상태 관리, Runnable 한계, 생산자 소비자 문제… 개발자가 직접 스레드 생성 및 관리하는 복잡함을 줄임 Future 패턴(Callable)은 마치 싱글 스레드 방식으로 개발하는 느낌 스레드 생성이나 join()으로 제어하는 코드가 없음 단순히 ExecutorService 에 필요한 작업을 요청하고 결과를 받아서 쓰면 된다! 주요 구성 요소 1 최상위 Executor 인터페이스 public interface Executor { void execute(Runnable command); } ExecutorService 인터페이스 (주로 사용) public interface ExecutorService extends Executor, AutoCloseable { <T> Future<T> submit(Callable<T> task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException @Override default void close(){...} ... } 주요 메서드로 작업 제출과 제어 기능 추가 제공 작업 단건 처리 - submit(), Future.get() 작업 컬렉션 처리 - invokeAll(), invokeAny() 여러 작업을 한 번에 요청하고 처리하는 메서드 제공 invokeAll() List<CallableTask> tasks = List.of(taskA, taskB, taskC); List<Future<Integer>> futures = es.invokeAll(tasks); //이 코드에서 메인스레드가 블로킹됨 for (Future<Integer> future : futures) { Integer value = future.get(); log("value = " + value); } 모든 Callable 작업이 완료될 때까지 기다림 타임아웃 설정도 가능 invokeAny() List<CallableTask> tasks = List.of(taskA, taskB, taskC); Integer value = es.invokeAny(tasks); //이 코드에서 메인 스레드가 블로킹됨 하나의 Callable 작업이 완료될 때까지 기다리고, 가장 먼저 완료된 작업 결과 반환 완료되지 않은 나머지 작업은 인터럽트를 통해 취소함 타임아웃 설정도 가능 ThreadPoolExecutor (ExecutorService의 기본 구현체) 크게 스레드풀 + 블로킹 큐로 구성 기본 사용 예시 ExecutorService es = new ThreadPoolExecutor(2,2,0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); //ExecutorService es = Executors.newFixedThreadPool(2); //편의 코드 es.execute(new RunnableTask("taskA")); es.execute(new RunnableTask("taskB")); es.close() 생산자 (main 스레드) es.execute(작업) 호출 시, 작업 인스턴스를 내부 BlockingQueue에 보관 소비자 (스레드 풀에 있는 스레드) 소비자 중 하나가 BlockingQueue에 들어 있는 작업을 받아 처리 작업 과정 ThreadPoolExecutor 생성 시점에는 스레드 풀에 스레드를 미리 만들지 않음 es.execute(작업) 호출로 작업이 올 때마다 corePoolSize 까지 스레드 생성 생산자 스레드는 작업만 전달하고 다음 코드 수행 (Non-Blocking) corePoolSize 까지 생성하고 나면, 이후 스레드 재사용 작업이 완료되면 스레드 풀에 스레드 반납 (= 스레드 상태 변경) = 스레드가 WAITING 상태로 스레드 풀에서 대기 반납된 스레드는 재사용 close() 호출 시, ThreadPoolExecutor 종료 스레드 풀에 대기하는 스레드도 함께 제거 생성자 사용 속성 corePoolSize 스레드 풀에서 관리되는 기본 스레드의 수 요청이 들어올 때마다 하나씩 생성 maximumPoolSize 스레드 풀에서 관리되는 최대 스레드 수 요청이 너무 많거나 급한 경우 최대 수만큼 초과 스레드 생성해 사용 급한 경우: 큐까지 가득찼는데 새로운 작업 요청이 오는 경우 keepAliveTime , TimeUnit unit 기본 스레드 수를 초과해서 만들어진 스레드가 생존할 수 있는 대기 시간 이 시간 동안 초과 스레드가 처리할 작업이 없다면 초과 스레드는 제거 BlockingQueue workQueue : 작업을 보관할 블로킹 큐 (생산자 소비자 문제 해결) 스레드 풀 상태 확인 메서드 getPoolSize(); //스레드 풀에서 관리되는 스레드의 숫자 getActiveCount(); //작업을 수행하는 스레드의 숫자 getQueue().size(); //큐에 대기중인 작업의 숫자 getCompletedTaskCount(); //완료된 작업의 숫자 주요 구성 요소 2 - Runnable 사용의 불편함 해소 Callable 인터페이스 - java.util.concurrent public interface Callable<V> { V call() throws Exception; } ExecutorService의 submit() 메서드를 통해 작업으로 전달 Runnable을 대신해 작업 정의 가능 call() 메서드는 값 반환 가능 (제네릭 V 반환 타입) throws Exception 예외가 선언되어 있어 체크 예외를 던질 수 있음 Future 인터페이스 public interface Future<V> { //아직 완료되지 않은 작업 취소하고 Future를 취소 상태로 변경 (CACELLED) //작업이 큐에 아직 있다면 취소 상태로 변경하는 것만으로도 작업이 수행되지 않음 //cancel(true): 작업이 실행 중이면 Thread.interrupt() 호출해 작업 중단 //cancel(false): 이미 실행 중인 작업은 중단하지 않음 boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); //작업이 취소되었는지 여부 확인 boolean isDone(); //작업 완료 여부 확인 (작업완료: 정상완료, 취소, 예외종료) //작업 완료까지 대기(Blocking), 완료되면 결과 반환 V get() throws InterruptedException, ExecutionException; //get()과 동일, 시간 초과되면 예외 발생시킴 V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; enum State { RUNNING, SUCCESS, FAILED, CANCELLED } default State state() {} //Future의 상태 반환 } 전달한 작업의 미래 결과를 받을 수 있는 객체 ExecutorService의 submit() 메서드 반환 타입 Future 객체는 내부에 3가지를 보관 작업(Callable 인스턴스) 작업의 완료 여부 (완료 상태) 작업의 결과값 (call() 메서드가 반환할 결과) 필요성 전달한 작업의 결과는 즉시 받을 수 없음 Callable을 submit()하면 미래 어떤 시점에 스레드풀의 스레드가 실행할 것 따라서, 언제 실행이 완료되어 결과를 반환할지 알 수 없음 Future 반환 덕분에 요청 스레드는 블로킹 되지 않고 필요한 작업 수행 가능 즉, 필요한 여러 작업을 ExecutorService에 계속 요청 가능 (동시작업 가능) 모든 작업 요청이 끝난 후, 필요할 때 get()을 호출해서 최종 결과 받으면 됨 반면에, 직접 결과값 반환 설계는 요청스레드가 블로킹됨 즉, 한 작업을 요청하면 블로킹되어 다른 작업을 이어 요청할 수 없음 FutureTask Future의 실제 구현체 Future 인터페이스 뿐만 아니라 Runnable 인터페이스도 함께 구현 run() 메서드가 작업의 call() 메서드를 호출하고 그 결과를 받아 처리 기본 사용 예시 public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService es = Executors.newFixedThreadPool(1); //편의 메서드 Future<Integer> future = es.submit(new MyCallable()); Integer result = future.get(); es.close(); } static class MyCallable implements Callable<Integer> { @Override public Integer call() { int value = new Random().nextInt(10); return value; } } 요청 스레드가 submit(작업) 호출하면 즉시 Future 객체 반환 ExecutorService는 Future 객체(FutureTask)를 생성 생성된 Future를 블로킹 큐에 전달 - 나중에 스레드 풀의 스레드가 처리 요청 스레드에게 Future 객체를 반환 스레드 풀의 스레드가 큐에 Future 객체를 꺼내서 작업 수행 FutureTask.run() 호출 -> MyCallable.call() 호출 요청 스레드는 본인이 필요할 때 future.get()을 호출한다. 이 때, Future가 완료 상태 요청스레드는 대기 없이 바로 결과값 반환 받음 Future가 미완료 상태 요청 스레드가 결과 받기 위해 블로킹 상태로 대기 (RUNNABLE -> WAITING) 스레드 풀의 소비자 스레드는 작업이 완료되면 Future에 작업 결과값 담음 Future 상태를 완료로 변경 요청 스레드를 깨움 (WAITING -> RUNNABLE) Future가 어떤 요청 스레드가 대기하는지 알고 있음 요청 스레드가 완료 상태 Future에서 결과를 반환 받음 작업 완료 소비자 스레드는 스레드 풀로 반환 (RUNNABLE -> WAITING) Future 요청 예시 바른 예시 - 수행 시간 2초 Future<Integer> future1 = es.submit(task1); // non-blocking Future<Integer> future2 = es.submit(task2); // non-blocking Integer sum1 = future1.get(); // blocking, 2초 대기 Integer sum2 = future2.get(); // blocking, 즉시 반환 잘못된 예시 1 - 수행 시간 4초 Future<Integer> future1 = es.submit(task1); // non-blocking Integer sum1 = future1.get(); // blocking, 2초 대기 Future<Integer> future2 = es.submit(task2); // non-blocking Integer sum2 = future2.get(); // blocking, 2초 대기 잘못된 예시 2 - 수행 시간 4초 Integer sum1 = es.submit(task1).get(); // get()에서 블로킹 Integer sum2 = es.submit(task2).get(); // get()에서 블로킹 close() VS shutdown() close()는 자바 19부터 지원되는 메서드다. 19 미만 버전을 사용한다면 shutdown()을 호출해야 한다. 블로킹 메서드 어떤 스레드가 결과를 얻기 위해 대기하는 것을 블로킹(Blocking)이라고 한다. 이 때, 스레드의 상태는 BLOCKED, WAITING에 해당한다. 그리고 Thread.join(), Future.get() 같이 다른 작업이 완료될 때까지 호출한 스레드를 대기하게 하는 메서드를 블로킹 메서드라고 한다. 우아한 종료 (Graceful Shutdown) - ExecutorService 실무 전략 기본적으로 우아한 종료를 선택 (보통 60초로 우아한 종료 시간 지정) 시간안에 우아한 종료가 되지 않으면 다음으로 강제 종료 시도 우아한 종료 (Graceful Shutdown) 문제 없이 안정적으로 종료하는 방식 e.g. 서버 재시작 시 새로운 요청은 막고, 이미 진행중인 요청은 모두 완료한 후 재시작하는게 이상적 ExecutorService 종료 메서드 서비스 종료 -> 풀의 스레드 자원 정리 shutdown() 새로운 작업을 받지 않고, 이미 제출된 작업을 완료한 후 종료 이미 제출된 작업 = 처리중 작업 + 큐에 남아있는 작업 서비스 정상 종료 시도 (이상적) Non-Blocking 메서드 List<Runnable> shutdownNow() 인터럽트를 통해 실행 중인 작업을 중단하고, 대기 중인 작업을 반환하며 즉시 종료 새로운 요청 거절 + 실행중 작업 중단 + 큐에 남아있는 작업은 반환 FutureTask 반환 (FutureTask는 Runnable을 구현한 것) 서비스 강제 종료 시도 Non-Blocking 메서드 close() 자바 19부터 지원, shutdown()과 동일 shutdown() 호출 후, 하루를 기다려도 작업이 미완료면 shutdownNow() 호출 실용적인 방식이지만, 하루는 너무 김 호출 스레드에 인터럽트가 발생해도 shutdownNow() 호출 서비스 상태 확인 boolean isShutdown() 서비스 종료 여부 확인 boolean isTerminated() shutdown() , shutdownNow() 호출 후, 모든 작업이 완료되었는지 확인 작업 완료 대기 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException 서비스 종료 시 모든 작업이 완료될 때까지 대기 (지정 시간까지만 대기) shutdown() 류의 서비스 종료와 함께 사용 Blocking 메서드 실무 구현 (shutdownAndAwaitTermination()) - ExecutorService 공식 API 문서 제안 방식 public static void main(String[] args) throws InterruptedException { ExecutorService es = Executors.newFixedThreadPool(2); es.execute(new RunnableTask("taskA")); es.execute(new RunnableTask("taskB")); es.execute(new RunnableTask("taskC")); es.execute(new RunnableTask("longTask", 100_000)); // 100초 대기 log("== shutdown 시작 =="); shutdownAndAwaitTermination(es); log("== shutdown 완료 =="); } static void shutdownAndAwaitTermination(ExecutorService es) { // non-blocking, 새로운 작업을 받지 않는다. // 처리 중이거나, 큐에 이미 대기중인 작업은 처리한다. 이후에 풀의 스레드를 종료한다. es.shutdown(); try { // 이미 대기중인 작업들을 모두 완료할 때 까지 10초 기다린다. log("서비스 정상 종료 시도"); if (!es.awaitTermination(10, TimeUnit.SECONDS)) { // 정상 종료가 너무 오래 걸리면... log("서비스 정상 종료 실패 -> 강제 종료 시도"); es.shutdownNow(); // 작업이 취소될 때 까지 대기한다. 인터럽트 이후 자원정리 등이 존재할 수 있음 if (!es.awaitTermination(10, TimeUnit.SECONDS)) { //이 구간이 되면 자바를 강제종료 해야함 //최악의 경우 스레드가 인터럽트를 받을 수 없는 코드 수행 중일 수 있음 //이런 스레드는 자바를 강제 종료해야 제거할 수 있음 //e.g. while(true) {...} //로그를 남겨두고 추후 문제 코드 수정 log("서비스가 종료되지 않았습니다."); } } } catch (InterruptedException ex) { // awaitTermination()으로 대기중인 현재 스레드가 인터럽트 될 수 있다. es.shutdownNow(); } } 우아한 종료가 이상적이지만 서비스가 너무 늦게 종료되거나 종료되지 않는 문제 발생 가능 갑자기 많은 요청으로 큐에 대기중인 작업이 많아 작업 완료가 어려움 작업이 너무 오래 걸림 버그 발생으로 특정 작업이 안끝남 보통 60초까지 우아하게 종료하는 시간을 정하고, 넘어가면 작업 강제 종료 시도 Executor 프레임워크의 스레드 풀 관리 대량의 요청을 별도의 스레드에서 어떻게 처리해야하는지에 대한 기본기 스레드 풀 관리 사이클 corePoolSize 크기까지는 작업 요청이 올 때마다 스레드를 생성하고 바로 작업 실행 corePoolSize를 초과하면 큐에 작업을 넣음 큐를 초과하면 maximumPoolSize 크기까지만 요청이 올 때마다 초과 스레드를 생성하고 작업 실행 maximumPoolSize를 초과하면 요청이 거절되고 예외 발생 (RejectedExecutionException) 즉, 큐도 가득차고 풀 최대 생성 가능한 스레드 수도 가득 차서 작업을 받을 수 없음 초과스레드는 지정 시간까지 작업 없이 대기하면 제거됨 긴급한 작업들이 끝난 것 shutdown() 진행 시 풀의 스레드가 모두 제거됨 예시 코드 public class PoolSizeMain { public static void main(String[] args) throws InterruptedException { BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2); ExecutorService es = new ThreadPoolExecutor(2, 4, 3000, TimeUnit.MILLISECONDS, workQueue); printState(es); es.execute(new RunnableTask("task1")); printState(es, "task1"); es.execute(new RunnableTask("task2")); printState(es, "task2"); es.execute(new RunnableTask("task3")); printState(es, "task3"); es.execute(new RunnableTask("task4")); printState(es, "task4"); es.execute(new RunnableTask("task5")); printState(es, "task5"); es.execute(new RunnableTask("task6")); printState(es, "task6"); try { es.execute(new RunnableTask("task7")); } catch (RejectedExecutionException e) { log("task7 실행 거절 예외 발생: " + e); } sleep(3000); log("== 작업 수행 완료 =="); printState(es); sleep(3000); log("== maximumPoolSize 대기 시간 초과 =="); printState(es); es.close(); log("== shutdown 완료 =="); printState(es); } } //실행 결과 11:36:23.260 [main] [pool=0, active=0, queuedTasks=0, completedTasks=0] 11:36:23.263 [pool-1-thread-1] task1 시작 11:36:23.267 [main] task1 -> [pool=1, active=1, queuedTasks=0, completedTasks=0] 11:36:23.267 [main] task2 -> [pool=2, active=2, queuedTasks=0, completedTasks=0] 11:36:23.267 [pool-1-thread-2] task2 시작 11:36:23.267 [main] task3 -> [pool=2, active=2, queuedTasks=1, completedTasks=0] 11:36:23.268 [main] task4 -> [pool=2, active=2, queuedTasks=2, completedTasks=0] 11:36:23.268 [main] task5 -> [pool=3, active=3, queuedTasks=2, completedTasks=0] 11:36:23.268 [pool-1-thread-3] task5 시작 11:36:23.268 [main] task6 -> [pool=4, active=4, queuedTasks=2, completedTasks=0] 11:36:23.268 [pool-1-thread-4] task6 시작 11:36:23.268 [main] task7 실행 거절 예외 발생: java.util.concurrent.RejectedExecutionException: Task thread.executor.RunnableTask@3abbfa04 rejected from java.util.concurrent.ThreadPoolExecutor@7f690630[Running, pool size = 4, active threads = 4, queued tasks = 2, completed tasks = 0] 11:36:24.268 [pool-1-thread-1] task1 완료 11:36:24.268 [pool-1-thread-1] task3 시작 11:36:24.269 [pool-1-thread-3] task5 완료 11:36:24.269 [pool-1-thread-3] task4 시작 11:36:24.269 [pool-1-thread-2] task2 완료 11:36:24.269 [pool-1-thread-4] task6 완료 11:36:25.273 [pool-1-thread-1] task3 완료 11:36:25.273 [pool-1-thread-3] task4 완료 11:36:26.273 [main] ==작업수행완료== 11:36:26.273 [main] [pool=4, active=0, queuedTasks=0, completedTasks=6] 11:36:29.276 [main] == maximumPoolSize 대기 시간 초과 == 11:36:29.277 [main] [pool=2, active=0, queuedTasks=0, completedTasks=6] 11:36:29.278 [main] == shutdown 완료 == 11:36:29.278 [main] [pool=0, active=0, queuedTasks=0, completedTasks=6] 스레드 미리 생성하기 서버는 고객의 첫 요청을 받기 전에 스레드 풀에 스레드를 미리 생성해두길 권장 처음 요청시 스레드 생성시간을 줄여 응답시간 빨라짐 처음 서버 올릴 때 CPU가 치고 올라오므로 미리 스레드 생성해두는게 좋음 ThreadPoolExecutor.prestartAllCoreThreads() 기본 스레드 미리 생성 ExecutorService는 해당 메서드 제공 X 스레드 풀 관리 전략 실무 전략 선택 - 개발자의 시간 아끼기 일반적인 상황이라면 고정 스레드 풀 전략이나 캐시 스레드 풀 전략 선택으로 충분 한 번에 처리할 수 있는 수를 제한해 안정적으로 처리 - 고정 풀 전략 돈이 많다면 풀의 수를 많이 늘리는 전략도 가능 (안정 + 사용자 요청 빠르게 대응) 사용자 요청에 빠르게 대응 - 캐시 스레드 풀 전략 일반 상황을 벗어날 정도로 서비스가 잘 운영되면, 그 때 최적화 시도 (사용자 정의 풀 전략) 각 전략의 특징 고정 스레드 풀 전략: 트래픽이 일정하고, 시스템 안전성이 가장 중요한 서비스 캐시 스레드 풀 전략: 일반적인 성장하는 서비스 사용자 정의 풀 전략: 다양한 상황에 대응 단일 스레드 풀 전략 (newSingleThreadPool()) 스레드 풀에 기본 스레드 1개만 사용 큐 사이즈 제한 X (LinkedBlockingQueue) 간단한 사용 및 테스트 용도 고정 스레드 풀 전략 (newFixedThreadPool(nThreads)) 스레드 풀에 nThreads 만큼의 기본 스레드 생성 (초과 스레드는 생성 X) 큐 사이즈 제한 X (LinkedBlockingQueue) 스레드 수가 고정되어 있어 CPU, 메모리 리소스가 어느정도 예측 가능한 안정적인 방식 장점 일반적인 상황에서 가장 안정적으로 서비스를 운영할 수 있음 단점 서버 자원(CPU, 메모리)이 여유가 있음에도 사용자가 증가하면 응답이 느려짐 실행되는 스레드 수가 고정되어 있어 요청 처리 시간 보다 큐에 쌓이는 시간이 빠름 e.g. 큐에 10000건 쌓여 있고, 고정 스레드 수가 10, 작업 처리 시간 1초 모든 작업 처리 시 1000초 걸림 서비스 초기 사용자 적을 때는 문제 없지만 사용자가 많아지면 문제 점진적 사용자 확대 시 서비스 응답이 점점 느려짐 갑작스런 요청 증가 시 고객이 응답을 받지 못함 캐시 스레드 풀 전략 (newCachedThreadPool()) 기본 스레드를 사용하지 않고 60초 생존 주기를 가진 초과 스레드만 사용 (스레드 수 제한 X) corePoolSize가 0, SynchronousQueue는 작업 넣기 불가, maxPoolSize는 무한 큐에 작업을 저장하지 않음 (SynchronousQueue, 저장 공간이 0인 특별한 큐) 생산자의 요청을스레드 풀의 소비자 스레드가 직접 받아서 바로 처리 모든 작업이 대기 없이 작업 수 만큼 초과 스레드가 생기면서 바로 실행 중간에 버퍼를 두지 않는 스레드 간 직거래 빠른 처리 O 장점 서버 자원을 최대로 사용할 수 있어 매우 빠름 (초과 스레드 수 제한 X) 작업 요청 수에 따라 스레드가 증감되어 유연한 처리 (생존 주기 내 스레드 적절히 재사용) 점진적 사용자 확대 시 크게 문제 되지 않음 사용자 증가 -> 스레드 사용량 증가 -> CPU, 메모리 사용량 증가 서버 자원에 한계를 고려해 적절한 시점에 시스템 증설 필요 단점 갑작스런 요청 증가 시 서버 자원의 임계점을 넘는 순간 시스템이 다운될 수 있음 사용자 급증 -> 스레드 수 급증 -> CPU, 메모리 사용량 급증 -> 시스템 전체 느려짐 너무 많은 스레드에 시스템이 잠식되어 장애 발생 수 천개 스레드가 처리하는 속도 보다 더 많은 작업 들어옴 수 천개 스레드로 메모리도 가득참 (1개 스레드는 1MB 이상) 사용자 정의 스레드 풀 관리 전략 목표 점진적인 사용자 확대 상황 처리 갑작스런 요청 증가 상황 처리 어떤 경우도 서버가 다운되어서는 안됨 전략 일반: 고정 크기 스레드로 서비스를 안정적으로 운영 (CPU, 메모리 예측 가능) 긴급: 사용자 요청 급증 시 초과 스레드 추가 투입 긴급 상황 때는 스레드 수가 늘어나므로 작업 처리 속도도 더 빨라짐 시스템 자원을 고려해 적정한 maxPoolSize를 설정해야함 거절: 긴급 대응도 어렵다면 추가되는 사용자 요청 거절 즉, 큐가 가득차고 초과 스레드도 모두 사용 중인데 작업이 더 들어오는 상황 = 처리 속도가 높아졌음에도 작업이 빠르게 소모되지 않는 상황 = 시스템이 감당하기 어려운 많은 요청이 들어오고 있는 것 구현 예시 ExecutorService es = new ThreadPoolExecutor(100, 200, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000)); 100개 기본 스레드 긴급 대응 초과 스레드 100개(60초 생존) 1000개 작업 가능한 큐 하나의 작업은 1초 걸림 일반 상황: static final int TASK_SIZE = 1100; 100개 기본 스레드로 처리 작업 처리 시간: 1100 / 100 = 11초 긴급 상황: static final int TASK_SIZE = 1200; 100개 기본 스레드 + 100개 초과 스레드로 처리 작업 처리 시간: 1200 / 200 = 6초 긴급 투입 스레드 덕분에 풀의 스레드 수가 2배가 되어 작업 2배 빠르게 처리 거절 상황: static final int TASK_SIZE = 1201; 100개 기본 스레드 + 100개 초과 스레드로 처리 1201번째 작업은 거절 (예외 발생) 실무 주의사항 new ThreadPoolExecutor(100, 200, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); 큐 사이즈: 무한대 큐 사이즈를 무한대로 해서는 절대로 안됨! 큐가 가득차야 긴급 상황으로 인지 가능 큐 사이즈가 무한대면 큐가 가득찰 수 없음 기본 스레드 100개만으로 무한대 작업을 처리하는 문제 발생 가장 좋은 최적화는 최적화하지 않는 것 예측 불가능한 먼 미래보다는 현재 상황에 맞는 최적화가 필요하다. 발생하지 않을 일에 최적화 시간을 쏟다가 버리는 경우가 많기 때문이다. 제일 비싼 자원은 개발자의 인건비다. 최적화에 쏟는 시간보다 간단히 서버 증설하는게 저렴할 수도 있다. 따라서, 중요한 것은 모니터링 환경을 잘 구축하는 것이다. 성장하는 서비스를 포함해 대부분의 서비스는 트래픽이 어느정도 예측 가능하다. 서비스 운영이 정말 잘되어 특출나게 유의미한 트래픽 상승이 예상된다면 그 때 최적화하자. 다만, 어떤 경우에도 절대 시스템이 다운되지 않도록 해야한다! Executor 거절 정책 ThreadPoolExecutor는 소비자가 처리할 수 없을 정도로 생산 요청이 가득 차면 후속 작업을 거절함 ThreadPoolExecutor는 작업을 거절하는 다양한 정책 제공 설정 방법 ThreadPoolExecutor 생성자 마지막에 원하는 정책을 인자로 전달 RejectedExecutionHandler 구현체 전달 ThreadPoolExecutor는 거절 상황이 발생 시 rejectedExecution() 호출 e.g. ExecutorService executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), ThreadPoolExecutor.AbortPolicy()); 정책 종류 (RejectedExecutionHandler 구현체) AbortPolicy (Default) 추가 작업 거절시 RejectedExecutionException 예외 발생시킴 개발자는 예외를 잡아서 작업을 포기하거나, 사용자에게 알리거나, 다시 시도 등 구현 가능 DiscardPolicy 추가 작업을 조용히 버림 CallerRunsPolicy 거절하지 않고 추가 작업을 제출하는 스레드가 대신해서 작업을 실행 = 소비자 스레드가 없어서 생산자 스레드가 작업을 대신 처리 해당 생산자 스레드의 속도가 저하될 수 있음 (Blocking) = 작업 생산 속도가 너무 빠를 때, 작업의 생산 속도를 늦출 수 있음 사용자 정의 개발자가 직접 정의한 거절 정책 사용 가능 (RejectedExecutionHandler 구현) e.g. static class MyRejectedExecutionHandler implements RejectedExecutionHandler { static AtomicInteger count = new AtomicInteger(0); @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { int i = count.incrementAndGet(); log("[경고] 거절된 누적 작업 수: " + i); } } Reference 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 Backend 멀티쓰레드 이해하고 통찰력 키우기
Java-Ecosystem
· 2024-10-02
자바 Collection Framework
자바 컬렉션 프레임 워크 자바는 컬렉션 프레임워크를 통해 다양한 자료구조를 인터페이스, 구현, 알고리즘으로 지원 데이터 컬렉션을 효율적으로 저장하고 처리하기 위한 통합 아키텍처 제공 (컬렉션 = 자료를 모아둔 것) 핵심 인터페이스 Collection 단일 루트 인터페이스로 모든 컬렉션 클래스가 상속 받음 필요성 가장 기본적인 인터페이스로 다양한 컬렉션 타입이 공통적으로 따라야하는 기본 규약 정의 이같은 설계는 일관성, 재사용성, 확장성 향상시키고 다형성 이점 제공 주요 메서드 add(E e) : 컬렉션에 요소를 추가 remove(Object o) : 주어진 객체를 컬렉션에서 제거 size() : 컬렉션에 포함된 요소의 수를 반환 isEmpty() : 컬렉션이 비어 있는지 확인 contains(Object o) : 컬렉션이 특정 요소를 포함하고 있는지 확인 iterator() : 컬렉션의 요소에 접근하기 위한 반복자를 반환 clear() : 컬렉션의 모든 요소를 제거 List 순서가 있는 컬렉션 중복 O 인덱스 통한 요소 접근 O 구현 ArrayList(주로 사용): 내부적으로 배열 사용 LinkedList: 내부적으로 연결 리스트 사용 Set 중복을 허용하지 않는 컬렉션 인덱스 통한 요소 접근 X 구현 HashSet(주로 사용): 내부적으로 해시 테이블 사용 LinkedHashSet: 내부적으로 해시 테이블과 연결리스트 사용 TreeSet: 내부적으로 레드-블랙 트리 사용 Queue 요소 처리 전 보관하는 컬렉션 구현 ArrayDeque(주로 사용): 내부적으로 배열 기반 원형 큐 사용 (대부분의 경우 빠름) LinkedList: 내부적으로 연결리스트 사용 PriorityQueue Map (Collection 상속 X) 키와 값 쌍으로 요소를 저장하는 객체 구현 HashMap(주로 사용): 내부적으로 해시 테이블 사용 LinkedHashMap: 내부적으로 해시 테이블과 연결리스트 사용 TreeMap: 내부적으로 레드-블랙 트리 사용 알고리즘 컬렉션 프레임워크는 데이터 처리 및 조작 알고리즘 제공 (정렬, 검색, 순환, 변환 등) 제공 방법 자료구조 자체적으로 기능 제공 Collections 와 Arrays 클래스에 정적 메서드 형태로 구현 실무 선택 전략 순서가 중요 O, 중복 허용 O 경우: List 인터페이스를 사용 ArrayList 선택 (주로 사용) 추가/삭제 작업이 앞쪽에서 빈번하다면 LinkedList (성능상 더 좋은 선택) 중복 허용 X 경우: Set 인터페이스 사용 순서가 중요하지 않다면 HashSet (주로 사용) 순서를 유지해야 하면 LinkedHashSet 정렬된 순서가 필요하면 TreeSet 요소를 키-값 쌍으로 저장하려는 경우: Map 인터페이스를 사용 순서가 중요하지 않다면 HashMap (주로 사용) 순서를 유지해야 한다면 LinkedHashMap 정렬된 순서가 필요하면 TreeMap 요소를 처리하기 전에 보관해야 하는 경우: Queue , Deque 인터페이스를 사용 ArrayDeque 선택 (주로 사용, 스택/큐 구조 모두에서 가장 빠름) 우선순위에 따라 요소를 처리해야 한다면 PriorityQueue 배열 (Array) 순서가 있고 중복을 허용하면서 크기가 정적으로 고정된 자료구조 가장 기본적인 자료구조 특징 데이터가 메모리 상에 순서대로 붙어서 존재 검색은 배열의 데이터를 하나하나 확인해야해서 한번에 찾을 수 없음 장점 인덱스 사용 시 최고의 효율 데이터가 아무리 많아도 인덱스는 한번의 계산으로 빠르게 자료 위치 찾음 공식: 배열의 시작 참조 + (자료의 크기 * 인덱스 위치) arr[0]: x100 + (4byte * 0): x100 arr[1]: x100 + (4byte * 1): x104 arr[2]: x100 + (4byte * 2): x108 단점 배열의 크기가 생성하는 시점에 정적으로 정해짐 처음부터 많이 확보하면 메모리 낭비 데이터 추가가 불편 기존 데이터가 오른쪽으로 한 칸씩 이동해야 함 시간 복잡도 데이터 추가 앞, 중간에 추가: O(N) 마지막에 추가: O(1) 데이터 삭제 앞, 중간에 삭제: O(N) 마지막에 삭제: O(1) 인덱스 조회, 입력, 변경: O(1) 데이터 검색: O(N) 리스트 (List) 순서가 있고 중복을 허용하면서 크기가 동적으로 변하는 자료구조 주요 메서드 add(E e): 리스트의 끝에 지정된 요소 추가 add(int index, E element): 리스트의 지정된 위치에 요소를 삽입 addAll(Collection<? extends E> c): 지정된 컬렉션의 모든 요소를 리스트의 끝에 추가 addAll(int index, Collection<? extends E> c): 지정된 컬렉션의 모든 요소를 리스트의 지정된 위치에 추가 get(int index): 리스트에서 지정된 위치의 요소를 반환 set(int index, E element): 지정한 위치의 요소를 변경하고, 이전 요소를 반환 remove(int index): 리스트에서 지정된 위치의 요소를 제거하고 그 요소를 반환 remove(Object o): 리스트에서 지정된 첫 번째 요소를 제거 clear(): 리스트에서 모든 요소를 제거 indexOf(Object o): 리스트에서 지정된 요소의 첫 번째 인덱스를 반환 lastIndexOf(Object o): 리스트에서 지정된 요소의 마지막 인덱스를 반환 contains(Object o): 리스트가 지정된 요소를 포함하고 있는지 여부를 반환 sort(Comparator<? super E> c): 리스트의 요소를 지정된 비교자에 따라 정렬 subList(int fromIndex, int toIndex): 리스트의 일부분의 뷰를 반환 size(): 리스트의 요소 수를 반환 isEmpty(): 리스트가 비어있는지 여부를 반환 iterator(): 리스트의 요소에 대한 반복자를 반환 toArray(): 리스트의 모든 요소를 배열로 반환 toArray(T[] a): 리스트의 모든 요소를 지정된 배열로 반환 실무 선택 전략 배열 리스트를 실무 기본 사용 (대부분의 경우 성능상 유리) 앞쪽에서 데이터 추가/삭제가 빈번하다면 연결 리스트 사용 고려 몇 천, 몇 만, 몇 십만 건 수준에서 유의미 몇 십, 몇 백 건 정도면 배열 리스트 사용 배열 리스트와 연결 리스트 실제 성능 비교 - 대부분 배열 리스트 유리 직접 구현한 구현체 비교 자바 구현체 비교 평균 추가는 이론적으로 연결 리스트가 빠를 수 있으나 실제로는 배열 리스트가 빠를 때가 많음 실제 성능은 현대 컴퓨터 시스템 환경의 다양한 요소에 의해 영향 받음 e.g. 요소의 순차적 접근 속도, 메모리 할당 및 해제 비용, CPU 캐시 활용도 등 배열 리스트는 CPU 캐시 효율과 메모리 접근 속도 좋음 요소들이 메모리에 연속적으로 위치 위치가 연속적이면 다음 데이터를 메모리에 미리 올릴 수 있음 CAPACITY 초과에 따른 배열 복사 과정은 드물기 때문에 성능 영향 X 이론과 실무는 차이가 있음! 자바의 배열 리스트는 앞, 중간 쪽 데이터 추가가 훨씬 빠르게 최적화됨 (메모리 고속 복사) 자바의 연결 리스트는 뒤 쪽에 데이터 추가하는 속도가 빠름 (이중 연결 리스트) 예시 구현 public interface MyList<E> { int size(); void add(E e); void add(int index, E e); E get(int index); E set(int index, E element); E remove(int index); int indexOf(E o); } 배열 리스트 (ArrayList) 데이터를 내부의 배열에 보관하는 리스트 구현체 특징 데이터 추가시 배열의 크기를 초과할 때마다 더 큰 크기의 배열을 새로 생성해 값 복사 후 사용 복사 전 기존 배열은 GC 대상 보통 50% 증가 사용 추가할 때마다 만들면 배열 복사 연산이 너무 많음 배열의 크기를 너무 크게 증가하면 메모리 낭비 발생 추가/삭제 시 인덱스로 위치 조회는 빠르지만 추가/삭제 작업 자체는 느림 인덱스로 위치 찾기: O(1) 추가/삭제 작업: O(N) - 데이터 이동 때문에 자바 배열 리스트 특징 기본 CAPACITY 는 10이고 넘어가면 50%씩 증가 메모리 고속 복사 사용해 최적화 (System.arraycopy()) 시스템 레벨에서 최적화된 메모리 고속 복사 연산을 사용 배열 요소 이동을 루프가 아니라 시스템 레벨에서 한 번에 빠르게 복사 (수 배 이상 빠름) 데이터가 많으면 고속 복사도 소용 없음 장점 조회가 빠름 끝 부분에 데이터 추가 및 삭제 작업 빠름 단점 앞, 중간 부분 데이터 추가 및 삭제 작업 느림 (데이터 이동으로 인한 성능 저하) 배열 뒷 부분에 낭비되는 메모리가 존재 시간 복잡도 데이터 추가: O(N) 앞, 중간에 추가: O(N) 마지막에 추가: O(1) 데이터 삭제: O(N) 앞, 중간에 삭제: O(N) 마지막에 삭제: O(1) 인덱스 조회: O(1) 데이터 검색: O(N) 예시 구현 public class MyArrayList<E> implements MyList<E> { private static final int DEFAULT_CAPACITY = 5; private Object[] elementData; private int size = 0; public MyArrayList() { elementData = new Object[DEFAULT_CAPACITY]; } public MyArrayList(int initialCapacity) { elementData = new Object[initialCapacity]; } @Override public int size() { return size; } @Override public void add(E e) { if (size == elementData.length) { grow(); } elementData[size] = e; size++; } @Override public void add(int index, E e) { if (size == elementData.length) { grow(); } shiftRightFrom(index); elementData[index] = e; size++; } //요소의 마지막부터 index까지 오른쪽으로 밀기 private void shiftRightFrom(int index) { for (int i = size; i > index; i--) { elementData[i] = elementData[i - 1]; } } @Override @SuppressWarnings("unchecked") public E get(int index) { return (E) elementData[index]; } @Override public E set(int index, E element) { E oldValue = get(index); elementData[index] = element; return oldValue; } @Override public E remove(int index) { E oldValue = get(index); shiftLeftFrom(index); size--; elementData[size] = null; return oldValue; } //요소의 index부터 마지막까지 왼쪽으로 밀기 private void shiftLeftFrom(int index) { for (int i = index; i < size - 1; i++) { elementData[i] = elementData[i + 1]; } } @Override public int indexOf(E o) { for (int i = 0; i < size; i++) { if (o.equals(elementData[i])) { return i; } } return -1; } private void grow() { int oldCapacity = elementData.length; int newCapacity = oldCapacity * 2; elementData = Arrays.copyOf(elementData, newCapacity); } @Override public String toString() { return Arrays.toString(Arrays.copyOf(elementData, size)) + " size=" + size + ", capacity=" + elementData.length; } } 연결 리스트 (LinkedList) 노드를 만들어 각 노드끼리 서로 연결하는 리스트 구현체 특징 노드와 링크로 구성 추가/삭제 시 인덱스로 위치 조회는 느리지만 추가/삭제 작업 자체는 빠름 인덱스로 위치 찾기: O(N) - 데이터 탐색 때문 추가/삭제 작업: O(1) - 필요한 노드끼리 참조만 변경하면 끝 자바 연결 리스트 특징 class Node { E item; Node next; Node prev; } class LinkedList { Node first; //첫 번째 노드 참조 Node last; //마지막 노드 참조 int size; } 이중 연결 리스트 구조 & 첫 번째 노드와 마지막 노드 둘 다 참조 데이터를 끝에 추가하는 경우도 O(1) 역방향 조회 가능 -> 인덱스 조회 성능 최적화 (size 절반을 기준으로 조회 시작 위치 최적화) 장점 앞 부분 데이터 추가 및 삭제 작업 빠름 필요한만큼만 동적으로 노드를 생성 및 연결하므로 메모리 낭비 X 다만 크게 봤을 때 배열에 비해 메모리가 엄청 절약 X (연결 유지 위한 추가 메모리 사용, next) 단점 중간, 끝 부분 데이터 추가 및 삭제 작업 느림 시간 복잡도 데이터 추가: O(N) 앞에 추가: O(1) 중간, 마지막에 추가: O(N) 데이터 삭제: O(N) 앞에 삭제: O(1) 중간, 마지막에 삭제: O(N) 인덱스 조회: O(N) 데이터 검색: O(N) 예시 구현 public class MyLinkedList<E> implements MyList<E> { private Node<E> first; private int size = 0; @Override public void add(E e) { Node<E> newNode = new Node<>(e); if (first == null) { first = newNode; } else { Node<E> lastNode = getLastNode(); lastNode.next = newNode; } size++; } private Node<E> getLastNode() { Node<E> x = first; while (x.next != null) { x = x.next; } return x; } @Override public void add(int index, E e) { Node<E> newNode = new Node<>(e); if (index == 0) { newNode.next = first; first = newNode; } else { Node<E> prev = getNode(index - 1); newNode.next = prev.next; prev.next = newNode; } size++; } @Override public E set(int index, E element) { Node<E> x = getNode(index); E oldValue = x.item; x.item = element; return oldValue; } @Override public E remove(int index) { Node<E> removeNode = getNode(index); E removedItem = removeNode.item; if (index == 0) { first = removeNode.next; } else { Node<E> prev = getNode(index - 1); prev.next = removeNode.next; } removeNode.item = null; removeNode.next = null; size--; return removedItem; } @Override public E get(int index) { Node<E> node = getNode(index); return node.item; } private Node<E> getNode(int index) { Node<E> x = first; for (int i = 0; i < index; i++) { x = x.next; } return x; } @Override public int indexOf(E o) { int index = 0; for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } return -1; } @Override public int size() { return size; } @Override public String toString() { return "MyLinkedList{" + "first=" + first + ", size=" + size + '}'; } private static class Node<E> { E item; Node<E> next; public Node(E item) { this.item = item; } @Override // 가독성 위해 직접 구현 e.g. [A->B->C] public String toString() { StringBuilder sb = new StringBuilder(); Node<E> temp = this; sb.append("["); while (temp != null) { sb.append(temp.item); if (temp.next != null) { sb.append("->"); } temp = temp.next; } sb.append("]"); return sb.toString(); } } } 자료구조와 제네릭 일반적으로 하나의 자료구조에는 같은 데이터 타입을 보관하고 관리한다. 숫자와 문자처럼 관계 없는 여러 데이터 타입을 섞어 보관하는 일은 거의 없다. 따라서, 자료구조에 제네릭을 사용하면 타입 안정성이 높은 자료구조를 만들 수 있어 매우 어울린다. 만약 배열을 사용하는 경우, 제네릭을 적용해도 내부 배열의 타입은 Object[] elementData을 사용할 것이다. 문제는 생성자 코드에서 배열을 생성할 때이다. new E[DEFAULT_CAPACITY] 제네릭은 타입 매개변수에 의한 new를 허용하지 않는다. 또한, 런타임에 타입 정보가 필요한 생성자에서 타입 매개변수를 사용할 수 없다. 이런 제네릭의 한계로 인해 Object[] 타입 배열을 적용하고 생성자에서 다음 코드를 사용해야 한다. new Object[DEFAULT_CAPACITY] Object[] 타입 적용은 결국 자료구조 내부에서 다운캐스팅을 사용하게되는데, 큰 문제는 없다. Object 자체는 모든 데이터를 담을 수 있어 신경쓸게 없으니, 조회하는 부분에 초점을 맞춰보자. 자료를 입력하는 add(E e) 메서드에서 E 타입만 보관하는 덕분에, get() 메서드에서 데이터를 조회 후 (E)로 다운캐스팅해 반환해도 전혀 문제가 없다. 이중 연결 리스트 노드 앞뒤로 연결하는 이중 연결 리스트는 성능을 더 개선할 수 있다. 특히, 자바가 제공하는 연결 리스트도 이중 연결 리스트다. 마지막 노드를 참조하는 변수를 가지고 있어서, 뒤에 추가하거나 삭제하는 경우에도 O(1) 성능을 제공한다. 재사용성 높이기 프로그래밍 세계에서는 결정을 나중으로 미루면 재사용성이 높아진다. e.g. 함수 매개변수, 제네릭 타입, 추상화 의존 & 구체적 구현 미루기 이론과 실무의 차이 자료구조를 배울 때 변경 작업이 많으면 LinkedList를 사용하라고 배우지만, 실제로는 ArrayList가 훨씬 빠르다. 이론과 실무의 차이를 유의해야 한다. 해시 알고리즘 (Hash) 주의점: 해시 자료구조 사용 시, 직접정의 객체는 hashCode()와 equals() 반드시 재정의해야 함 (IDE) 동등성을 확보해야 함 hashCode(): 참조 값 기반이 아닌 내부 값 기반으로 해시 코드 생성 equals(): contains() 메서드 실행 시 버킷 내 각각의 값 비교할 때 필요 직접 오버라이딩 하지 않을 시, Object 기본 동일성 비교 구현 실행되어서 문제 hashCode() 구현 X, equals() 구현 X 경우 참조값 기반으로 해시코드가 생성되어 실행 때마다 값이 다름 논리적으로 같은 데이터가 다른 메모리 위치에 중복 저장 다른 위치에서 데이터 조회해 검색 실패 hashCode() 구현 O, equals() 구현 X 경우 같은 해시코드가 생성되어 같은 해시 인덱스 메모리 위치에 저장 equals()가 동일성 비교 수행해, 논리적으로 같은 데이터 중복 저장 해시 인덱스는 정확히 찾으나, 동일성 비교로 인해 검색 실패 hashCode() 구현 O, equals() 구현 O 경우 논리적으로 같은 데이터는 중복 저장 X 해시 인덱스도 정확히 찾고, 동등성 비교로 검색도 성공 기본 아이디어 나머지 연산을 사용해 데이터 값 자체를 배열의 인덱스로 사용하자 배열의 크기만 적절히 확보하면 데이터가 고루 분산 입력 데이터 수와 비교해 배열의 크기가 클수록 충돌 확률은 낮아짐 통계적으로 입력 데이터 수가 배열 크기의 75%를 넘지 않으면 해시 충돌이 자주 발생 X 결과: 데이터 검색 성능 비약적 향상 기존 순차 데이터 검색: O(N) 해시 알고리즘: 해시 충돌이 적도록 제어하면 대부분 O(1) 해결 과정 1단계: 배열 인덱스 사용 데이터 검색 성능이 O(N) 문제 -> O(1) 개선 2단계: 해시 인덱스를 배열의 인덱스로 사용 (feat. 나머지 연산) 입력 값 범위가 크면 그만큼 큰 배열을 사용해서 메모리가 낭비되는 문제 해결 참고로 int 범위 만큼의 큰 배열은 약 17기가 바이트 소모 해시 인덱스: 배열의 인덱스로 사용할 수 있도록 원래 값을 계산한 인덱스 e.g. CAPACITY=10일 때, 14의 해시 인덱스는 4, 99의 해시 인덱스는 9 해시 인덱스 생성 O(1) + 해시 인덱스를 사용해 배열에서 값 조회 O(1) => O(1) 3단계: 해시 충돌 가능성을 인정하고 배열 내 배열 혹은 배열 내 리스트를 이중 사용해 실제 값 보관 최악의 경우 O(N)이지만 확률적으로 어느정도 넓게 퍼지므로 대부분 O(1) 성능일 것 e.g. 9, 19, 29, 99 해시 충돌이 일어나면 해당 인덱스의 배열에서 모든 값을 비교해 검색 - O(N) 해시 충돌 가끔 발생해도 내부에서 값 몇 번만 비교하는 수준이므로 대부분 매우 빠른 조회 해시 충돌: 다른 값을 입력했지만 같은 해시 코드가 나오는 것 e.g. CAPACITY=10일 때, 9와 99의 해시 인덱스는 모두 9로 겹침 예시 코드 public class HashStart { static final int CAPACITY = 10; public static void main(String[] args) { //{1, 2, 5, 8, 14, 99 ,9} LinkedList<Integer>[] buckets = new LinkedList[CAPACITY]; for (int i = 0; i < CAPACITY; i++) { buckets[i] = new LinkedList<>(); } add(buckets, 1); ... add(buckets, 99); add(buckets, 9); //중복 //검색 int searchValue = 9; boolean contains = contains(buckets, searchValue); // true } private static void add(LinkedList<Integer>[] buckets, int value) { int hashIndex = hashIndex(value); LinkedList<Integer> bucket = buckets[hashIndex]; // O(1) if (!bucket.contains(value)) { // O(n) bucket.add(value); } } private static boolean contains(LinkedList<Integer>[] buckets, int searchValue) { int hashIndex = hashIndex(searchValue); LinkedList<Integer> bucket = buckets[hashIndex]; // O(1) return bucket.contains(searchValue); // O(n) } static int hashIndex(int value) { return value % CAPACITY; } } 해시 용어 해시 함수 임의의 길이의 데이터를 입력 받아 고정된 길이의 해시 값(해시 코드)을 출력하는 함수 고정된 길이는 저장 공간의 크기를 의미 e.g. int 형 1, 100은 둘다 4byte 같은 데이터를 입력하면 항상 같은 해시 코드 출력 다른 데이터를 입력해도 같은 해시코드가 출력될 수 있음 (해시 충돌) 해시 함수는 해시 코드가 최대한 충돌하지 않도록 설계해야 함 해시 충돌은 결과적으로 성능 하락 좋은 해시 함수는 해시 코드를 균일하게 분산시키는 것 -> 해시 인덱스도 분산 자바 해시 함수는 내부에 복잡한 연산으로 다양한 범위의 해시 코드 생성 -> 성능 최적화 해시 코드 (해시 함수를 통해 생성) 데이터를 대표하는 값 모든 문자 데이터는 고유한 숫자로 표현 가능 (ASCII 코드) 컴퓨터는 문자를 이해하지 못해 각 문자에 고유한 숫자를 할당해 인식 및 저장 char -> int형 캐스팅으로 확인 가능 char charA = 'A'; (int) charA // 65 연속된 문자는 각 문자의 고유 숫자 합으로 표현 가능 e.g. “AB” -> 65 + 66 = 131 물론, 자바 해시 함수는 더 복잡한 연산 수행 어떤 객체든 정수 해시 코드만 정의하면 해시 인덱스 사용 가능 Object.hashCode() 모든 객체가 자신만의 해시 코드를 표현할 수 있는 기능 보통 오버라이딩해 내부 값 기반으로 동등성 확보하도록 사용 (IDE 이용) 기본 구현: 객체의 참조값을 기반으로 해시 코드 생성 인스턴스가 다르면 해시코드 다름 자바 기본 클래스는 이미 재정의해 둠 값이 같으면 같은 해시 코드 hashCode() 결과는 음수가 나올 수 있는데 절대 값 사용 새 객체 정의 시 직접 오버라이딩 e.g. Member 객체 정의할 때 id 기반으로 해시 코드 생성 @Override public int hashCode() { return Objects.hash(id); } 해시 인덱스(해시 코드를 사용해 생성) 데이터의 저장 위치를 결정하는 값 보통 해시 인덱스 = 해시 코드 % 배열의 크기 셋 (Set) 순서가 없고 중복을 허용하지 않는 자료구조 특징 요소의 유무(=중복 데이터 체크)를 빠르게 확인 가능 (contains()) 해시 알고리즘을 통해 데이터 검색 성능 향상 O(N) -> O(1) 데이터 추가/삭제 시 반드시 필요한 중복 체크에도 유용 덕분에 검색, 추가, 삭제 모두 O(N) -> O(1) 개선 주요 메서드 add(E e): 지정된 요소를 셋에 추가 (이미 존재하는 경우 추가하지 않음) addAll(Collection<? extends E> c): 지정된 컬렉션의 모든 요소를 셋에 추가 contains(Object o): 셋이 지정된 요소를 포함하고 있는지 여부를 반환 containsAll(Collection<?> c): 셋이 지정 컬렉션의 모든 요소를 포함하고 있는지 여부 반환 remove(Object o): 지정된 요소를셋에서 제거 removeAll(Collection<?> c): 지정된 컬렉션에 포함된 요소를 셋에서 모두 제거 retainAll(Collection<?> c): 지정 컬렉션에 포함된 요소만 유지, 나머지 요소는 셋에서 제거 clear(): 셋에서 모든 요소를 제거 size(): 셋에 있는 요소의 수를 반환 isEmpty(): 셋이 비어 있는지 여부를 반환 iterator(): 셋의 요소에 대한 반복자를 반환 toArray(): 셋의 모든 요소를 배열로 반환 toArray(T[] a): 셋의 모든 요소를 지정된 배열로 반환 실무 선택 전략 HashSet 권장 입력 순서 유지, 값 정렬의 필요에 따라서 LinkedHashSet, TreeSet 고려 코드 예시 - 합집합, 교집합, 차집합 public class SetOperationsTest { public static void main(String[] args) { Set<Integer> set1 = new HashSet<>(List.of(1, 2, 3, 4, 5)); Set<Integer> set2 = new HashSet<>(List.of(3, 4, 5, 6, 7)); Set<Integer> union = new HashSet<>(set1); union.addAll(set2); Set<Integer> intersection = new HashSet<>(set1); intersection.retainAll(set2); Set<Integer> difference = new HashSet<>(set1); difference.removeAll(set2); System.out.println("합집합: " + union); System.out.println("교집합: " + intersection); System.out.println("차집합: " + difference); } } HashSet 배열에 해시 알고리즘을 적용해 구현 요소의 순서 보장 X 자바 HashSet 특징 재해싱 (rehashing) 최적화 배열 크기의 75%를 넘어가면 배열 크기를 2배로 늘리고 모든 요소에 해시 인덱스를 다시 적용 재적용 시간은 걸리지만, 해시 충돌을 줄이고 O(N) 성능 문제를 예방 키만 저장하는 특수한 형태의 해시 테이블 해시 테이블: 해시를 사용해서 키와 값을 저장하는 자료 구조 (HashMap) 자바는 해시 테이블의 원리를 이용하나 Value만 비워두고 사용 (HashMap 구현 활용) 시간 복잡도 데이터 추가: O(1) 데이터 삭제: O(1) 데이터 검색: O(1) 예시 코드 (HashSet) public class MyHashSet<E> implements MySet<E> { static final int DEFAULT_INITIAL_CAPACITY = 16; private LinkedList<E>[] buckets; private int size = 0; private int capacity = DEFAULT_INITIAL_CAPACITY; public MyHashSet() { initBuckets(); } public MyHashSet(int capacity) { this.capacity = capacity; initBuckets(); } private void initBuckets() { buckets = new LinkedList[capacity]; for (int i = 0; i < capacity; i++) { buckets[i] = new LinkedList<>(); } } @Override public boolean add(E value) { int hashIndex = hashIndex(value); LinkedList<E> bucket = buckets[hashIndex]; if (bucket.contains(value)) { return false; } bucket.add(value); size++; return true; } @Override public boolean contains(E searchValue) { int hashIndex = hashIndex(searchValue); LinkedList<E> bucket = buckets[hashIndex]; return bucket.contains(searchValue); } @Override public boolean remove(E value) { int hashIndex = hashIndex(value); LinkedList<E> bucket = buckets[hashIndex]; boolean result = bucket.remove(value); if (result) { size--; return true; } else { return false; } } private int hashIndex(Object value) { //hashCode의 결과로 음수가 나올 수 있다. abs()를 사용해서 마이너스를 제거한다. return Math.abs(value.hashCode()) % capacity; } public int getSize() { return size; } @Override public String toString() { return "MyHashSet{" + "buckets=" + Arrays.toString(buckets) + ", size=" + size + ", capacity=" + capacity + '}'; } } LinkedHashSet HashSet에 연결 리스트를 추가해 구현 요소의 입력된 순서 보장 O 연결 링크 유지로 인해 HashSet 보다 조금 더 무거움 연결 링크는 데이터를 입력한 순서대로 연결 (양방향 연결) first 부터 따라가면 입력 순서대로 데이터 순회 가능 시간 복잡도 데이터 추가: O(1) 데이터 삭제: O(1) 데이터 검색: O(1) TreeSet 이진 탐색 트리를 개선한 레드-블랙 트리를 내부에서 사용해 구현 이진 트리: 자식이 2개까지 올 수 있는 트리 이진 탐색 트리: 부모 노드의 값과 비교해 왼쪽 자식이 더 작은 값, 오른쪽 자식이 더 큰 값 가지는 트리 이진 탐색 트리 개선 이진 탐색 트리는 데이터 균형이 맞지 않으면 최악의 경우 O(N) 해결 방안: 동적으로 균형 다시 맞추기 균형 맞추기 알고리즘 사용 (AVL 트리, 레드-블랙 트리) 자바 TreeSet은 레드-블랙 트리를 사용해 최악의 경우에도 O(log N) 성능 제공 데이터 값의 순서 보장 O 데이터를 값 기준 정렬된 순서로 저장 (값 기준은 Comparator 비교자 이용) 중위 순회를 통해 데이터를 오름차순으로 순회 가능 e.g. 3, 1, 2 입력해도 1, 2, 3 순서로 출력 시간 복잡도 - HashSet 보다 느리지만 데이터 검색 시 한 번의 계산에 절반을 날리는 특징 데이터 추가: O(log N) 데이터 삭제: O(log N) 데이터 검색: O(log N) 맵(Map) 키-값 쌍을 저장하는 자료구조 주의점 HashMap, LinkedHashMap: Key로 쓰이는 객체는 hashCode(), equals() 반드시 구현할 것 특징 키는 중복 X, 값은 중복 O, 순서 유지 X Collection 상속 X 내부에서 Entry(인터페이스)를 통해 키 값을 묶어 저장 맵의 모든 것은 Key를 중심으로 동작하고 Key는 Set과 같은 구조 Map과 Set은 거의 같음 (단순히 Value가 있는지 없는지 차이) Key 옆에 Value만 단순히 추가해주면 Map 따라서, Map과 Set의 구현체도 거의 동일 실제로 자바는 HashSet 구현에 HashMap의 구현을 가져다 씀 Map에서 Value만 비워두면 Set으로 사용 가능 키를 통해 빠르게 검색 가능 - O(1) 주요 메서드 put(K key, V value): 지정된 키와 값을 맵에 저장 (같은 키가 있으면 값을 변경) putIfAbsent(K key, V value): 지정된 키가 없는 경우에 키와 값을 맵에 저장 putAll(Map<? extends K,? extends V> m): 지정된 맵의 모든 매핑을 현재 맵에 복사 get(Object key): 지정된 키에 연결된 값을 반환 getOrDefault(Object key, V defaultValue): 지정된 키에 연결된 값을 반환, 키가 없는 경우 defaultValue 로 지정한 값을 대신 반환 remove(Object key): 지정된 키와 그에 연결된 값을 맵에서 제거 clear(): 맵에서 모든 키와 값을 제거 containsKey(Object key): 맵이 지정된 키를 포함하고 있는지 여부를 반환 - O(1) containsValue(Object value): 맵에 값이 있는지 여부 반환 - O(N), 다 뒤져봐야 함 keySet(): 맵의 키들을 Set 형태로 반환 - 키가 중복을 허용하지 않으므로 Set 반환 values(): 맵의 값들을 Collection 형태로 반환 - List, Set이 애매하므로 단순히 값의 모음이라는 의미의 Collection 반환 (맵의 값들은 중복 O, 순서 보장 X) entrySet(): 맵의 키-값 쌍을 Set<Map.Entry<K,V>> 형태로 환한다. size(): 맵에 있는 키-값 쌍의 개수를 반환 isEmpty(): 맵이 비어 있는지 여부를 반환 실무 선택 전략 HashMap 권장 순서 유지, 정렬의 필요에 따라서 LinkedHashMap, TreeMap 고려 코드 예시 - keySet(), entrySet(), values() 활용 Map<String, Integer> studentMap = new HashMap<>(); System.out.println("keySet 활용"); Set<String> keySet = studentMap.keySet(); for (String key : keySet) { Integer value = studentMap.get(key); System.out.println("key=" + key + ", value=" + value); } System.out.println("entrySet 활용"); Set<Map.Entry<String, Integer>> entries = studentMap.entrySet(); for (Map.Entry<String, Integer> entry : entries) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println("key=" + key + ", value=" + value); } System.out.println("values 활용"); Collection<Integer> values = studentMap.values(); for (Integer value : values) { System.out.println("value = " + value); } 코드 예시 - 단어 수 세기 public class WordFrequencyTest { public static void main(String[] args) { String text = "orange banana apple apple banana apple"; Map<String, Integer> map = new HashMap<>(); String[] words = text.split(" "); for (String word : words) { map.put(word, map.getOrDefault(word, 0) + 1); } System.out.println(map); } } HashMap 해시를 사용해 키와 값을 저장하는 자료구조 (=해시 테이블 =딕셔너리) HashSet과 동작 원리 동일 Key 값을 사용해 해시 코드 생성 다만, Entry 사용해 Key, Value 묶어 저장 순서 보장 X 시간 복잡도 데이터 추가: O(1) 데이터 삭제: O(1) 데이터 검색: O(1) LinkedHashMap HashMap에 연결 리스트를 추가해 구현 요소의 입력된 순서 보장 O - 입력 순서대로 데이터 순회 가능 연결 링크 유지로 인해 HashMap 보다 조금 더 무거움 시간 복잡도 데이터 추가: O(1) 데이터 삭제: O(1) 데이터 검색: O(1) TreeMap 레드-블랙 트리를 내부에서 사용해 구현 키 자체 데이터 값 기준으로 정렬된 순서 보장 O (값 기준은 Comparator 비교자 이용) 시간 복잡도 데이터 추가: O(log N) 데이터 삭제: O(log N) 데이터 검색: O(log N) Stack, Queue, Deque 실무 선택 전략 스택, 큐 모두 deque의 ArrayDeque 구현체 사용 권장 (성능이 빠름) 큐 사용 시 단순히 큐 기능만 필요하면 Queue 인터페이스 사용 더 많은 기능이 필요하면 Deque 인터페이스 사용 스택 (Stack) 후입선출 (LIFO, Last In First Out) 자료구조 전통적으로 값을 넣는 것을 push, 값을 꺼내는 것을 pop 이라고 함 Deque을 사용해 구현해야 함 Stack 구현체 클래스는 내부에서 Vector 사용하는데 하위호환을 존재하므로 사용 권장 X 큐 (Queue) 선입선출 (FIFO, First In First Out) 자료구조 전통적으로 값을 넣는 것을 offer, 값을 꺼내는 것을 poll 이라고 함 덱 (Deque, Double Ended Queue) 양쪽 끝에서 요소를 추가하거나 제거 가능 offerFirst() : 앞에 추가 offerLast() : 뒤에 추가 pollFirst() : 앞에서 꺼냄 pollLast() : 뒤에서 꺼냄 일반적인 큐(Queue)와 스택(Stack)의 기능을 모두 포함하고 있어, 매우 유연한 자료 구조 스택 사용 위한 메서드 제공 push(): 앞에서 입력 pop(): 앞에서 꺼냄 큐 사용 위한 메서드 제공 offer(): 뒤에서 입력 poll(): 앞에서 꺼냄 참고로 둘 다 다음 데이터 단순 확인용으로 peek() 사용 가능 (앞에서 확인) 구현체: ArrayDeque, LinkedList ArrayDeque 이 모든 면에서 가장 빠름 배열 사용은 현대 컴퓨터 시스템에서 더 나은 성능 발휘할 때가 많음 ArrayList vs LinkedList 차이와 비슷 추가로 원형 큐 자료 구조 사용해 앞, 뒤 입력 모두 O(1) 물론 자바 LinkedList도 앞, 뒤 입력이 O(1) 성능 비교 100만 건 입력 (앞, 뒤 평균) ArrayDeque : 110ms LinkedList : 480ms 100만 건 조회 (앞, 뒤 평균) ArrayDeque : 9ms LinkedList : 20ms Iterable, Iterator - Iterator 디자인 패턴 순회 자료구조에 들어 있는 데이터를 차례대로 접근해서 처리하는 것 Iterator(반복자) 디자인 패턴 객체 지향 프로그래밍에서 컬렉션의 요소들을 순회할 때 사용되는 디자인 패턴 컬렉션의 구현과 독립적으로 요소들을 탐색할 수 있는 방법 제공 코드 복잡성 감소, 재사용성 상승 문제: 자료 구조마다 데이터를 접근하는 방법이 모두 다름 e.g. 배열 리스트는 인덱스, 연결 리스트는 노드 순회 개발자가 각 자료구조 내부구조와 순회 방법을 배워야 함 해결책: Iterable, Iterator 인터페이스 (자바 제공) 자료구조 구현과 관계 없이 모든 자료 구조를 일관성 있는 동일한 방법으로 순회 가능 추상화한 순회 과정 반복 과정: 다음 요소가 있는지 물어보기 & 있으면 다음 요소 꺼내기 다음 요소가 없으면 종료 자바 컬렉션 프레임워크는 Iterable 인터페이스와 각 구현체에 맞는 Iterator 구현해 제공 Collection 인터페이스 상위에 Iterable 존재 -> 모든 컬렉션이 순회 가능 Map은 Iterable이 없어 바로 순회 불가 keySet(), values(), entrySet()으로 Set이나 Collection 받아 순회 Iterable을 구현한 자료구조는 iterator를 반환하고 for-each 문이 작동한다는 의미 개발자는 hasNext(), next() 페어 혹은 for-each 문으로 쉽게 자료구조 순회 가능 Iterable public interface Iterable<T> { Iterator<T> iterator(); } 단순히 Iterator 반복자를 반환 Iterator public interface Iterator<E> { boolean hasNext(); E next(); } hasNext() : 다음 요소가 있는지 확인, 다음 요소가 없으면 false 를 반환 next() : 다음 요소를 반환, 내부에 있는 위치를 다음으로 이동 Enhanced For Loop와 Iterable 자바는 Iterable 인터페이스를 구현한 객체에 대해서 향상된 for 문을 사용 지원 자바는 컴파일 시점에 다음과 같이 코드 변경 변경 전 for (int value : myArray) { System.out.println("value = " + value); } 변경 후 while (iterator.hasNext()) { Integer value = iterator.next(); System.out.println("value = " + value); } 코드 예시 public class MyArrayIterator implements Iterator<Integer> { private int currentIndex = -1; private int[] targetArr; public MyArrayIterator(int[] targetArr) { this.targetArr = targetArr; } @Override public boolean hasNext() { return currentIndex < targetArr.length - 1; } @Override public Integer next() { return targetArr[++currentIndex]; } } public class MyArray implements Iterable<Integer> { private int[] numbers; public MyArray(int[] numbers) { this.numbers = numbers; } @Override public Iterator<Integer> iterator() { return new MyArrayIterator(numbers); } } Comparable, Comparator 실무 사용법 객체 기본 정렬 방법은 객체에 Comparable 구현해 정의 기본 정렬 외 다른 정렬을 사용해야 하는 경우 Comparator 구현해 정렬 메서드에 전달 이 경우 전달한 Comparator가 항상 우선권 가짐 정렬 비교 기준 설정 (추상화를 통해 정렬 기준만 간단히 변경 가능) e.g. 배열 정렬 - Arrays.sort() (비교자 전달 가능) List 정렬 - Collections.sort(list), list.sort(null) (비교자 전달 가능) Tree 구조 정렬 저장부터 정렬 필요하므로 TreeSet, TreeMap은 Comparable, Comparator 필수 new TreeSet<>() - 객체의 Comparable로 정렬 new TreeSet<>(new IdComparator()) - 인자로 넘긴 Comparator로 정렬 정렬 시 Comparable, Comparator 둘 다 없으면 런타임 오류 java.lang.ClassCastException: class collection.compare.MyUser cannot be cast to class java.lang.Comparable Comparable 없어도 Comparator 주면 괜찮음! Comparator 인터페이스 (비교자) public interface Comparator<T> { int compare(T o1, T o2); } 두 값을 비교할 때 비교 기준을 제공 compare(): 두 인수를 비교해 결과값 반환 첫 번째 인수가 더 작으면 음수 e.g. -1 두 값이 같으면 제로 e.g. 0 첫 번째 인수가 더 크면 양수 e.g. 1 Comparable 인터페이스 public interface Comparable<T> { public int compareTo(T o); } 사용자 정의 객체에 정렬 비교 기준 설정 (비교 기능 추가) Comparable 통해 구현한 순서를 자연 순서(Natural Ordering)라고 함 compareTo(): 자기 자신과 인수로 넘어온 객체 비교해 결과값 반환 현재 객체가 인수로 주어진 객체보다 더 작으면 음수 e.g. -1 두 객체의 크기가 같으면 제로 e.g. 0 현재 객체가 인수로 주어진 객체보다 더 크면 양수 e.g. 1 예시 코드 - 정렬 (Comparator 전달) public class SortMain { public static void main(String[] args) { Integer[] array = {3, 2, 1}; System.out.println("Comparator 비교"); Arrays.sort(array, new AscComparator()); // 1, 2, 3 Arrays.sort(array, new DescComparator()); // 3, 2, 1 //DescComparator와 같다. Arrays.sort(array, new AscComparator().reversed()); // 3, 2, 1 } static class AscComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return (o1 < o2) ? -1 : ((o1 == o2) ? 0 : 1); } } static class DescComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return (o1 < o2) ? -1 : ((o1 == o2) ? 0 : 1) * -1; } } } 예시 코드 - 객체 Comparable 정의 public class MyUser implements Comparable<MyUser> { private String id; private int age; ... //나이 오름차순으로 구현 @Override public int compareTo(MyUser o) { return this.age < o.age ? -1 : (this.age == o.age ? 0 : 1); } } Arrays 유틸 Arrays.toString() 배열을 문자열로 보기 좋게 정제해 반환 Arrays.copyOf(기존배열, 새로운 길이) 새로운 길이로 배열을 생성하고 기존 배열 값을 새로운 배열에 복사 Arrays.sort() 배열에 들어있는 데이터를 순서대로 정렬 시간 복잡도: O(N log N) 자바 구현 알고리즘 초기: 퀵소트 현재: 데이터가 적을 때(32개 이하) 듀얼 피벗 퀵소트(Dual-Pivot QuickSort) 사용 데이터가 많을 때 팀소트(Tim Sort) 사용 종류 Arrays.sort(배열) 자연 순서 기준으로 정렬 (Comparable 기준) Arrays.sort(배열, Comparator) Comparator 기준으로 정렬 Comparator 전달 시 객체 Comparable 보다 우선순위 가짐 컬렉션 유틸 컬렉션을 편리하게 다룰 수 있는 다양한 기능 제공 Collections 정렬 관련 메서드 max : 정렬 기준으로 최대 값을 찾아서 반환 min : 정렬 기준으로 최소 값을 찾아서 반환 shuffle : 컬렉션을 랜덤하게 섞음 sort : 정렬 기준으로 컬렉션을 정렬 reverse : 정렬 기준의 반대로 컬렉션을 정렬 편리한 컬렉션 생성 불변 컬렉션 생성 (of(), 사용 권장) 생성한 객체는 불변 (add(), put(), remove() 불가) 변경 시도 시 UnsupportedOperationException 예외 발생 불변을 위한 다른 구현체 사용 e.g. class java.util.ImmutableCollections$ListN List 인터페이스에 불변을 위한 다른 구현체 제공 List, Set, Map 모두 of() 지원 List<Integer> list = List.of(1, 2, 3); Set<Integer> set = Set.of(1, 2, 3); Map<String, Integer> map = Map.of("A", 1, "B", 2, "C", 3); 배열을 리스트로 변환하기도 지원 Integer[] inputArr = {30, 20, 20, 10, 10}; List<Integer> list = Arrays.asList(inputArr); List<Integer> list = List.of(inputArr); 참고: 생성자 전달 방법 Set은 생성자에 List를 받을 수 있음 (배열은 못 받음) Integer[] inputArr = {30, 20, 20, 10, 10}; Set<Integer> set = new LinkedHashSet<>(List.of(inputArr)); 가변 컬렉션으로 전환 불변 -> 가변 (new XxxXxx<>(list)) List<Integer> list = List.of(1, 2, 3);// 불변 리스트 생성 ArrayList<Integer> mutableList = new ArrayList<>(list);// 가변 가변 -> 불변 (Collections.unmodifiableXxx()) List<Integer> unmodifiableList = Collections.unmodifiableList(mutableList); //java.util.Collections$UnmodifiableRandomAccessList 빈 컬렉션 생성 빈 가변 리스트 생성: 구현체 직접 생성 e.g. new ArrayList<>(); 빈 불변 리스트 생성 List.of() (자바 9, 권장) Collections.emptyList() (자바 5) 멀티스레드 동기화 컬렉션 변환 일반 리스트를 동기화된 리스트로 변경 가능 일반 리스트 보다 성능 느림 e.g. ArrayList<Integer> list = new ArrayList<>(); List<Integer> synchronizedList = Collections.synchronizedList(list); List.of() VS Arrays.asList() 두 메서드 모두 리스트를 생성할 수 있다. List<Integer> list = Arrays.asList(1, 2, 3); List<Integer> list = List.of(1, 2, 3); 일반적으로 자바 9 이상은 List.of()를 권장한다. 혹시나 하위 호환성을 위함이거나 내부 요소를 변경해야 하는 경우 Arrays.asList() 선택할 수 있다. Array.asList()는 고정된 크기를 가지지만, 내부 요소를 변경할 수 있다. (set()은 가능하지만 add(), remove() 불가) 즉, 고정도 가변도 아닌 애매한 리스트여서 거의 사용하지 않는다. Reference 김영한의 실전 자바 - 중급 2편
Java-Ecosystem
· 2024-09-05
자바 제네릭
제네릭(Generic) 제네릭 실무 이미 만들어진 코드의 제네릭을 읽고 이해하는 정도면 충분 실무에서 제네릭 사용은 거의 드물고, 적용한다면 단순하게 사용 제네릭 타입 public class GenericBox<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } } 타입 매개변수를 사용하는 클래스나 인터페이스 핵심: 타입 결정을 생성시점으로 미룸 타입 매개변수 선언 e.g. T 생성 시점에 타입 인자 전달 e.g. Integer, String… 컴파일 과정에서 타입 정보 반영 유의점 지정할 수 있는 타입은 참조형만 가능 (기본형 X, 래퍼 클래스 O) 반환 타입에 void를 사용해야할 경우 Void를 사용하고 null 리턴할 것 한번에 여러 타입 매개변수 선언 가능 class Data<K, V> {} Raw 타입은 사용하지 말아야 함 원시 타입(Raw Type): 제네릭 타입 생성시 타입 지정을 하지 않는 것 GenericBox integerBox = new GenericBox(); 내부에서 타입 매개변수에 Object 사용 원시 타입은 과거 코드와 하위 호환을 위해 존재하므로 사용해서는 안됨 Object 타입을 사용해야 하는 경우, 타입 인자로 전달하자 GenericBox<Object> integerBox = new GenericBox<>(); static 메서드에 타입 매개변수를 사용 불가 class Box<T> { T instanceMethod(T t) {} //가능 static T staticMethod1(T t) {} //제네릭 타입의 T 사용 불가능 } 제네릭 타입은 객체 생성시점에 타입이 정해짐 static 메서드는 클래스 단위로 작동하므로 인스턴스 생성과 무관 static 메서드에 제네릭을 도입하려면 제네릭 메서드를 사용해야 함 선언 방법 class GenericBox<T> 클래스 명 오른쪽에 <>(다이아몬드)를 사용해 타입 매개변수(T) 선언 클래스 내부에 T 타입이 필요한 곳에 T 적용 생성 방법 GenericBox<Integer> integerBox = new GenericBox<Integer>(); 생성 시점에 원하는 타입 인자 지정 GenericBox<Integer> integerBox = new GenericBox<>(); 자바 컴파일러의 타입 추론을 사용해 생성 부분에 타입 정보 생략도 가능 컴파일러가 대신 타입 인자 전달 제네릭 용어 정리 제네릭 (Generic) 컴파일러가 다운 캐스팅 코드를 대신 처리해주는 것 제네릭 타입 (Generic Type) 타입 매개변수를 사용하는 클래스나 인터페이스 (제네릭 클래스, 제네릭 인터페이스) e.g. GenericBox<T> 타입 매개변수 (Type Parameter) 제네릭 타입이나 메서드에서 사용되는 변수 e.g. T, E, K, V 타입 인자 (Type Argument) 제네릭 타입을 사용할 때 전달되는 실제 타입 e.g. Integer, String… 타입 매개변수 명명 관례 일반적으로 용도에 맞는 단어의 첫글자를 대문자로 사용 종류 E - Element K - Key N - Number T - Type V - Value S, U, V etc. - 2nd, 3rd, 4th types 코드 재사용성 상승시키기 프로그래밍 세계에서 무언가에 대한 결정을 정의 시점이 아니라 사용 시점으로 미루면 재사용성이 크게 상승한다. 즉, 매개변수를 활용해 실행 시점에 인자를 전달하면 재사용이 크게 상승한다. (e.g. 제네릭, 메서드…) 제네릭의 필요성 제네릭은 코드 재사용성과 타입 안정성을 동시에 잡으면서 중복 제거 가능 중복 제거 시도 과정 문제 상황: 데이터 보관 및 꺼내는 객체 만들기 서로 다른 타입의 데이터를 어떻게 다룰지 문제 해결 1: 각 타입 별 클래스 정의 (IntegerBox, StringBox) 코드 재사용X, 타입 안정성 O XxxBox 클래스 수십개 만드는 것은 비효율적 해결 2: 다형성을 활용한 하나의 클래스 정의 (ObjectBox) 코드 재사용 O, 타입 안정성 X 반환 타입이 Object라 사용 시 위험한 다운 캐스팅 필요 실수로 잘못된 타입의 인수가 전달될 수 있는 문제 (입력 제약이 느슨) e.g. integerBox.set("문자100"); Integer result = (Integer) integerBox.get(); -> ClassCastException 발생 해결 3: 제네릭 클래스 사용하기 코드 재사용 O, 타입 안정성 O 타입 매개변수 상한 타입 매개변수 상한은 제네릭의 타입 안정성을 더욱 견고히 지킴 e.g. <T extends Animal> 타입 매개변수를 Animal과 그 자식만 받을 수 있도록 제한 덕분에 자바 컴파일러가 타입 매개변수에 입력될 수 있는 값 범위를 예측 가능 제네릭의 타입 안정성 개선 과정 문제 상황: 개 병원은 개만 받을 수 있고, 고양이 병원은 고양이만 받을 수 있어야 함 해결 1: 각 타입 별 클래스 정의 (DogHospital, CatHospital) 코드 재사용X, 타입 안정성 O 해결 2: 다형성을 활용한 하나의 클래스 정의 (AnimalHospital) 코드 재사용 O, 타입 안정성 X 반환 타입이 Animal라 사용 시 위험한 다운 캐스팅 필요 실수로 잘못된 타입의 인수가 전달될 수 있는 문제 (입력 제약이 느슨) 해결 3: 제네릭 도입 여전히 타입 안정성이 불안 원치 않는 타입의 인수 전달되는 문제 타입 매개변수(T)가 Object 타입으로 취급되어 Animal의 기능 사용 불가 해결 4: 타입 매개변수 상한 코드 재사용 O, 타입 안정성 O 올바른 타입의 인수 전달이 가능해져 타입 안정성 향상 상위 타입의 원하는 기능 사용 가능 제네릭 메서드 public class GenericMethod { public static Object objMethod(Object obj) { System.out.println("object print: " + obj); return obj; } public static <T> T genericMethod(T t) { System.out.println("generic print: " + t); return t; } public static <T extends Number> T numberMethod(T t) { System.out.println("bound print: " + t); return t; } } 타입 매개변수를 사용하는 메서드 클래스 전체가 아니라 특정 메서드 단위로 제네릭을 도입할 때 사용 핵심: 타입 결정을 메서드 호출 시점으로 미룸 선언 방법 public static <T> T genericMethod(T t) {...} 반환타입 왼쪽에 타입 매개변수 선언 호출 방법 GenericMethod.<Integer>genericMethod(10) 호출 시점에 원하는 타입 인자 지정 Integer integerValue = GenericMethod.numberMethod(10); 보통 타입 추론을 통해 생략해 사용 자바 컴파일러는 전달되는 인자 타입과 반환 타입을 보고 타입 추론 컴파일러가 대신 타입 인자 전달 타입 매개변수 상한 가능 public static <T extends Number> T numberMethod(T t) {} 유의점 인스턴스 메서드, static 메서드 모두 적용 가능 (제네릭 타입은 static 메서드에 사용 불가) class Box<T> { //제네릭 타입 static <V> V staticMethod(V t) {} //static 메서드에 제네릭 메서드 도입 <Z> Z instanceMethod(Z z) {} //인스턴스 메서드에 제네릭 메서드 도입 가능 } 제네릭 타입과 제네릭 메서드의 타입 매개변수 이름은 다르게 하자! (모호함 X) 인스턴스 메서드 동시 적용에서 제네릭 메서드가 우선순위 가지지만 모호한 것은 좋지 않다! public class ComplexBox<T extends Animal> { private T animal; public void set(T animal) { this.animal = animal; } public <T> T printAndReturn(T t) { System.out.println("animal.className: " + animal.getClass().getName()); System.out.println("t.className: " + t.getClass().getName()); // 호출 불가! 메서드는 <T> 타입이다. <T extends Animal> 타입이 아니다. // t.getName(); return t; } } 와일드 카드 (Wild Card) public class WildcardEx { //이것은 제네릭 메서드이다. //Box<Dog> dogBox를 전달한다. 타입 추론에 의해 타입 T가 Dog가 된다. static <T> void printGenericV1(Box<T> box) { System.out.println("T = " + box.get()); } //이것은 제네릭 메서드가 아니다. 일반적인 메서드이다. //Box<Dog> dogBox를 전달한다. 와일드카드 ?는 모든 타입을 받을 수 있다. static void printWildcardV1(Box<?> box) { System.out.println("? = " + box.get()); } static <T extends Animal> void printGenericV2(Box<T> box) { T t = box.get(); System.out.println("이름 = " + t.getName()); } static void printWildcardV2(Box<? extends Animal> box) { Animal animal = box.get(); System.out.println("이름 = " + animal.getName()); } static <T extends Animal> T printAndReturnGeneric(Box<T> box) { T t = box.get(); System.out.println("이름 = " + t.getName()); return t; } static Animal printAndReturnWildcard(Box<? extends Animal> box) { Animal animal = box.get(); System.out.println("이름 = " + animal.getName()); return animal; } } 이미 타입이 정해진 제네릭 타입을 편리하게 전달 받을 수 있도록 하는 키워드 (*, ?) 와일드 카드는 제네릭 타입, 제네릭 메서드를 선언하는 것이 아님 단순히 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 하나 있는 것 따라서, 일반적인 메서드에 사용 가능 e.g. Box<Dog>, Box<Cat> 등의 제네릭 타입을 전달 받음 유의점 제네릭 타입, 제네릭 메서드와 달리, 호출 시에 타입을 지정할 필요가 없음 비제한 와일드카드 (?) 모든 타입을 다 받을 수 있음 상한 와일드 카드 & 하한 와일드카드 사용 가능 상한 와일드 카드 static void printWildcardV2(Box<? extends Animal> box) {...} Animal 타입 + Animal 타입의 하위 타입만 입력 가능 하한 와일드 카드 (제네릭 타입, 제네릭 메서드에는 없음) static void writeBox(Box<? super Animal> box) {...} Animal 타입 + Animal 타입의 상위 타입만 입력 가능 (e.g. Box<Dog> 전달 불가) 사용 전략 보통의 경우 더 단순한 와일드 카드 사용 권장 제네릭 메서드처럼 타입을 전달해 결정하는 것은 내부 과정이 복잡 꼭 필요한 상황에만 제네릭 타입/제네릭 메서드로 정의 전달할 타입을 명확하게 반환해야 하는 경우 제네릭 타입, 제네릭 메서드 사용 Dog dog = WildcardEx.printAndReturnGeneric(dogBox) 전달할 타입을 명확하게 반환하지 않아도 되는 경우 와일드 카드 사용 Animal animal = WildcardEx.printAndReturnWildcard(dogBox) 타입 이레이저 (Type Eraser) 제네릭 정보가 자바 컴파일 단계에서만 사용되고, 컴파일 이후에는 삭제되는 것 컴파일 전(소스코드, .java): 제네릭 타입 매개변수 존재 O 컴파일 후(바이트코드, .class): 제네릭 타입 매개변수 존재 X (<T>…) 결국, 제네릭은 컴파일러가 다운 캐스팅 코드를 대신 처리해주는 작업 자바는 컴파일 시점 제네릭 코드를 완벽히 검증하고 다운 캐스팅 코드 삽입 하위호환 유지를 위해 컴파일러가 조금 더 고생 (제네릭은 중간에 도입됨) 따라서, 런타임 코드는 옛날 자바 코드와 똑같이 실행됨 타입 이레이저 한계 런타임에 타입을 활용하는 코드 작성 불가 T는 런타임에 모두 Object가 됨 e.g. param instanceof T; //오류 -> param instanceof Object; 자바는 타입 매개변수에 instanceof 허용 X -> Object는 항상 참이므로 new T(); //오류 -> new Object(); 자바는 타입 매개변수에 new 허용 X -> 항상 Object가 생성되므로 대략적인 작동 방식 컴파일 시점 소스코드에서 제네릭 타입(GenericBox) 선언하고 타입 인자(Integer) 전달 public class GenericBox<T> {...} GenericBox<Integer> box = new GenericBox<Integer>(); 컴파일러는 제네릭 정보를 활용해 new GenericBox<Integer>() 에 대해 다음과 같이 이해 public class GenericBox<Integer> { private Integer value; public void set(Integer value) { this.value = value; } public Integer get() { return value; } } 컴파일 후 컴파일이 모두 끝나면 자바는 제네릭 정보를 삭제하고 다음과 같은 .class 정보 생성 상한 제한 없이 선언한 타입 매개변수 T는 Object로 변환 public class GenericBox { private Object value; public void set(Object value) { this.value = value; } public Object get() { return value; } 상한 제한 있는 타입 매개변수(T)는 제한한 타입(Animal)으로 변환 소스코드 AnimalHospital<Dog> hospital = new AnimalHospital<>(); 컴파일 전 public class AnimalHospital<T extends Animal> {...} 컴파일 후 public class AnimalHospital { private Animal animal; public void set(Animal animal) {...} public void checkup() {...} public Animal getBigger(Animal target) {...} } 필요한 곳에 컴파일러가 다운 캐스팅 코드 삽입 e.g.1 GenericBox box = new GenericBox(); box.set(10); Integer result = (Integer) box.get(); //컴파일러가 캐스팅 추가 e.g.2 Dog dog = (Dog) animalHospital.getBigger(new Dog()); 자바는 컴파일 시점에 제네릭 코드를 완벽히 검증하므로 다운 캐스팅 추가에 문제 X Reference 김영한의 실전 자바 - 중급 2편
Java-Ecosystem
· 2024-09-04
자바 예외 기본
예외 계층 Object: 모든 객체의 최상위 부모 Throwable: 최상위 예외, 잡으면 안됨 (Error까지 잡히므로) Error 애플리케이션에서 복구 불가능한 시스템 예외 (메모리 부족, 심각한 시스템 오류…) 애플리케이션 개발자는 이 예외를 잡지 않아야 함 언체크 예외 Exception: 체크 예외 (런타임 예외 제외), 애플리케이션에서 개발자가 잡아야 할 실질적최상위 예외 RuntimeException: 언체크 예외 (=런타임 예외) 체크예외 VS 언체크 예외 핵심 언체크 예외는 throws 선언하지 않고 생략 가능 (자동 예외 던지기) 나머지는 동일 체크 예외 컴파일러가 체크하는 예외 체크 예외의 장단점 예외를 누락하지 않도록 컴파일러가 안전 장치 역할 (누락 시 컴파일 오류) 크게 신경쓰지 않고 싶은 예외까지 모두 반드시 잡거나 던져야 함 언체크 예외 컴파일러가 체크하지 않는 예외 중요 예외의 경우 throws를 선언해두면 IDE를 통해 개발자가 편리하게 인지 가능 (보통은 생략) 언체크 예외의 장단점 신경쓰고 싶지 않은 언체크 예외 무시 가능 개발자가 실수로 예외 누락 가능 예외 처리 기본 기본 규칙 예외는 잡아서 처리하거나 던져야 한다 예외를 잡는 코드: catch 예외를 던지는 코드: throws 예외를 잡거나 던질 때 지정한 예외 뿐만 아니라 그 예외의 자식들도 함께 처리된다 기본적으로 언체크(런타임) 예외를 사용하자 체크 예외들은 바깥으로 던져야 하는데 이 과정에서 의존 관계 문제 발생 실무에서 발생하는 대부분의 예외는 복구 불가능한 시스템 예외 (애플리케이션 단에서 처리 불가) 의존 관계 문제 컨트롤러, 서비스는 본인이 처리할 수 없어도 throws를 선언해 예외를 던져야 함 컨트롤러, 서비스가 해당 체크 예외에 의존하게 되어 구현 기술 변경 시 OCP 위반 예를 들어, DB 접근 기술을 변경한다면 예외를 포함한 컨트롤러, 서비스 코드를 수정 수 많은 체크 예외를 일일이 명시해 던지는 것도 부담 런타임 예외를 사용하면 처리할 수 없는 예외를 별도 선언 없이 그냥 두면 됨 의존성 발생 X -> 기술 변경이 있어도 컨트롤러, 서비스 코드 변경 X -> OCP 준수 대부분의 최근 라이브러리는 런타임 예외를 기본으로 제공 (스프링, JPA…) 런타임 예외는 놓칠 수 있기 때문에 문서화가 중요 (혹은 명시적으로 코드에 throws 남기기) 처리할 수 없는 예외들은 한 곳에서 공통처리 서블릿 오류 페이지, 스프링 MVC ControllerAdvice 예외 공통 처리 고객: 오류 페이지 내부 개발자: 별도 로그, 슬랙, 문자, 메일을 통해 개발자가 빠르게 인지 API는 상태코드 500 응답 예시 코드 public class Main { public static void main(String[] args) { NetworkService networkService = new NetworkService(); try { networkService.sendMessage(); } catch (Exception e) { // 모든 예외를 잡아서 처리 exceptionHandler(e); } } //공통 예외 처리 private static void exceptionHandler(Exception e) { //공통 처리 System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다."); System.out.println("==개발자용 디버깅 메시지=="); e.printStackTrace(System.out); // 스택 트레이스 출력 //e.printStackTrace(); // System.err에 스택 트레이스 출력 //실무에서는 보통 Slf4j 사용해 로그를 콘솔 출력 + 파일로 저장 //e.printStackTrace는 콘솔에만 출력하므로 사용 X //필요하면 예외 별로 별도의 추가 처리 가능 if (e instanceof SendException sendEx) { System.out.println("[전송 오류] 전송 데이터: " + sendEx.getSendData()); } } } 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용 매우 심각한 문제는 개발자가 실수로 예외를 놓치면 안된다고 판단할 수 있음 체크 예외 예시 (무조건 해야하는 건 아님) 계좌 이체 실패 예외 결제시 포인트 부족 예외 로그인 ID, PW 불일치 예외 Exception을 던지지 말자 코드가 깔끔해지는 것 같지만, 모든 체크 예외를 다 던져 버려서 중요한 체크 예외를 놓침 꼭 필요한 경우가 아니면 Exception 자체를 밖으로 던지는 것은 좋은 방법이 아님 스택 트레이스를 남기자 로그 남기기 log.info("예외 처리, message={}", e.getMessage(), e); 로그의 마지막 인수에 예외 객체 전달하면 로그에 스택 트레이스를 출력 예외를 전환할 때는 반드시 기존 예외를 포함하자 throw new RuntimeSQLException(e); - 기존 예외 e 포함 덕분에 기존 예외와 스택 트레이스까지 확인 가능 포함하지 않으면 실제 DB에서 발생한 근본적인 원인을 확인할 수 없는 심각한 문제 발생 예외를 계속 던지면 벌어지는 상황 자바 main() 쓰레드의 경우 예외 로그를 출력하면서 시스템이 종료 웹 애플리케이션의 경우 WAS가 예외를 받아 처리하고 개발자가 지정한 오류 페이지 보여줌 (예외 하나로 시스템이 종료되면 안됨) 사용자 예외 만들기 Exception을 상속 받으면 체크 예외 RuntimeException을 상속 받으면 언체크 예외 오류 메시지 보관하기 생성자를 통해 오류 메시지를 보관할 것 (예외가 제공하는 기본 기능) super(message)로 전달한 메시지는 Throwable의 detailMessage에 보관됨 getMessage()로 조회 가능 public class MyCheckedException extends Exception { public MyCheckedException(String message) { super(message); } } 적당한 예외 계층화하기 (너무 많아도 문제) 상속을 사용해 예외를 계층화하면 보다 세밀한 예외 처리 가능 e.g. NetworkClientException (부모) ConnectException (자식, 내부 연결 시도 address 보관) SendException (자식, 내부 전송 시도 데이터 sendData 보관) 각각의 하위 예외에 고유 기능을 만들어 활용 가능 e.g. e.getAddress(), e.getSendData() 부모 예외를 잡아 자식까지 한 번에 처리하거나 특정 하위 예외만 잡아 처리 가능 중요한 특정 하위 예외만 메시지를 명확히 남기고 나머지는 공통 처리 가능 e.g. [연결 오류] 주소: ... (하위 예외) [네트워크 오류] 메시지: ... (부모 예외) [알 수 없는 오류] 메시지: ... (그 외 예외 공통 처리) 예외 처리 발전 과정 예시 (e.g. NetworkClient) 반환 값(문자열)으로 예외 처리 분기 처리 및 return으로 네트워크 연결 및 해제, 데이터 전송 관리 등이 가능 가장 중요한 정상 흐름이 한눈에 들어오지 않음 (정상 흐름과 예외 흐름 분리 X, 가독성 감소) 예외 처리 메커니즘 사용 (try ~ catch ~ finally) 성공 여부를 반환값이 아닌 메서드 정상 종료 여부로 판단 정상 흐름과 예외 흐름을 명확히 분리해 가독성 상승 (try 블록, catch 블록) 반드시 실행되어야 하는 코드를 안전하게 호출하도록 보장 (finally) finally가 없으면 catch에서 잡지 못한 예외가 발생할 때 문제가 생김 외부 자원 해제 등에 편리 finally finally 블록은 어떤 경우라도 반드시 호출된다. 주로 try에서 사용한 자원을 해제할 때 사용한다. 예외를 직접 잡을 일이 없다면, try ~ finally만 사용하는 것도 가능하다. 정상 흐름 (try) -> finally 예외 잡음 (catch) -> finally 예외 던짐 -> finally (finally 블록 끝난 이후 예외가 밖으로 던져짐) 자원 해제 외부 리소스는 사용 후 반드시 연결을 해제하고 자원을 반납해야 메모리 고갈을 피할 수 있다. (네트워크 연결 자원, DB 연결 자원…) 자바는 GC로 JVM 메모리 상 인스턴스들을 자동으로 해제하지만, 외부 연결 같은 자바 외부 자원은 자동으로 해제되지 않는다. 여러 예외 한 번에 잡는 Syntax catch 블록에서 | 키워드를 사용해 예외를 나열할 수 있다. 다만, 이 경우 각 예외들의 공통 부모 기능만 사용할 수 있다. e.g. catch (ConnectException | SendException e) {...} // 이 경우 공통 부모인 NetworkClientException의 기능만 사용 가능 try-with-resources 구문 finally 없이 편리한 외부 자원 해제 지원 사용 방법 외부 자원 클래스에 AutoCloseable 인터페이스를 구현 (implements AutoCloseable) close() 메서드를 오버라이드해 자원 반납 방법 정의 메서드가 예외를 던지지 않으면 인터페이스의 throws Exception은 생략 try-with-resources 구문 사용 try (Resource resource = new Resource()) { // 리소스를 사용하는 코드 } try 괄호 안에 사용할 자원을 명시 e.g. try (NetworkClient client = new NetworkClient(address)) {...} try 블록이 끝나면 자동으로 AutoCloseable.close() 호출해 자원 해제 try 블록만 단독으로도 사용 가능 (catch, finally 없이 사용 가능) 장점 리소스 누수 방지 실수로 finally 블록 혹은 그 내부에 자원 해제 코드를 누락하는 문제 예방 코드 간결성 및 가독성 향상 리소스 스코프 범위 한정 리소스 객체 변수의 스코프를 try 블록으로 한정해 코드 유지보수성 향상 조금 더 빠른 자원 해제 기존에는 catch 이후에 자원을 반납 (try -> catch -> finally) try with resources는 try 블록이 끝나면 즉시 close() 호출 Reference 김영한의 실전 자바 - 중급 1편
Java-Ecosystem
· 2024-08-24
자바 중첩 클래스
중첩 클래스의 분류 클래스를 정의하는 위치에 따라 총 4가지, 크게 2가지로 분류 (변수 선언 위치와 동일) 정적 중첩 클래스 (정적 변수와 같은 위치) 내부 클래스 내부 클래스 (인스턴스 변수와 같은 위치) 지역 클래스 (지역 변수와 같은 위치) 익명 클래스 (지역 클래스의 특별 버전) 중첩 클래스 사용 상황 => 패키지 내 꼭 필요한 클래스들만 노출시켜 개발자의 혼란을 줄임 특정 클래스가 다른 하나의 클래스 안에서만 사용됨 (다른 클래스에서 사용시 중첩 클래스 사용 X) 둘이 아주 긴밀하게 연결되어 있는 경우 장점 논리적 그룹화 캡슐화 다른 곳에서 사용될 필요 없는 중첩 클래스가 외부에 노출 X 중첩 클래스는 바깥 클래스의 private 멤버에 접근 가능 불필요한 public 제거 및 긴밀한 연결 정적 중첩 클래스 VS 내부 클래스 사용 상황 바깥 클래스의 인스턴스 상태에 의존하고 인스턴스 변수를 사용할 것 같다면 내부 클래스 사용 아닐 것 같다면 정적 중첩 클래스 사용 정적 중첩 클래스 (Nested) public class NestedOuter { private static int outClassValue = 3; private int outInstanceValue = 2; static class Nested { private int nestedInstanceValue = 1; public void print() { // 자신의 멤버에 접근 System.out.println(nestedInstanceValue); // 바깥 클래스의 인스턴스 멤버에는 접근할 수 없다. // System.out.println(outInstanceValue); // 바깥 클래스의 클래스 멤버에는 접근할 수 있다. private도 접근 가능 System.out.println(NestedOuter.outClassValue); System.out.println(outClassValue); } } } public class NestedOuterMain { public static void main(String[] args) { //단독 생성 가능 NestedOuter.Nested nested = new NestedOuter.Nested(); nested.print(); } } static 키워드 O 바깥 클래스의 인스턴스에 소속 X 바깥 클래스의 내부에 있지만, 바깥 클래스와 관계 없는 전혀 다른 클래스 (내 것이 아닌 것) 단지 구조상 중첩해뒀을 뿐 클래스 2개와 큰 차이 없음 class NestedOuter { } class Nested { } 바깥 클래스의 인스턴스 변수 접근 불가 (클래스 변수는 접근 가능) 바깥 인스턴스의 참조값이 없기 때문 private 접근 제어자 관점 바깥 클래스와 중첩 클래스는 서로의 private 접근 제어자에 접근 가능 둘은 접근제어자 관점에서 한 식구 생성 방법 바깥 클래스의 바깥에서 접근 및 생성 접근: 바깥클래스.중첩클래스 생성: new 바깥클래스.중첩클래스() 바깥 클래스 생성과 상관없이 단독 생성 가능 바깥 클래스의 내부에서 접근 및 생성 (권장) 접근: 중첩클래스 생성: new 중첩클래스() 정적 중첩 클래스는 private으로 두고 바깥 접근 및 생성을 막는게 옳음 중첩 클래스의 용도는 소속된 바깥 클래스의 내부에서 사용되는 것이므로 public class Network { public void sendMessage(String text) { NetworkMessage networkMessage = new NetworkMessage(text); } private static class NetworkMessage { ... } } 내부 클래스 (Inner) 내부 클래스 (공통 개념) public class InnerOuter { private static int outClassValue = 3; private int outInstanceValue = 2; class Inner { private int innerInstanceValue = 1; public void print() { // 자신의 멤버에 접근 System.out.println(innerInstanceValue); // 외부 클래스의 인스턴스 멤버에 접근 가능, private도 접근 가능 System.out.println(outInstanceValue); // 외부 클래스의 클래스 멤버에 접근 가능. private도 접근 가능 System.out.println(InnerOuter.outClassValue); } } } public class InnerOuterMain { public static void main(String[] args) { InnerOuter outer = new InnerOuter(); InnerOuter.Inner inner = outer.new Inner(); inner.print(); } } static 키워드 X 바깥 클래스의 인스턴스에 소속 O 바깥 클래스의 내부에 있으면서, 바깥 클래스를 구성하는 요소 (나를 구성하는 요소) 개념상으로는 바깥 인스턴스 안에 내부 인스턴스가 생성 실제로는 내부 인스턴스가 바깥 인스턴스의 참조값 보관 바깥 클래스의 인스턴스 변수 접근 가능 (클래스 변수도 접근 가능) 참조값을 가짐 private 접근 제어자 관점 바깥 클래스와 내부 클래스는 서로의 private 접근 제어자에 접근 가능 생성 방법 바깥 클래스의 바깥에서 생성 생성: 바깥클래스의 인스턴스 참조.new 내부클래스() 바깥 클래스의 인스턴스를 먼저 생성해야 내부 클래스의 인스턴스 생성 가능 바깥 클래스의 내부에서 생성 (권장) 생성: new 내부 클래스() 내부 클래스의 인스턴스는 자신을 생성한 바깥 클래스의 인스턴스를 자동으로 참조 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 + "의 엔진을 구동합니다."); } } } } 종류 내부 클래스: 바깥 클래스의 인스턴스 멤버에 접근 지역 클래스: 내부 클래스의 특징 + 지역 변수에 접근 익명 클래스: 지역 클래스의 특징 + 클래스 이름 X 지역 클래스 class Outer { public void process() { //지역 변수 int localVar = 0; //지역 클래스 class Local {...} Local local = new Local(); } } 내부 클래스의 종류 중 하나로 지역 변수와 같이 코드 블럭 안에서 정의 지역 변수에 접근 가능 (접근하는 지역 변수는 final이거나 사실상 final이어야 함) 접근 제어자 사용 불가 (Like 지역 변수) 사용 상황 특정 메서드 내 간단히 사용할 목적 지역 변수 캡처 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("localVar=" + localVar); System.out.println("paramVar=" + paramVar); System.out.println("outInstanceVar=" + outInstanceVar); } } Printer printer = new LocalPrinter(); // 지역클래스가 접근하는 지역 변수는 final이거나 사실상 final이어야 한다. // localVar = 10; // 컴파일 오류 // paramVar = 20; // 컴파일 오류 //printer.print()를 여기서 실행하지 않고 Printer 인스턴스만 반환한다. return printer; } public static void main(String[] args) { LocalOuter localOuter = new LocalOuter(); 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.LocalOuter$1LocalPrinter.value //캡처 변수 //field = final int nested.local.LocalOuter$1LocalPrinter.val$localVar //field = final int nested.local.LocalOuter$1LocalPrinter.val$paramVar //바깥 클래스 참조 //field = final nested.local.LocalOuter nested.local.LocalOuter$1LocalPrinter.this$0 지역 클래스 인스턴스 생성 시점에 접근이 필요한 지역 변수는 복사해서 인스턴스에 보관하는 것 지역 클래스 인스턴스에서 지역 변수에 접근하면, 실제로는 인스턴스에 캡처한 변수로 접근 힙 영역의 인스턴스가 스택 영역의 지역 변수에 접근하는 것은 복잡한 상황을 동반하기 때문 변수 생명 주기 차이 문제 process() 메서드 종료 후, 생존 중인 LocalPrinter 인스턴스의 print() 메서드 호출 변수 생명주기를 고려하면 지역변수(localVar, paramVar)는 print() 메서드 호출 시점 전 이미 소멸 process()의 스택 프레임이 사라지므로 지역 변수도 함께 소멸 그러나 실행 결과는 지역 변수들 값까지 모두 정상 출력 자바의 해결책: 지역 변수 캡처 LocalPrinter 인스턴스 생성 시점에 지역 클래스가 접근하는 지역 변수 확인 해당 지역 변수들을 복사해 인스턴스에 포함하여 생성 (paramVar, localVar) print() 메서드에서 paramVar, localVar에 접근 시 인스턴스에 있는 캡처 변수에 접근 캡처한 paramVar , localVar 의 생명주기 = LocalPrinter 인스턴스의 생명주기 변수 생명 주기 차이 문제 해결 실제로 LocalPrinter 인스턴스 내에서 캡쳐 변수 확인 가능 (자바가 내부 생성 및 사용) 유의점: 지역 클래스가 접근하는 지역 변수 값은 변경하면 안됨 지역 클래스 접근 지역 변수는 final로 선언하거나 사실상 final이어야 함\ 지역 클래스 생성 시점에 지역 변수를 캡처하므로 생성 이후에는 값을 변경해서는 안됨 스택 영역 지역 변수 값과 인스턴스 캡처 변수 값이 서로 달라지는 동기화 문제 예방 두 변수를 동기화할 시 디버깅이 어렵고 멀티스레드 상황에서도 복잡하고 성능 저하 존재 동기화 문제가 일어나지 않게 지역 변수 변경을 원천 차단하는게 깔끔 자바 문법 규칙이고 어길시 컴파일 오류 발생 사실상 final(effectively final) final을 사용하지 않았지만, 중간에 값을 변경하지 않는 것 final을 사용해도 동일하게 작동해야 함 만일 캡처된 변수를 바꿔야 한다면, 새로 선언하면 됨 (굳이 사이드이펙트 만들 필요 X) e.g. int x = localVar; x++ 익명 클래스 지역 클래스의 종류 중 하나 클래스 이름을 생략하고 지역 클래스의 상속과 구현, 선언과 생성을 한번에 처리 지역 클래스의 선언과 생성 //선언 class LocalPrinter implements Printer{ //body } //생성 Printer printer = new LocalPrinter(); 익명 클래스 Printer printer = new Printer(){ //body } new 다음에 상속 혹은 구현할 부모 타입 입력 부모 타입이 인터페이스일 시, 해당 인터페이스를 구현하는 익명 클래스를 생성 사용 상황 인스턴스를 한 번만 생성할 수 있어 일회성 사용에 좋고 코드가 간결해짐 복잡하거나 재사용이 필요한 경우 별도의 클래스를 정의하는 것이 나음 특징 반드시 부모 클래스를 상속 받거나 인터페이스를 구현해야 함 생성자를 가질 수 없고, 기본 생성자만 사용됨 자바 내부에서 바깥 클래스 이름 + $ + 숫자로 정의 (e.g. AnonymousOuter$1) 익명 클래스가 여러개면 숫자가 증가하며 구분 메서드의 재사용성을 높이는 팁 메서드(함수)의 재사용성을 높이기 위해 변하는 부분과 변하지 않는 부분을 분리하고 변하는 부분은 외부에서 전달 받자. (데이터 혹은 코드 조각을 파라미터로 전달) 중복을 제거하고 좋은 코드를 유지할 수 있다. 외부에서 코드 조각을 전달하는 방법 인스턴스를 전달 메서드에 코드 조각을 담아두고 실행 다만, 코드 조각을 전달하기 위해 클래스를 정의하고 인스턴스를 생성하는 것이 번거로움 e.g. 지역 클래스로 생성해 전달 익명 클래스로 생성해 전달 람다(Lambda)로 전달 인수의 타입이 되는 인터페이스가 메서드를 1개만 가지면 사용 가능 자바 8 이전: 메서드 인수로 기본형 타입과 참조형 타입만 전달할 수 있었음 자바 8 이후: 메서드 인수로 함수를 전달 가능 람다가 매우 편리 (람다가 없을 때는 코드 조각을 항상 익명 클래스로 전달했음…) 섀도잉 (Shadowing) // 내부 클래스 예시 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(); } } 바깥 클래스의 인스턴스 변수 이름과 내부 클래스의 인스턴스 변수 이름이 같다면? 변수 이름이 같을 때 프로그래밍에서는 대부분 더 가깝거나 구체적인 것이 우선권을 가짐 새도잉: 다른 변수들을 가려서 보이지 않게 하는 것 value = 3 this.value = 2 ShadowingMain.value = 1 다른 변수를 가리더라도 인스턴스 참조를 사용해 외부 변수 접근 가능 내부 클래스 인스턴스 접근: this 바깥 클래스 인스턴스 접근: 바깥클래스이름.this 물론, 이름이 같은 경우 처음부터 이름을 서로 다르게 지어서 명확하게 구분하는 것이 더 나은 방법 public class LocalOuter { private int outInstanceVar = 3; public void process(int paramVar) { int localVar = 1; class LocalPrinter { int value = 0; public void printData() { System.out.println("value=" + value); //0 System.out.println("localVar=" + localVar); //1 System.out.println("paramVar=" + paramVar); //2 System.out.println("outInstanceVar=" + outInstanceVar); //3 } } LocalPrinter printer = new LocalPrinter(); printer.printData(); } public static void main(String[] args) { LocalOuter localOuter = new LocalOuterV1(); localOuter.process(2); } } Reference 김영한의 실전 자바 - 중급 1편
Java-Ecosystem
· 2024-08-17
자바 날짜 시간 라이브러리
날짜 시간 라이브러리의 필요성 날짜 라이브러리는 복잡한 계산을 추상화해 제공하므로, 안정적이고 정확한 개발 가능 자바 8에서 java.time 패키지(JSR-310)를 표준 API(기능의 모음)로 도입 외부 라이브러리였던 Joda-Time의 개발자를 데려와 새로운 자바 표준 API를 함께 정의 이전 문제가 많던 API를 크게 개선 (사용성, 성능, 스레드 안정성, 타임존 처리, 불변 객체 설계 등) 날짜 계산이 어려운 이유 각 달은 28~31일로 다르게 분포 윤년 (Leap Year) 지구가 태양을 한 바퀴 도는 평균 시간은 약 365.2425일 (약 365일 5시간 48분 45초) 우리가 사용하는 그레고리력은 1년이 365일 윤년은 둘의 간극을 매우기 위한 해결책으로 4년마다 하루를 추가 (2월 29일) 100년 단위는 윤년이 아니며 400년 단위는 다시 윤년 일광 절약 시간 (Daylight Saving Time, DST) - 썸머타임 보통 3월 중순~11월 초 태양이 일찍 뜨는 것에 맞춰 1시간 앞당기는 제도 국가나 지역에 따라 적용 여부 및 시작 종료 날짜가 다름 타임존 계산 각각의 타임존은 UTC(세계 협정시)로부터의 시간 차이로 정의 London / UTC / GMT는 세계 시간의 기준이 되는 00:00 시간대 GMT (그리니치 평균시, Greenwich Mean Time) 처음 세계 시간은 영국 런던 그리니치 천문대를 기준으로 만듦 UTC (협정 세계시, Universal Time Coordinated) GMT를 대체하기 위해 도입 (둘은 실질적으로 같은 시간대) 다만, UTC는 원자 시계를 사용해 측정해 보다 정확한 시간 유지 타임존 예시 Europe/London GMT UTC America/New_York -05:00 Asia/Seoul +09:00 자바 날짜 시간 라이브러리 (time) - 클래스 분류표 유의점 모든 날짜 클래스는 불변 -> 변경이 발생하는 경우 새로운 객체를 생성해 반환 초는 나노초 정밀도로 캡처 가능 Year, Month, YearMonth, MonthDay: 자주 사용 X DayOfWeek: 월, 화, 수, 목, 금, 토, 일을 나타내는 Enum (ChronoField) 날짜와 시간 핵심 인터페이스 TemporalAccessor 인터페이스 날짜와 시간을 읽기 위한 기본 인터페이스 날짜와 시간의 2가지 개념 (특정 시점의 시간 & 시간의 간격) Temporal 인터페이스 - 특정시점의 시간 날짜와 시간을 조작하기 위한 기능 추가 제공 상위 인터페이스 덕분에 읽기와 쓰기 모두 지원 구현체 LocalDateTime, LocalDate, LocalTime ZonedDateTime, OffsetDateTime Instant TemporalAmount 인터페이스 - 시간의 간격 특정 날짜 시간 객체에 일정 기간을 더하거나 빼는데 사용 구현체 Period , Duration 시간의 단위와 필드 - 단독 사용 X, 날짜 시간 조회나 조작에 사용 TemporalUnit 인터페이스 - 시간의 단위 날짜와 시간을 측정하는 단위 구현체(Enum): ChronoUnit 시간: NANOS, MICROS, MILLIS, SECONDS, MINUTES, HOURS 날짜: DAYS, WEEKS, MONTHS, YEARS, DECADES, CENTURIES, MILLENNIA 기타: ERAS, FOREVER 주요 메서드 between(Temporal, Temporal) 두 Temporal 객체 사이의 시간을 현재 ChronoUnit 단위로 측정하여 반환 e.g. LocalTime lt1 = LocalTime.of(1, 10, 0); LocalTime lt2 = LocalTime.of(1, 20, 0); long secondsBetween = ChronoUnit.SECONDS.between(lt1, lt2); long minutesBetween = ChronoUnit.MINUTES.between(lt1, lt2); getDuration() 현재 ChronoUnit의 기간을 Duration 객체로 반환 e.g. ChronoUnit.HOURS.getDuration().getSeconds() //3600 ChronoUnit.DAYS.getDuration().getSeconds() //86400 TemporalField 인터페이스 - 시간의 각 필드 날짜와 시간의 특정 부분을 나타냄 (연도, 월, 일, 시간, 분) 예를 들어, 일(day)은 31보다 클 수 없는 것처럼 범위가 생김 구현체(Enum): ChronoField 연도: ERA, YEAR_OF_ERA, YEAR, EPOCH_DAY 월 MONTH_OF_YEAR: 월 (1월 = 1) 주 및 일 DAY_OF_MONTH: 월의 일 (1일 = 1) DAY_OF_WEEK: 요일 (월요일 = 1) DAY_OF_YEAR: 연의 일 (1월 1일 = 1) 시간 HOUR_OF_DAY: 시간 (0-23) HOUR_OF_AMPM: 오전/오후 시간 (0-11) CLOCK_HOUR_OF_DAY: 시계 시간 (1-24) CLOCK_HOUR_OF_AMPM: 오전/오후 시계 시간 (1-12) MINUTE_OF_HOUR: 분 (0-59) SECOND_OF_MINUTE: 초 (0-59) MILLI_OF_SECOND: 초의 밀리초 (0-999) MICRO_OF_SECOND: 초의 마이크로초 (0-999,999) NANO_OF_SECOND: 초의 나노초 (0-999,999,999) 기타 AMPM_OF_DAY: 하루의 AM/PM 부분 주요 메서드 range() 필드 값의 유효 범위를 ValueRange 객체로 반환 (최소값과 최대값을 제공) e.g. ChronoField.MONTH_OF_YEAR.range() //1 - 12 ChronoField.DAY_OF_MONTH.range() //1 - 28/31 Temporal - 특정 시점의 시간 기본 날짜 시간 표현 (LocalXxx) 특정 지역의 날짜와 시간만 고려할 때 사용 (타임존 적용 X, 시간대 고려 X) 국내 서비스만 고려할 때 권장 종류 LocalDate: 날짜만 표현 (년, 월, 일) 예) 2013-11-21 LocalTime: 시간만 표현 (시, 분, 초) 예) 08:20:30.213 밀리초, 나노초 단위도 포함 가능 LocalDateTime: LocalDate + LocalTime 예) 2013-11-21T08:20:30.213 클래스 내부에 LocalDate와 LocalTime을 필드로 가지고 있음 public class LocalDateTime { private final LocalDate date; private final LocalTime time; ... } 주요 메서드 공통 메서드 생성 now(): 현재 시간 기준으로 생성 of(...): 특정 날짜를 기준으로 생성 계산 dt.plusXxx(): 특정 날짜 시간 단위를 더함 e.g. plusYears(1), plusDays(10), plusSeconds(30) LocalDatetime 날짜와 시간 분리 dt.toLocalDate(): 주어진 LocalDateTime에서 날짜만 반환 dt.toLocalTime(): 주어진 LocalDateTime에서 시간만 반환 날짜와 시간 합체 of(...): 날짜와 시간을 묶어서 LocalDateTime으로 만들기 e.g. LocalDateTime.of(localDate, localTime) 비교 dt.isBefore(): 현재 날짜시간이 지정 날짜시간보다 이전이라면 true 를 반환 dt.isAfter(): 현재 날짜시간이 지정 날짜시간보다 이후라면 true 를 반환 dt.isEqual(): 현재 날짜시간과 지정 날짜시간 시간적으로 동일하면 true 를 반환 isEqual() 객체가 다르고 타임존이 달라도 시간적으로 같으면 true e.g. 서울의 9시와 UTC의 0시는 시간적으로 동일 equals() 객체 타입, 타임존 등등 내부 데이터의 모든 구성요소가 같아야 true e.g. 서울의 9시와 UTC의 0시는 타임존이 다르므로 false 시간대 적용 날짜 시간 표현 (ZonedDateTime, OffsetDateTime) 글로벌 서비스 개발 시에만 사용 (그러지 않으면 거의 사용 X) 용어 타임존(Time Zone) 오프셋과 일광 절약 시간제에 대한 정보 담김 -> 타임존을 알면 일광 절약 시간제를 알 수 있음 예) Asia/Seoul 오프셋(Offset) UTC로 부터의 시간대 차이 예) +9:00 종류 ZoneId 자바가 제공하는 타임존 클래스 내부에 오프셋과 일광 절약 시간제 정보 포함 ZonedDateTime 시간대를 표현하는 타임존이 포함 (LocalDateTime + ZoneId) 일광 절약 시간제 적용 실제 사용 날짜와 시간 정보 표현에 적합 (비행기 시간, 회의 시간, 일상 시간 표현…) 예) 2013-11-21T08:20:30.213+9:00[Asia/Seoul] 클래스 내부에 LocalDatetime, ZoneId, ZoneOffset을 필드로 가지고 있음 public class ZonedDateTime { private final LocalDateTime dateTime; private final ZoneOffset offset; private final ZoneId zone; ... } OffsetDateTime 타임존은 없고, 고정된 오프셋만 포함 (LocalDateTime + ZoneOffset) 일광 절약 시간제 적용 X 시간대 변환 없이 로그를 기록하고 처리할 때 적합 로그는 순차적으로 쌓여야 함, 썸머타임 적용으로 1시간 당겨지는 상황 있으면 안됨 예) 2013-11-21T08:20:30.213+9:00 클래스 내부에 LocalDatetime, ZoneOffset을 필드로 가지고 있음 public class OffsetDateTime { private final LocalDateTime dateTime; private final ZoneOffset offset; ... } 주요 메서드 공통 메서드 생성 now(): 현재 시간 기준으로 생성 (ZoneId는 현재 시스템을 따름) of(...): 특정 날짜를 기준으로 생성 ZonedDatetime of(...) 사용법 단순 생성 ZonedDateTime zdt = ZonedDateTime.of(2030, 1, 1, 13, 30, 50, 0, ZoneId.of("Asia/Seoul")); LocalDatetime + ZoneId로 생성하기 LocalDateTime ldt = LocalDateTime.of(2030, 1, 1, 13, 30, 50); ZonedDateTime zdt = ZonedDateTime.of(ldt, ZoneId.of("Asia/Seoul")); 타임존 변경하기 zdt.withZoneSameInstant(): 입력한 타임존으로 변경 e.g. zdt.withZoneSameInstant(ZoneId.of("UTC")) OffsetDatetime of(...) 사용법 LocalDatetime + ZoneOffset로 생성하기 LocalDateTime ldt = LocalDateTime.of(2030, 1, 1, 13, 30, 50); OffsetDateTime odt = OffsetDateTime.of(ldt, ZoneOffset.of("+01:00")); 이외 ZoneId getAvailableZoneIds(): 이용 가능한 모든 ZoneId 반환 systemDefault(): 시스템이 사용하는 기본 ZoneId 반환 of(...): 타임존을 직접 제공해서 ZoneId로 변환 e.g. ZoneId.of("Asia/Seoul") 기계 중심의 시간 (Instant) UTC를 기준으로 하는 시간의 한 지점 1970년 1월 1일 0시 0분 0초(UTC)를 기준으로 경과한 시간으로 계산 (초 데이터) 클래스 내부에 초 데이터를 필드로 가짐 (나노초 정밀도) public class Instant { private final long seconds; private final int nanos; ... } 일반적으로 LocalDateTime , ZonedDateTime를 사용하고 Instant는 특별한 경우에 사용 기준점이 명확하나(UTC), 사람이 읽기 어렵고 초 단위 간단한 연산만 가능 사용 예 로그 기록, 트랜잭션 타임스탬프, 서버 간 시간 동기화 등 전 세계적으로 일관된 시점 표현 시 지속 시간 계산 등 시간대 변화 없는 순수한 시간 흐름만을 다룰 때 DB에 날짜 시간 저장하거나 다른 시스템과 날짜 시간 정보를 교환할 때 주요 메서드 생성 now() UTC를 기준 현재 시간의 Instant 를 생성 from() 다른 타입의 날짜와 시간을 기준으로 Instant 를 생성 LocalDateTime 사용 불가 (Instant 는 UTC 기준이어서 시간대 정보가 필요) e.g. ZonedDateTime zdt = ZonedDateTime.now(); Instant from = Instant.from(zdt); ofEpochSecond() 에포크 시간을 기준으로 Instant 를 생성 ofEpochSecond(0) -> 에포크 시간인 1970년 1월 1일 0시 0분 0초로 생성 ofEpochSecond(30) -> 1970/1/1/0/0/30 계산 plusSeconds() : 초, 밀리초, 나노초 정도만 더하는 간단한 메서드 조회 getEpochSecond() : UTC 1970년 1월 1일 0시 0분 0초를 기준으로 흐른 초를 반환 Epoch 시간 Epoch time(에포크 시간) 또는 Unix timestamp는 컴퓨터 시스템에서 시간을 나타내는 방법 중 하나이다. 1970년 1월 1일 00:00:00 UTC부터 현재까지 경과된 시간을 초 단위로 표현한 것이다. 즉, 시간대에 영향을 받지 않는 절대적인 시간 표현 방식이다. Instant는 Epoch 시간을 다루는 클래스이다. TemporalAmount - 시간의 간격 (기간, 시간의 양, amount of time) 년, 월, 일 단위 표현 (Period) 클래스 내부에 년, 월, 일을 필드로 가짐 public class Period { private final int years; private final int months; private final int days; } 주요 메서드 생성 of() : 특정 기간을 지정해서 Period 를 생성 of(년, 월, 일) ofDays() ofMonths() ofYears() 계산 더하기 특정 날짜 인스턴스의 plus() 메서드를 사용해 기간을 더할 수 있음 e.g. LocalDate currentDate = LocalDate.of(2030, 1, 1); Period period = Period.ofDays(10); LocalDate plusDate = currentDate.plus(period); between(): 기간 차이 구하기 (Period 반환) LocalDate startDate = LocalDate.of(2023, 1, 1); LocalDate endDate = LocalDate.of(2023, 4, 2); Period between = Period.between(startDate, endDate); //Period 반환 조회 getYears(), getMonths(), getDays() 시, 분, 초(나노초) 단위 표현 (Duration) 클래스 내부에 초 데이터만 필드로 가짐 내부에서 초를 기반으로 시, 분, 초를 계산해서 사용 public class Duration { private final long seconds; private final int nanos; } 주요 메서드 생성 of() : 특정 시간을 지정해서 Duration 를 생성 of(지정) ofSeconds() ofMinutes() ofHours() 계산 더하기 특정 시간 인스턴스의 plus() 메서드를 사용해 시간을 더할 수 있음 e.g. LocalTime lt = LocalTime.of(1, 0); Duration duration = Duration.ofMinutes(30); LocalTime plusTime = lt.plus(duration); between(): 시간 차이 구하기 (Duration 반환) LocalTime start = LocalTime.of(9, 0); LocalTime end = LocalTime.of(10, 0); Duration between = Duration.between(start, end); //Duration 반환 조회 get은 바로 가져오는 느낌, to는 계산을 하는 느낌 (Duration은 내부에 초 데이터만 보유) toHours(), toMinutes() getSeconds(), getNano() 일반적인 x시간 x분을 출력할 때는 toHoursPart() + toMinutesPart() 조합 사용 toHoursPart(), toMinutesPart(), toSecondsPart() 날짜와 시간 조회 및 조작 일관성 있는 시간 조회 및 조작 기능 제공 (인터페이스 설계가 잘되어 있음) 불변 객체이므로 메서드 체이닝 가능 기본 규칙 조회 방법 편의 메서드 사용 (가독성을 위해 권장) 자주 사용하는 조회 필드는 간단한 편의 메서드 제공 getYear(), getMonthValue(), getDayOfMonth(), getHour(), getMinute(), getSecond(), getDayOfWeek() TemporalAccessor.get(TemporalField field) ChronoField 인수로 전달해, 날짜 시간 객체에서 원하는 단위로 조회 가능 get(ChronoField.YEAR), get(ChronoField.MONTH_OF_YEAR), get(ChronoField.DAY_OF_MONTH), get(ChronoField.HOUR_OF_DAY), get(ChronoField.MINUTE_OF_HOUR), get(ChronoField.SECOND_OF_MINUTE), get(ChronoField.DAY_OF_WEEK) 편의 메서드에 없는 경우 사용 조작 방법 편의 메서드 사용 자주 사용하는 메서드는 간단한 편의 메서드 제공 plus -> plusXxx, minus -> minusXxx Temporal plus(TemporalAmount amount) Period, Duration 인수로 전달해 조작 가능 Temporal plus(long amountToAdd, TemporalUnit unit) 시간의 양과 ChronoUnit 인수로 전달해, 특정 시점의 시간을 조작 가능 isSupported() - TemporalAccessor & Temporal 인터페이스 현재 타입에서 특정 시간 단위나 필드를 사용할 수 있는지 확인 e.g. LocalDate에는 시, 분, 초 단위 관련 조회 및 조작을 할 수 없음 LocalDate now = LocalDate.now(); boolean supported = now.isSupported(ChronoField.SECOND_OF_MINUTE);//false if (supported) { int minute = now.get(ChronoField.SECOND_OF_MINUTE); } 기간 차이 구하기 남은 기간 Period, Duration의 between() e.g. Period period = Period.between(startDate, endDate); 년: period.getYears() / 월: period.getMonths() / 일: period.getDays() 디데이 ChronoUnit의 between(Temporal, Temporal) e.g. long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); with() 복잡한 날짜 계산에 적합 날짜와 시간의 특정 필드 값만 변경하는 것이 가능 방법 편의 메서드 자주 사용하는 메서드는 간단한 편의 메서드 제공 dt.with(ChronoField.YEAR, 2020) -> dt.withYear(2020) TemporalAdjusters 사용 TemporalAdjuster 인터페이스의 구현체 묶음 (자바가 만들어 둠) 더욱 복잡한 날짜 계산 가능 e.g dt.with(TemporalAdjusters.next(DayOfWeek.FRIDAY)) 다음주 금요일 구하기 dt.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY)) 이번 달의 마지막 일요일 구하기 Temporal with(TemporalField field, long newValue) 단순한 날짜만 변경 가능 e.g. dt.with(ChronoField.YEAR, 2020) 날짜와 시간 문자열 파싱과 포멧팅 포멧팅과 파싱 포멧팅: Date -> String 파싱: String -> Date DateTimeFormatter 날짜와 시간 포멧팅 및 파싱에 사용 포멧팅: ofPattern() LocalDate date = LocalDate.of(2024, 12, 31); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd 일"); String formattedDate = date.format(formatter); //2024년 12월 31일 파싱: 특정 날짜 객체의 parse() LocalDate date = LocalDate.of(2024, 12, 31); String input = "2030년 01월 01일"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd 일"); LocalDate parsedDate = LocalDate.parse(input, formatter); 자주 쓰이는 패턴 y: 연대의 연도 M: 연중 월 d: 월의 일수 H: 24시간제 시(0-23) m: 분 s: 초 패턴 예시 “yyyy년 MM월 dd 일” “yyyy-MM-dd HH:mm:ss” ISO 8601 날짜와 시간의 표준 출력. Reference 김영한의 실전 자바 - 중급 1편
Java-Ecosystem
· 2024-08-06
자바 lang 패키지
java.lang 패키지 자바 언어를 이루는 가장 기본이 되는 클래스들을 보관하는 패키지 모든 자바 애플리케이션에서 자동으로 import됨 (import 생략 가능) 대표 클래스 Object: 모든 자바 객체의 부모 클래스 String: 문자열 Integer, Long, Double: 래퍼타입, 기본형 데이터 타입을 객체로 만든 것 Class: 클래스 메타 정보 System: 시스템과 관련된 기본 기능들 제공 Object 클래스 자바에서 모든 클래스의 최상위 부모 클래스 클래스에 상속 받을 부모 클래스가 없으면 묵시적으로 Object 클래스를 상속 받음 public class Parent {...} == public class Parent extends Object {...} 묵시적 상속으로 인해 Object는 메모리에도 함께 생성됨 Object 클래스가 최상위 부모 클래스인 이유 공통 기능 제공 모든 객체에 필요한 기본 기능을 구현 모든 개발자들이 직접 만들 필요 없이 일관성 있게 사용 & 프로그래밍이 단순화 Object가 없다면 수많은 개발자들이 유사한 공통 부모 클래스를 구현해 일관성 X… 다형성의 기본 구현 (한계 존재) 다형성의 올바른 활용 = 다형적 참조 + 메서드 오버라이딩 장점: 다형적 참조 가능 모든 객체를 다 담을 수 있으므로, 다양한 타입의 객체를 통합적으로 처리 가능 한계: 메서드 오버라이딩 불가 자식 클래스의 기능을 사용하려면 다운캐스팅을 해야만 함 toString 같은 Object가 보유한 메서드는 당연히 오버라이딩 가능 제공 메서드 toString(): 객체의 정보를 문자열 형태로 제공 기본 로직: 패키지 포함 객체의 이름 + 16진수화된 객체의 참조값 (해시 코드) IDE를 통해 재정의하면 편리 참고) System.out.println() 메서드 내부에서 호출됨 public void println(Object x) {...} -> String.valueOf(x) -> (obj == null) ? "null" : obj.toString() 다형성을 활용한 OCP의 좋은 예 Object를 인자로 받아 다형적 참조 Open: toString을 오버라이딩해 기능 확장 Closed: 클라이언트 코드인 println()은 변경 X -> 덕분에 세상 모든 객체의 정보를 편리하게 출력 가능 equals(): 객체의 같음을 비교 (동등성 비교) “두 객체가 같다”는 2가지 의미 동일성(Identity) 두 객체가 참조값이 같은 동일한 객체인지 확인 (==) 자바 머신 기준, 메모리 참조, 물리적 동등성(Equality) 두 객체가 논리적으로 동등한지 확인 (equals()) 사람 기준, 논리적 기본 로직: == 동일성 비교 제공 동등성 개념은 각각의 클래스마다 다르기 때문에, 동등성이 필요한 경우 재정의 (IDE 활용) getClass(): 객체의 클래스 정보를 제공 hashCode() clone(): 객체 복사 (잘 사용 X) notify(), notifyAll(), wait(): 멀티 쓰레드용 메서드 … 객체의 참조값 출력 toString()의 기본 사용 이외에 다음 코드를 사용하면 객체의 참조값을 직접 출력할 수 있다. String refValue = Integer.toHexString(System.identityHashCode(dog1)); System.out.println("refValue = " + refValue); 출력값: refValue = 72ea2f77 equals() 메서드 구현 시 지켜야할 규칙 (중요 X) 반사성(Reflexivity): 객체는 자기 자신과 동등해야 한다. ( x.equals(x) 는 항상 true ). 대칭성(Symmetry): 두 객체가 서로에 대해 동일하다고 판단하면, 이는 양방향으로 동일해야 한다. (x.equals(y) 가 true 이면 y.equals(x) 도 true ). 추이성(Transitivity): 만약 한 객체가 두 번째 객체와 동일하고, 두 번째 객체가 세 번째 객체와 동일하다면, 첫 번째 객체는 세 번째 객체와도 동일해야 한다. 일관성(Consistency): 두 객체의 상태가 변경되지 않는 한, equals() 메소드는 항상 동일한 값을 반환해야 한다. null에 대한 비교: 모든 객체는 null 과 비교했을 때 false 를 반환해야 한다. 불변 객체 핵심: 불변이라는 단순한 제약을 사용해 사이드 이펙트라는 큰 문제 막을 수 있음 참조형 객체 공유의 문제 기본형과 참조형의 공유 기본형(Primitive Type): 하나의 값을 여러 변수에서 절대로 공유하지 않음 (값 복사 후 대입) 참조형(Reference Type): 하나의 객체를 참조값을 통해 여러 변수에서 공유 가능 따라서, 참조형은 사이드 이펙트 발생 가능성이 높음 사이드 이펙트: 특정 변경이 의도치 않게 다른 부분에 영향을 미침 디버깅이 어려움 & 코드 안정성 저하 또한, 여러 변수가 하나의 객체를 공유하는 것을 막을 방법 X 객체 공유는 개발자가 변수마다 인스턴스를 생성하여 방지 가능 하지만 자바 문법상 참조형 변수 대입은 문제 없기 때문에, 객체 공유를 완벽히 막을 방법은 없음 참조형 변수 대입 (Address b = a) -> 여러 변수가 하나의 객체 공유 사실 근본 원인은 객체를 공유한 것이 아니라 공유될 수 있는 객체의 값을 변경한 것이 문제 결론: 객체의 값을 변경하지 못하게 설계하면 사이드 이펙트 원천 차단 가능 불변 객체 (Immutable Object) 객체의 상태가 변하지 않는 객체 (객체 내부의 값, 필드, 멤버 변수) 설계 전략: 생성자를 통해서만 값을 설정하고, 이후 값 변경 막기 내부 필드를 final로 선언 setXxx() 메서드 제거 개발자는 컴파일 오류 변경 메서드가 없다는 사실 인지 -> 어쩔 수 없이 새 인스턴스 생성 값 변경 필요 시 계산 결과를 새로운 객체로 만들어 반환 기존 값은 변경 X, 계산 결과를 바탕으로 새로운 객체 만들어 반환 (불변 유지 + 새로운 결과) 불변 객체의 변경 관련 메서드들은 보통 새 객체를 만들어 반환하므로 반환 값을 받아야 함 불변 객체 예시 코드 public class ImmutableAddress { private final String value; public ImmutableAddress(String value) { this.value = value; } public String getValue() { return value; } @Override public String toString() { return "Address{" + "value='" + value + '\'' + '}'; } } 불변 객체 값 변경 예시 코드 public class ImmutableObj { private final int value; public ImmutableObj(int value) { this.value = value; } public ImmutableObj add(int addValue) { int result = value + addValue; return new ImmutableObj(result); } public int getValue() { return value; } } 불변 객체의 의의 자바가 기본으로 제공하는 수많은 클래스들은 불변으로 설계되어 있음 가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에 만들어서 사용 같은 기능의 클래스를 하나는 불변, 하나는 가변으로 각각 만드는 경우도 있음 활용 예시 캐시 안정성 멀티 쓰레드 안정성 엔터티의 값 타입 설정에 유용 가변 객체 vs 불변 객체 가변 객체 (Mutable): 처음 만든 이후로 상태가 변할 수 있는 객체 불변 객체 (Mutable): 처음 만든 이후로 상태가 변하지 않는 객체 withXxx() 네이밍 컨벤션 불변 객체에서 값을 변경하는 경우, 메서드 이름이 “with”로 시작하는 경우가 많다. 이는 원래의 상태를 변경하여 새로운 변형을 만든다는 의미를 함유한다. (= coffee with sugar) 즉, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현하는 것이고, 불변 객체의 변경 메서드 내용은 이와 잘 어울린다. String 클래스 문자열을 편리하게 다룰 수 있도록 기능 제공 (char[]로 여러 문자를 직접 다루는 불편함을 해소) 클래스이므로 참조형 문자열 객체 생성 String 클래스를 통한 문자열 생성 방법 쌍따옴표 사용: "hello" 객체 생성: new String("hello"); 문자열은 매우 자주 다루어지므로, 편의상 "", + 등의 연산을 제공해 문자열 처리 문자열 비교 public class StringEqualsMain1 { 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 결론: 항상 equals() 동등성 비교해야 함 String 인스턴스는 new String() 혹은 문자열 리터럴로 만들어질 수 있음 메서드를 사용할 때 String 타입 인자로 둘 중 무엇이 들어올지 알 수 없기 때문 new String() 끼리 비교 시: 동일성 비교 실패 & 동등성 비교 성공 서로 다른 인스턴스이므로 동일성 비교 실패 String 클래스는 동등성 비교를 할 수 있도록 equals() 메서드를 재정의해둠 문자열 리터럴 끼리 비교 시: 동일성 비교 성공 & 동등성 비교 성공 문자열 리터럴을 사용하는 경우, 자바는 메모리 효율성과 성능 최적화를 위해 문자열 풀을 사용 문자열 풀은 힙 영역을 사용하며 메모리 사용과 문자를 만드는 시간을 줄임 자바는로딩 시점에 클래스들을 읽어들이면서 클래스에 문자열 리터럴이 있으면 문자열 풀에 String 인스턴스를 미리 생성해둠 이 때, 같은 문자열이 있으면 만들지 않음 실행 시점에 문자열 리터럴을 사용하면, 문자열 풀에서 String 인스턴스를 찾음 해시 알고리즘을 사용해 매우 빠르게 인스턴스를 찾음 String str3 = "hello", String str4 = "hello"은 같은 참조값 사용 -> 동일성 비교 성공 String은 불변 객체로 설계됨 생성 이후 내부 문자열 값을 변경 불가 & 변경 관련 메서드도 새로운 String 객체를 만들어 반환 불변으로 설계된 이유 사이드 이펙트를 막기 위해 문자열 풀에 있는 String 인스턴스 값 변경 -> 같은 문자열을 참조하는 다른 변수도 함께 변경 구조 public final class String { //문자열 보관 private final char[] value; // 자바 9 이전 private final byte[] value; // 자바 9 이후 //여러 메서드 public String concat(String str) {...} public int length() {...} ... } 문자열 보관 Java 9 이후에는 메모리를 더 효율적으로 사용하기 위해 문자열 보관에 byte[] 사용 char는 문자 하나당 무조건 2byte를 차지 다만, 영어, 숫자는 보통 1byte 표현 가능하고 나머지는 2byte UTF-16 인코딩 사용 가능 주요 메서드 length() : 문자열의 길이를 반환 charAt(int index) : 특정 인덱스의 문자를 반환 indexOf(String str) : 특정 문자열이 시작되는 인덱스를 반환 substring(int beginIndex, int endIndex) : 문자열의 부분 문자열을 반환 contains(CharSequence s) : 문자열이 특정 문자열을 포함하고 있는지 확인 toLowerCase() , toUpperCase() : 문자열을 소문자 또는 대문자로 변환 trim() : 문자열 양 끝의 공백을 제거 concat(String str) : 문자열을 더함 (+ 연산도 concat 사용) valueOf(Object obj) : 다양한 타입을 문자열로 변환 (숫자, 불리언, 객체…) format(String format, Object... args e.g.1 String.format("num: %d, bool: %b, str: %s", num, bool, str); e.g.2 String.format("숫자: %.2f", 10.1234); //10.12 e.g.3 System.out.printf("숫자: %.2f\n", 10.1234); //10.12 split(String regex) : 문자열을 정규 표현식을 기준으로 분할 join(CharSequence delimiter, CharSequence... elements) : 주어진 구분자로 여러 문자열을 결합 e.g.1 String.join("-", "A", "B", "C"); //A-B-C e.g.2 String[] splitStr = str.split(","); String.join("-", splitStr); 자바의 String 최적화 불변 String 클래스의 단점 String str = "A" + "B" + "C" + "D"; String str = String("A") + String("B") + String("C") + String("D"); String str = new String("AB") + String("C") + String("D"); String str = new String("ABC") + String("D"); String str = new String("ABCD"); 문자를 더하거나 변경할 때 마다 계속해서 새로운 객체를 생성 new String("AB"), new String("ABC") 는 제대로 사용되지도 않고, GC 대상 많은 CPU, 메모리 자원 소모 StringBuilder는 성능과 메모리면에서 효율적 (가변 String) StringBuilder 는 내부에 final 이 아닌 변경할 수 있는 byte[] 을 가짐 가변은 사이드 이펙트에 유의해 사용해야 함 문자열을 변경하는 동안만 사용하다가 변경이 끝나면 안전한(불변) String 으로 변환할 것 예시 코드 1 public class StringBuilderMain { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); sb.append("A"); sb.append("B"); sb.append("C"); sb.append("D"); System.out.println("sb = " + sb); //ABCD sb.insert(4, "Java"); System.out.println("insert = " + sb); //ABCDJava sb.delete(4, 8); System.out.println("delete = " + sb); //ABCD sb.reverse(); System.out.println("reverse = " + sb); //DCBA //StringBuilder -> String String string = sb.toString(); System.out.println("string = " + string); //DCBA } } 예시 코드 2 (메서드 체이닝) public class StringBuilderMain1_2 { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); String string = sb.append("A").append("B").append("C").append("D") .insert(4, "Java") .delete(4, 8) .reverse() .toString(); System.out.println("string = " + string); } } 실무 사용 전략 대부분의 경우 최적화가 되므로 + 연산 사용 문자열 리터럴 최적화 자바 컴파일러는 문자열 리터럴 더하기를 자동으로 합쳐줌 컴파일 전: String helloWorld = "Hello, " + "World!"; 컴파일 후: String helloWorld = "Hello, World!"; 런타임에 별도 문자열 결합 연산을 수행하지 않으므로 성능 향상 String 변수 최적화 문자열 변수의 경우 안에 어떤 값이 들어있는지 컴파일 시점에는 알 수 없음 따라서 컴파일러가 StringBuilder()를 사용해 자동으로 최적화 String result = str1 + str2; -> String result = new StringBuilder().append(str1).append(str2).toString(); 최적화가 어려운 경우에만 StringBuilder 사용 루프 안에서 문자열을 더하는 경우, 최적화가 이루어지지 않음 String result = ""; for (int i = 0; i < 100000; i++) { result += "Hello Java "; } //의도와 다르게 최적화되는 코드 //String result = ""; //for (int i = 0; i < 100000; i++) { // result = new StringBuilder().append(result).append("Hello Java").toString(); //} 컴파일러가 반복을 예측할 수 없음 따라서, 최적화에 실패하고 대략 10만번 문자열 객체를 생성할 것 (2490ms) 이런 경우, 직접 StringBuilder 사용할 것 (3ms) StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100000; i++) { sb.append("Hello Java "); } String result = sb.toString(); 최적화가 어려운 경우 반복문에서 반복해서 문자 연결 (1000번 넘게 간다 싶을 때 빌더 사용) 조건문을 통해 동적으로 문자열 조합 복잡한 문자열의 특정 부분 변경 매우 긴 대용량 문자열 다루기 CharSequence CharSequence는 String, StringBuilder의 상위 타입이다. 문자열을 처리하는 다양한 객체를 받을 수 있다. StringBuilder VS StringBuffer StringBuffer는 StringBuilder와 똑같은 기능을 수행한다. 차이점은 StringBuffer는 내부에 동기화가 되어 있어서, 멀티쓰레드 상황에 안전하다. 물론, 동기화 오버헤드로 인해 성능은 느리다. 메서드 체이닝 (Method Chaining) 메서드 호출의 결과로 자기 자신의 참조값을 반환하도록 설계하는 것이다. (return this;) 이 경우, 반환된 참조값을 사용해서 .을 찍고 메서드 호출을 계속 이어갈 수 있다. StringBuilder를 포함해 자바의 라이브러리와 오픈 소스들이 종종 사용한다. 메서드 체이닝은 코드를 간결하고 읽기 쉽게 만들어주는 효과가 있다. 문자열 뒤집기 StringBuilder의 reverse()를 사용하면 편리하게 문자열을 역순으로 얻을 수 있다. String reversed = new StringBuilder(str).reverse().toString(); 래퍼 클래스 (Wrapper Class) 기본형을 감싸서 만드는 클래스 (=기본형의 객체 버전) 기본형(Primitive Type)이 객체가 아니어서 발생하는 한계 객체 지향의 장점을 살릴 수 없음 메서드 제공 X, 객체 참조가 필요한 컬렉션 프레임워크 사용 불가, 제네릭 사용 불가 null 값을 가질 수 없음 데이터가 없음이라는 상태도 필요성이 있는데 불가능 래퍼 클래스는 기본형의 한계를 해결 자바는 기본형에 대응하는 래퍼 클래스를 기본 제공 특징 불변 객체로 설계됨 equals()로 비교해야 함 (==는 참조형이라 안맞음) equals()와 toString()은 재정의 되어 있음 박싱(Boxing) 기본형을 래퍼 클래스로 변경하는 것 e.g. Integer.valueOf(10) valueOf(...) 사용 권장 성능 최적화 기능 존재 Integer의 경우 개발자들이 일반적으로 자주 사용하는 -128 ~ 127 범위 해당 범위의 Integer 객체를 미리 생성해두고 조회시 미리 생성된 값 반환 (캐싱) 해당 범위가 아닌 값을 조회시 new Integer()를 호출 valueOf는 내부에서 new Integer(...)을 사용해 객체를 생성하고 돌려줌 new Integer(10) 방식은 향후 자바에서 제거될 예정이므로, 직접 사용 X 언박싱(Unboxing) 래퍼 클래스에 들어있는 기본형 값을 다시 꺼내는 것 intValue(), longValue() 등의 메서드 사용 오토 박싱(Auto-boxing) 컴파일러가 개발자 대신 valueOf, xxxValue() 등의 코드를 추가해주는 기능 (컴파일 단계) 기본형과 래퍼형의 편리한 변환 가능 자바는 1.5부터 오토박싱, 오토 언박싱 지원 예시 int value = 7; Integer boxedValue = value; // 오토 박싱(Auto-boxing) int unboxedValue = boxedValue; // 오토 언박싱(Auto-Unboxing) 주요 메서드 valueOf() : 래퍼 타입을 반환 (숫자, 문자열을 모두 지원) parseInt() : 기본형 반환 (문자열 전용, parseXxx) compareTo() : 내 값과 인수로 넘어온 값을 비교 (내 값이 크면 1 ,같으면 0 , 작으면 -1 을 반환) Integer.sum() , Integer.min() , Integer.max() : static 메서드 래퍼 클래스 실무 사용 전략 CPU 연산을 많이 수행하는 특수한 경우에만 기본형 사용해 최적화 (수만~수십만 이상 연속한 연산) 이외에는 코드 유지보수에 더 나은 방향 선택 최신 컴퓨터는 매우 빠르므로 적은 연산 차이는 실질적 도움 X 성능 최적화는 대부분 더 많은 복잡한 코드 요구 특히 웹 애플리케이션의 경우 네트워크 호출을 한 번 줄이는 게 더 효과적 메모리 내 연산 하나보다 네트워크 호출 한 번이 많게는 수십만배 더 오래 걸림 개발 이후 성능 테스트 해보고 정말 문제가 되는 부분을 찾아 최적화 기본형과 래퍼 클래스의 성능 차이 (Integer 기준) 속도 10억번 누적합을 구하는 시나리오 기본형 연산은 래퍼 클래스 연산보다 5배 빠름 (318ms VS 1454ms) 메모리 차이 기본형: 4byte 래퍼 클래스: 4byte + 8~16byte (내부 필드 기본형 값 + 객체 메타데이터) Class 클래스 클래스의 정보(메타데이터)를 다루는데 사용 런타임에 필요한 클래스의 속성과 메서드 정보를 조회하고 조작 가능 주요 기능 타입 정보 얻기: 클래스의 이름, 슈퍼클래스, 인터페이스, 접근 제한자 등과 같은 정보를 조회 리플렉션: 클래스에 정의된 메서드, 필드, 생성자 등을 조회하고, 이들을 통해 객체 인스턴스를 생성하거나 메서드 를 호출하는 등의 작업 가능 동적 로딩과 생성: Class.forName() 메서드를 사용하여 클래스를 동적으로 로드하고, newInstance() 메서드를 통해 새로운 인스턴스를 생성 애노테이션 처리: 클래스에 적용된 애노테이션(annotation)을 조회하고 처리하는 기능을 제공 주요 메서드 클래스 객체 조회 Class clazz = String.class; // 1.클래스에서 조회 Class clazz = new String().getClass(); // 2.인스턴스에서 조회 Class clazz = Class.forName("java.lang.String"); // 3.문자열로 조회 클래스 객체 메서드 getDeclaredFields(): 클래스의 모든 필드를 조회 getDeclaredMethods(): 클래스의 모든 메서드를 조회 getSuperclass(): 클래스의 부모 클래스를 조회 getInterfaces(): 클래스의 인터페이스들을 조회 리플렉션 예시: 클래스 메타 정보 기반 인스턴스 생성하기 Class helloClass = Hello.class; Hello hello = (Hello) helloClass.getDeclaredConstructor().newInstance(); class VS clazz class는 자바의 예약어이므로, 패키지명 및 변수명으로 사용할 수 없다. 자바 개발자들은 이를 대신하여 clazz를 관행으로 사용한다. System 클래스 시스템과 관련된 기본 기능들 제공 주요 기능 System.in , System.out , System.err: 표준 입력, 표준 출력, 오류 스트림 System.currentTimeMillis(), System.nanoTime(): 밀리초, 나노초 단위 현재 시간 제공 System.getenv(): OS에서 설정한 환경 변수의 값 제공 System.getProperties(): 현재 모든 시스템 속성 제공 (자바에서 사용하는 설정 값) System.getProperty(String key): 특정 시스템 속성 제공 System.exit(int status): 프로그램 종료 및 OS에 프로그램 종료의 상태 코드 전달 (사용 지양) 상태코드 0: 정상종료 상태 코드 0 이 아님: 오류나 예외적인 종료 System.arraycopy: 배열 고속 복사 시스템 레벨에서 최적화된 메모리 복사 연산 사용 직접 반복문 을 사용해서 배열을 복사할 때 보다 수 배 이상 빠른 성능을 제공 e.g. System.arraycopy(originalArray, 0, copiedArray, 0, originalArray.length); Math, Random 클래스 Math 클래스 다양한 수학 문제를 해결해주는 클래스 주요 메서드 abs(x) : 절대값 max(a, b) : 최대값 min(a, b) : 최소값 exp(x) : e^x 계산 log(x) : 자연 로그 log10(x) : 로그 10 pow(a, b) : a의 b제곱 ceil(x) : 올림 floor(x) : 내림 round(x) : 반올림 sqrt(x) : 제곱근 cbrt(x) : 세제곱근 random() : 0.0과 1.0 사이의 무작위 값 생성 (double 값) Random 클래스 (java.util 패키지) Math.random() 보다 다양한 랜덤값을 구할 수 있도록 기능 제공 Math.random()도 내부에서는 Random 클래스 사용 Random 객체 생성 방법 기본 생성자 Random random = new Random(); 생성자를 비워두면 씨드값을 자동 생성해 사용 (매 반복마다 결과가 달라짐) System.nanoTime() + 여러가지 복잡한 알고리즘 => 씨드값을 생성 생성자의 Seed 전달 Random random = new Random(1); 랜덤은 내부에서 씨드값을 사용해 랜덤값을 구함 씨드값이 같으면 항상 같은 결과를 출력 주요 메서드 random.nextInt() : 랜덤 int 값을 반환 nextInt(int bound) : 0 ~ bound 미만의 숫자를 랜덤으로 반환 예를 들어서 3을 입력하면 0, 1, 2 를 반환한다. 활용: 1 ~ 10 까지 반환하기 random.nextInt(10) + 1 nextDouble() : 0.0d ~ 1.0d 사이의 랜덤 double 값을 반환 nextBoolean() : 랜덤 boolean 값을 반환 정밀 계산에는 BigDecimal을 활용하자. 열거형 - Enum 단순 문자열 처리는 오타나 유효하지 않은 값이 입력될 수 있어 타입 안정성이 떨어짐 (컴파일 오류 감지 X) e.g. 회원 등급 별 할인 - DIAMOND, GOLD, BASIC 특정 범위로 값 제한 필요 해결 과정 단계 1단계: 문자열 상수 처리 e.g. public static final String BASIC = "BASIC" 장점: 문자열 상수를 사용하면 유효하지 않은 값에 대해 컴파일 오류 발생 단점: 개발자가 실수로 정의해둔 문자열 상수를 사용하지 않으면, 여전히 직접 문자열 입력 가능 public int discount(String grade, int price) {} 위 코드를 보면 개발자는 당연히 모든 문자열을 입력할 수 있다고 생각하게 됨 2단계: 타입 안전 열거형 패턴 (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 ClassGrade() {} } 핵심: 나열한 항목만 사용할 수 있게 제한 애플리케이션 로딩 시점에(static) 각각의 상수에 별도 인스턴스를 생성해 구분 외부에서 new ClassGrade() 생성 및 전달을 막기 위해 private 생성자를 둠 장점 타입 안정성 및 데이터 일관성 향상 (컴파일 오류 체크 가능) public int discount(ClassGrade classGrade, int price) {} 사전에 정의해둔 인스턴스만 사용할 수 있음 == 동일성 비교 가능 (문자열 처리 시 equals()를 사용해야 했음) 단점: 많은 코드 작성 & private 생성자 추가 유의점 3단계: 열거형 (Enum Type) 타입 안전 열거형 패턴을 쉽게 사용할 수 있도록 프로그래밍 언어에서 지원 예상 가능한 집합을 표현하는 데 사용 Enumeration(in 프로그래밍): 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합 타입 안정성 및 코드 가독성 향상 static import 사용 시 가독성 더욱 향상 기본 사용법 public enum Grade { BASIC, GOLD, DIAMOND } 열거형도 (제약이 추가된) 클래스 (class 대신 enum 키워드를 사용할 뿐) 열거형은 자동으로 java.lang.Enum을 상속 받음 (extends Enum, 추가 상속 불가) 외부 임의 생성 불가 (private 생성자) 인터페이스 구현이 가능 열거형에 추상 메서드 선언 및 구현 가능 (익명 클래스와 같은 방식 사용) 주요 메서드 values(): 모든 ENUM 상수를 포함하는 배열을 반환 Enum 상수 valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환 name(): ENUM 상수의 이름을 문자열로 반환 ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환 (사용 지양) 중간에 상수 선언 위치가 변경되면 전체 상수 위치가 모두 변경됨 toString(): ENUM 상수의 이름을 문자열로 반환 name() 메서드와 유사하지만, toString() 은 직접 오버라이드 가능 객체 지향적 예시코드 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 클래스 내 필드 추가하고 생성자를 통해 필드 값 저장 열거형은 접근제어자 선언을 막아두었기 때문에, 생성자 선언은 private이 적용 상수 끝에 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출됨 (BASIC(10)) 열거형도 클래스이므로 메서드 추가 가능 (getDiscountPercent()) Reference 김영한의 실전 자바 - 중급 1편
Java-Ecosystem
· 2024-08-01
자바 객체 지향 설계
클래스가 필요한 이유 String[] studentNames = {"학생1", "학생3", "학생4", "학생5"}; int[] studentAges = {15, 17, 10, 16}; int[] studentGrades = {90, 100, 80, 50}; 학생이라는 개념을 다룰 때, 배열과 반복문으로 데이터를 처리해야 하므로 데이터 변경 시 실수할 가능성이 높다. 따라서 사람이 관리하기 좋은 코드를 만들기 위해 학생이라는 개념을 하나의 클래스로 묶어야 한다. 클래스 특징 클래스를 통해 마음껏 사용자 정의 타입을 만들 수 있다. (설계도) 클래스에 정의한 변수들 = 멤버 변수(Member variable) = 필드(Field) 실제 메모리에 만들어진 실체를 객체 혹은 인스턴스라 한다. 클래스 타입 변수는 객체를 생성하면 해당 객체의 참조값을 담는다. System.out.println(student); // 출력값 // (패키지 + 클래스 정보 @ 16진수 참조값) class1.Student@7a81197d 클래스 & 인스턴스 & 객체 클래스 객체 생성을 위한 ‘틀’ 또는 ‘설계도’ 객체가 가져야 할 속성(변수)과 기능(메서드)를 정의한다. 인스턴스 클래스로부터 생성된 객체 인스턴스 = 객체 어떤 클래스에 속해 있는지 강조 (관계에 초점) 객체 클래스의 속성과 기능을 가진 실체 세상 모든 사물을 단순하게 추상화해보면 속성과 기능 2가지만 남는다. 변수의 값 초기화 멤버 변수: 자동 초기화 인스턴스 생성시 자동 초기화 (new로 만드는 객체들의 멤버 변수들은 모두 자동 초기화된다.) int = 0, boolean = fasle, 참조형 = null 직접 초기화 지정 가능 지역 변수: 수동 초기화 null 참조형 변수에서 아직 가리키는 대상이 없다면 null을 넣어둘 수 있다. Data data = null; 아무도 참조하지 않는 인스턴스 (feat. GC) 참조형 변수에 null을 할당하면 해당 참조 데이터가 메모리에 남아 있다가 GC(Garbage Collector)에 의해 제거된다. 메소드가 종료되어 지역변수가 사라질 때, 지역변수가 참조하고 있던 인스턴스 역시 메모리에 남아 있다가 GC에 의해 제거된다. NullPointerException null에 .을 찍을 때 발생하는 에러이므로 디버깅시 유의하자. this 인스턴스 자신의 참조값을 가리킨다. 생성자에서 지역변수 이름이 겹친다면 this를 통해 멤버변수에 접근할 수 있다. this는 생략이 가능하다. 과거에는 명시적으로 보이지 않아 멤버 변수 접근시 항상 this를 사용하는 코딩 스타일이 존재했다. 그러나 최근엔 IDE의 발달 덕분에 멤버변수와 지역변수 구분이 잘되기 때문에, 꼭 필요한 경우에만 사용하고 생략하는게 권장된다. 변수 탐색 변수를 찾을 때 가까운 지역변수(매개변수 포함)를 먼저 찾고 없으면 그 다음으로 멤버변수를 찾는다. 멤버변수도 없으면 오류가 발생한다. 생성자 규칙 생성자의 이름은 클래스 이름과 같아야 한다. 반환타입이 없으므로 비워둬야 한다. 나머지는 메서드와 동일 인스턴스 생성 후 즉시 호출된다. new 키워드 이후 ()는 생성자 호출을 의미한다. 생성자 덕분에 자동 초기화로 인한 더미 데이터 생성을 방지하여 초기화를 강제할 수 있다. 기본 생성자 public class MemberInit { // 기본 생성자 public MemberInit() { } } 매개 변수가 없는 생성자 따로 정의한 생성자가 없는 경우 자바 컴파일러가 매개변수와 코드가 없는 기본생성자를 자동으로 만들어 준다. 생성자 오버로딩 생성자도 메서드 오버로딩처럼 여러 생성자 제공 가능 public class MemberConstruct { String name; int age; int grade; //추가 MemberConstruct(String name, int age) { this.name = name; this.age = age; this.grade = 50; } MemberConstruct(String name, int age, int grade) { this.name = name; this.age = age; this.grade = grade; } } this() 생성자 내부에서 자신의 생성자를 호출할 수 있다. (중복 제거를 위해) 단, this()는 생성자 코드 첫줄에만 작성할 수 있다. (아니면 컴파일 오류 발생) public class MemberConstruct { String name; int age; int grade; MemberConstruct(String name, int age) { this(name, age, 50); //변경 } MemberConstruct(String name, int age, int grade) { this.name = name; this.age = age; this.grade = grade; } } 절차 지향 프로그래밍 VS 객체 지향 프로그래밍 절차 지향 프로그래밍 프로그램의 흐름을 순차적으로 따르며 처리하는 방식 데이터와 기능이 분리되어 있다. 데이터와 기능의 분리는 유지보수 관점에서 관리 포인트가 2곳으로 늘어난다. 객체 지향 프로그래밍 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식 (실제 세계의 사물이나 사건을 단순하게 추상화) 속성과 기능(메서드)이 객체 안에 함께 포함되어 있다. (캡슐화) 장점 객체 사용자의 입장에서 코드가 보다 친숙하고 가독성이 높다. 유연하고 변경이 용이하다. (OCP 원칙을 지키는 확장 가능한 설계) 실세계를 역할(인터페이스)과 구현(구현한 클래스 혹은 객체)으로 구분 (다형성) 클라이언트 코드를 변경하지 않고 서버의 구현 기능을 변경할 수 있다. (= 클라이언트는 인터페이스만 알면 내부 구조를 몰라도 되고 내부 구조를 변경해도 영향을 받지 않는다.) 한계 인터페이스가 변하면 클라이언트, 서버 모두 큰 변경이 발생한다. 따라서 인터페이스를 안정적으로 잘 설계하는 것이 중요하다. 캡슐화(Encapsulation) 속성과 기능을 하나로 묶어서 꼭 필요한 기능만 메서드를 통해 외부에 제공하고 나머지는 모두 내부로 숨기는 것 속성과 기능 묶기 + 접근 제어자를 통해 실현 좋은 캡슐화 속성은 반드시 숨기자. 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해서 접근해야 한다. 데이터를 외부에 열어두면 클래스 내 데이터를 다루는 로직을 무시하고 데이터를 변경할 수 있음 꼭 필요한 기능만 노출하자. 클래스 내부에서만 사용하는 기능들은 모두 감추는 것 좋다. 사용하는 개발자 입장에서 필요한 기능만 정리되어 복잡도가 낮아진다. 음악 플레이어 예제 메소드 추출 팁 자신이 가진 데이터로 계산한다면, 일반적으로 자기자신이 메서드로 계산하는게 좋다. 나중에 수정이 생기거나 변경이 생길 때 본인만 바꾸면 되므로 관리가 편하다. 접근 제어자 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다. 필드, 메서드, 생성자에 사용된다. 지역변수는 스코프 내에서만 사용하므로 접근제어자를 사용하는 의미가 없고 사용할 수도 없다. 클래스에는 일부만 사용가능하다. (public, default) public 클래스는 반드시 파일명과 이름이 같아야 한다. 하나의 자바 파일에 public 클래스는 하나만, default 클래스는 무한정 만들 수 있다. 종류 private: 모든 외부 호출을 막는다. default(package-private): 같은 패키지안에서 호출은 허용한다. protected: default + 다른 패키지여도 상속 관계의 호출은 허용한다. public: 모든 외부 호출을 허용한다. 상속(Inheritance) extends 기존 부모 클래스의 필드와 메서드를 새로운 자식 클래스에서 재사용하는 것 중복을 줄이고 편리하게 확장할 수 있음 단일 상속만 할 수 있다. (다중 상속은 불가능) 만일, 두 부모를 상속받았는데 둘 다 move()라는 메서드를 가지고 있다면 어떤 메서드를 실행해야할지 애매하다. (다이아몬드 문제) 클래스 계층구조가 매우 복잡해질 수 있다. 메서드 오버라이딩 상속 받은 기능을 자식이 재정의하는 것 멤버변수는 오버라이딩되지 않는다. @Override 메모리 구조 상속관계 객체 생성 시 그 내부에 부모와 자식이 모두 생성된다. (하나의 참조값에 두 클래스 정보가 공존) 상속관계 호출시 대원칙 (3개) 상속관계 객체 호출 시, 호출자의 타입을 기준으로 먼저 찾는다. 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행한다. (끝까지 올라가도 없으면 컴파일 오류 발생) 자식 클래스에 오버라이딩된 메서드가 있다면 항상 우선하여 호출된다. Car와 ElectricCar 예제 super 상속관계에서 부모와 자식의 필드 이름과 메서드 이름이 같은 경우, 부모를 참조하고 싶을 때 super를 통해 부모 클래스로 접근한다. 생성자 상속관계를 사용하면 자식 클래스의 생성자와 부모 클래스의 생성자를 반드시 호출해야 한다. 상속 시 생성자 첫 줄에 super()를 사용해 부모 클래스 생성자를 호출해야 한다. 예외로 첫 줄에 this()(=나말고 다른 생성자를 호출해줘)를 사용할 수 있다. 그러나 자식 생성자 내에서 언젠간 super()가 호출되어야 한다. 부모 클래스의 생성자가 기본생성자라면 super()를 생략할 수 있다. 결과적으로 상속관계 생성자 호출은 부모에서 자식 순으로 실행된다. 다형성(Polymorphism) 다른 타입의 객체를 하나인 것처럼 처리해 주는 것 (아래 두가지 특성 덕분에 실현됨) (= 한 객체가 여러 타입의 객체로 취급될 수 있는 것) 다형성의 본질은 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다는 것 다형적 참조 부모는 자식을 품을 수 있다. (부모 타입의 변수가 다양한 자식 인스턴스를 참조할 수 있다.) Parent poly = new Child() = 업캐스팅 (업캐스팅은 생략이 가능하고 권장된다.) 업캐스팅은 메모리상에 인스턴스가 항상 존재하므로 안전하다. 반면에, 자식은 부모를 품을 수 없다. Child child = poly // 컴파일 에러 만약 부모 클래스에서 자식 클래스의 메서드를 호출하고 싶다면 다운캐스팅 해야한다. Child child = (Child) poly ((Child) poly).childMehtod() (일시적 다운 캐스팅도 가능) 다만, 다운캐스팅은 자식 타입이 메모리상에 존재하지 않을 경우 ClassCastException 런타임 에러를 발생시키므로 매우 주의가 필요하다. 다운 캐스팅 시 instance of를 사용하면 안전하다. 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스 타입이 들어갈 수 있으면 true, 아니면 false new Parent() instanceof Parent // true new Child() instanceof Parent // true new Parent() instanceof Child // false 자바 16부터는 instanceof와 동시에 변수 선언도 가능하다. if (parent instanceof Child child) {...} 다형적 참조 덕분에 자식 인스턴스들을 함수의 부모 타입 매개변수로 참조하거나, 배열의 타입을 부모 타입으로 가져가 자식 인스턴스들을 참조할 수 있다. (중복 제거 및 반복 가능) 메서드 오버라이딩 오버라이딩된 메서드는 항상 우선권을 가진다. 자식에서도 오버라이딩하고 손자에서도 오버라이딩했다면, 손자의 오버라이딩 메서드가 우선권을 가진다. 만일 메서드 오버라이딩이 없다면 항상 부모 타입의 메서드를 호출했을 것이다. 다형성 덕분에 IoC, OCP, DIP, 전략 패턴 등이 가능해짐 다형성이 매우 중요하다. OCP 원칙 좋은 객체 지향 설계 원칙 중 하나 Open for extension, Closed for modification (확장에는 열려있고 변경에는 닫혀 있다) 기존의 코드 수정 없이 새로운 기능을 추가할 수 있다는 의미 다형성을 보완하는 추상 클래스 추상 클래스는 다형성만으로 생기는 두 가지 문제를 해결한다. 부모 클래스를 인스턴스로 생성할 수 있는 문제 (추상적인 개념이 실제로 존재하는 것은 이상함) 부모 클래스를 상속 받는 자식 클래스가 메서드 오버라이딩을 하지 않을 가능성 (개발자의 실수) 추상 클래스 부모 클래스는 제공하지만 실제 생성되면 안되는 클래스 추상적인 개념을 제공하며 부모 클래스 역할로서 상속 목적으로 사용 인스턴스를 생성할 수 없음 (제약 1) abstract class AbstractAnimal {...} 추상 메서드 자식 클래스가 반드시 오버라이딩해야 하는 메서드 (제약 2) 메서드 바디가 없음 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다. public abstract void sound() 인터페이스 - 순수 추상 클래스를 지원 인터페이스 등장 배경 추상 클래스는 여전히 자신의 메서드를 가질 수 있다. 반면에, 순수 추상 클래스는 추상 클래스를 실행 로직이 전혀 없는 추상 메서드로만 구성한 것을 의미한다. 이는 다형성을 위한 규격, 마치 USB 인터페이스 같은 느낌을 준다. 자바는 이러한 순수 추상 클래스를 편리하게 사용할 수 있도록 인터페이스를 지원한다. 특징 interface 키워드, 구현시 implements 키워드 사용 인터페이스의 메서드는 모두 public abstract이다. (직접 쓸 수도 있지만 생략 권장) 인터페이스의 멤버 변수는 public static final이다. (마찬가지로 생략 권장) 구현이라는 용어 사용 상속은 부모의 기능을 물려 받는 것이지만, 인터페이스는 모든 메서드가 추상 메서드이므로 물려받을 기능이 없고 오히려 자식이 오버라이딩해서 메서드를 구현해야 한다. 다만, 자바 입장에서는 상속이나 구현이나 동일하게 동작한다. 클래스 & 추상 클래스 & 인터페이스는 코드와 메모리 구조상 모두 동일하다. 다중 구현을 지원 유용한 이유 제약 인터페이스의 메서드를 반드시 구현하라는 규약을 준다. 순수 추상 클래스를 지향해도 추상 클래스는 다른 개발자가 미래에 메서드를 추가할 수 있기 때문에, 인터페이스는 이를 예방한다. 다중 구현 클래스의 상속이 하나의 부모만 지정할 수 있는 것과 달리, 인터페이스는 여러 부모를 둘 수 있다. 인터페이스는 자신이 구현을 가지지 않고, 자식이 메서드를 구현한다. 또한 어차피 오버라이딩으로 인해 자식의 메서드가 호출된다. 따라서, 다이아몬드 문제가 발생하지 않는다. 실무적 장단점 인터페이스는 기획이나 사용 기술이 구체화되지 않았을 때, 구현을 미룰 수 있다. (장점) 어떤 DB를 사용할지 미정이라면, 인터페이스만 구현 후 메모리 레포지토리를 사용 할인 정책이 미정이라면, 인터페이스만 구현 후 0원 할인으로 미리 개발 가능 인터페이스는 추상화라는 비용을 발생시킨다. (단점) 개발자가 코드를 읽을 때 인터페이스를 항상 본 후 구현체를 보게 되어 읽는 시간이 증가한다. 대부분 모든 곳에 인터페이스를 먼저 구현하는 것이 이상적이지만, 기능을 확장할 가능성이 없다면 구체 클래스를 직접 사용하고 향후 꼭 필요할 때 리팩토링해서 인터페이스를 도입하는 것도 좋다. 의존 관련 용어 정리 A -> B (UML) = A가 B를 안다. = A가 B를 의존한다. = A가 B를 상속받았다. (A가 자식이고 B가 부모다) = A가 B를 사용한다. 좋은 객체 지향 설계의 5가지 원칙 (SOLID) 클린 코드 저자 Robert Martin(로버트 마틴)은 좋은 객체 지향 설계의 5가지 원칙을 제시한다. 단일 책임 원칙(SRP, Single responsibility principle) 한 클래스는 하나의 책임만 가져야 한다. 책임의 추상적인 표현이지만, 변경을 기준으로 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것 개방-폐쇄 원칙(OCP, Open/closed principle) 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다. 다형성을 활용해 기존 코드는 변경하지 않고 새로운 기능들을 추가할 수 있다. 리스코프 치환 원칙(LSP, Liskov substitution principle) 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 다형성의 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것 다형성에 신뢰성을 부여하고 지원하는 원칙 예시 자동차 인터페이스의 엑셀은 앞으로 가야하는 기능인데, 뒤로 가게 구현하면 LSP 원칙 위반 느리게 가더라도 앞으로 가야한다. 인터페이스 분리 원칙(ISP, Interface segregation principle) 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 인터페이스가 명확해지고 대체 가능성이 높아진다. 예시 자동차 인터페이스 -> 운전 인터페이스 & 정비 인터페이스로 분리 사용자 클라이언트 -> 운전자 클라이언트 & 정비사 클라이언트로 분리 정비 인터페이스가 변해도 운전자 클라이언트에 영향을 주지 않음 의존 관계 역전 원칙(DIP, Dependency inversion principle) 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다. 즉, 클라이언트 코드가 구현 클래스에 의존하지 말고 인터페이스에 의존하라는 의미 다만, 다형성만으로는 OCP, DIP 원칙을 지킬 수 없다. OCP, DIP를 지키려고 하다보면 결국 스프링 프레임워크를 만들게 된다. Reference 김영한의 실전 자바 - 기본편 스프링 핵심 원리 - 기본편
Java-Ecosystem
· 2024-02-06
자바 메모리 구조와 변수, 메서드 종류
자바 메모리 구조 메서드 영역 프로그램을 실행하는데 필요한 공통 데이터를 관리 프로그램의 모든 영역에서 공유됨 구성 클래스 정보: 클래스 실행 코드 (바이트 코드) - 필드, 메서드, 생성자 코드 등 static 영역: static 변수, 메서드, 클래스들을 보관 (프로그램 시작부터 끝까지 메모리 할당) 런타임 상수 풀: 프로그램을 최적화하기 위해 공통 리터럴 상수를 보관 스택 영역 실제 프로그램이 실행되는 영역 실행 스택을 생성하고 메서드가 호출될 때마다 스택에 스택 프레임을 쌓는다. 메서드가 종료되면 스택프레임을 제거한다. 지역변수, 중간 연산 결과, 메서드 호출 정보 등이 스택 프레임에 포함된다. 처음 자바를 실행하면 main()을 실행하기 위해 실행 스택에 main() 스택 프레임을 하나 생성한다. 힙 영역 인스턴스가 생성되는 영역 (new 명령어를 사용하면 여기를 사용) 가비지 컬렉션이 이루어지는 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다. 메서드 코드의 위치 객체가 생성될 때, 인스턴스 내부 변수 값은 각각 힙 영역에 할당되어 독립적으로 존재하지만, 메서드는 새로운 메모리 할당없이 공통된 코드를 공유한다. 따라서, 인스턴스 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 힙 영역으로 불러와서 수행한다. 단, static 메서드는 메서드 영역의 클래스 정보 코드를 사용하겠지만, 실행도 메서드 영역의 클래스 정보에서 한다. 멤버 변수의 종류 인스턴스 변수 (static이 붙지 않음) 각각의 인스턴스에 독립적으로 소속되어 있는 변수 static 변수 (static이 붙음) 클래스 자체에 소속되어 공용으로 함께 사용할 수 있는 변수 static 변수 = 정적 변수 = 클래스 변수 메서드 영역 (static 영역)에서 관리 클래스명 + . 으로 접근 (자신의 클래스에 있는 정적 변수라면 클래스명 생략 가능) 처음 자바가 로딩될 때 하나만 생성 일반적으로 자바 프로그램이 실행되고 JVM이 처음 뜰 때, 클래스 정보를 메소드 영역에 모두 불러 들이고, static이 붙은 변수들은 메모리(static 영역)에 할당해버린다. 이런 static 변수들은 이 때 딱 하나 만들어지고 Java가 끝날 때까지 계속 쓸 수 있다. 변수의 생명주기 지역변수(매개변수 포함): 스택 영역의 스택 프레임에 존재 (메서드 종료 시 소멸) 인스턴스 변수: 힙 영역에 존재 (GC 발동 시 소멸) 클래스 변수: 메서드 영역의 static 영역에 존재 (JVM 종료 시 소멸) 지역변수 < 인스턴스 변수 < 클래스 변수 지역 변수가 제일 짧고, 클래스 변수가 제일 길다. static이 정적인 이유 힙 영역에 생성되는 인스턴스 변수는 런타임에서 동적으로 생성되고 제거되지만, static 변수는 프로그램 시작 시점에 만들어지고 프로그램 종료 시점에 제거되므로 상대적으로 매우 정적이다. 멤버 메서드의 종류 인스턴스 메서드 (static이 붙지 않음) 인스턴스에 소속되어 인스턴스를 생성해야 사용할 수 있는 메서드 static 메서드 (static이 붙음) 클래스에 소속되어 클래스에 바로 접근해 사용할 수 있는 메서드 static 메서드 = 정적 메서드 = 클래스 메서드 인스턴스 변수를 필요로 하지 않는 단순 기능만 제공하는 경우 사용 (유틸리티성 메서드) static 메서드는 static만 사용할 수 있다.(정적변수나 정적 메서드) main()가 대표적 정적 메서드 (main()이 같은 클래스에서 호출하는 메서드도 정적 메서드) 자주 호출해야 한다면 static import를 통해 클래스 명을 생략하고 메서드를 호출할 수 있다. final 변수에 final 키워드가 붙으면 더는 값을 변경할 수 없다. 특정 변수의 값을 할당한 이후 변경하지 않아야 한다면 사용하자. (고객 id 같은 부분) 의미 있는 경우 static final 필드(클래스 멤버 변수)를 필드 초기화 하는 것 (메모리 중복 없음) 상수도 static final을 지정한다. 생성자를 이용해서 final 필드(인스턴스 멤버 변수)를 초기화 하는 것 의미 없는 경우 final 필드(인스턴스 멤버 변수)를 필드 초기화 하는 것 (인스턴스마다 값이 중복되어 메모리 낭비) 클래스 final 상속의 끝을 의미, final로 선언된 클래스는 상속할 수 없다. 메서드 final 오버라이딩의 끝을 의미, final로 선언된 메서드는 오버라이드 될 수 없다. Reference 김영한의 실전 자바 - 기본편
Java-Ecosystem
· 2024-02-06
IntelliJ 단축키 정리
자동완성 계열 iter For Each Syntax sout println soutv println + 원하는 변수 soutm 현재 클래스와 메소드 이름을 출력한다. 리팩토링 계열 command + shift + T 클래스 지정하면 그에 대한 테스트 틀 생성 command + shift + 위/아래 위/아래 메서드와 위치 변경 (메서드에 커서 선택 후 진행) command + shift + 8 Column Selection Mode (여러 라인 동시 수정 가능) command + n 파일 혹은 코드 생성 command + option + N Inline variable로 리팩토링 command + option + V 변수 추출 및 추천 (Introduce variable) command + option + M 메소드 추출 및 추천 command + option + shift + L 파일 코드 재정렬 option + Enter Context에 따른 가능 액션 종류를 보여줌 (create method, create class 등) 자동 import, static import(Add on-demand static import) 지원 option + option + 위/아래 Multi-line select option + shift + click Specific line select ctrl + T 리팩토링 ctrl + O 오버라이드 shift + F6 클래스, 변수, 파라미터 이름을 일괄적으로 변경 유틸 계열 command + B 코드가 사용된 모든 곳들을 추적 command + option + B 해당 인터페이스의 모든 구현체들 추적 command + P 파라미터 정보 제공 command + E 과거 행동 이력 및 이전 파일 이동 command + O Navigate shift X 2 Search (Navigate과 유사) ctrl + R 바로 이전 것 실행 ctrl + shift + R 클래스 레벨 실행 option + ↑ 범위 블록 설정 F2 오류가 발생한 곳으로 커서 이동 command + 숫자 0 Commit 1 프로젝트 디렉토리 영역 혹은 코드 작업 영역 선택 4 Run window 5 Debug window 6 Problems 9 Git log command + F_number F12 해당 클래스의 멤버 변수, 메서드, 상속 클래스 등의 전체 정보 보기
Java-Ecosystem
· 2024-02-05
자바 주요 syntax 정리
대원칙 자바는 항상 변수의 값을 복사해서 대입한다. 변수 선언과 초기화 변수 선언 메모리 공간을 확보해서 데이터를 저장할 그릇을 만드는 것 변수 이름을 통해 해당 메모리공간에 접근한다. 변수 초기화 선언한 변수에 처음으로 값을 저장하는 것 초기화하지 않고 사용할 경우 컴파일 에러 변수를 선언하면 어떤 메모리 공간을 차지하지만, 해당 공간은 다른 프로그램이 사용하고 종료되어 남겨진 알 수 없는 값이 있을 수 있다. 따라서, 초기화하지 않으면 이상한 값이 출력될 수 있으므로, 자바는 문제를 예방하기 위해 변수 접근 전에 초기화를 강제 선언 및 초기화 방식 예제 // 1. 변수 선언, 초기화 각각 따로 int a; a = 1; System.out.println(a); // 2. 변수 선언과 초기화를 한번에 int b = 2; System.out.println(b); // 3. 여러 변수 선언과 초기화를 한번에 int c = 3, d = 4; System.out.println(c); System.out.println(d); // 4. 여러 변수 선언을 한번에 int e, f; 컴파일 에러 & 런타임 에러 컴파일 에러는 문법에 맞지 않았을 때 발생하는 에러로 오류를 빠르고 명확하게 찾을 수 있어서 좋은 에러이다. 반면에, 런타임 에러는 프로그램 실행 시 발생하는 에러로 미리 예방이 어려워 나쁜 에러이다. 예를 들어, 고객이 계좌이체를 했는데 내 돈은 나가고 상대방에게 돈이 안들어간 경우 돈이 증발되는 비극이 런타임에서 발생할 수 있다. 주로 사용하는 변수 타입 정수 int (기본) long (리터럴 값이 20억이 넘을 것 같을 때 사용) 실수 double 불린형 boolean 문자열 String 문자 하나든 문자열이든 모두 String을 사용하는 것이 편리하다. 문자 길이에 따라 메모리 공간이 동적으로 달라짐 예외: 파일 다루기 byte (파일은 바이트 단위로 다루므로 파일 전송, 파일 복사 등에 주로 사용) 변수 명명 핵심 관례 클래스는 대문자로 시작, 나머지는 소문자로 시작 예외 상수는 모두 대문자를 사용하고 언더바로 구분 패키지는 모두 소문자로 사용 전위 & 후위 증감 연산자 전위 증감 연산자 증감 연산이 먼저 수행된 후 나머지 연산 수행 후위 증감 연산자 다른 연산이 먼저 수행된 후 증감 연산 수행 예제 public class OperatorAdd { public static void main(String[] args) { // 전위 증감연산자 int a = 1; int b = 0; b = ++a; System.out.println("a = " + a + ", b = " + b); // a = 2, b = 2 // 후위 증감연산자 a = 1; b = 0; b = a++; System.out.println("a = " + a + ", b = " + b); // a = 2, b = 1 } } 새로운 switch syntax 자바 14부터 조금 더 깔끔한 switch 문법이 도입되었다. public class Switch { public static void main(String[] args) { int grade = 2; int coupon = switch (grade) { case 1 -> 1000; case 2 -> 2000; case 3 -> 3000; default -> 500; }; System.out.println("발급받은 쿠폰 " + coupon); } } 삼항 연산자 syntax 예제 String coffee = (time < 3) ? "caffeine" : "decaffeine"; do-while문 조건에 상관없이 무조건 한 번은 코드를 실행한다. public class DoWhile { public static void main(String[] args) { int i = 5; do { System.out.println(i); i++; } while (i < 3); } } 무한 루프 For문 For문은 초기식, 조건식, 증감식의 생략이 가능하고 모두 생략할 경우 무한 루프 반복문이 된다. (= while true) public class ForLoopInfinite { public static void main(String[] args) { int i = 1; for (;;) { if (i >= 10) { System.out.println("Exit on 10"); break; } System.out.println(i); i++; } } } 다음과 같이 특정식만 생략하는 경우도 가능하다. 결과는 위와 동일하다. public class ForLoopInfinite { public static void main(String[] args) { for (int i = 1; ; i++) { if (i >= 5) { System.out.println("Exit on 5"); break; } System.out.println(i); } } } 지역 변수와 스코프 지역 변수(Local Variable) 특정 지역에서만 사용할 수 있는 변수 지역은 코드 블록({})을 의미 자신이 선언된 코드 블록 안에서만 생존하고, 블록을 벗어나면 제거된다. 스코프(Scope) 변수의 접근 가능한 범위 for문의 초기식은 for문 내 scope에서만 접근 가능하고 바깥에서는 사용할 수 없다. 변수의 스코프를 꼭 필요한 곳에 한정해 사용해야 메모리를 효율적으로 사용하고 더 유지보수하기 좋은 코드가 된다. main() 코드 스코프의 변수는 프로그램이 종료될 때까지 메모리에 유지되어 비효율적이다. (비효율적인 메모리 사용) 필요한 곳 바깥에서부터 변수를 선언하면 생각해야 할 변수가 늘어 복잡하다. (코드 복잡성 증가) 연산 시 주요 핵심 같은 타입끼리의 연산 결과는 타입이 동일하다. (int + int는 int다) 서로 다른 타입의 계산은 큰 범위로 자동 형변환이 일어난다. (int + long은 long + long) 자동 형변환과 명시적 형변환 자동 형변환 작은 범위에서 큰 범위로 대입은 허용한다. int < long < double 큰 범위에서 작은 범위는 문제가 발생 소수점 버림 오버플로우 다른 타입끼리의 연산 시 큰 범위로 자동 형변환이 발생한다. 명시적 형변환 위험을 감수하고 데이터 타입을 강제로 변경하는 것 자바는 기본적으로 큰 범위에서 작은 범위의 대입에 대해 컴파일 에러를 발생시킨다. 은행 이자를 계산하는데 타입 문제로 (double -> int) 이자가 날아가버리는 등의 큰 문제를 방지하기 위해 따라서, 큰 범위에서 작은 범위 대입은 명시적 형변환이 필요하다. (int) 3.0 형변환 예제 public class Casting { public static void main(String[] args) { int div1 = 3 / 2; System.out.println("div1 = " + div1); //1 double div2 = 3 / 2; System.out.println("div2 = " + div2); //1.0 double div3 = 3.0 / 2; System.out.println("div3 = " + div3); //1.5 double div4 = (double) 3 / 2; System.out.println("div4 = " + div4); //1.5 int a = 3; int b = 2; double result = (double) a / b; System.out.println("result = " + result); //1.5 } } 데이터 타입 분류 기본형(Primitive type) int, long, double, boolean 처럼 데이터 값을 변수에 직접 저장할 수 있는 데이터 타입 데이터 사이즈가 정해져 있음 (정적) 더 빠르고 효율적인 메모리 처리 참조형(Reference Type) 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입 (배열, 객체) 생성 시점에서 동적 메모리 할당 유연성 있고 더 복잡한 데이터 구조를 다룰 수 있음 배열 따로 초기화 하지 않는 경우, 배열 생성시 내부 값이 자동으로 초기화된다. 숫자는 0, 불린형은 false, 문자열은 null 배열 변수는 실제 배열이 존재하는 메모리 공간에 대한 주소(참조값)를 담는다. 배열 초기화 예제 ```java public class ArrayInitialization { public static void main(String[] args) { // 기본 선언 및 배열 생성 int[] students1; students1 = new int[5]; // 선언 & 초기화 int[] students2 = new int[]{1, 2, 3, 4, 5}; // 선언 & 더 간단한 초기화 int[] students3 = {1, 2, 3, 4, 5}; // 오류 케이스 // int[] students4; // students4 = {1, 2, 3, 4, 5}; } } ``` For-Each문(Enhanced For Loop) 컬렉션(배열, set etc…)의 요소를 탐색할 때 조금 더 편리한 기능을 제공하는 문법이다. 컬렉션 요소들을 처음부터 끝까지 탐색한다. public class EnhancedForLoop { public static void main(String[] args) { int[] numbers = {1, 2, 3, 4, 5}; for (int number : numbers) { System.out.println(number); } } } 메서드(Method) 유의점 자바는 항상 변수의 값을 복사해서 대입한다. (인자가 파라미터로 전달될 때도 마찬가지다.) 메서드 호출이 끝나면 메서드 내 파라미터 변수, 로컬 변수들이 모두 메모리에서 제거된다. 메서드 호출 시에도 인자의 타입이 메서드 파라미터의 타입과 똑같아야 하므로, 명시적 형변환이 필요하거나 자동 형변환이 일어날 수 있다. 메서드 오버로딩(Method Overloading) 이름이 같고 매개변수가 다른 메서드를 여러개 정의하는 것을 의미한다. 메서드 시그니처(method signature) 자바에서 메서드를 구분할 수 있는 고유한 식별자 메서드 시그니처 = 메서드 이름 + 매개변수 타입(순서) 반환 타입은 시그니처에 포함되지 않는다. 규칙 메서드의 이름이 같아도 매개변수의 타입 및 순서가 다르면 오버로딩할 수 있다. (=메서드 시그니처가 달라서 가능) 반환 타입은 인정하지 않는다. 여러 개의 메서드를 오버로딩했을 때, 자바는 먼저 본인의 타입에 최대한 맞는 메서드를 찾아 실행하고, 그래도 없으면 형변환 가능한 타입의 메서드를 찾아 실행한다. 메서드 오버로딩 유의 케이스 아래 두 가지는 메서드 오버로딩 하지 못한다. 메서드 시그니처가 같기 때문! int add(int a, int b) int add(int c, int d) Reference 김영한의 자바 입문 - 코드로 시작하는 자바 첫걸음
Java-Ecosystem
· 2024-01-28
Java 기본 특징
자바 표준 스펙 자바는 표준 스펙이 존재하고 여러 회사가 자신에 입맞에 맞게 이를 구현한다. 자바 표준 스펙 자바의 설계도 문서 자바 커뮤니티 프로세스(JCP)를 통해 관리 구현 자바 표준 스펙에 맞춰 여러 회사가 각자에 최적화된 자바 프로그램을 개발 오라클 Open JDK, Adoptium Eclipse Temurin, Amazon Corretto etc… 오라클 Open JDK 사용하다가 Amazon Corretto 사용해도 대부분 큰 문제 없음 각 회사들은 다양한 OS(Mac, Windows, 리눅스)에 맞는 자바도 함께 제공 컴파일과 실행 소스코드(Source code) 개발자가 .java 확장자의 자바 소스코드를 작성한다. 컴파일(Compile) 단계 자바가 제공하는 javac 를 사용해, .java -> .class 파일 생성 command: javac Hello.java 즉, 자바 컴파일러가 소스코드를 바이트 코드로 변환 자바 가상 머신에서 더 빠르게 실행될 수 있게 최적화하고 syntax error 검출 실행(Runtime) 단계 java 프로그램을 사용해 자바를 띄우고 바이트코드인 .class 파일을 실행하면, JVM(실제 자바 프로그램 = 자바 가상 머신)이 띄워지면서 바이트코드를 읽고 프로그램을 실행한다. command: java Hello (Hello.class 의 .class 확장자를 빼고 입력) 운영체제 독립성 일반적인 프로그램은 다른 운영체제에서 실행할 수 없다. Windows 프로그램은 Windows OS가 사용하는 명령어들로 구성되어 있어서, 다른 OS와 호환되지 않음. 반면에, 자바 프로그램은 자바가 설치된 모든 OS에서 실행 가능 (호환성) 각 OS에 맞게 설치된 자바는 해당 OS의 명령어들로 컴파일된 .class 바이트코드를 실행 덕분에 개발할 때와 서버 실행 시 환경에 맞춰 다른 자바를 사용할 수 있다. 개발: Mac, Windows 서버: AWS Linux (Amazon Corretto 자바 설치) Reference 김영한의 자바 입문 - 코드로 시작하는 자바 첫걸음
Java-Ecosystem
· 2024-01-28
<
>
Touch background to close