기말 점검 및 실습 문제 풀이
요약 문제 풀이
1. String 클래스와 StringBuilder, StringBuffer 클래스의 차이를 정리해 주세요.
String, StringBuffer, StringBuilder 모두 CharSequence를 구현한 문자열을 다루는 클래스다.
String 객체는 immutable 하기에 한 번 만들면 값을 바꿀 수 없다.
기존 문자열에 String 문자열을 더하면 기존 객체는 버려지고 새로운 객체가 생성된다.
StringBuilder와 StringBuffer는 이러한 String의 단점을 보완하기 위해서 문자열을 더하더라도 새로운 객체가 생성되지 않는다.
StringBuffer는 Thread Safe 하기 때문에 여러 쓰레드에서 동시에 접근하는 경우 사용하기에 적합하다.
StringBuilder는 Thread Unsafe 하지만 StringBuffer 보다 빠르다.
2. String 클래스의 indexOf(), substring(), split(), getBytes() 메소드가 어떤 용도로 사용되는지 정리해 주세요.
indexOf() : 해당 객체의 특정 문자열, char가 있는 위치를 리턴하며 존재하지 않는 경우 -1 을 리턴한다.
substring() : 매개 변수로 시작 위치, 끝 위치를 받아서 문자열의 일부를 잘라낸다.
split() : 문자열을 여러 개의 String 배열로 나누며 주로 정규 표현식을 사용해서 문자열을 나누는 경우에 사용한다.
getBytes() : 문자열을 byte 배열로 나타내기 위해서 사용한다.
3. System 클래스의 주요 용도를 정리해 주세요.
System 클래스는 시스템 / 장비에 대한 정보를 확인하기 위해 사용되는 클래스다.
JVM에서 사용하는 속성 값인 Property 는 값을 추가하고 수정할 수 있다.
시스템, 장비에서 사용하는 값인 Environment 는 값을 수정, 추가하지 못하고 읽기만 가능하다.
4. System 클래스에서 여러분들이 절대로 사용해서는 안되는 메소드들은 무엇이고, 그 이유를 정리해 주세요
절대 사용해서는 안되는 메소드는 "GC 수행" 관련 메소드와 "JVM 종료"와 관련된 메소드들은 사용해서는 안된다.
GC 수행과 관련된 메소드로는 가비지 컬렉터를 실행시키는 static void gc(),
GC에 대해 기다리는 모든 객체에 대해서 finalize()를 수행하는 static void runFinalization() 가 있다.
자바에서 메모리 처리를 개발자가 별도로 하지 않고 JVM에 의해서 알아서 GC를 수행하기에 GC를 명시적으로 처리하는 경우 효율적이지 않다.
JVM 종료와 관련된 메소드로는 수행 중인 JVM을 멈추는 static void exit(int status) 가 있다.
애플리케이션의 JVM이 종료되어 장애로 이어지게 된다.
5. 제네릭을 사용하는 가장 큰 이유는 무엇인지 정리해 주세요.
제네릭은 일반적인 형 변환(casting) 시 컴파일 시점에서 에러가 발생할 수 있는 것을 사전에 방지하기 위해서 사용한다.
6. Set의 용도와 주요 클래스를 정리해 주세요.
Set은 List와 유사하지만 순서와 상관 없이 사용하기에 중복을 허용하지 않는다는 특징이 있는 자료구조다.
그렇기에 유일한 값을 찾을 때 유용하게 사용할 수 있다.
주요 클래스로는 HashSet, TreeSet, LinkedHashSet이 있다.
HashSet은 순서가 전혀 필요 없는 데이터를 HashTable에 저장하며 정렬 작업을 필요로 하지 않기에 가장 성능이 좋다.
TreeSet은 저장된 값에 따라 정렬하는 Set이다. red-black 이라는 tree 타입으로 저장된다.
LinkedHashSet은 연결된 목록 타입으로 구현된 HashTable에 데이터 저장하며, 저장된 순서에 따라 값이 정렬되며 이 중에 성능이 가장 나쁘다.
7. Set의 데이터를 하나씩 꺼내는 방법을 정리해 주세요.
for 루프를 사용해 값을 꺼내는 방식
Set의 메소드인 Iterator<E> iterator()를 통해 얻은 iterator 객체의 next() 메소드를 호출하는 방식
8. ArrayList와 같은 List와 배열의 차이가 무엇인지 정리해 주세요.
배열은 크기가 고정적이지만 List는 크기가 가변적이기에 공간이 꽉차게 되면 크기를 확장하는 작업이 내부에서 자동으로 수행된다.
9. Queue의 용도는 무엇이며, LinkedList의 특징이 무엇인지 정리해 주세요.
Queue는 FIFO 구조로 선입 선출을 가능하게 한다.
그렇기에 데이터가 들어온 순서대로 처리할 때 용이하기에 웹 서버와 같이 여러 쓰레드에서 들어오는 작업을 순차적으로 처리할 때 많이 사용한다.
LinkedList는 List, Queue 인터페이스를 모두 구현하고 있다.
각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료 구조이다.
데이터를 담고 있는 노드들이 연결되어 있는데, 노드의 포인터가 다음이나 이전의 노드와의 연결을 담당한다.
그렇기 때문에 중간에 있는 데이터를 삭제하더라도 삭제된 노드의 이전 노드와 다음 노드만 연결하면 되기에 index가 변하지 않아 메모리 공간 효율이 좋다.
생성자를 통해 데이터의 초기 크기를 지정할 수 없다.
10. Map의 용도와 주요 클래스를 정리해 주세요.
Map은 키-값의 형태로 데이터를 저장하는 구조이기에 key-value로 데이터를 관리해야할 때 사용한다.
key 는 중복되지 않고 유일해야하며 키가 유일하다면 값은 중복되어도 상관 없다.
주요 클래스로는 HashTable, HashMap, TreeMap, Properties 가 있다.
HashTable은 다른 Map 클래스들과 달리 null 값을 저장하지 못하며, Thread Safe 하다는 특징 등이 있다.
HashMap은 키-값에 null이 포함될 수 있으며 Thread Unsafe 하며 hashCode() 값에 따라 버킷에 저장된다.
TreeMap은 데이터를 저장하면서 키를 숫자 > 대문자 > 소문자 > 한글 순서로 정렬한다.
Properties는 시스템 속성을 제공하는 클래스다.
11. Arrays 클래스의 주요 용도는 무엇인지 정리해 주세요.
배열을 다루기 위한 sort()와 같은 다양한 메소드를 포함하고 있는 클래스로 Map의 키를 정렬하기 위해서 사용하는 경우도 있지만 불 필요한 객체가 생긴다는 단점이 있다.
12. StringTokenizer 클래스의 용도는 무엇인지 정리해 주세요.
문자열을 여러 개의 String 배열로 나누는 방식으로 정규 표현식이 아닌 일반적으로 특정 문자열을 나눌 때 사용하기 편하다.
13. Thread 클래스를 확장한 클래스를 구현할 때 항상 만들어야 하는 메소드는 무엇인가요? 접근 제어자, 리턴 타입, 메소드 이름을 적어주세요.
Thread 클래스 구현 시 항상 구현해야하는 메소드는 public void run()이다.
public void run() 메소드는 쓰레드 실행 시 수행되는 문장들이다.
14. Thread 클래스와 Runnable 인터페이스를 구현하여 만든 쓰레드를 실행하려면 각각 어떻게 해야하는지 정리해 주세요.
Runnable 인터페이스를 구현하여 만든 쓰레드를 실행하려면 쓰레드 생성자에 객체를 매개 변수로 넣고 start() 메소드를 호출해야 한다.
Thread 클래스를 상속받아 만든 쓰레드를 실행하기 위해서는 바로 start() 메소드를 호출해서 실행할 수 있다.
15. Synchronized에 대해서 정리해 주세요.
쓰레드와 관련된 자바 예약어 중 하나로 여러 쓰레드가 동일한 한 객체에 선언된 메소드에 접근하여 데이터를 처리하려고 하면 동시에 연산을 수행해 값이 꼬이는 경우가 발생할 수 있는데 이때 어떤 클래스나 메소드가 Thread Safe 하기 위해서 사용해야 한다.
메소드 자체를 선언 시 synchronized로 선언하는 방식은 한 쓰레드가 메소드를 종료할 때까지 기다릴 필요가 없는 문장들 조차도 대기 시간이 발생하기에 성능적인 문제가 발생할 수 있다.
메소드 내 특정 문장만 synchronized 블록으로 감싸는 방법은 synchronized (객체) { } 형태로 별도의 객체를 선언해 사용함으로 객체가 문지기 역할을 할 수 있도록 한다.
16. InputStream 클래스의 용도는 무엇이고, Reader 클래스와 어떤 점이 다른지 정리해 주세요.
InputStream 클래스는 데이터를 읽기 위해서 사용한다.
Reader 클래스도 마찬가지로 데이터를 읽기 위해서 사용하지만 char 기반 데이터를 대상으로 사용한다.
17. 클래스 선언 시 Serializable을 구현하는 이유는 무엇인지 정리해 주세요.
Serializable을 구현 시 클래스가 파일에 읽거나 쓸 수 있도록 하며 다른 서버로 보내거나 받을 수 있도록 한다. Serializable 인터페이스 구현 시 JVM에서 해당 객체를 저장하거나 다른 서버로 전송할 수 있도록 직렬화, 역직렬화 해준다.
18. transient로 선언한 변수는 다른 변수와 어떻게 다른지 정리해 주세요.
Serializable을 구현한 객체에서 transient로 선언한 변수는 객체 내 전송 / 저장하지 않도록 지정된다.
transient 변수를 전송 / 저장 시 그 값이 포함되지 않아 기본 값으로 전송 / 저장된다.
19. TCP 통신을 처리하기 위해서 사용하는 클래스는 무엇인지 정리해 주세요.
TCP 통신을 처리하기 위해서 사용하는 클래스는 Socket 이다.
데이터를 전달하고 받기 위해서 사용한다.
ServerSocket 클래스는 데이터를 수신하기 위해서 사용하는 클래스로 클라이언트에서 데이터를 전송 시 Socket 객체로 받아서 처리할 수 있게 해준다.
20. UDP 통신을 처리하기 위해서 사용하는 클래스는 무엇인지 정리해 주세요.
UDP 통신을 처리하기 위해서 사용하는 클래스는 Datagram 클래스다.
DatagramPacket 클래스는 데이터를 전달하고 받기 위해서 사용한다.
DatagramSocket 클래스는 데이터를 수신하기 위해서 사용하는 클래스로 클라이언트에서 데이터를 전송 시 DatagramPacket 객체로 받아서 처리할 수 있게 해준다.
실습 문제 풀이
IO, Network, Thread 등 이제까지 배운 것을 바탕으로 간단한 웹 서버 제작
자세한 내용은 책 P.276 ~ 281 참고
- 웹 서버를 시작을 위한 메인 클래스 SimpleWebServer
- TCP 통신을 위한 ServerSocket 객체 생성
- 사용자 요청 연결 시 Socket 객체 반환
- 동시에 접근할 수 있도록 Thread 클래스의 run() 메소드 호출
package com.basicjava.server;
import com.basicjava.server.handler.RequestHandler;
import java.net.ServerSocket;
import java.net.Socket;
public class SimpleWebServer {
public static void main(String[] args) {
SimpleWebServer server = new SimpleWebServer();
int port = 9000;
server.run(port);
}
public void run(int port) {
ServerSocket server = null;
Socket socket = null;
try {
server = new ServerSocket(port);
while (true) {
socket = server.accept();
RequestHandler requestHandler = new RequestHandler(socket);
requestHandler.start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (server != null) {
try {
server.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
- 동시에 들어오는 요청을 처리하기 위해 쓰레드 클래스를 확장한 클래스 RequestHandler
- 쓰레드 클래스의 메소드인 run() 메소드 내에 사용자 요청을 분석하고 응답하는 클래스의 메소드를 호출함으로 동시에 여러 사용자의 요청을 분석 및 처리 응답할 수 있다.
package com.basicjava.server.handler;
import com.basicjava.server.dto.RequestDTO;
import com.basicjava.server.manager.RequestManager;
import com.basicjava.server.manager.ResponseManager;
import java.net.Socket;
public class RequestHandler extends Thread { // 동시에 들어온 요청을 처리하기 위한 쓰레드 클래스
private Socket socket;
public RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
RequestManager requestManager = new RequestManager(socket);
RequestDTO dto = requestManager.readRequest();
ResponseManager responseManager = new ResponseManager(socket, dto);
responseManager.writeResponse();
}
}
- 사용자의 요청을 읽은 데이터를 보관하기 위한 클래스 RequestDTO
package com.basicjava.server.dto;
public class RequestDTO { //요청을 읽은 데이터를 잠시 보관하기 위한 DTO 클래스
private String requestMethod = "GET";
private String uri = "/";
private String httpVersion = "HTTP/1.1";
public String getRequestMethod() {
return requestMethod;
}
public void setRequestMethod(String requestMethod) {
this.requestMethod = requestMethod;
}
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getHttpVersion() {
return httpVersion;
}
public void setHttpVersion(String httpVersion) {
this.httpVersion = httpVersion;
}
}
- 사용자의 요청을 읽어 분석하기 위한 클래스 RequestManager
- Socket 객체의 getInputStream() 메소드로 얻은 Stream을 통해 byte 형태의 데이터를 받아온다.
- byte 형태의 데이터를 String 문자열 형태로 변환한다.
- String 문자열 중 DTO 클래스에 저장하기에 필요한 데이터만 파싱하여 저장한다.
package com.basicjava.server.manager;
import com.basicjava.server.dto.RequestDTO;
import java.io.InputStream;
import java.net.Socket;
import java.util.StringTokenizer;
public class RequestManager {
private final Socket socket;
private final int BUFFER_SIZE = 2048;
public RequestManager(Socket socket) {
this.socket = socket;
}
// 사용자의 요청을 읽어 분석하는 메소드
public RequestDTO readRequest() {
RequestDTO dto = new RequestDTO();
try {
InputStream request = socket.getInputStream();
byte[] receivedBytes = new byte[BUFFER_SIZE];
request.read(receivedBytes);
String requestData = new String(receivedBytes);
System.out.println("RequestData =\n" + requestData);
System.out.println("---------");
// RequestDTO 객체에 필요한 정보를 얻기 위해 요청 헤더 정보에서 문자열 파싱 작업
// DTO 객체에 필요한 정보는 uri, httpVersion
int endIndex = requestData.indexOf("Host");
String substring = requestData.substring(0, endIndex);
StringTokenizer st = new StringTokenizer(substring);
// NoSuchElementException 예외 발생에 대비
if (st.countTokens() == 3) {
dto.setRequestMethod(st.nextToken());
dto.setUri(st.nextToken());
dto.setHttpVersion(st.nextToken());
}
} catch (Exception e) {
e.printStackTrace();
}
return dto;
}
}
- 사용자에게 전달할 응답의 내용을 만들기 위한 클래스 ResponseManager\
- DTO 클래스에 저장된 데이터의 헤더 정보를 기반으로 사용자에게 응답을 한다.
package com.basicjava.server.manager;
import com.basicjava.server.dto.RequestDTO;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Date;
public class ResponseManager {
private final Socket socket;
private final RequestDTO dto;
public ResponseManager(Socket socket, RequestDTO dto) {
this.socket = socket;
this.dto = dto;
}
// 사용자에게 전달할 응답의 내용을 만들기 위한 메소드
public void writeResponse() {
String uri = dto.getUri();
String httpVersion = dto.getHttpVersion();
try {
PrintStream response = new PrintStream(socket.getOutputStream());
response.println(httpVersion + " 200 OK");
response.println("Content-type: text/html");
response.println();
if (uri.equals("/today")) {
response.println(new Date());
} else {
response.println("It is working");
}
response.flush();
response.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}