블로킹과 논블로킹
블로킹과 논블로킹
개념
- 블로킹과 논블로킹은 컴퓨터 프로그래밍에서 입출력(IO) 작업을 다룰 때 사용되는 개념이다
블로킹 (Blocking)
블로킹은 입출력 작업을 수행하는 동안 해당 작업이 완료될 때까지 프로그램이 멈추는 것을 의미한다.
예를 들어, 파일을 읽거나 네트워크 소켓에서 데이터를 받는 작업을 수행할 때, 해당 작업이 완료될 때까지 대기하면서 다른 작업을 수행할 수 없다.
블로킹 방식은 단순하고 직관적이지만, 입출력 작업이 많거나 큰 데이터를 처리해야 할 때는 효율적이지 않다.
블로킹 입출력 예시
import java.io.*;
import java.net.*;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("서버가 시작되었습니다.");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("클라이언트가 접속했습니다: " + clientSocket.getInetAddress());
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String message = in.readLine();
System.out.println("클라이언트로부터 받은 메시지: " + message);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
out.println("안녕하세요 클라이언트님!");
out.flush();
clientSocket.close();
}
}
}
논블로킹 (Non-blocking)
논블로킹은 입출력 작업을 수행하는 동안 프로그램이 멈추지 않고 다른 작업을 수행할 수 있도록 하는 방식이다.
입출력 작업이 완료되지 않았더라도 바로 다음 코드를 실행한다.
이 때, 입출력 작업이 완료되었는지 주기적으로 체크하고, 완료되었으면 결과를 받아와 처리한다.
이러한 방식은 입출력 작업이 많거나 대규모 데이터를 처리할 때 효율적이다.
논블로킹 입출력 예시
import java.io.*;
import java.net.*;
import java.util.*;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("서버가 시작되었습니다.");
// 소켓을 논블로킹 모드로 설정합니다.
serverSocket.setSoTimeout(0);
// 클라이언트와 통신할 때 사용할 소켓 리스트입니다.
List<Socket> clientSockets = new ArrayList<>();
while (true) {
try {
Socket clientSocket = serverSocket.accept();
System.out.println("클라이언트가 접속했습니다: " + clientSocket.getInetAddress());
// 새로운 클라이언트와 통신할 소켓을 논블로킹 모드로 설정합니다.
clientSocket.setSoTimeout(0);
clientSockets.add(clientSocket);
} catch (SocketTimeoutException e) {
// 블로킹이 일어나지 않았으므로 다른 작업을 수행합니다.
// (예: 새로운 클라이언트 접속 대기, 클라이언트 메시지 처리)
}
for (Iterator<Socket> it = clientSockets.iterator(); it.hasNext();) {
Socket clientSocket = it.next();
try {
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String message = in.readLine();
System.out.println("클라이언트로부터 받은 메시지: " + message);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
비동기 (Asynchronous)
비동기 방식은 논블로킹 방식과 유사하지만, 작업이 완료되면 콜백 함수를 호출하여 결과를 전달하는 방식이다.
작업을 수행하는 동안 다른 작업을 수행할 수 있으며, 작업이 완료되면 해당 콜백 함수가 실행된다. 비동기 방식은 대부분 논블로킹 방식과 함께 사용되며, 보다 복잡한 애플리케이션에서 사용된다.
비동기 입출력 예시
import java.net.*;
import java.io.*;
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static void main(String[] args) throws Exception {
URL url = new URL("<https://www.naver.com>");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setDoOutput(true);
CompletableFuture.supplyAsync(() -> {
try {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
} catch (IOException e) {
return "";
}
}).thenAccept(result -> System.out.println(result));
}
}
동기(Synchronous)
동기(Synchronous)와 비동기(Asynchronous) 프로그래밍의 차이는 함수가 작업을 수행하는 동안 대기 여부이다.
동기 함수는 작업이 완료될 때까지 대기하며, 비동기 함수는 작업이 완료될 때까지 대기하지 않는다. 다음과 같은 코드로 동기/비동기 프로그래밍을 구현할 수 있다.
동기 입출력 예시
import java.net.*;
import java.io.*;
public class SyncExample {
public static void main(String[] args) throws Exception {
URL url = new URL("<https://www.naver.com>");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setDoOutput(true);
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println(response.toString());
}
}
동기 블로킹 방식
getUser메소드가 userRepository를 통해 데이터베이스에서 유저 정보를 가져온 후, 다른 작업을 수행한 뒤에 결과를 반환한다.
이 코드는 동기 블로킹 방식으로 작성되었다. 즉, getUser메소드는 userRepository에서 데이터를 가져올 때까지 블로킹되고, 다른 작업을 수행할 수 없다. 이 때문에 getUser메소드가 처리되는 동안 다른 요청은 대기 상태에 있게 된다.
만약 userRepository에서 데이터를 가져오는 작업이 오래 걸릴 경우, 애플리케이션이 느려질 수 있고, 이러한 경우에는 비동기 방식을 고려해볼 수 있다.
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public User getUser(@PathVariable("id") Long id) {
return userService.getUser(id);
}
}
@Service
public class UserService {
public User getUser(Long id) {
// 데이터베이스에서 유저 정보를 가져오는 메소드
User user = userRepository.findById(id);
// 다른 작업을 수행
// ...
return user;
}
}
동기 논블로킹 방식
해당 코드에서 getUser메소드는 CompletableFuture를 반환한다. CompletableFuture는 자바에서 제공하는 비동기 처리를 위한 라이브러리로, 비동기 작업의 결과를 포함하는 컨테이너이다.
getUser메소드에서는 CompletableFuture.supplyAsync() 메소드를 사용하여 비동기 작업을 실행한다. 이 때, 람다 표현식을 사용하여 데이터베이스에서 유저 정보를 가져오는 작업을 수행한다. supplyAsync 메소드는 새로운 스레드에서 비동기 작업을 실행하고, 작업이 완료되면 결과를 에CompletableFuture 담아 반환한다.
이렇게 작성된 코드는 동기 논블로킹 방식으로 작성되어 있다.
즉, getUser메소드에서 CompletableFuture를 반환하고, 작업이 완료되기 전까지 블로킹되지 않는다. 이 때문에 getUser 메소드가 처리되는 동안 다른 요청도 처리될 수 있습니다.
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public CompletableFuture<User> getUser(@PathVariable("id") Long id) {
return userService.getUser(id);
}
}
@Service
public class UserService {
public CompletableFuture<User> getUser(Long id) {
return CompletableFuture.supplyAsync(() -> {
// 데이터베이스에서 유저 정보를 가져오는 메소드
User user = userRepository.findById(id);
// 다른 작업을 수행
// ...
return user;
});
}
}
비동기 블로킹 방식
위 코드에서 getUser메소드에서는 CompletableFuture.get()메소드를 호출하여 작업이 완료될 때까지 블로킹된다. get()메소드는 작업이 완료될 때까지 대기하다가 결과를 반환하며, 작업이 완료되지 않은 상태에서는 블로킹된다. 이 때문에 getUser메소드가 실행될 때는 블로킹되며, 다른 요청이 처리될 수 없다.
위 코드에서는 비동기 방식으로 작성되어 있지만 get()메소드를 호출하여 블로킹되었기 때문에 비동기 블로킹 방식으로 동작한다. 이 때문에 요청을 처리하는 스레드가 블로킹되므로 다른 요청도 처리되지 않는다..
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable("id") Long id) throws InterruptedException, ExecutionException {
CompletableFuture<User> future = userService.getUser(id);
User user = future.get();
return ResponseEntity.ok(user);
}
}
@Service
public class UserService {
public CompletableFuture<User> getUser(Long id) {
return CompletableFuture.supplyAsync(() -> {
// 데이터베이스에서 유저 정보를 가져오는 메소드
User user = userRepository.findById(id);
// 다른 작업을 수행
// ...
return user;
});
}
}
비동기 논블로킹 방식
해당 코드에서 getUser메소드에서는 CompletableFuture.thenApply() 메소드를 호출하여 작업이 완료될 때 콜백 함수를 실행하고 결과를 반환한다. 이 방식은 작업이 완료될 때까지 블로킹되지 않으며, 요청을 처리하는 스레드가 다른 요청을 처리할 수 있다.
getUser메소드에서는 CompletableFuture.supplyAsync()메소드를 호출하여 비동기로 작업을 실행한다. 이 메소드는 작업을 다른 스레드에서 실행하고, 작업이 완료되면 결과를 반환한다. 작업이 완료될 때까지 블로킹되지 않으며, 다른 요청을 처리할 수 있다.
따라서 해당 코드는 비동기 논블로킹 방식으로 작성되어 있으며, 요청을 처리하는 스레드가 블로킹되지 않으므로 다른 요청도 처리할 수 있다.
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users/{id}")
public CompletableFuture<ResponseEntity<User>> getUser(@PathVariable("id") Long id) {
return userService.getUser(id).thenApply(ResponseEntity::ok);
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public CompletableFuture<User> getUser(Long id) {
return CompletableFuture.supplyAsync(() -> {
// 데이터베이스에서 유저 정보를 가져오는 메소드
User user = userRepository.findById(id);
// 다른 작업을 수행
// ...
return user;
});
}
}