학습목표
- 팀코칭 질문 답변
TIL
- 개념 학습
- 시스템 캐시 (L1, L2, L3)
- 데몬 스레드와 유저 스레드 차이
- Serializable 인터페이스
- UDP 와 TCP의 차이
- NI/O
- Try with resource
시스템 캐시에 대하여 설명해주세요. (L1, L2, L3)
캐시(cache)는 CPU와 주기억장치 사이에서 빠르게 데이터를 저장하고 가져오기 위한 중간 저장소입니다. CPU가 필요한 데이터를 찾을 때, 캐시에 먼저 접근하게 되는데, 이는 캐시가 주기억장치에 비해 훨씬 빠르게 접근할 수 있기 때문입니다. 필요한 데이터가 캐시에 있을 경우 (이를 캐시 히트라고 합니다), CPU는 매우 빠르게 해당 데이터를 가져올 수 있습니다. 그러나 필요한 데이터가 캐시에 없을 경우 (이를 캐시 미스라고 합니다), CPU는 주기억장치에서 데이터를 가져와야 하며, 이 과정은 상대적으로 느립니다.
캐시에는 주로 L1, L2, L3 등의 여러 레벨이 있습니다:
1. L1 캐시: 이는 가장 기본적인 캐시 수준으로, CPU 코어당 하나씩 있으며 가장 빠른 캐시입니다. 그러나 그 크기는 상대적으로 작으며, 보통 수십 KB에서 수백 KB 정도입니다.
2. L2 캐시: L1 캐시보다는 느리지만, 대개는 L1 캐시보다 큽니다. L2 캐시는 보통 CPU 코어당 하나씩 있지만, 일부 시스템에서는 모든 코어가 공유하는 경우도 있습니다.
3. L3 캐시: 이는 모든 CPU 코어가 공유하는 캐시로, L1과 L2 캐시보다는 느리지만 훨씬 크기가 큽니다.
이렇게 여러 레벨의 캐시를 통해 CPU는 필요한 데이터를 가능한 한 빠르게 가져오려고 시도합니다. 이는 컴퓨터의 성능을 크게 향상시키는 중요한 요소 중 하나입니다.
캐시에는 L1, L2, L3 캐시가 있잖아 그러면 CPU에서는 처음에는 어디에 접근해서 확인하는지 한번만 확인하는지 아니면 다른 곳에 한번 더 확인하는지 아니면 램에서 캐시로 가져오는지 등 알려줘 나는 잘 모르겟어
CPU가 데이터를 찾을 때, 일반적인 순서는 다음과 같습니다:
1. CPU는 먼저 L1 캐시를 확인합니다. 이는 가장 빠른 캐시이므로 CPU는 먼저 여기서 필요한 데이터가 있는지 확인합니다.
2. 만약 L1 캐시에 원하는 데이터가 없다면 (이를 캐시 미스라고 합니다), 그 다음으로 L2 캐시를 확인합니다.
3. L2 캐시에도 원하는 데이터가 없다면, 그 다음은 L3 캐시를 확인합니다.
4. L3 캐시에도 원하는 데이터가 없다면, 마지막으로 CPU는 주기억장치인 RAM에서 데이터를 찾습니다.
이러한 과정은 보통 '캐시 계층'이라고 하며, 이는 CPU가 필요한 데이터를 가능한 한 빠르게 가져오도록 도와줍니다.
데이터가 RAM에서 가져와지면, 그 데이터는 일반적으로 L3, L2, 그리고 L1 캐시로 복사됩니다. 이렇게 함으로써, 다음에 CPU가 동일한 데이터를 필요로 할 때, 그 데이터는 이미 빠른 캐시에 존재하게 되어, CPU는 그 데이터를 더 빠르게 가져올 수 있게 됩니다. 이는 소위 '캐시 히트'라고 불리며, 이는 CPU 성능 향상에 크게 기여합니다.
그러면 캐시는 언제 비워지고 어느 떄까지 데이터를 보관해? 캐시의 데이터 생명 주기에 대해 말해줘 그리고 cpu가 데이터를 찾을 떄 무조건 캐시 먼저 확인하는 거야?? 캐시 용량이 너무 작아서 없을 경우가 더 많은 것 같은데?
캐시의 데이터 생명 주기는 상당히 복잡한 주제입니다. CPU 캐시의 데이터는 다양한 알고리즘에 따라 관리되며, 그 중 가장 일반적인 것들은 LRU(Least Recently Used), LFU(Least Frequently Used), FIFO(First-In, First-Out) 등입니다.
1. LRU(Least Recently Used): 이 알고리즘은 가장 오랫동안 사용되지 않은 항목을 제거합니다. 즉, 최근에 가장 덜 사용된 데이터가 캐시에서 먼저 제거됩니다.
2. LFU(Least Frequently Used): 이 알고리즘은 가장 적게 사용된 항목을 제거합니다. 즉, 가장 빈도수가 낮은 항목이 캐시에서 먼저 제거됩니다.
3. FIFO(First-In, First-Out): 이 알고리즘은 가장 먼저 들어온 항목을 제거합니다. 즉, 가장 오래된 데이터가 캐시에서 먼저 제거됩니다.
캐시에 자리가 없을 때 새로운 데이터를 추가하려면, 먼저 이러한 정책 중 하나를 사용하여 공간을 만들어야 합니다.
CPU가 데이터를 찾을 때, 일반적으로 가장 먼저 캐시를 확인합니다. 이는 캐시가 메인 메모리에 비해 훨씬 빠른 접근 속도를 가지고 있기 때문입니다. 캐시 용량이 작을 수는 있지만, 최근에 사용된 데이터나 자주 사용되는 데이터가 캐시에 보관되므로, CPU가 필요로 하는 데이터가 캐시에 존재할 확률이 상당히 높습니다. 이를 '캐시 히트'라고 합니다.
캐시 미스의 경우, CPU는 L1, L2, L3 캐시를 모두 확인한 후 마지막으로 메인 메모리를 확인하게 됩니다. 캐시 미스가 자주 발생한다면, 이는 CPU의 성능을 저하시키므로, 시스템의 성능을 최적화하기 위해 캐시 정책과 알고리즘이 중요한 역할을 합니다.
캐시 용량이 몇 메가 안되는 것으로 알고 있는데 CPU 특성상 자주나 반복해서 호출하는 경우의 데이터가 많은 가봐??
그렇습니다, 맞습니다. CPU는 많은 작업을 처리하기 때문에 특정 패턴이나 반복되는 작업이 많습니다. 이는 소프트웨어가 반복적인 루프, 재귀적인 호출, 공통 라이브러리 함수 등을 자주 사용하기 때문입니다.
이러한 데이터는 자주 호출되므로 캐시에 저장되어 있을 확률이 높습니다. 이렇게 자주 사용되는 데이터를 캐시에 저장함으로써, CPU는 데이터를 빠르게 액세스할 수 있으며, 이는 전체적인 시스템 성능을 향상시킵니다.
또한, 캐시는 일반적으로 캐시 라인(cache line) 또는 캐시 블록(cache block)이라는 단위로 데이터를 저장합니다. CPU가 특정 주소의 데이터를 요청하면, 그 주소를 포함하는 전체 캐시 라인이 캐시에 로드됩니다. 이는 공간 지역성(spatial locality)라는 개념을 활용한 것으로, CPU가 한 번에 하나의 바이트만 요청하는 대신, 해당 바이트 주변의 데이터도 함께 요청하는 경향이 있기 때문입니다.
따라서, 캐시의 작은 용량이 CPU의 성능을 효과적으로 향상시킬 수 있는 이유 중 하나는 이러한 액세스 패턴과 데이터의 지역성 때문입니다.
데몬 스레드와 유저 스레드의 차이는 무엇일까요?
데몬 스레드와 유저 스레드는 프로그래밍에서 스레드의 두 가지 주요 유형입니다. 이들은 프로세스의 종료에 어떤 영향을 미치는지에 따라 구분됩니다.
데몬 스레드(Daemon Thread): 데몬 스레드는 주로 백그라운드에서 실행되는 작업을 수행합니다. 이들 스레드는 일반적으로 프로그램이 종료될 때 자동으로 종료됩니다. 예를 들어, 가비지 컬렉션, 백그라운드 I/O 작업 등에 사용됩니다. 프로그램이 종료될 때 데몬 스레드가 아직 실행 중이라면, 데몬 스레드는 프로그램의 종료를 방해하지 않으며, 프로그램이 종료되면 데몬 스레드 역시 종료됩니다.
유저 스레드(User Thread): 유저 스레드는 일반적으로 프로그램의 메인 작업을 수행합니다. 이들 스레드는 프로그램이 종료되려면 모든 유저 스레드가 완료되어야 합니다. 즉, 하나 이상의 유저 스레드가 아직 실행 중인 경우, 프로그램은 종료되지 않습니다.
이 둘의 주요 차이점은 프로그램의 종료에 대한 영향입니다. 데몬 스레드는 프로그램이 종료될 때 함께 종료되지만, 유저 스레드는 프로그램이 종료되기 전에 모두 완료되어야 합니다. 이러한 이유로, 데몬 스레드는 일반적으로 백그라운드 작업에 사용되며, 유저 스레드는 프로그램의 주 작업에 사용됩니다.
Serializable 인터페이스는 무엇인가요?
`Serializable` 인터페이스는 자바(JAVA)에서 객체 직렬화를 가능하게 하는 마커 인터페이스입니다. 객체 직렬화란 객체의 상태를 바이트 스트림으로 변환하는 과정을 말하며, 이렇게 변환된 바이트 스트림은 네트워크를 통해 전송하거나 파일에 저장할 수 있습니다.
직렬화는 원격 메소드 호출(Remote Method Invocation, RMI)과 같은 분산 컴퓨팅 환경에서 중요한 역할을 합니다. 여러 컴퓨터 간에 데이터를 주고 받아야 하는 경우에 객체의 상태를 바이트 스트림으로 변환하여 네트워크를 통해 전송하고, 수신측에서는 이 바이트 스트림을 다시 원래의 객체 상태로 복원(deserialization)하는 과정이 필요합니다.
`Serializable` 인터페이스는 메소드 없이 단순히 클래스가 직렬화를 지원한다는 것을 표시하기 위한 마커 인터페이스입니다. 클래스가 `Serializable` 인터페이스를 구현하면, 자바의 직렬화 메커니즘은 그 클래스의 인스턴스를 직렬화하는 데 필요한 모든 것을 제공합니다.
자바에서는 이 외에도 직렬화를 좀 더 세밀하게 제어하고 싶을 때 사용할 수 있는 `Externalizable` 인터페이스를 제공하고 있습니다. `Externalizable`은 `writeExternal`과 `readExternal` 두 개의 메소드를 제공하여 개발자가 직접 객체의 직렬화 방식을 제어할 수 있게 합니다.
마커 인터페이스는 뭐야??
마커 인터페이스(Marker Interface)는 메서드를 포함하지 않는 인터페이스를 말합니다. 이는 단순히 클래스에 특정한 속성을 부여하는 역할을 합니다.
마커 인터페이스를 구현하는 클래스는 특정한 종류로 분류되며, 이를 통해 특정한 동작을 할 수 있게 됩니다. 이 동작은 보통 해당 인터페이스를 검사하는 코드에서 결정됩니다.
예를 들어, 자바의 `Serializable` 인터페이스는 마커 인터페이스입니다. 이 인터페이스를 구현하는 클래스의 객체는 직렬화할 수 있다는 것을 나타냅니다. 다시 말해, `Serializable` 인터페이스를 구현하는 클래스의 객체는 ObjectOutputStream을 통해 바이트 스트림으로 변환할 수 있고, 그 반대의 작업도 가능합니다.
마커 인터페이스는 특정 기능을 강제하기보다는 클래스가 특정한 성질을 가지고 있음을 나타내는 역할을 하므로, 메서드 없이 클래스에 메타데이터를 추가하는 방법으로 이해할 수 있습니다.
Serializable 인터페이스은 자바에서 다른 서버 등과 객체나 다른 데이터를 전송하고 받는 쓰이는 인터페이스구나 데이터는 바이트 스트림이라는 규격으로 변환되어서 효율적으로 전송되고 다시 월래의 데이터 형태로 바뀌겠구나
맞습니다, 정확하게 이해하셨습니다. Serializable 인터페이스는 Java에서 객체의 상태를 바이트 스트림으로 변환하고(이를 직렬화라고 합니다), 그 변환된 바이트 스트림을 다시 객체로 변환하는 것을 가능하게 합니다(이를 역직렬화라고 합니다).
이러한 직렬화 과정은 원격 메소드 호출(Remote Method Invocation, RMI)과 같은 분산 네트워크 환경에서 데이터를 전송하거나, 객체 상태를 파일로 저장하는 등의 용도로 사용됩니다.
네트워크를 통해 데이터를 전송할 때 바이트 스트림 형태로 변환하는 이유는, 데이터의 복잡한 구조를 단순화하여 효율적으로 전송하고, 다른 환경에서도 해당 데이터를 동일하게 해석할 수 있게 하기 위함입니다. 따라서 Serializable 인터페이스는 네트워크 통신, 데이터베이스 입출력, 파일 입출력 등 다양한 상황에서 객체의 상태를 유지하고 전송하는데 중요한 역할을 수행합니다.
UDP 와 TCP의 차이는 무엇일까요?
UDP(User Datagram Protocol)와 TCP(Transmission Control Protocol)는 데이터를 인터넷을 통해 전송하는 데 사용되는 두 가지 주요 프로토콜입니다. 이 두 프로토콜은 서로 다른 특징과 용도를 가지고 있습니다.
TCP (Transmission Control Protocol)
- TCP는 연결 지향적인 프로토콜입니다. 즉, 통신을 시작하기 전에 송신자와 수신자 사이에 연결을 설정하며, 데이터가 성공적으로 전송되었는지 확인하고, 오류가 발생하면 데이터를 재전송합니다.
- TCP는 데이터의 순서를 보장합니다. 데이터 패킷들이 네트워크를 통해 전송되는 동안 순서가 바뀔 수 있는데, TCP는 이들 패킷을 올바른 순서로 재배열합니다.
- TCP는 신뢰성 있는 데이터 전송을 보장하지만, 이런 기능들로 인해 UDP에 비해 상대적으로 느릴 수 있습니다.
UDP (User Datagram Protocol)
- UDP는 비연결 지향적인 프로토콜입니다. 즉, 데이터를 전송하기 전에 연결을 설정하지 않습니다.
- UDP는 데이터의 도착을 확인하지 않으며, 데이터가 손실되었을 경우 재전송하지 않습니다. 따라서, TCP에 비해 빠르지만 신뢰성은 낮습니다.
- UDP는 실시간 스트리밍, 게임, VoIP(Voice over IP) 등과 같이 속도가 중요하고 일부 데이터 손실이 허용되는 상황에서 주로 사용됩니다.
따라서, TCP와 UDP 중 어느 것을 사용할지는 사용 사례에 따라 달라집니다. 신뢰성이 중요한 경우 TCP를, 속도나 효율성이 중요한 경우 UDP를 선택할 수 있습니다.
NI/O는 무엇인가요?
NIO (New Input/Output)는 자바(JAVA)에서 입출력 작업을 수행하기 위한 API입니다. NIO는 기존의 자바 I/O와 비교했을 때 비동기적이며, 더 높은 처리량을 갖는 어플리케이션을 만들 수 있도록 설계되었습니다.
NIO는 세 가지 주요 구성 요소를 가지고 있습니다: 채널(Channel), 버퍼(Buffer), 셀렉터(Selector).
채널(Channel): 채널은 데이터를 읽고 쓰기 위한 연결 통로입니다. 기본적으로, 채널은 블로킹 모드와 넌블로킹 모드 두 가지로 동작할 수 있습니다. 블로킹 모드에서는 데이터가 읽거나 쓰여질 때까지 작업이 블로킹됩니다. 반면에, 넌블로킹 모드에서는 작업이 즉시 반환되며, 데이터가 아직 준비되지 않았다면 아무것도 하지 않습니다.
버퍼(Buffer): 버퍼는 데이터를 저장하는 데 사용되는 메모리 영역입니다. NIO에서 모든 데이터는 버퍼를 통해 처리됩니다. 버퍼는 다양한 기본 데이터 타입(int, long, double 등)을 위한 클래스를 가지고 있습니다.
셀렉터(Selector): 셀렉터는 채널의 상태 변화(예: 데이터가 읽기 가능한 상태 등)를 감지하는 컴포넌트입니다. 이를 사용하면 단일 스레드가 여러 채널을 동시에 모니터링 할 수 있습니다. 이는 멀티스레딩의 복잡성을 피하면서도 높은 처리량의 I/O를 가능하게 합니다.
이러한 기능들은 자바 NIO를 통해 고성능 I/O를 가능하게 하며, 특히 네트워크 프로그래밍에서 주로 사용됩니다.
채널에서 말하는 블로킹 모드의 작업이 블로킹 된다는 게 어떤 의미야?
"블로킹"이라는 용어는 프로그래밍에서 주로 사용되며, 어떤 작업이 완료될 때까지 현재의 스레드(실행 중인 프로그램의 가장 작은 단위)가 대기 상태에 있다는 것을 의미합니다.
블로킹 I/O 모델에서는, 데이터가 읽어지거나 쓰여질 때까지 호출한 함수(혹은 메소드)가 반환되지 않습니다. 즉, I/O 작업이 완료될 때까지 현재 스레드가 차단(블로킹)되고 다른 작업을 수행하지 않습니다. 이는 데이터가 준비될 때까지 프로그램이 "막히게" 됩니다.
예를 들어, 서버가 클라이언트로부터의 입력을 기다리는 경우를 생각해보세요. 블로킹 모드에서는 클라이언트가 데이터를 보낼 때까지 서버는 대기 상태에 있게 됩니다. 즉, 서버는 그동안 다른 어떠한 작업도 수행할 수 없습니다.
반대로, 넌블로킹 I/O 모델에서는 데이터가 아직 준비되지 않은 경우, I/O 작업을 호출하는 함수는 즉시 반환하며, 이는 프로그램이 다른 작업을 계속 수행할 수 있게 합니다. 따라서, 넌블로킹 모드에서는 I/O 작업이 완료될 때까지 기다리지 않고, 그 사이에 다른 작업을 계속 수행할 수 있습니다. 이는 효율성을 높이며, 특히 동시에 많은 연결을 처리해야 하는 서버와 같은 경우에 유용합니다.
데이터가 읽어지거나 쓰여질 떄까지는 어디에서 데이터가 읽어지거나 쓰여진다는 거야?? 채널에서야?
네, 맞습니다. 블로킹 I/O에서 "데이터가 읽어지거나 쓰여질 때까지"라는 표현은, I/O 작업이 채널에 대해 수행되는 경우를 의미합니다.
채널은 기본적으로 데이터를 전송하는 연결 통로입니다. 파일에 대한 채널, 소켓에 대한 채널 등 다양한 유형의 채널이 있습니다.
예를 들어, 네트워크 소켓에 데이터를 쓰려는 경우, 블로킹 모드에서는 네트워크를 통해 데이터를 전송할 준비가 될 때까지 해당 작업이 블로킹됩니다. 비슷하게, 파일로부터 데이터를 읽어들이려는 경우, 블로킹 모드에서는 파일 시스템에서 데이터를 읽을 준비가 될 때까지 해당 작업이 블로킹됩니다.
이렇게 블로킹 모드에서는 I/O 작업이 완료될 때까지 해당 스레드는 다른 어떤 작업도 수행할 수 없습니다. 반면에, 넌블로킹 모드에서는 데이터가 아직 준비되지 않아도 해당 작업이 즉시 반환되므로, 다른 작업을 계속 진행할 수 있습니다. 이러한 차이는 블로킹과 넌블로킹 I/O의 주요 차이점 중 하나입니다.
블로킹하면 입출력이 완전히 완료(메소드가 반환값으로 돌려줄때까지) 해당 호출했던 클래스나 메소드는 정지 되는 거야??대신 입출력은 확실히 하겠는데 어플리이션의 쓰래드?? 프로세스??는 멈추는 거 아냐?? 많이 좋아보이지는 않는데?
블로킹 I/O 작업에서는 네, 맞습니다. 데이터가 읽히거나 쓰여질 때까지 해당 스레드는 대기 상태에 있게 되고, 그러므로 다른 작업을 수행하지 못합니다. 이것이 블로킹 I/O 모델의 한계점입니다.
많은 연결을 동시에 처리해야 하는 서버와 같은 애플리케이션에서는, 이런 블로킹 I/O 모델이 문제가 될 수 있습니다. 예를 들어, 한 사용자의 요청을 처리하는 동안 서버가 블로킹되면, 다른 사용자들은 응답을 기다리는 동안 지연됩니다.
이 문제를 해결하기 위해 넌블로킹 I/O 또는 비동기 I/O 모델이 사용됩니다. 이 모델에서는 I/O 작업이 즉시 반환되므로, 데이터가 아직 준비되지 않았더라도 애플리케이션은 다른 작업을 계속 수행할 수 있습니다.
그러나 모든 애플리케이션에 넌블로킹 또는 비동기 I/O가 필요한 것은 아닙니다. 일부 애플리케이션에서는 블로킹 I/O가 더 단순하고 적합하며, 성능에도 충분할 수 있습니다. 결국 선택은 애플리케이션의 요구사항과 상황에 따라 달라집니다.
미완료 작업을 우선 반환하고 완료되면 온전한 작업으로 바꾸거나 업데이트 하는 거야?? 프론트엔드에서도 비슷한 개념 본 적 있어 axios api호출하면 그런 객체를 받앗엇어
네, 맞습니다. 비동기 프로그래밍 모델에서는 작업이 즉시 반환되고, 작업의 완료를 알리기 위해 콜백 함수나 프로미스(Promise) 같은 메커니즘이 사용될 수 있습니다. 이는 특히 네트워크 I/O나 디스크 I/O 같은 비동기 연산에서 자주 볼 수 있는 패턴입니다.
예를 들어, JavaScript의 Promise나 axios 라이브러리는 비동기 HTTP 요청을 다루는 좋은 예입니다. axios.get 같은 메서드를 호출하면, 이 메서드는 즉시 Promise 객체를 반환합니다. 이 Promise 객체는 나중에 HTTP 응답을 사용할 수 있게 될 것이라는 "약속"을 나타냅니다.
// axios 예제
axios.get('/api/data').then(response => {
console.log(response.data);
}).catch(error => {
console.error(error);
});
이 코드에서 axios.get 메서드는 Promise를 반환하며, 이 Promise는 HTTP 요청이 완료될 때까지 기다린 다음, 그 결과를 처리하는데 사용되는 then과 catch 메서드를 가지고 있습니다.
이와 같이 비동기 모델에서는 작업이 완료되지 않았다는 것을 나타내는 객체(예: Promise)가 즉시 반환되고, 나중에 그 작업이 완료되면 결과를 가져오는 방식으로 작동합니다. 이 모델을 통해, 애플리케이션은 I/O 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속 수행할 수 있습니다. 이는 애플리케이션의 효율성과 반응성을 향상시킬 수 있습니다.
논블로킹에서는 어떤 패턴의 객체를 이용하길래 비동기식이 가능하고 또 구현한거야??ㅎ
논블로킹 I/O에서 비동기식 처리를 지원하는 메커니즘은 여러 가지가 있습니다. 운영 체제나 프로그래밍 언어에 따라 지원하는 비동기 메커니즘이 다를 수 있습니다. 다음은 몇 가지 일반적인 방법입니다:
1. 콜백 함수: 비동기 작업이 완료되면 호출되는 함수입니다. Node.js의 논블로킹 I/O 모델은 이런 방식을 사용합니다. 콜백 함수는 비동기 작업의 결과를 처리하거나 에러를 처리하는 데 사용됩니다.
2. 퓨처(Future) 또는 프로미스(Promise): 비동기 작업의 결과를 대표하는 객체입니다. 작업이 완료되면 이 객체를 통해 결과를 가져올 수 있습니다. JavaScript의 Promise나 Java의 Future가 이런 방식을 사용합니다.
3. 이벤트 루프와 메시지 큐: 이벤트 루프는 메시지 큐에서 이벤트(또는 메시지)를 꺼내와 해당 이벤트를 처리하는 함수(또는 이벤트 핸들러)를 호출하는 방식으로 작동합니다. 비동기 I/O 작업이 완료되면 완료 이벤트가 메시지 큐에 추가되며, 이벤트 루프는 이 이벤트를 꺼내와 관련된 이벤트 핸들러를 호출합니다. 이런 방식은 GUI 애플리케이션 또는 Node.js와 같은 비동기 프로그래밍 환경에서 자주 볼 수 있습니다.
이러한 비동기 메커니즘을 사용하면, 애플리케이션은 I/O 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속 수행할 수 있습니다. 따라서, 논블로킹 I/O 모델은 애플리케이션의 효율성과 반응성을 향상시킬 수 있습니다.
퓨처(Future)
Java에서 `Future`는 비동기 연산의 결과를 나타내는 인터페이스입니다. `Future`를 사용하면, 메소드가 비동기적으로 수행될 수 있고, 그 결과는 나중에 사용할 수 있습니다. `Future` 객체는 아직 계산되지 않은 결과를 나타내기 때문에, 그 결과를 얻기 위해선 해당 계산이 완료될 때까지 기다려야 합니다.
`Future` 인터페이스는 다음과 같은 메서드들을 제공합니다:
- `isDone()`: 작업이 완료되었는지 여부를 반환합니다. 작업이 정상적으로 완료되었거나, 예외가 발생했거나, 취소되었다면 true를 반환합니다.
- `get()`: 계산이 완료될 때까지 기다린 후, 계산 결과를 반환합니다. 계산이 완료되지 않았다면 블로킹 상태에 빠질 수 있습니다. 이 메서드는 작업이 정상적으로 완료되었을 때 결과를 반환하거나, 작업이 예외로 종료되었을 때 이 예외를 던집니다.
- `get(long timeout, TimeUnit unit)`: 주어진 타임아웃 동안만 계산이 완료될 때까지 기다립니다. 계산이 주어진 시간 내에 완료되지 않으면 `TimeoutException`을 던집니다.
- `cancel(boolean mayInterruptIfRunning)`: 작업을 취소합니다. 작업이 이미 완료되었거나 취소되었거나, 또는 어떤 이유로 취소할 수 없는 경우에는 이 메서드는 false를 반환합니다. 그렇지 않으면 작업을 취소하고 true를 반환합니다.
- `isCancelled()`: 작업이 취소되었는지 여부를 반환합니다.
이러한 메소드들을 통해, `Future` 인터페이스는 비동기 연산의 상태를 확인하고, 결과를 가져오고, 연산을 취소하는 등의 작업을 수행할 수 있습니다. 이런 기능은 비동기 프로그래밍에서 중요한 부분을 차지합니다.
Try with resource
Java 7부터 도입된 'try-with-resources'는 자원을 사용한 후에 안전하게 반환하는 것을 보장하는 구문입니다. '자원'이라는 말은 시스템 리소스, 파일, 네트워크 연결 등과 같이 사용 후에 해제해야 하는 것들을 의미합니다.
전통적인 try-catch-finally 블록에서는 이러한 자원을 해제하기 위해 개발자가 명시적으로 `finally` 블록에서 자원을 해제해야 했습니다. 그러나 이는 누락되거나 잘못 처리되기 쉬워서 문제가 발생할 수 있었습니다.
try-with-resources 구문은 이러한 문제를 해결하고 코드를 더 간결하고 안전하게 만듭니다. 이 구조에서는 `try` 키워드 뒤에 괄호를 사용하여 자원을 선언하고 초기화할 수 있습니다. 그러면 이 자원은 try 블록의 범위를 벗어나는 즉시 자동으로 해제됩니다.
다음은 try-with-resources 구문의 예입니다:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// 파일에서 데이터를 읽음
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
이 코드에서 BufferedReader는 `try` 블록이 종료될 때 자동으로 해제됩니다. 따라서 개발자가 명시적으로 `finally` 블록에서 이를 해제할 필요가 없습니다.
이러한 방식은 파일 또는 데이터베이스 연결과 같은 자원을 사용하는 코드를 보다 안전하고 간결하게 작성할 수 있게 합니다. 이 구문을 사용하려면 자원 클래스가 `java.lang.AutoCloseable` 인터페이스 또는 이를 확장하는 `java.io.Closeable` 인터페이스를 구현해야 합니다. 이 인터페이스들은 자원이 해제되는 방법을 정의하는 `close` 메서드를 제공합니다.
try catch resource 문이 따로 있는 건 아닌 건가? try catch 문에서 try 옆에 소괄호에서 쓰는 자원을 선언하고 쓰면 되는건가?? 이런 문법이 자바7 부터 지원이 되는 거고
맞습니다. "try-with-resources"는 따로 있는 문이 아닌, 기존의 `try-catch` 문에 자원 관리 기능이 추가된 형태입니다. 자바 7 버전부터 지원이 시작되었습니다.
"try-with-resources"는 `try` 문에 소괄호(`()`)를 추가하여 자원을 선언하고 초기화하는 것을 허용합니다. 이렇게 선언된 자원은 `try` 블록이 끝날 때 자동으로 해제됩니다.
이 기능을 사용하기 위해서는 사용하려는 리소스가 `java.lang.AutoCloseable` 인터페이스나 그 하위 인터페이스인 `java.io.Closeable` 인터페이스를 구현해야 합니다. 이들 인터페이스는 자원이 해제되는 방법을 정의하는 `close()` 메서드를 제공합니다.
다음은 "try-with-resources"를 사용한 예시입니다:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// some code that uses br...
} catch (IOException e) {
// exception handling...
}
이 예시에서 `BufferedReader` 인스턴스는 `try` 블록이 끝나는 즉시 자동으로 해제됩니다. 따라서 개발자가 `finally` 블록에서 `br.close()`를 호출하는 등의 명시적인 자원 해제 코드를 작성할 필요가 없습니다. 이는 오류를 방지하고 코드의 가독성을 향상시키는 데 도움을 줍니다.
자바 7이면 현대에서는 지원하는 꽤 오래된 국밥같은 버전인데 난 처음 봤네 이거 많이는 안써?? 엄청 좋은 기능인 것 같은데 딱히 본 기억이 안나네
"try-with-resources"는 자바 7부터 도입된 기능으로, 코드를 더 안전하고 간결하게 만드는 데 도움을 주는 매우 유용한 기능입니다. 파일 I/O, 데이터베이스 연결, 네트워크 연결 등의 자원을 다루는 경우에는 특히 더 그렇습니다.
하지만 모든 자바 개발자가 이 기능을 적극적으로 사용하고 있는 것은 아닙니다. 그 이유는 다양합니다:
1. 일부 개발자는 자바 7 이전의 버전을 사용하거나, 하위 호환성을 유지하려는 목적으로 이 기능을 사용하지 않을 수 있습니다.
2. 자바 7 이전에 작성된 코드는 이 기능을 사용하지 않을 수 있습니다. 이러한 코드를 리팩토링하지 않는 한, "try-with-resources"를 사용하지 않을 것입니다.
3. 또한, 개발자 개개인의 습관, 프로젝트의 코드 스타일 가이드, 또는 팀의 코드 리뷰 과정 등으로 인해 이 기능을 사용하지 않는 경우도 있습니다.
그럼에도 불구하고 "try-with-resources"는 매우 유용한 기능이므로, 가능하면 사용하는 것이 좋습니다. 이 기능을 사용하면 자원 누수와 같은 일반적인 버그를 피할 수 있습니다.
간혹 try-catch-finally에서 finally 블록안에 close()로 리소스를 닫아도 안 닫기는 경우가 있다고 한다.
내가 알아본 결과 close()나 finally 블록 둘 다 자체 코드적으로는 문제는 없는데 자바에서 제어하지 못하는 외부 환경에서 해당 리소스에 영향을 미치는 변경/변화가 일어나면 자바에서는 그 부분은 컨트롤/대처 하지 못하기 때문에 에러가 발생하는 경우가 있다고 한다.
그래서 이럴 경우를 대비해서 close()에도 예외 처리를 해야 한다고 한다. 즉 finally 블록 안이긴 하지만 결국 예외 처리를 하기 위해서 예외 처리문을 쓰는데 예외 처리하는 예외 처리 문 안에 또 예외 처리 문을 써야하는 먼가 이상한 경우가 생긴다.
아래는 간단한 예시이다.
아래는 `FileInputStream`을 사용하여 파일을 읽는 예제 코드입니다. `finally` 블록 안에서 `close()`를 호출하도록 해서 파일의 리소스를 반납하려 하지만, `close()` 메소드 자체에서 예외가 발생하는 상황을 표현했습니다.
import java.io.*;
public class Main {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("somefile.txt");
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close(); // close()에서 예외가 발생할 수 있다.
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
이 코드에서 `fis.close();`는 `IOException`을 던질 수 있는 메소드입니다. 만약 `close()` 메소드가 파일을 닫는 과정에서 어떤 문제가 발생하면 `IOException`이 발생하고, 이는 `catch` 블록에서 처리됩니다. 이런 상황은 예를 들어 파일이 이미 닫혀 있거나, 네트워크 문제 등 외부 상황에 따라 발생할 수 있습니다.
이렇게 리소스를 닫는 과정에서 예외가 발생할 수 있으므로, 이를 처리하기 위해서는 `close()` 호출을 `try-catch` 구문으로 묶어주는 것이 좋습니다. 이렇게 하면 `close()`에서 발생하는 예외를 안전하게 처리할 수 있습니다.
즉 간단하게 말씀드리면 close()도 하나의 메소드에 예외 처리가 필요하다. 이것 까지는 생각 못 했는데 그렇다고 한다. 경험이 이래서 중요한 것 같다.
이런 너무나 불편한 점이 있어서 try-with-resources가 나온 것 같다.
이런 저런 경우에도 try 옆의 소괄호에 해당 리소스를 선언해서 사용한다면 잘 닫는다고 한다. 이제 알고 난 이상 이렇게 쓰는 건 거의 필수 인 것 같다.
'[F-Lab 66해빗 페이백 챌린지 ]' 카테고리의 다른 글
| [F-Lab 모각코 페이백 25일차] ORM, Spring MVC (작동과정과 컴포넌트) (0) | 2023.06.12 |
|---|---|
| [F-Lab 모각코 페이백 24일차] 팀코칭 질문 정리 (인터뷰 준비, 개념 학습) 쿠키, 세션, JDBC, 트랜잭션 (3) | 2023.06.09 |
| [F-Lab 모각코 페이백 22일차] 스프링 트랜잭션 (Spring Transaction) (1) | 2023.06.07 |
| [F-Lab 모각코 페이백 21일차] 어댑터 패턴, SOLID 원칙, 스프링 프레임워크, 스프링 부트 (0) | 2023.06.06 |
| [F-Lab 모각코 페이백 20일차] 싱글톤 패턴, 팩토리 패턴 (0) | 2023.06.05 |