JAVA8 vs JAVA11
(1)JAVA8 버전의 특징 : 람다식, Stream, defultMethod, Optional
-람다식 : 메서드의 선언부 없이, 구현부로 간략히 함수를 표현한 것으로 화살표를 사용해서 간단히 한 줄로 함수를 만들 수 있습니다. (람다는 long running process엔 적합x 시간 오래걸린다)
-Stream : Stream은 Java 8에서 새롭게 도입된 기능으로, 병렬 처리를 지원하여 대용량 데이터 처리를 간결하고 효율적으로 하도록 하는 것입니다. 주요 메소드로는 map, filter, reduce 등이 있습니다.
(map은 Stream 내의 요소들에 대해 주어진 함수를 적용하여 새로운 Stream을 생성하는 메소드입니다.
filter는 주어진 조건에 따라 Stream 내의 요소들을 필터링하는 메소드입니다.
reduce는 Stream 내의 모든 요소들을 주어진 연산을 통해 단일 결과로 만드는 메소드입니다.
-Dault Method : 디폴트 메서드는 Java 8에서 도입된 기능으로, 인터페이스에 메서드의 기본 구현을 제공할 수 있게 해줍니다. 이는 default 키워드를 사용하여 정의합니다.
디폴트 메서드는 인터페이스를 구현하는 클래스가 직접 메서드를 구현하지 않아도 되며, 필요한 경우에만 디폴트 메서드를 오버라이드(재정의)할 수 있습니다. 이는 기존의 인터페이스를 확장하면서도, 기존에 해당 인터페이스를 구현하는 클래스들에게 영향을 미치지 않게 하는데 도움을 줍니다.
-Optional : Optional은 Java 8에서 도입된 또 다른 기능으로, null을 처리하기 위한 안전한 대안을 제공합니다. Java에서 null 포인터 예외는 흔한 소스의 문제점 중 하나이며, 이를 방지하기 위해 Optional이 도입되었습니다. Optional 객체는 특정 타입의 객체를 포함할 수도, 포함하지 않을 수도 있는 컨테이너입니다. Optional 객체가 값을 포함하면 해당 값에 액세스할 수 있습니다. 그렇지 않으면 값이 없다는 것을 안전하게 표현할 수 있습니다. 이러한 기능을 이용하면, NullPointerException을 방지하고, 코드를 더욱 명확하고 가독성이 좋게 만들 수 있습니다.
(2)JAVA11 버전의 특징 : 람다식의 파라미터로 Var 사용 가능
GC란? 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거하는 프로세스 입니다. Java 프로세스가 한정된 메모리를 효율적으로 사용할수 있게 하고, 개발자 입장에서 메모리 관리, 메모리 누수(Memory Leak) 문제에서 대해 관리하지 않아도 되어 오롯이 개발에만 집중할 수 있다는 장점이 있습니다.
GC가 발생하면 어떻게 되는지 / 동작 과정
: 시스템에서 더이상 사용하지 않는 동적할당된 메모리 블럭을 찾아 자동으로 다시 사용가능한 자원으로 회수합니다. 먼저 어떤 객체를 참조하는지 찾고 (MARK), 필요 없는 객체를 제거(SWEEP)하는 mark and sweep 방식입니다. 이 과정에서 GC 스레드 이외에 다른 스레드들은 동작을 멈추게 되고 이를 Stop the world 라고 합니다.
GC의 종류 / 영역 :
Young 영역에 있는 객체는 새로 생겨난 객체들이 저장되는 공간이고, 각 하위 영역이 가득 차면 Miner GC 가 동작하여 더 이상 참조되지 않는 객체를 제거합니다. 그리고 Young 영역에서 오래 살아남은 (미리 정해진 횟수만큼 살아남은) 객체는 Old 영역으로 보내집니다. 그리고 Old 영역에 있는 객체는 영역이 가득 차면 Major GC(Full GC)가 동작하여 더 이상 참조되지 않는 객체 제거합니다.
(참고 : Young 영역은 eden 에덴 , suvivor0/1 수바이버 0,1로 이뤄지고 수바이버 둘 중 하나는 비어있어야 함)
GC알고리즘의 종류
(1) Serial GC:
Serial GC는 가장 기본적인 GC 알고리즘으로, GC를 처리하는 동안에는 사용자 애플리케이션의 실행을 멈춥니다. 이를 "Stop-The-World"라고 합니다. 이 알고리즘은 단일 코어 CPU 환경에서 사용하기에 적합하며, 주로 간단한 타입의 애플리케이션에 사용됩니다.
(2) Parallel GC:
Parallel GC는 Serial GC의 병렬 처리 버전입니다. 시리얼 GC와 비슷하지만 Minor GC 영역을 멀티 스레드로 처리합니다. 이 알고리즘은 멀티 코어 CPU 환경에서 사용하기에 적합하며, 메모리가 크고 코어 수가 많은 서버 환경에서 사용됩니다.
(3) Parallel Old GC:
Parallel GC를 개선한 버전입니다. minor GC뿐만 아니라 Major GC도 멀티 스레드로 처리합니다.
(4) CMS(Concurrent Mark and Sweep) GC:
CMS GC는 "Stop-The-World" 시간을 최소화하기 위한 GC 알고리즘입니다. GC는 사용자 애플리케이션과 동시에 수행되며, 애플리케이션의 실행을 완전히 멈추는 시간을 줄입니다. 이는 반응 시간이 중요한 대화형 애플리케이션에 주로 사용됩니다. CPU 사용량이 굉장히 높습니다.
(5) G1(Garbage-First) GC:
G1 GC는 대용량 메모리를 가진 시스템에서 장기 실행 애플리케이션에 사용하기 위해 설계된 알고리즘입니다. 메모리를 여러 영역으로 나누고, GC의 대상이 되는 영역(가비지가 가장 많이 있는 영역)을 우선적으로 처리합니다. 이를 통해 GC의 실행 시간을 예측 가능하게 하고 "Stop-The-World" 시간을 일정하게 유지합니다.
(6) 검색 알고리즘 : mark and sweep
(7) 참조 카운팅 : 객체가 참조되면 카운트를 증가하고, 객체 참조가 감소하면 카운트를 감소시켜서 카운트가 0이 되면 GC를 실행 시키는 알고리즘
(8) 세대별 GC : 객체의 참조 생존기간에 따라 세대별로 나누고 관리하여 GC 수행
(9) 부분적 GC : 메모리 영역 전체를 한번에 GC하는 것이 아니라 인접한 부분 혹은 작은 부분을 순차적으로 GC
Stop the world : GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미합니다.
GC가 작동하는 동안 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있기 때문에 이 시간을 최소화 시키는 것이 쟁점이라고 할 수 있습니다. (GC의 단점: STOP THE WORLD & 실행 시간을 예측하기 어렵다) --> GC 튜닝
GC 최적화 : 불필요한 객체를 만들지 않고, 객체의 크기를 가능한 최소화 하여 생성합니다. 또한 객체가 더이상 필요하지 않으면 명시적으로 참조를 해제해 줍니다. (블럭 고려)
JVM : Java Virtual Machine의 약자로, 가상머신을 의미합니다. .java의 바이트 코드를 운영체제가 이해할 수 있는 기계어로 변환하여 전달하고, 자동 메모리관리(Garbage Collection)을 수행 합니다.
Java 컴파일 과정 설명 : OS로 부터 필요한 메모리를 할당 받고 나면, 자바 컴파일러가 .JAVA 로 된소스 코드를 .CLASS 파일의 바이트 코드로 변환합니다. 변환된 클래스 파일을 클래스로더가 JVM 내부로 로딩하고 나면, 실행엔진이 바이트코드를 기계어로 해석하고 해석된 기계어가 메모리상에 배치되어 실행됩니다.
클래스로더 : 클래스 파일을 JVM 내부로 로드하고 작업을 수행합니다.
실행 엔진 : 로딩된 클래스 파일을 실행 시키는 역할을 수행합니다. 바이트코드를 기계어로 변환해 줍니다.
인터프리터 : 자바 소스 코드를 직접 실행하는 프로그램입니다. 인터프리터는 소스 코드를 한 줄씩 읽고 해석하여 실행하는 방식으로 동작합니다. 이때 속도가 느리기 때문에 JIT가 실행 되기도 합니다.
JIT : 인터프리터의 단점을 보완하기 위해 도입된 것으로, 인터프리터로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 기계어로 변경하여 캐시에 저장합니다. 캐시에 보관된 코드를 사용하기 때문에 한번 컴파일 된 후에는 빠르게 수행 가능하지만, jit로 컴파일 하는 것이 인터프리팅보다 느리기에 한번만 실행되는 코드라면 인터프리팅 하는 것이 유용합니다.
객체 지향의 특징 : 코드 재사용이 용이하고 유지보수가 쉽습니다. 절차지향은 코드를 순서대로 읽으면서 수정할 부분을 찾아야하는데, 객체 지향은 필요한 부분만 찾아서 수정하면 되기 때문입니다. 또 상속이나 다형성 등의 특성을 이용해서 기존 코드를 활용하기 편합니다.
(1) 다형성 : 어떤 변수나 메서드가 상황에 따라 다른 결과를 내는 것
-오버라이딩: 부모클래스의 메서드를 자식클래스에서 재정의
-오버로딩(overloading): 한 클래스에서 메소드 이름은 같지만 파라미터 개수나 타입을 다르게 하여 서로 다르게 동작하게 하는 것
(2) 캡슐화 : 데이터 보호, 필요가 없는 정보는 외부에서 접근하지 못하도록 제한하는 것(키워드: private)
(3) 상속 : 자식클래스가 부모클래스의 특징, 기능을 물려받는 것(부모 클래스만 수정하면 여러 자식 클래스들도 수정-> 코드 재사용 막음) (extends)
(4) 추상화 : 공통의 속성이나 기능을 묶어 이름을 붙이는 것 (예: 토끼, 강아지, 고양이 같은 각각의 객체가 있을 때 동물이라는 추상적인 객체로 묶는 것)
객체 지향의 단점 : 처리 속도가 느림, 많은 양의 메모리 필요, 설계시 많은 시간과 노력 필요하다.
Solid원칙을 업무에 적용한 예시 :
S : 단일 책임 원칙 (SRP, Single Responsibility Principle)
→ 객체는 단 하나의 책임만 가져야 한다.
O : 개방-폐쇄 원칙 (OCP, Open Closed Principle)
→ 기존의 코드를 변경하지 않으면서 기능을 추가 할 수 있도록 설계가 되어야 한다.
L : 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
→ 일반화 관계에 대한 이야기며, 자식 클래스는 최소한 자신의 부모 클래스에서 가능한 행위는 수행할 수 있어야 한다.
I : 의존 역전 원칙 (DIP, Dependencdy Inversion Principle)
→ 의존 관계를 맺을 때, 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 것이다.
D : 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
→ 인터페이스를 클라이언트에 특화되도록 분리시키라는 설계 원칙이다.
★ 업무 적용 :
보험금 지급과 관련되어 조사를 내보내야 할 때, 조사자에게 조사의 종류별로 다른 알림톡을 전송해야 하는 로직이 있었습니다. 기존에는 조사 종류에 따라 알림톡을 발송하는 로직이 각각 존재했습니다. 저는 이를 개선하고자, 알림톡을 발송하는 하나의 로직을 만들었고 해당 로직 안에서 전달 받는 파라미터(조사 종류, 알림톡 내용을 관리하는 고유번호 등)에 따라 해당하는 알림톡을 발송할 수 있도록 개선했습니다. 즉 공통된 기능을 별도로 분리하여 재사용 한 것이기 때문에 단일 책임 원칙과 개발 폐쇄 원칙에 맞게 개선한 예라고 생각합니다.
단일 책임 원칙(SRP): 알림톡 발송 기능을 하나가 전담하게 됨으로써 해당 기능에 대한 책임이 명확해졌습니다.
개방-폐쇄 원칙(OCP): 알림톡 발송 기능을 분리하면서, 해당 로직에 대한 변경이 필요할 때 다른 클래스의 코드를 건드리지 않고, 해당 기능만 수정하면 되므로 확장성이 좋아졌습니다.
이렇게 공통 기능을 분리함으로써 코드의 재사용성과 유지보수성을 높일 수 있으며, 이는 객체 지향 설계의 원칙을 잘 따르고 있다고 볼 수 있습니다.
java의 경우 함수에 전달되는 인자의 데이터 타입에 따라 함수 호출 방식이 달라집니다.
call by value의 경우는, 값에 의한 호출로 함수 호출시 전달되는 변수의 값을 "복사" 하여 전달하기 때문에 안전하고 원래의 값이 보존되지만 복사를 하기 때문에 메모리 사용량이 늘어납니다.
call by reference의 경우는, 복사를 하지 않고 참조를 하기 때문에 빠르지만 원래 값이 영향을 받을 수 있습니다.
예외처리(Exception handling)
정의 - 프로그램 실행 시 발생할 수 있는 예외 발생에 대비한 코드를 작성하는 것
목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것. 실행을 멈추지 않고 진행할 수 있게 하는 데에 이점이 있다.
try {
connection.setAutoCommit(false); // Disable auto-commit mode
// Insert data code here
connection.commit(); // If all insert operations succeed, commit the transaction
} catch (SQLException e) {
try {
connection.rollback(); // If any insert operation fails, rollback the transaction
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
try {
connection.setAutoCommit(true); // Enable auto-commit mode
} catch (SQLException e) {
e.printStackTrace();
}
}
3) 데이터 유효성 체크에서 예외가 발생하는 경우, 사용자에게 재 입력을 요구하도록 할 수 있습니다. 그리고 네트워크 이슈나 데이터베이스 연결, 등에서 예외가 발생한 경우 미리 횟수를 정해놓고 그 횟수만큼 재 시도를 하도록 할 수 있습니다.
(1) 다중 catch 블록: Java 7부터 도입된 기능으로, 하나의 catch 블록에서 여러 종류의 예외를 처리할 수 있습니다. 이 방식은 각 예외 유형마다 다른 처리 방식이 아니라, 공통된 처리 방식을 사용할 때 유용합니다.
try {
// risky code
} catch (IOException | SQLException e) {
// handle both IOException and SQLException
}
(2) 슈퍼클래스를 이용한 예외 처리: 예외 클래스들은 계층구조를 가지고 있습니다. 예외들의 슈퍼클래스를 catch에서 잡으면, 그 하위의 모든 예외도 함께 처리됩니다. 가장 일반적인 예로는 모든 checked exception의 슈퍼클래스인 Exception을 잡는 경우가 있습니다.
try {
// risky code
} catch (Exception e) {
// handle all exceptions that are subclasses of Exception
}
이 방식은 모든 예외를 동일하게 처리하는 경우에 사용되지만, 가능한 한 구체적인 예외를 잡는 것이 좋습니다. 그 이유는 특정 예외에 대해 구체적인 조치를 취할 수 있고, 예상하지 못한 예외가 발생했을 때 그것을 발견하기 쉽기 때문입니다.
(3) 공통 예외 클래스 생성: 여러 예외 사이에 공통된 처리 로직이 필요하다면, 공통의 슈퍼클래스를 가진 예외 클래스를 만들어 사용할 수도 있습니다.
public class CommonException extends Exception {
// custom exception handling logic
}
try {
// risky code
} catch (CommonException e) {
// handle exceptions that are instances of CommonException
}
이렇게 여러 종류의 예외를 한 번에 처리하는 방법은 코드의 간결성을 위해 사용할 수 있지만, 각 예외 유형마다 구체적인 처리 방법이 필요한 경우에는 각각의 예외를 별도로 잡는 것이 좋습니다.
컬렉션 프레임워크란?
자바에서 컬렉션 프레임워크(Collection Framework)는 데이터를 저장, 관리, 조작하기 위한 클래스와 인터페이스의 집합입니다. 이 프레임워크는 자바의 표준 라이브러리로 제공되며, 데이터 구조를 다루는 데 필요한 다양한 기능과 알고리즘을 제공합니다.
컬렉션 프레임워크의 종류 :
순서나 집합적인 저장공간을 나타내는 컬렉션 인터페이스와 키와 값으로 데이터를 핸들링하는 맵 인터페이스로 나눌 수 있습니다.
리스트는 순서가 있고 중복이 허용되며, SET은 중복을 허용하지 않는 순서가 없는 데이터 집합입니다. 마지막으로 MAP은 KEY와 VALUE로 구성되어 KEY의 중복을 허용하지 않는 구조입니다.
총 세가지 : list, set, map (list:대기자명단, set: 양의정수집합, 소수집합, map: 지역번호, 우편번호)
정리 :
(1) List 인터페이스:
순서가 있는 데이터의 집합으로, 중복된 값 허용
대표적인 구현 클래스: ArrayList, LinkedList, Vector
(2) Set 인터페이스:
순서가 없는 고유한 데이터의 집합으로, 중복된 값은 허용되지 않음
대표적인 구현 클래스: HashSet, TreeSet
(3) Map 인터페이스:
키와 값의 쌍으로 이루어진 데이터의 집합
중복된 키는 허용되지 않으며, 값은 중복될 수 있음
대표적인 구현 클래스: HashMap, TreeMap, LinkedHashMap
컬렉션 프레임워크의 인터페이스와 구현 클래스를 활용하여 데이터를 쉽게 저장하고 조작할 수 있습니다. 이를 통해 데이터의 추가, 삭제, 검색, 정렬 등의 작업을 효율적으로 처리할 수 있습니다. 또한, 컬렉션 프레임워크는 제네릭(Generic)을 이용하여 타입 안정성을 제공하므로, 컴파일 시 타입 체크를 할 수 있습니다.
Process vs Thread : 프로세스는 운영체제에서 실행 중인 하나의 프로그램이고 쓰레드는 프로세스 내에서 동시에 실행되는 독립적인 실행 단위입니다. 하나의 프로세스는 하나 이상의 스레드를 포함합니다.
멀티 쓰레드란? 멀티 쓰레딩(Multi-threading)은 여러 쓰레드를 동시에 실행하여 병렬 처리를 수행하는 방식입니다. 자바에서는 Thread 클래스 또는 Runnable 인터페이스를 사용하여 쓰레드를 생성하고 관리할 수 있습니다.
문제점 :
(1) deadLock(교착상태) : 두 개 이상의 쓰레드가 서로가 보유한 자원의 해제를 기다리며 무한 대기하는 상태입니다. 이러한 상태는 시스템의 작동을 마비시킬 수 있습니다. 교착상태는 두 개 이상의 쓰레드가 서로가 가진 자원을 기다리며 작업을 완료할 수 없는 상태를 말합니다. 예를 들어, 쓰레드 A가 자원 1을 가지고 있고 자원 2를 기다리는 동안, 쓰레드 B가 자원 2를 가지고 있고 자원 1을 기다리는 상황이 발생하면, 두 쓰레드 모두 자신이 요구하는 자원을 얻을 수 없어 무한 대기 상태에 빠지게 됩니다. 이러한 상황을 방지하기 위해서는 자원의 요청 순서, 홀드 앤 웨이트(hold-and-wait), 상호 배제(mutual exclusion) 등의 조건을 깨는 방법을 사용할 수 있습니다.
(1-2) 교착 상태의 조건 네가지 :
교착 상태(Deadlock)가 발생하려면 네 가지 필수 조건이 모두 충족되어야 합니다. 이를 '커피-홀트(Coffman-Holt)의 교착상태 발생 조건'이라고도 합니다.
-상호 배제(Mutual Exclusion): 자원은 한 번에 한 프로세스(또는 쓰레드)만이 사용할 수 있어야 합니다. 다른 프로세스가 그 자원을 요청하면, 그 프로세스는 자원이 방출될 때까지 기다려야 합니다.
-보유 및 대기(Hold and Wait): 프로세스는 최소한 하나의 자원을 보유한 상태에서, 현재 다른 프로세스에 의해 보유 중인 추가 자원을 기다려야 합니다.
-비선점(No Preemption): 자원은 그 자원을 보유하고 있는 프로세스가 자발적으로 방출하기 전까지는 강제로 뺏을 수 없습니다. 다시 말해, 이미 할당된 자원은 사용이 끝날 때까지 그 프로세스가 사용할 수 있어야 합니다.
-순환 대기(Circular Wait): 두 개 이상의 프로세스가 각각 다음 프로세스가 요청하는 자원을 보유하고 있어서, 프로세스 간에 자원 요청의 순환 대기 상황이 발생해야 합니다.
이러한 조건들이 동시에 충족되면 교착 상태가 발생합니다. 따라서, 교착 상태를 예방하거나 해결하기 위해서는 이 중 하나 이상의 조건을 깨는 것입니다.
(2) context-switching(문맥 교환 비용) : 스레드가 현재까지 작업한 내용과 다음 작업에 필요한 각종 데이터와 정보를 저장하고, 불러올 때 (정보공유)시 발생하는 비용입니다. 따라서 단순한 계산은 싱글 스레드를 사용하는 것이 낫습니다.
(3) 보안 : 프로세스는 고유의 메모리 영역을 갖지만 쓰레드는 메모리 영역을 공유하기 때문에 안정성과 보안이 중요시되는 경우에는 멀티 쓰레드보다 멀티 프로세스가 낫습니다.
멀티 쓰레드의 장점 : 멀티스레딩의 주요 장점은 성능 향상입니다. 각 쓰레드가 서로 다른 작업을 동시에 수행하므로 CPU 사용률을 높일 수 있습니다. 두번째로는 사용자 인터페이스가 있는 애플리케이션에서 한 쓰레드가 작업을 처리하는 동안 다른 쓰레드가 사용자 입력을 받아들일 수 있으므로 응답성이 향상됩니다. 마지막으로 쓰레드들은 같은 프로세스 내에서 메모리와 자원을 공유할 수 있으므로 자원을 효율적으로 활용할 수 있습니다.
thread safe란? 멀티 쓰레드 프로그래밍에서, 어떤 공유 자원에 여러 쓰레드가 동시에 접근해도, 프로그램 실행에 문제가 없는 상태를 의미합니다. "Thread-safe"는 여러 스레드가 동시에 접근하더라도 프로그램의 실행 결과가 정상적으로 보장되는 특성을 말합니다.
thread safe를 위해선?
멀티 쓰레드의 동시성과 병렬성 :
(1) 동시성: 멀티작업을 위해 싱글 코어에서 여러개의 쓰레드가 번갈아가면서 실행
(2) 병렬성: 멀티작업을 위해 멀티코어에서 한개이상의 쓰레드를 포함하는 각 코어들을 동시에 실행
thread 구현 방법 : Java에서 멀티스레딩을 구현하는 방법은 크게 두 가지입니다: Thread 클래스를 확장하는 방법과 Runnable 인터페이스를 구현하는 방법입니다. 이때, 자바는 다중상속이 불가능하므로 대부분 runnable 인터페이스를 구현합니다. (Thread 클래스엔 start()메서드가 있고, runnable 에는 run()메소드가 있다.)
동기화 vs 비동기화 :
-동기식: 요청과 결과가 동시에 이루어지는 것, 설계가 간단하지만 결과가 주어질때까지 아무것도 못하고 대기하므로 비동기식보다 비효율적 (콜센터 전화)
-비동기식: 요청과 결과가 동시에 이루어지지 않는 것, 하나의 요청을 처리하는 동안 다른 요청도 처리가 가능. 동기보다 복잡하고 결과가 주어지는데 시간이 걸리더라도 그동안 다른 작업을 할 수 있으므로 자원을 효율적으로 사용(이메일 전송)
동기화(Synchronization): 동기화는 두 개 이상의 스레드가 공유 자원에 동시에 접근하려고 할 때, 이러한 접근을 조율하는 메커니즘이라고 할 수 있습니다. 즉, 한 스레드가 공유 자원을 사용하고 있는 동안 다른 스레드가 그 자원에 접근하는 것을 차단하는 것입니다. 이를 위해 Java에서는 synchronized 키워드를 사용합니다. 동기화는 데드락(Deadlock)과 같은 문제를 방지하지만, 퍼포먼스가 저하될 수 있습니다.
비동기화(Asynchronization): 비동기화는 스레드가 독립적으로 실행되어, 다른 스레드의 실행 상태에 영향을 받지 않도록 합니다. 한 스레드가 특정 작업을 수행하는 동안 다른 스레드가 대기하지 않고 병렬로 수행될 수 있습니다. 이는 시스템의 효율성을 높이지만, 스레드 간의 조율이 필요한 경우 문제가 될 수 있습니다.
ExecutorService executor = Executors.newFixedThreadPool(10); // create a pool of 10 threads
executor.execute(new RunnableTask()); // execute task
executor.shutdown(); // shutdown executor
스레드 풀을 사용하면 많은 수의 스레드를 생성하고 제거하는 비용을 줄일 수 있으며, 시스템의 안정성을 높일 수 있습니다. 또한, 리소스 관리가 용이하며, 작업 처리 시간을 예측하기 쉬워집니다.
<참고>
쓰레드가 과도하게 증가하면 쓰레드 생성과 스케줄링로 인해 CPU가 바빠져 메모리 사용량이 급격히 늘어나는 문제점이 발생하고, 이에 따라 애플리케이션의 성능 저하도 발생하게 된다.이러한 단점을 상쇄하기 위해 쓰레드 풀(Threadpool)을 사용한다. 쓰레드 풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리하는 것이다. 작업 처리가 끝난 쓰레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다. 그렇기 때문에 작업 처리 요청이 증폭되어도 쓰레드의 전체 개수가 늘어나지 않으므로 애플리케이션의 성능이 급격히 저하되지 않는다.
쓰레드풀을 사용하는 이유를 2가지로 정리하면 다음과 같다.
프로그램 성능 저하를 방지하기 위해
매번 발생하는 작업을 병렬처리하기 위해 쓰레드를 생성/수거하는 데 따른 부담은 프로그램 전체적인 퍼포먼스를 저하시킨다. 따라서 쓰레드풀을 만들어 놓고 사용한다.
쓰레드가 생성될 때 OS가 메모리 공간을 확보해주고 메모리를 쓰레드에게 할당해준다.
쓰레드 풀을 미리 만들어 두기 때문에 처음에 생성하는 비용은 들지만 이전의 쓰레드를 재사용할 수 있으므로 시스템 자원을 줄일 수 있고, 작업을 요청 시 이미 쓰레드가 대기 중인 상태이기 때문에 작업을 실행하는 데 딜레이가 발생하지 않는다.
다수의 사용자 요청을 처리하기 위해
대규모 프로젝트에서 특히 중요하다. 다수의 사용자의 요청을 수용하고, 빠르게 처리하고 대응하기 위해 쓰레드풀을 사용한다.
Thread pool의 단점은, thread pool에 thread를 너무 많이 생성해 두었다가 사용하지 않으면 메모리 낭비가 발생한다.
프로세스와 스레드의 처리방식 차이점
프로세스는 독립적으로 실행되므로, 한 프로세스에서 발생한 오류가 다른 프로세스에 영향을 주지 않습니다. 그러나 이는 각 프로세스마다 독립적인 메모리 공간과 자원을 필요로 하므로, 자원 소모가 큰 단점이 있습니다.
스레드는 리소스를 공유하기 때문에 메모리 소모가 적고, 프로세스보다 빠르게 생성하고 전환할 수 있습니다. 그러나 스레드간의 통신에서 동기화 문제를 처리하는 것이 중요하며, 한 스레드에서 발생한 오류가 동일 프로세스 내의 다른 스레드에 영향을 줄 수 있습니다.
따라서 프로세스와 스레드를 선택하는 것은 자원 사용, 공유 메모리의 필요성, 병렬성 요구 등 다양한 요인을 고려해야 합니다.
독립성이 필요한 경우: 각 프로세스는 독립된 메모리 공간을 가지므로, 다른 프로세스에 영향을 받지 않고 독립적으로 실행할 수 있습니다. 따라서, 오류가 발생했을 때 해당 프로세스만 영향을 받고 다른 프로세스에는 영향을 미치지 않습니다.
다른 언어로 개발된 프로그램들과의 통신이 필요한 경우: 서로 다른 프로그램들 사이에서의 통신은 IPC(Inter Process Communication)를 통해 이루어지며, 이는 프로세스 사이에서만 가능합니다.
스레드를 사용하는 경우:
경량성이 필요한 경우: 스레드는 프로세스보다 생성과 종료에 드는 비용이 적으며, 컨텍스트 스위칭 비용도 적습니다. 따라서, 빠르게 생성하고 소멸할 작업에는 스레드를 사용하는 것이 효율적일 수 있습니다.
자원 공유가 필요한 경우: 스레드는 프로세스 내에서 메모리를 공유하기 때문에, 데이터를 공유해야 하는 작업에는 스레드를 사용하는 것이 효율적입니다. 예를 들어, 멀티스레딩을 활용하여 하나의 애플리케이션 내에서 동시에 여러 작업을 처리할 수 있습니다.
병렬성이 필요한 경우: 멀티스레딩은 CPU를 효율적으로 활용하여 여러 작업을 동시에 처리할 수 있게 해주므로, 병렬 처리가 필요한 경우에 적합합니다. 예를 들어, 웹 서버에서 동시에 여러 클라이언트의 요청을 처리하는 경우에는 각 요청을 처리하는 작업을 별도의 스레드로 처리할 수 있습니다.
사전 개념 - Critical Section(임계 구역): 여러 스레드가 동시에 접근해서는 안되는 공유자원에 접근하는 코드 블럭을 얘기합니다. 한 임계구역에 하나의 스레드 혹은 프로세스만 접근이 가능합니다. 임계 구역에 접근하는 것을 제어하기 위해 세마포어, 뮤텍스와 같은 매커니즘을 사용합니다.
1. 뮤텍스: lock을 사용해서 하나의 프로세스나 스레드를 단독으로 실행하는 것
2. 세마포어: 공유자원의 개수만큼의 프로세스나 스레드를 실행하는 것.
3. 차이점 : 뮤텍스와 세마포어는 동시성 제어를 위한 도구로, 여러 스레드나 프로세스가 공유 자원에 동시에 접근하는 것을 제어하는 데 사용됩니다. 이 둘의 주요 차이점은 뮤텍스가 공유 자원에 대한 단일 스레드 접근을 제어하는 반면, 세마포어는 한 시점에 여러 스레드나 프로세스가 접근할 수 있는 개수를 제어한다는 것입니다.
뮤텍스 (Mutex) : 뮤텍스는 한 번에 하나의 스레드만 공유 자원에 접근할 수 있도록 하는 데 사용됩니다. 뮤텍스는 바이너리 세마포어로 볼 수 있으며, 잠금(Lock)과 해제(Unlock) 메커니즘이 있습니다. 스레드가 잠금을 얻지 못하면, 그 스레드는 블록되거나 잠금이 해제될 때까지 대기합니다. 만약 스레드가 뮤텍스를 소유하고 있다면, 다른 어떤 스레드도 그 뮤텍스를 소유할 수 없습니다. 그 스레드가 뮤텍스를 반환하기 전까지, 다른 모든 스레드는 블록되거나 대기 상태에 있어야 합니다.
세마포어 (Semaphore) : 세마포어는 하나 이상의 스레드나 프로세스가 동시에 공유 자원에 접근할 수 있도록 하는 데 사용됩니다. 세마포어는 공유 자원에 대한 동시 접근을 허용하는 단위의 수를 가지고 있으며, 이 수는 세마포어가 생성될 때 초기화됩니다. 스레드나 프로세스는 세마포어가 비어 있지 않다면 (즉, 값이 0이 아니라면) 자원에 접근할 수 있고, 세마포어를 줄여나갑니다. 세마포어가 비어 있으면 (즉, 값이 0이면), 스레드나 프로세스는 세마포어가 다시 사용 가능해질 때까지 블록됩니다.
뮤텍스와 세마포어는 비슷하면서도 다른 개념입니다. 둘 다 동시성 제어 도구로, 여러 스레드나 프로세스가 공유 자원에 동시에 접근하는 것을 제어하는 데 사용됩니다. 그러나 이들은 공유 자원에 대한 접근 제어 방식에 있어서 차이가 있습니다. 따라서, 뮤텍스와 세마포어는 공유 자원에 대한 접근을 제어하는 방식이 서로 다릅니다. 뮤텍스는 한 번에 하나의 스레드만 접근을 허용하는 반면, 세마포어는 한 번에 여러 스레드의 접근을 허용할 수 있습니다.
abstract : 추상 클래스는 하나 이상의 추상 메서드를 포함하는 클래스입니다. 추상 클래스는 abstract 키워드를 사용하여 선언되며, 객체를 직접 생성할 수 없습니다. 즉, 추상 클래스를 상속하는 하위 클래스를 생성하여 사용해야 합니다. 추상 클래스를 상속하는 하위 클래스는 추상 클래스에서 정의한 모든 추상 메서드를 구현해야 합니다.
default Method: 디폴트 메서드는 Java 8에서 도입된 기능으로, 인터페이스에 메서드의 기본 구현을 제공할 수 있게 해줍니다. 이는 default 키워드를 사용하여 정의합니다.
디폴트 메서드는 인터페이스를 구현하는 클래스가 직접 메서드를 구현하지 않아도 되며, 필요한 경우에만 디폴트 메서드를 오버라이드(재정의)할 수 있습니다. 이는 기존의 인터페이스를 확장하면서도, 기존에 해당 인터페이스를 구현하는 클래스들에게 영향을 미치지 않게 하는데 도움을 줍니다.
Interface : 인터페이스는 클래스가 어떠한 메서드를 반드시 구현하도록 강제하는 역할을 수행합니다. 인터페이스는 일반적으로, 상수와 추상 메서드(구현부 없는 메서드)만을 가지며 implement 키워드를 사용하여 클래스에서 인터페이스를 구현합니다.
★abstract vs interface
-차이점 : 추상 클래스는 단일 상속만 가능하지만, 인터페이스는 다중 상속이 가능합니다. 또한 추상 클래스는 멤버 변수를 가질 수 있고 일반 메서드를 포함해도 되며 상속 키워드로 extends를 사용하지만, 인터페이스는 상수만 가질 수 있으며 일반 메서드를 포함할 수 없고 상속 키워드로 implements를 사용합니다.(JAVA 8 부터 Default, static 메서드 사용 가능)
-공통점 : 객체를 생성할 수 없습니다.(인스턴스화 할 수 없다) 추상 메서드를 포함하고 있어야 하며 상속받는 클래스에서 추상 메서드를 구현해주어야 합니다.
-사용해야하는 상황 : 추상 클래스는 단일 상속만 필요하고, 공통된 속성과 메서드를 가지는 클래스의 일반화가 필요한 경우에 사용합니다. 인터페이스는 여러 클래스 간에 공통된 동작 규약을 정의하고, 다형성을 구현해야 할 때 사용합니다. (즉 추상클래스는 공통된 것을 일반화 해주고 인터페이스는 여러 클래스들간에 공통된 규약을 정의!)
★★★추상 클래스는 extends(기능 확장의 느낌) 키워드 그대로 자신의 기능들을 하위로 확장시키는 것으로 볼 수 있습니다.
인터페이스는 implements(실행하는 기능 구현의 느낌) 키워드처럼 인터페이스에 정의된 메서드를 각 클래스의 목적에 맞게 동일한 기능으로 구현하는 것으로 볼 수 있습니다.
또 다른 관점에서는 추상 클래스는 이를 상속할 각 객체들의 공통점을 찾아 추상화시켜놓은 것으로 상속 관계를 타고 올라갔을 때, 같은 부모 클래스를 상속하며, 부모 클래스가 가진 기능들을 구현해야 하는 경우에 사용합니다.
반면 인터페이스는 상속 관계를 타고 올라갔을 때, 다른 부모 클래스를 상속하더라도 같은 기능이 필요한 경우에 사용됩니다.
*참고) 해시 관련 질문
해시(Hash)란 무엇인가요?
해시는 임의의 크기를 갖는 데이터를 고정된 크기의 값으로 매핑하는 것을 말합니다. 해시 함수를 사용하여 데이터를 해시로 변환할 수 있습니다.
해시 함수(Hash Function)의 특징은 무엇인가요?
해시 함수는 동일한 입력에 대해서는 항상 동일한 해시 값을 반환해야 합니다.
입력 데이터의 조금이라도 다른 경우에는 다른 해시 값을 반환해야 합니다.
해시 함수는 빠르게 계산할 수 있어야 하며, 해시 충돌이 적어야 합니다.
해시 충돌(Hash Collision)이란 무엇인가요?
해시 충돌은 서로 다른 입력 데이터에 대해 동일한 해시 값을 가지는 상황을 의미합니다. 즉, 해시 함수의 출력값이 충돌이 발생하는 경우입니다.
hashCode() 메서드는 무엇이고 어떤 역할을 하나요?
hashCode() 메서드는 Java의 모든 객체가 상속하는 Object 클래스에 정의된 메서드입니다.
hashCode() 메서드는 객체의 해시 코드를 반환하며, 객체의 동등성 비교를 위해 사용됩니다.
동일한 객체에 대해서는 항상 동일한 해시 코드를 반환해야 하며, equals() 메서드와 함께 사용하여 객체의 동등성을 판단하는 데 활용됩니다.
hashCode() 메서드를 재정의하는 이유는 무엇인가요?
hashCode() 메서드를 재정의하는 이유는 객체의 동등성 비교를 정확하게 수행하기 위함입니다.
equals() 메서드를 재정의할 때는 일반적으로 hashCode() 메서드도 함께 재정의해야 합니다.
객체의 동등성이 같은 경우에는 동일한 해시 코드를 반환해야 해시 기반 자료구조(예: HashMap, HashSet)에서 올바르게 동작할 수 있습니다.
해시 함수의 사용 예시는 어떤 것들이 있나요?
해시 함수는 비밀번호 저장, 데이터 검색, 데이터 무결성 확인, 암호화 등 다양한 분야에서 사용됩니다.
예를 들어, 비밀번호 저장 시에는 실제 비밀번호를 저장하기보다 해시 함수를 사용하여 비밀번호의 해시 값을 저장합니다.
참고) 로그레벨
⛔️ Error 의도하지 않은 에러가 발생한 경우
프로그램이 종료되지 않음
프로그램 내에서 개발자가 의도하지 않은 예외를 나타날 때 사용
⚠️ Warn 에러가 될 수 있는 잠재적 가능성이 있는 경우
Warn 로그가 발생했을 시, 알람을 통해 개발자가 크리티컬한 에러를 맞닥뜨리기 전에 확인할 수 있는 역할을 겸함
✅ Info 명확한 의도가 있는 정보성 로그
요구사항에 따라 시스템 동작을 보여줄 떄
⚙️ Debug Info 레벨보다 더 자세한 정보가 필요한 경우
주로 Develop 환경에서 사용
📑 Trace Debug 레벨보다 더 자세한 내용을 포함
Dev 환경에서 버그를 해결하기위해 사용
최종 프로덕션이나 커밋에 포함되면 안된다고 합니다.
16. 롬복 : Lombok 이란 Java 라이브러리로 반복되는 getter, setter, toString .. 등의 반복 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리입니다.Lombok은 여러가지 @어노테이션을 제공하고 이를 기반으로 반복 소스코드를 컴파일 과정에서 생성해주는 방식으로 동작하는 라이브러리입니다.
Lombok 장점
어노테이션 기반의 코드 자동생성을 통한 생산성 향상
반복코드 다이어트를 통해 가독성 및 유지보수성 향상
Getter/Setter 외 빌더 패턴이나 로그생성 등 다양한 방면으로 활용 가능
Lombok 단점 및 주의사항
롬복 라이브러리는 개발자마다 호불호가 나뉠 수 있다. 일부 개발자들은 코드가 직접 눈에 보이는 직관성을 유지하는 것이 좋다고 보는 의견도 있는 만큼 자신의 프로젝트나 성향에 따라 사용하면 좋을 것이다.
또한 API설명과 내부동작을 어느정도 숙지하고 사용해야 한다.
방법 : 클래스에 @Getter, @ToString 등의 어노테이션 붙히거나 @DATA만 붙히면 모든 반복 메서드 구현 됨
17.테스트 관련
단위 테스트(Unit Test): 이는 개별적인 코드의 작은 부분, 예를 들면 함수나 메소드 단위로 실행하는 테스트입니다. 개발자가 작성한 코드가 의도한대로 잘 동작하는지를 확인하기 위한 것이며, 보통 개발자 스스로 작성하고 실행합니다. 단위 테스트는 코드가 예상대로 동작하는지 확인함으로써, 버그를 빠르게 찾아내고 수정할 수 있게 해주고, 코드를 리팩토링하거나 업그레이드할 때 기존 기능이 계속 작동하는지 확인할 수 있게 해줍니다.
통합 테스트(Integration Test): 통합 테스트는 여러 개의 코드 단위가 함께 잘 작동하는지 확인하는 테스트입니다. 이는 보통 단위 테스트 이후에 수행되며, 복합적인 시스템에서 개별 코드 조각들이 상호작용하면서 예상한대로 작동하는지를 확인합니다. 통합 테스트를 통해 각 코드 단위간의 상호작용에 대한 문제나 버그를 찾아낼 수 있습니다.
테스트 주도 개발(TDD, Test-Driven Development): TDD는 소프트웨어 개발 방법론 중 하나로, 코드를 작성하기 전에 테스트를 먼저 작성하는 방식을 말합니다. 이 방법은 단위 테스트를 중심으로, 먼저 실패하는 테스트를 작성한 후 이를 통과시키는 코드를 작성하고, 코드를 개선하는 단계를 반복하며 진행합니다. TDD의 핵심 목표는 코드의 버그를 최소화하고 명확성과 신뢰성을 높이는 것입니다.
도메인 주도 설계(DDD, Domain-Driven Design): DDD는 복잡한 소프트웨어를 설계하는 접근법입니다. 이는 비즈니스 도메인을 모델링하고 이를 소프트웨어에 적용함으로써, 비즈니스 도메인의 복잡성을 소프트웨어에 잘 반영하려는 방식입니다. 이를 위해 DDD는 풍부한 도메인 모델, 언어의 사용(ubiquitous language), 바운디드 컨텍스트(bounded context) 등의 원칙과 기법을 사용합니다. DDD의 주요 목표는 소프트웨어와 비즈니스 사이의 간극을 줄이는 것입니다.