40. 프로젝트에 예외처리 적용
try~catch~finally
server app - DAO 메서드 호출할 때 try 적용
client app - execute 적용
41. 여러 클라이언트의 요청을 순차적으로 처리하기 : Stateful 방식
- 클라이언트 요청을 순차적으로 처리하는 방법
먼저 접속한 클라이언트가 접속을 끊을때까지 대기.
특징
1. 접속 후 연결 상태로 유지한다. -> 서버 자원을 점유 -> 동시 많은 클라이언트 접속을 유지할 수 없다.(stateless에 비해)
2. 클라이언트 정보를 유지하기 때문에 -> 클라이언트가 요청한 작업 결과 유지 -> 연결작업을 처리하기 수월하다.
while로 무한 루프 server앱 소켓부분
try ~ catch문 안에 또 try~catch 들어가도 됨 ! 중복 가능
try 문안에 선언된건 try 바깥에서 못 씀.
finally { // close도 예외 발생하는 메서드 , 소켓을 닫아야하니까 각각 따로 해야함.
try {in.close();} catch (Exception e) {}
try {out.close();} catch (Exception e) {}
try {socket.close();} catch (Exception e) {}
}
= try with resources
try(Socket s = socket; DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
//Socket s = socket close를 위해 새로 넣은 것. try () 안 마지막코드는 ; 안 붙여도 됨
// 파라미터 값 먼저 돌고 본 코드 돌아감
42. 여러 클라이언트의 요청을 순차적으로 처리하기 : Stateless 방식
특징:
1. 요청할 때마다 매번 연결 -> 요청시간이 길어짐/(연결시간이 포함되기 때문에) -> 클라이언트 작업 결과를 유지할 수 없음 -> 연결 작업을 수행하기 어렵다(매번 연결하기 때문에, 서버는 클라이언트를 식별하기 힘들다.)
2. 응답 후 클라이언트와 연결을 끊는다. -> 서버에 클라이언트 정보를 유지하지 않는다. -> 서버 자원을 덜 사용한다.(statefull에 비해) -> statefull 보다 같은 하드웨어를 가지고 더 많은 클라이언트 동시 접속 처리 가능
serverapp 수정
클라이언트 DaoBuilder
client 수정
43. 여러 클라이언트 요청을 동시에 처리하기: Thread 적용
스레드의 구동원리와 사용법
- 스레드의 라이프사이클 이해
- 스레드 클래스와 Runnable 인터페이스 사용법
멀티태스킹의 메커니즘 이해
- 프로세스 스케쥴링: Round robin 방식, priority + Aging 방식
- 컨텍스트 스위칭
- 프로세스 복제(fork)방식과 스레드 방식 비교
- 임계영역(Critical region), 세마포어(Semaphore) 와 뮤텍스(Mutex)
// 상대편으로부터 연결 요청 받기 - 서버(server)
public class Receiver {
public static void main(String[] args) throws Exception {
System.out.println("서버 실행!");
// 1) 다른 컴퓨터의 연결 요청을 기다린다.
// - new ServerSocket(포트번호)
// - 포트번호:
// - 호스트에서 실행 중인 서버 프로그램을 구분하는 번호이다.
// - 1024 ~ 49151 사이의 값 사용한다.
// - 1 ~ 1023 사이의 포트 번호는 특정 서버가 사용하기 위해 미리 예약된 번호다.
// - 가능한 이 범위의 포트 번호는 사용하지 않는 것이 좋다.
// - 유명 프로그램의 포트 번호도 가능한 사용하지 말라.
// - 예) Oracle DBMS(1521), MySQL DBMS(3306) 등
// - 같은 컴퓨터에서 다른 프로그램이 이미 사용중인 포트 번호는 지정할 수 없다.
// - 포트 번호는 중복으로 사용될 수 없다.
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("ServerSocket 생성!");
// 2) 연결을 기다리고 있는 클라이언트가 있다면 맨 먼저 접속한 클라이언트의 연결을 승인한다.
// - 클라이언트가 서버에 접속을 요청하면 그 정보를 "대기열"이라고 불리는 목록으로 관리한다.
// - accept()를 호출하면 대기열에서 순서대로 꺼내 해당 클라이언트와 연결된 소켓을 만든다.
Socket socket = serverSocket.accept();
System.out.println("클라이언트와 연결된 Socket 생성!");
// 3) 소켓 객체를 통해 읽고 쓸 수 있도록 입출력 스트림을 얻는다.
// - 연결된 클라이언트로 데이터를 보내고 받으려면 입출력 스트림을 꺼내야 한다.
// - 소켓이 리턴해준 입출력 스트림에 적절한 데코레이터를 붙여서 사용한다.
PrintStream out = new PrintStream(socket.getOutputStream());
Scanner in = new Scanner(socket.getInputStream());
System.out.println("데이터 송수신을 위한 입출력 스트림 준비!");
// 4) 상대편이 보낸 문자열을 한 줄 읽는다.
// => 상대편이 한 줄의 데이터를 보낼 때까지 리턴하지 않는다.
// => 이런 메서드를 블로킹 메서드라 부른다.
String str = in.nextLine();
System.out.printf("상대편> %s\n", str);
// 5) 상대편으로 문자열을 한 줄 보낸다.
out.println("나는 엄진영입니다. 반갑습니다!");
// 6) 항상 입출력 도구는 사용 후 닫아야 한다.
in.close();
out.close();
// 7) 네트워크 연결도 닫는다.
socket.close(); // 클라이언트와 연결을 끊는다.
serverSocket.close(); // 클라이언트의 연결 요청을 받지 않는다.
}
}
// 상대편에 연결을 요청하기 - 클라이언트(client)
public class Sender {
public static void main(String[] args) throws Exception {
System.out.println("클라이언트 실행!");
// 1) 다른 컴퓨터와 네트워크로 연결한다.
// => 서버와 연결되면 Socket 객체가 생성된다.
// => 서버와 연결될 때까지 리턴하지 않는다.
// => 서버에 연결할 수 없으면 예외가 발생한다.
// - 기본으로 설정된 타임아웃 시간까지 연결되지 않으면 예외가 발생한다.
//
// new Socket(원격 호스트의 IP 주소/도메인이름, 원격 호스트 프로그램의 포트번호)
// - 로컬 호스트(애플리케이션을 실행하는 현재 컴퓨터)일 경우: 127.0.0.1 또는 localhost
Socket socket = new Socket("localhost", 8888); // 서버의 대기열에 등록된다면 리턴한다.
System.out.println("서버와 연결된 Socket 생성!");
// 2) 소켓 객체를 통해 읽고 쓸 수 있도록 입출력 스트림을 얻는다.
PrintStream out = new PrintStream(socket.getOutputStream());
Scanner in = new Scanner(socket.getInputStream());
System.out.println("서버와 데이터를 송수신할 스트림 준비!");
// 3) 상대편으로 문자열을 한 줄 보낸다.
// => 보낸 데이터는 NIC의 메모리에 임시 보관된다.
// => 임시 보관된 데이터가 상대편으로 완전히 보내졌는지 따지지 않고 즉시 리턴한다.
out.println("엄진영입니다. 안녕하세요!");
// 4) 상대편에서 보낸 문자열을 한 줄 읽는다.
// => 상대편이 한 줄 데이터를 보낼 때까지 리턴하지 않는다.
// => 이런 메서드를 블로킹 메서드라 부른다.
String str = in.nextLine();
System.out.println(str);
// 5) 항상 입출력 도구는 사용 후 닫아야 한다.
in.close();
out.close();
// 6) 네트워크 연결도 닫는다.
socket.close();
}
}
println 한다고 해서 데이터가 보내지는게 아님
프로토콜: 통신 규칙
버퍼사용해서 통신하기 sender 5
서버 만들기
System.out.println("서버 실행!");
// 1) 네트워크 연결을 기다리는 역할을 수행할 객체를 준비
// => new ServerSocket(포트번호)
// => 현재 실행 중인 프로그램과 포트 번호가 중복되어서는 안된다.
ServerSocket ss = new ServerSocket(8888);
// 포트번호
// => 한 컴퓨터에서 네트워크 연결을 기다리는 프로그램의 식별번호이다.
// => OS는 이 번호를 가지고 데이터를 받을 프로그램을 결정한다.
System.out.println("클라이언트 연결을 기다리는 중...");
// 잠깐 멈추기
keyboard.nextLine(); // 사용자가 엔터를 칠 때까지 리턴하지 않는다.
ss.close();
System.out.println("서버 종료!");
클라이언트 만들기
public static void main(String[] args) throws Exception {
// 1) 서버에 연결 요청을 할 때 사용할 도구를 준비한다.
// => 서버와의 연결이 이루어지면 Socket 객체를 리턴한다.
// => 클라이언트 측의 포트 번호는 OS가 자동으로 부여한다.
// 서버 측은 개발자가 명시적으로 부여해야 한다.
Socket socket = new Socket(
// 서버 IP 주소(ex: 234.3.4.56) 또는 도메인명(ex: www.daum.net)
"localhost",
// 특수 IP : 127.0.0.1 - 로컬 컴퓨터를 가리킨다.
// 특수 도메인명 : localhost - 127.0.0.1을 가리킨다.
//
// 서버 포트번호 - 서버를 구분하는 식별 번호.
8888
// IP 주소가 회사 대표 번호라면, 포트 번호는 내선 번호라 할 수 있다.
);
System.out.println("서버와 연결되었음!");
// 2) 서버와 연결 해제
// => 작업이 끝난 후에는 항상 서버와의 연결을 해제해야 한다.
// => 물론 해제하지 않아도 서버측에서 일정 시간이 지나면 자동으로 연결과 관련된 자원을 해제한다.
// => 그러나 가능한 명시적으로 연결을 해제하는 것이 좋다. 자원회수 및 다음 사용자위해
//
socket.close();
System.out.println("서버와 연결을 끊었음!");
대기열? 클라이언트 몇명까지 대기할지 허락하는.
=> 클라이언트가 접속을 요청하면 대기열에 클라이언트 정보를 저장한다.
=> 저장은 큐(FIFO) 방식으로 관리한다.
=> 대기열의 크기가 클라이언트의 연결을 허락하는 최대 개수이다.
=> 대기열을 초과하여 클라이언트 요청을 들어 왔을 때 서버는 응답하지 않는다.
클라이언트는 내부에 설정된 시간(timeout)동안 기다리다 응답을 받지 못하면
예외를 던지고 연결 요청을 취소한다.
=> new ServerSocket(포트번호, 대기열크기);
다음과 같이 대기열의 개수를 지정하지 않으면, 기본이 50개이다. ==> 클라이언트 50개까지 줄 세울 수 있다는 소리.
ServerSocket ss = new ServerSocket(8888);
ServerSocket ss = new ServerSocket(8888, 2); //2 :백로그 : 접속할 수 있는 최대 갯수
로컬에서 할 땐 바로 오류뜨지만 원격인 경우엔 일정시간 후 오류 뜸...
타임아웃 에러 / 타임아웃 시간 설정하기
// 실행을 잠시 중단시키기 위해 사용
Scanner keyScan = new Scanner(System.in);
System.out.println("클라이언트 실행!");
// 1) 소켓을 생성한다.
Socket socket = new Socket();
// 2) 연결할 서버의 주소를 준비한다.
SocketAddress socketAddress = new InetSocketAddress("192.168.0.xx", 8888);
// 3) 서버와의 연결을 시도한다.
// => 타임아웃으로 지정된 시간 안에 서버가 승인(accept())하지 않으면 즉시 예외가 발생한다.
// => Windows의 경우,
// - 로컬에 접속할 때 타임아웃 설정이 정상적으로 동작되지 않는다.(확인 할 것!)
// - 원격 윈도우 PC에 서버를 실행하여 접속한다면 정상적으로 동작한다.
socket.connect(socketAddress, 10000); // timeout : milliseconds
keyScan.nextLine(); // 사용자가 엔터를 칠 때까지 다음 코드로 이동하지 않는다.
socket.close();
keyScan.close();
대기열에서 accept로 꺼내지면 대기열 비어짐.
Scanner keyboard = new Scanner(System.in);
ServerSocket ss = new ServerSocket(8888, 2);
// 테스트1) 대기열에 클라이언트가 없을 때,
// => accept()는 블로킹 상태에 놓인다.
// 즉 리턴하지 않는다.
// 큐(queue)에 대기중인 클라이언트 중 첫 번째 클라이언트를 꺼내서 연결을 승인한다.
// => 클라이언트가 서버에 연결을 요청하면, 서버는 대기열에 추가한다.
// => 서버소켓에서 연결을 승인하면 클라이언트와 통신할 수 있는 소켓을 리턴한다.
// => 대기열에 기다리고 있는 클라이언트가 없으면 접속할 때까지 기다린다.
Socket socket = ss.accept();
keyboard.nextLine();
socket.close();
ss.close();
keyboard.close();
byte 배열
try (Socket socket = new Socket("localhost", 8888);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) {
// 서버에 데이터를 보내기 전에 잠깐 멈춤!
keyScan.nextLine();
// 서버에 보낼 바이트 배열을 준비한다. => 0 ~ 99 의 값을 가진 배열이다.
byte[] bytes = new byte[100];
for (int i = 0; i < 100; i++) {
bytes[i] = (byte) i;
}
// 서버에 바이트 배열을 전송한다.
out.write(bytes);
// out.flush();
// byte stream 을 사용할 때는 바로 출력한다. 따라서 flush()를 호출하지 않아도 된다.
클라이언트앱은 들어온 순서대로 읽어오면 됨.
printStream (출력 스트림임) // 비추
한줄 읽기 , out.flush() 필요 없음
try (Socket socket = serverSocket.accept();
Scanner in = new Scanner(socket.getInputStream());
PrintStream out = new PrintStream(socket.getOutputStream())) { ) // 비추
byte stream + buffer
데이터를 보내기 위해 반드시 flush()를 호출해야 한다.
flush 전에는 데이터가 버퍼에 저장되고, flush 후에 서버에 전달됨.
bufferedOutputStream 주의 !!
프린터 스트림은 바이트 스트림
프린터 라이터는 캐릭터스트림
character stream(print writer + println) - byte stream 반드시 flush 호출해야함
character stream 클래스의 경우
// 출력 데이터를 내부 버퍼에 보관하고 있다가 버퍼가 꽉차거나 flush()를 호출할 때 출력을 수행한다.
// - BufferedWriter를 붙이지 않아도 이렇게 동작하기 때문에 주의하라!
character stream
character stream - 문자 단위로 출력하기
0x 7a5f0041 | write() | FileOutputStream | 00(1byte) 41(1byte) => byte stream은 변환없이 1byte를 그대로 출력함 |
0x 7a6bac00 JVM은 문자 데이터를 다룰 때 UCS2(UTF16BE, 2바이트) 유니코드를 사용 |
write() | FileWriter | AC00(2byte) - JVM Argument file.encoding =utf-8 / Ms 949 / EUC-KR 지정된 방식으로 변환됨 eab080 => character stream은 UCS2를 UTF-8로 변환하여 출력 |
// UCS2에서 한글 '가'는 ac00이다. out.write(0x7a6bac00); // - 앞의 2바이트(7a6b)는 버린다. // - 뒤의 2바이트(ac00)은 UTF-8(eab080) 코드 값으로 변환되어 파일에 출력된다. // UCS2에서 영어 A는 0041이다. // 출력하면, UTF-8 코드 값(41)이 파일에 출력된다. out.write(0x7a5f0041); // // - 앞의 2바이트(7a5f)는 버린다. // - 뒤의 2바이트(0041)는 UTF-8(41) 코드 값으로 변환되어 파일에 출력된다. |
file encoding 옵션을 설정하지 않으면 어떤os 에 출력하느냐에 따라 출력 코드가 달라짐
java-w exe : 윈도우에서 쓰는 JVM
텍스트 파일이 아닌 바이너리파일을 reader writer로 읽으면 큰일남
reader writer는 출력할때 원본데이터 안 따지고 UCS2 방식으로 앞에꺼 짤라버리고 뒤에 2바이트만 읽어버려서 ;
character stream - 문자 단위로 출력하기
JVM의 문자열을 파일로 출력할 때
// FileOutputStream 과 같은 바이트 스트림 클래스를 사용하면 문자집합을 지정해야 하는 번거로움이 있었다. // => 이런 번거로움을 해결하기 위해 만든 스트림 클래스가 있으니, 문자 스트림 클래스이다. // => Reader/Writer 계열의 클래스이다. // 1) 문자 단위로 출력할 도구 준비 FileWriter out = new FileWriter("temp/test2.txt"); // 2) 문자 출력하기 // - JVM은 문자 데이터를 다룰 때 UCS2(UTF16BE, 2바이트) 유니코드를 사용한다. // - character stream 클래스 FileWriter는 문자 데이터를 출력할 때 // UCS2 코드를 JVM 환경변수 file.encoding 에 설정된 character set 코드로 변환하여 출력한다. // - JVM을 실행할 때 -Dfile.encoding=문자집합 옵션으로 기본 문자 집합을 설정한다. // 만약 file.encoding 옵션을 설정하지 않으면 OS의 기본 문자집합으로 자동 설정된다. // // Linux, macOS 의 기본 character set => UTF-8 // Windows 의 기본 character set => MS-949 // // - OS에 상관없이 동일한 character set으로 출력하고 싶다면 // JVM을 실행할 때 file.encoding 프로퍼티에 character set 이름을 지정하라. // // - JVM을 실행할 때 출력 데이터의 문자 코드표를 지정하는 방법 // java -Dfile.encoding=문자코드표 -cp 클래스경로 클래스명 // 예) java -Dfile.encoding=UTF-8 -cp bin/main com.eomcs.io.ex03.Exam0110 // // - 단, character set을 지정할 때는 해당 OS에서 사용가능한 문자표이어야 한다. // MS Windows에서는 ms949 문자표를 사용할 수 있지만, // 리눅스나 macOS에서는 국제 표준이 아니기 때문에 ms949 문자표를 사용할 수 없다. // // [결론] // - OS에 영향 받지 않으려면, // JVM을 실행할 때 반드시 file.encoding JVM 환경 변수를 설정하라. // - 문자집합은 UTF-8을 사용하라. // - 국제 표준이다. // - linux, macOS의 기본 문자 집합이다. // // 현재 JVM 환경 변수 'file.encoding' 값 알아내기 System.out.printf("file.encoding=%s\n", System.getProperty("file.encoding")); out.close(); } } |
character stream - 문자 단위로 읽기
character stream - 문자 단위로 읽기
// 1) 파일의 데이터를 읽는 일을 하는 객체를 준비한다.
FileReader in = new FileReader("sample/utf8.txt"); // 41 42 ea b0 81 ea b0 81
// 2) JVM 환경 변수 'file.encoding'에 설정된 문자코드표에 따라
// 바이트를 읽어서 UCS2로 바꾼 후에 리턴한다.
// file.encoding이 UTF-8로 되어 있다면,
// => 영어는 1바이트를 읽어서 2바이트 UCS2로 변환한다.
int ch1 = in.read(); // 41 => 0041('A')
int ch2 = in.read(); // 42 => 0042('B')
// => 한글은 3바이트를 읽어서 2바이트 UCS2로 변환한다.
int ch3 = in.read(); // ea b0 80 => ac00('가')
int ch4 = in.read(); // ea b0 81 => ac01('각')
// 3) 읽기 도구를 닫는다.
in.close();
utf-8말고 다른 방식으로 읽고 싶으면 ,,! 팩토리메서드 선언
=> 출력 스트림 객체를 생성할 때 문자 집합을 지정하면 UCS2 문자열을 해당 문자집합으로 인코딩 한다.
Charset charset = Charset.forName("EUC-KR"); // 팩토리 메서드
FileWriter out = new FileWriter("temp/test2.txt", charset);
□ System.out.println(Charset.isSupported("EUC-KR")); // EUC-KR 제공하는지 확인
Character Stream - 문자 배열 출력하기
FileWriter out = new FileWriter("temp/test2.txt");
char[] chars = new char[] {'A', 'B', 'C', '0', '1', '2', '가', '각', '간', '똘', '똥'};
// FileOutputStream 은 byte[] 을 출력하지만,
// FileWriter 는 char[] 을 출력한다.
out.write(chars); // 문자 배열 전체를 출력한다.
// 당연히 UCS2를 JVM 환경 변수 'file.encoding'에 설정된 문자 코드표에 따라 변환하여 출력한다.
// JVM이 입출력 문자 코드표로 UTF-8을 사용한다면
// 영어는 1바이트로 변환되어 출력될 것이고,
// 한글은 3바이트로 변환되어 출력될 것이다.
// JVM(UCS2) File(UTF-8)
// 00 41 ==> 41
// 00 42 ==> 42
// 00 43 ==> 43
// 00 30 ==> 30
// 00 31 ==> 31
// 00 32 ==> 32
// ac 00 ==> ea b0 80
// ac 01 ==> ea b0 81
// ac 04 ==> ea b0 84
// b6 18 ==> eb 98 98
// b6 25 ==> eb 98 a5
out.close();
8로 내보내고
16으로 읽음
Character Stream - 문자 배열 읽기
public static void main(String[] args) throws Exception {
FileReader in = new FileReader("temp/test2.txt");
// UCS2 문자 코드 값을 저장할 배열을 준비한다.
// => 이렇게 임시 데이터를 저장하기 위해 만든 바이트 배열을 보통 "버퍼(buffer)"라 한다.
char[] buf = new char[100];
// read(버퍼의주소)
// => 버퍼가 꽉 찰 때까지 읽는다.
// => 물론 버퍼 크기보다 파일의 데이터가 적으면 파일을 모두 읽어 버퍼에 저장한다.
// => 리턴 값은 읽은 문자의 개수이다. 바이트의 개수가 아니다!!!!!
// FileInputStream.read()의 리턴 값은 읽은 바이트의 개수였다.
// => 파일을 읽을 때 JVM 환경 변수 'file.encoding'에 설정된 문자코드표에 따라 바이트를 읽는다.
// 그리고 2바이트 UCS2 코드 값으로 변환하여 리턴한다.
// => JVM의 문자코드표가 UTF-8이라면,
// 파일을 읽을 때, 영어나 숫자, 특수기호는 1바이트를 읽어 UCS2으로 변환할 것이고
// 한글은 3바이트를 읽어 UCS2으로 변환할 것이다.
int count = in.read(buf);
// File(UTF-8) JVM(UCS2)
// 41 ==> 00 41
// 42 ==> 00 42
// 43 ==> 00 43
// 30 ==> 00 30
// 31 ==> 00 31
// 32 ==> 00 32
// ea b0 80 ==> ac 00
// ea b0 81 ==> ac 01
// ea b0 84 ==> ac 04
// eb 98 98 ==> b6 18
// eb 98 a5 ==> b6 25
in.close();
Character Stream - 문자 배열의 특정 부분을 출력하기
FileWriter out = new FileWriter("temp/test2.txt");
char[] chars = new char[] {'A','B','C','가','각','간','똘','똥'};
out.write(chars, 2, 3); // 2번 문자부터 3 개의 문자를 출력한다.
out.close();
Character Stream - 읽은 데이터를 문자 배열의 특정 위치에 저장하기
FileReader in = new FileReader("temp/test2.txt");
char[] buf = new char[100];
// read(버퍼의주소, 저장할위치, 읽을바이트개수)
// => 리턴 값은 실제 읽은 문자의 개수이다.
int count = in.read(buf, 10, 40); // 40개의 문자를 읽어 10번 방부터 저장한다.
in.close();
Character Stream - 텍스트 읽기
FileReader in = new FileReader("temp/test2.txt");
// FileReader 객체가 읽을 데이터를 저장할 메모리를 준비한다.
CharBuffer charBuf = CharBuffer.allocate(100);
// 읽은 데이터를 CharBuffer 에 저장한다.
int count = in.read(charBuf);
in.close();
// 버퍼의 데이터를 꺼내기 전에 읽은 위치를 0으로 초기화시킨다.
// - read() 메서드가 파일에서 데이터를 읽어서 버퍼에 채울 때 마다 커서의 위치는 다음으로 이동한다.
// - 버퍼의 데이터를 읽으려면 커서의 위치를 처음으로 되돌려야 한다.(flip)
// - flip() 메서드를 호출하여 커서를 처음으로 옮긴다. 그런 후에 버퍼의 텍스를 읽어야 한다.
charBuf.flip();
System.out.printf("[%s]\n", charBuf.toString());
Character Stream - 텍스트 읽기 II - 한 줄씩 읽을 때 사용
FileReader in = new FileReader("temp/test2.txt");
// 데코레이터를 붙인다.
// => 버퍼 기능 + 한 줄 읽기 기능
BufferedReader in2 = new BufferedReader(in);
System.out.println(in2.readLine());
in.close();
'[네이버클라우드] 클라우드 기반의 개발자 과정 7기 > 웹프로그래밍' 카테고리의 다른 글
[NC7기-56일차(7월13일)] - 웹프로그래밍 37일차 (0) | 2023.07.13 |
---|---|
[NC7기-55일차(7월12일)] - 웹프로그래밍 36일차 (0) | 2023.07.12 |
[NC7기-53일차(7월10일)] - 웹프로그래밍 34일차 (0) | 2023.07.10 |
[NC7기-52일차(7월7일)] - 웹프로그래밍 33일차 (0) | 2023.07.07 |
[NC7기-51일차(7월6일)] - 웹프로그래밍 32일차 (0) | 2023.07.06 |