알고리즘 문제에서 큐에대한 개념이 나왔는데, 조건문을 사용하면 쉽게 해결할 수 있는 문제였습니다.
그런데 단순히 정답을 위해서 풀면 나중에 큐(Queue)관련 문제가 나올 때 제대로 풀 지 못할것 같았습니다.
그래서 좀 더 깊게 파보고 큐(Queue)를 직접 구현해보기로 했습니다.
자료구조를 공부하면서 정말 많은것을 배우고 있습니다.
자바의 내부함수 구조를 직접 뜯어보고 어떻게 구현되어있는지 확인하는 계기
단순히 자바에서 잘 구현되어있는 List를 계속 사용만 한다면 추상화되어있는 개념만 가질 수 있었을겁니다. 그런데 '한번 구현해보면 어떨까?' 라는 생각으로 인해 자료구조에 더 친해지는 계기가 되었습니다.
❓ 큐(Queue)란
큐는 한국어로 "대기줄" 을 뜻합니다. => 그래서 큐를 설명하는 강의를 보면 줄서서 기다리는 사람들을 예시로 많이 설명하는 것을 볼 수 있습니다.
먼저 집어 넣은 데이터가 먼저 나오는 FIFO(First In First Out)구조로 저장하는 형식을 말합니다.
일렬로 늘어선 사람들로 이루어진 줄을 말하기도 하며, 먼너 주을 선 사람이 먼저 나가는 것과 같습니다.
💻 구현한 소스코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
StringTokenizer st;
StringBuilder sb = new StringBuilder();
customQueue<Integer> customQueue = new customQueue<>();
for (int i = 0; i < n; i++) {
st = new StringTokenizer(br.readLine());
String command = st.nextToken();
switch (command) {
case "push":
customQueue.push(Integer.parseInt(st.nextToken()));
break;
case "pop":
sb.append(customQueue.pop()).append("\n");
break;
case "size":
sb.append(customQueue.size()).append("\n");
break;
case "empty":
sb.append(customQueue.empty()).append("\n");
break;
case "front":
sb.append(customQueue.front()).append("\n");
break;
case "back":
sb.append(customQueue.back()).append("\n");
break;
}
}
System.out.println(sb);
}
}
class customQueue<T> {
transient Node<T> first;
transient Node<T> last;
int size = 0;
public void push(T e) {
final Node<T> l = last;
final Node<T> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
}
public String pop() {
if (size ==0) return "-1";
final Node<T> f = first;
final T element = f.item;
final Node<T> next = f.next;
f.item = null;
f.next = null;
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
return element.toString();
}
public int size() {
return size;
}
public int empty() {
if (size == 0) return 1;
else return 0;
}
public T front() {
if (first == null) return (T) "-1";
return first.item;
}
public T back() {
if (last == null) return (T) "-1";
return last.item;
}
private class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
/user/create?userId=gaemi&password=1234&name=Gaemi&email=gaemi@daum.net 사용자가 입력한 값은 위와 같이 서버에 전달된다.
uri에 /create가 포함되어있는지 확인한다. => 포함되어 있으면 split() 메소드를 사용해서 "?" 를 기준으로 나눕니다. => 나눈 값에서 유저 정보가 담긴 queryString 값만 String 자료형인 userInfoQueryString 변수에 저장합니다.
저장된 값을 UserService 클래스의 signupUser() 메소드를 실행시킵니다.
UserService클래스
public class UserService {
public boolean signupUser(String userInfoqueryString) {
Map<String, String> userInfo = HttpRequestUtils.parseQueryString(userInfoqueryString);
String userId = userInfo.getOrDefault("userId", "");
String password = userInfo.getOrDefault("password", "");
String name = userInfo.getOrDefault("name", "");
String email = userInfo.getOrDefault("email", "");
User user = new User(userId, password, name, email);
DataBase.addUser(user);
return true;
}
}
RequestHandler 클래스에 회원가입 로직을 추가하는 것보다는 분리하는 게 좋겠다고 생각해서 UserService를 만들어서 분리를 했습니다.
저자인 자바지기님이 미리 만들어놓으신 HttpRequestUtils 클래스의 parseQueryString() 메소드를 사용해서, 쿼리 스트링의 값을 분리 "&" 기준으로 분리를 해서 Map<String, String>을 리턴 받습니다.
리턴 받은 값을 userInfo 객체에 할당하고 유저의 정보를 각 String 변수에 할당합니다. => 여기서 유저가 값을 입력하지 않으면 빈 문자열을 집어넣도록 했습니다. => 참고로 유저 정보 중에 빈 값 관련 처리를 아직 안 했습니다.
입력받은 유저 정보를 바탕으로 User 객체를 생성합니다.
저자인 자바지기님이 만들어놓은 DataBase 클래스 addUser() 메소드를 사용해서 저장합니다.
구현하면서 느낀 점
예외 처리를 제대로 안 해놔서 조금 아쉬웠습니다. => 책에서 다음 과제가 GET 방식이 아닌 POST 방식으로 값을 받아온다. 그때는 더 견고하게 코드를 짤 예정입니다.
과제를 진행하면 할수록 코드가 조금 더러워지고 있습니다. => 리팩터링은 틈틈이 계속 진행해야겠다고 생각했습니다.
다른 사람들이 제출한 코드를 직접 보고 나의 코드를 보완해봐야겠다고 생각했습니다.
3️⃣ 4️⃣ 요구사항 3, 4
POST 방식으로 회원가입하기
http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 회원가입이 정상적으로 동작하도록 구현한다.
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(),
connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
InputStreamReader inputStreamReader = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(inputStreamReader);
ArrayList<String> httpHeaderList = new ArrayList<>();
String line;
while ( !(line = br.readLine()).equals("") ) {
httpHeaderList.add(line);
}
String firstHttpHeader = httpHeaderList.get(0);
if (firstHttpHeader == null) { return; }
String[] tokens = firstHttpHeader.split(" ");
String httpMethod = tokens[0];
uri = tokens[1];
statusCode = "200";
description = "OK";
if (httpMethod.equals("POST")) {
int contentLength = Integer.parseInt(httpHeaderList.get(3).split(" ")[1]);
String userInfoQueryString = IOUtils.readData(br, contentLength);
uri = userSignupCheck(userInfoQueryString);
statusCode = "302";
description = "Redirection";
} else if (httpMethod.equals("GET")) {
if (uri.contains("/create")) {
String userInfoQueryString = uri.split("\\?")[1];
uri = userSignupCheck(userInfoQueryString);
}
}
log.debug("connect URL : {}", uri);
Path path = Paths.get("./webapp", uri);
// ToDo : 해당 파일이 없으면 에러 페이지로 이동되도록 구현 필요
if (! Files.exists(path)) {
log.debug("Path : {} not exists", path);
return;
}
File file = path.toFile();
path.isAbsolute();
String fileExtension = FilenameUtils.getExtension(file.getName());
byte[] bytes = Files.readAllBytes(file.toPath());
DataOutputStream dos = new DataOutputStream(out);
response200Header(dos, bytes.length, fileExtension);
responseBody(dos, bytes);
} catch (IOException e) {
log.error(e.getMessage());
}
}
클라이언트로부터 받은 HttpHeader의 값을 각 라인별로 ArrayList<String>에 저장했습니다.
ArrayList에서 0번째 인덱스의 값을 " " 을 기준으로 나눠 int 배열인 tokens[] 에 할당한다.
Http Header 첫 줄에서 uri와 HttpMethod 문자열을 저장합니다.
Response로 보낼 statuscode와 description에 값을 넣습니다.
여기서 HttpMethod의 문자열이 POST인지 GET인지 확인하는 조건문을 넣어놨다. => POST이면 HttpHeader에 있는 ContentLength 값을 찾아서 int 변수에 할당한다. * Body의 값을 어떻게 가져올까 계속 고민했었는데 저자인 자바지기님이 구현한 함수를 사용하면 됐었다. IOUtils클래스의 readdata() 메소드를 사용해서 body의 값을 파싱한다.
받아온 Body의 값을 userSignupCheck() 함수에 인자로 넣고 userService클래스의 signupUser() 메소드로 DB에 저장
완료가 되면 redirect url인 index.html의 값을 받아와 uri 변수에 할당한다.
Response를 보낼 때 Http Header에 담을 status code와 description 값을 할당한다. => 여기서 redirect를 하기 위해서는 status code에 302를 넣어줘야 한다. =>위키 => Description에 Redirection 문자열을 넣어준다.
GET과 POST 메소드에도 사용하기 위해 userSignupCheck() 함수의 라인수가 줄어들었다.
구현하면서 느낀 점
리팩터링을 제대로 안 하고 구현만하고 있어서 코드가 점점 더러워지고 있다. => 다음 과제를 하기 전에 리팩터링을 하는 시간을 한번 가지자.
HTTP 책을 제대로 읽었다고 생각했지만 모르는 게 너무 많았다. => HTTP Header에 들어있는 값이 무엇이고 어떤 의미를 가지는지 알 수 있도록 책을 한번 더 읽어보자.
response200Header에 302를 처리하는 로직이 들어가 있다. => 분리를 꼭 하자. => 함수를 분리를 제대로 하지 않아서 중복되는 코드가 꽤 많아졌다. 리팩터링을 꼭 하자.
리팩터링한 소스코드
*저자인 자바지기님이 작성한 코드를 참고해서 코드가 거의 비슷합니다.
ReuqestHandler 클래스의 run() 메소드
// ... 생략 ...
InputStreamReader inputStreamReader = new InputStreamReader(in, "UTF-8");
BufferedReader br = new BufferedReader(inputStreamReader);
String requestLine = br.readLine();
if (requestLine == null) return;
log.debug("request line : {}", requestLine);
String[] tokens = requestLine.split(" ");
String uri = getDefaultUrl(tokens[1]);
int contentLength = 0;
boolean logined = false;
while (!requestLine.equals("")) {
requestLine = br.readLine();
log.debug("header : {} ", requestLine);
if (requestLine.contains("Content-Length")) {
contentLength = getContentLength(requestLine);
}
if (requestLine.contains("Cookie")) {
logined = isLogin(requestLine);
}
}
if ("/user/create".equals(uri)) {
String body = IOUtils.readData(br, contentLength);
Map<String, String> userInfo = HttpRequestUtils.parseQueryString(body);
User user = new User(userInfo.get("userId"), userInfo.get("password"),
userInfo.get("name"), userInfo.get("email"));
DataBase.addUser(user);
DataOutputStream dos = new DataOutputStream(out);
response302Header(dos);
// ... 생략 ...
클라이언트의 Request에 첫 줄만 BufferReader로 읽어온다. => 여기서 첫 줄이 null이면 run() 메서드를 종료한다.
Http Request 첫 라인에서 uri만 String 변수에 할당합니다.
while문을 사용해서 BufferdReader에 있는 request가 빈 문자열이 나올 때까지 읽습니다. => "Content-Length"가 포함된 문자열은 getContentLength() 메서드를 사용해 값을 int 자료형에 할당합니다. => "Cookie"가 포함된 문자열은 로그인 여부를 확인하는 isLogin() 메서드를 사용해서 로그인 여부를 확인합니다.
uri가 "/user/create"와 같은지 확인합니다. => 같으면 자바지기 저자가 만들어놓은 IOUtils.readData() 메서를 사용해서 body의 값을 읽어 String 변수에 할당 => 자바지기 저자가 만들어놓은 HttpRequestUtils.parseQueryString()을 사용해서 body값에서 request에 담긴 유저정보를 Map<String, Strin> 자료형에 할당합니다. => userInfo에 있는 데이터를 가지고 User 객체를 만들고 저자가 만들어놓은 임시 DataBase에 추가합니다.(인메모리)
DataOutputStream의 객체를 만들어서 response302Header() 메서드의 인자에 넣습니다.
저자가 최대한 깔끔하게 작성한 코드이지만, 구조적으로 문제가 있다는 것을 느끼게 되었습니다. => run 메소드가 점점 방대해 질 수 밖에 없는 구조 => 조건문이 계속 늘어나고 나중에 리팩터링 하거나 기능을 수정하기 너무 힘들어진다. => 하드코딩하는 부분이 늘어날 수 밖에 없다.
남이 구현한 코드를 갔다가 사용했어도 해당 코드를 직접 뜯어보고 글로 작성해보자.
5️⃣ 요구사항 5단계
로그인하기
"로그인" 메뉴를 클릭하면 http://localhost:8080/user/login.html으로 이동해 로그인할 수 있다.
로그인이 성공하면 /index.html로 이동하고, 로그인이 실패하면 /user/login_failed.html로 이동해야 한다.
앞에서 회원가입한 사용자로 로그인할 수 있어야 한다.
로그인이 성공 하면 쿠키를 활용해 로그인 상태를 유지할 수 있어야 한다.
로그인이 성공할 경우 요청 헤더의 Cookie 헤더 값이 logined=true, 로그인이 실패하면 Cookie 헤더 값이 logined=false로 전달되어야 한다.
구현한 소스코드
Requesthandler 클래스의 httpMethodCheck() 메소드
private void httpMethodCheck(
String httpMethod,
ArrayList<String> httpHeaderList,
BufferedReader br
) throws IOException {
if (httpMethod.equals("POST")) {
int contentLength = Integer.parseInt(httpHeaderList.get(3).split(" ")[1]);
String userInfoQueryString = IOUtils.readData(br, contentLength);
if (uri.contains("/user/login")) {
if (userService.userLogin(userInfoQueryString)) {
uri = "/index.html";
cookie = "Set-cookie: logined=true";
} else {
uri = "/user/login_failed.html";
cookie = "Set-cookie: logined=false";
}
} else {
uri = userService.signupUser(userInfoQueryString);
}
statusCode = "302";
description = "Redirection";
} else if (httpMethod.equals("GET")) {
statusCode = "200";
description = "OK";
if (uri.contains("/create")) {
String userInfoQueryString = uri.split("\\?")[1];
uri = userService.signupUser(userInfoQueryString);
}
}
}
클라이언트에서 request를 POST method로 서버로 보낼 때 /user/login URI이면 로그인 로직을 처리하도록 했습니다.
"Set-cookie: key=value" 문자열을 클라이언트에게 Response를 보낼 때 헤더에 담아서 보냅니다.
로그인이 정상적으로 작동하면 index.html 페이지로 redirect 되도록 했습니다.
로그인이 정상적으로 작동되지 않으면 /user/login_failed.html로 redirect 되도록 했습니다.
UserService 클래스의 userLogin() 메소드
public boolean userLogin(String userInfoQueryString) {
Map<String, String> userInfo = HttpRequestUtils.parseQueryString(userInfoQueryString);
String userId = userInfo.getOrDefault("userId", "");
User user = DataBase.findUserById(userId);
if (user == null) {
return false;
}
return true;
}
로그인을 처리하는 로직을 Service 레이어에 따로 분리해놨습니다. => 분리를 해서 RequestHandler의 코드가 좀 줄어들기는 했는데 여러가지 에로사항이 있었습니다. * 구현하면서 느낀점에 에로사항 정리
구현하면서 느낀 점
스프링에서는 Service, Controller 분리가 어렵지 않았었습니다. 이렇게 분리 처리를 하는 것에서 스프링 프레임워크에서 많은 것들을 대신 해준다는 것을 알게되었습니다.
스프링과 같은 아키텍쳐로 분리를 해보려고 하고 있습니다. => 지금은 RequestHandler의 코드가 너무 길어지고 복잡해지고 있습니다. => 아직은 코드가 많이 더러운데 하나씩 리팩토링을 하면서 바꿔갈 예정입니다.
조건문(if)을 너무 많이 사용하게 되고 가독성을 떨어트리는 것 같습니다. => 책에도 적혀있는 내용처럼 else()를 사용하지 않고 구현하는 방식으로 진행해볼 예정입니다.
아직 많이 부족한게 많지만 그래도 구현이 되어서 신기했습니다. 그리고 기존에는 이해가 안되었던 MVC의 아키텍쳐에 대해서 점점 알아가게 되었습니다.
6️⃣ 요구사항 7단계
사용자 목록 출력
접근하고 있는 사용자가 "로그인" 상태일 경우(Cookie 값이 logined=true) http://localhost:8080/user/list로 접근 했을 때 사용자 목록을 출력한다. => 만약 로그인 하지 않은 상태라면 로그인 패이지(login.html)로 이동한다.
블로그 <doohong's blog> 자바의 기본 개념 정리-4.String pool => 링크
✍ 공부하게 된 계기
'자바의신' 이라는 책을 읽고 String에 대한 챕터를 읽다가 위의 내용들이 나왔습니다.
이번에 자바를 좀 더 깊게 공부하기 위해 모르는 단어를 직접 찾아보고 정리하기로 했습니다.
단어에 대해서 찾다보면 부가적으로 배우는 것들도 많아서 꼭 해야할 것들이라고 생각합니다.
String은 immutable(불변) 타입
String 객체의 값은 변경할 수 없다.
String text = "my Text";
text = "my new Text";
위 예제와 같이 String 자료형인 text 변수를 최초로 "my Text"로 할당했다.
그 다음줄을 보면 text 변수에 "my new Text" 값을 변경 시킨 것처럼 보인다.
하지만 실제로는 메모리에 "my new Text" 를 다시 만들고 text를 다시 참조하게 된다.
자바에서 String은 constant 형태로 한 번 생성이 된다면 update 라는 개념이 사라진다.
값을 수정할려고 하면 새로운 객체를 생성하고 그 값을 재할당해야 한다.
자바에서 String을 immutable 하게 한 이유 3가지
캐싱 JVM이 String Constant Pool 이라는 영역을 만들고 문자열들을 Constant화 하여 다른 변수 혹은 객체들과 공유하게 되는데, 이 과정에서 데이터 캐싱이 일어나고 그 만큼 성능적 이득을 취할 수 있다.
동기화 데이터가 immutable 하다면 Multi-Thread 환경에서 동기화 문제가 발생하지 않기 때문에 더욱 safe한 결과를 낸다.
보안(Security) 여러 참조 변수가 같은 String 객체를 참조하고 있다고 하더라도 안전하다. String 객체를 누가 조작할 수 없기 때문이다. 그래서 원본 String 객체가 저장된 주소 자체를 넘겨도 안전하다. 전달받은 곳에서 원본 값을 직접 읽을수 있으나 조작할 수는 없기 때문이다. 예를들어 데이터베이스로 유저의 id나 비밀번호를 전송할 때 String 변수가 제 3자(해커)가 변경하지 못하도록 한다.
❓ String pool이란?
String은 재사용을 고려해서 Heap 영역에 내에 문자열 상수의 Pool 을 유지한다.
해당 Pool로 사용자가 정의한 변수가 가지고 있는 value 들을 담게 된다.
String Constant Pool 을 이용하기 위해서는 Literal을 이용해 생성해야 한다. new 연산자를 통해 String을 생성하면 Heap 영역에 존재하게 된다. 리터럴을 이용할 경우 String constant pool 이라는 영역에 존재하게 된다.
String pool의 위치
java 6 버전까지는 Perm 영역에 있었다. - Perm 영역은 고정된 사이즈이며 Runtime에 사이즈가 확장되지 않는 문제 발생
intern 되는 String 값이 커지면 OutOfMemoryException을 발생시킬 수 있었고, 그에 따라서 java7 버전은 Heap 영역으로 String pool의 위치를 변경하였다.
현재 금융권 대기업에 파견을 나가있는데 파일을 전송할 때 Base64로 변환을 해서 전송을 하고 있었습니다.
여기서 왜 굳이 Base64로 변환해서 전송을 하는지에 대해서 궁금해졌습니다.
인코딩, 디코딩 해야하는 불편함이 있는데 굳이 사용하는 이유는 뭘까?
Base64로 변환하면 용량이 30퍼센트 정도 늘어나 비효율적이지 않나?
multipart/form-data를 사용하지 않는 이유는 뭘까?
❓ 굳이 Base64로 인코딩 하는 이유는?
바이너리 데이터를 텍스트 기반의 규격으로 다룰 수 있기 때문이다.
이밎 파일등을 Web에서 필요로 할 때 Base64로 인코딩하면 UTF-8과 호환 가능한 문자열을 얻을 수 있다.
기존 ASCII 코드는 시스템간 데이터를 전달하기에 안전하지 않다. 모든 Binary 데이터가 ASCII 코드에 포함되지 않으므로 제대로 읽지 못한다. 반면 Base64는 ASCII 중 제어문자와 일부 특수문자를 제외한 53개의 안전한 출력 문자만 이용하므로 데이터 전달에 더 적합하다.
위 내용을 보면 용량이 30 ~ 33퍼센트 증가해도 안정성을 위해 Base64 인코딩을 사용한다는 것을 알 수 있게 되었다.
그런데 multipart/form-data를 사용하면 되지 않을까?
위 의문에 대해서는 정확한 의도는 모르지만 아래와 같은 이유가 있지 않을까 정의해봤습니다.
Base64로 인코딩된 텍스트에 Header와 Tailer 양식을 만들어 정보를 담아서 전송하기 위해서 사용한다.
그 정보를 바탕으로 해당 파일이 무엇인지 파악할 수 있게 해놓는 시스템이 구축되어 있기 때문이 아닐까 생각해봤습니다.
❓ Base64는 암호화이다?
Base64를 사용 하는 이유에서 암호화이기 때문이라는 얘기를 들었습니다.
여러 블로그를 확인해본결과 암호화라고 부르는 경우도 있고 단순히 인코딩이라고 하는 경우도 있었습니다.
좀 더 깊게 찾아보기 위해 스택오버플로우(Stack Overflow)와 쿼라(Quora)에 찾아보기로 했습니다.
그 결과 Base64는 바이너리 데이터를 출력 가능한 문자로 나타내기 위한 인코딩이라는 것을 알게되었습니다.
생각해보면 Base64가 나열된 문자는 디코딩을 하기 전까지는 읽기 힘들다는 것 밖에 없었습니다.
암호화가 되어 있었다고 하면 암호화 키와 복호화 키가 있을텐데 그런 것도 없었습니다.
단순히 해당 텍스트가 Base64 형식이라는 것을 알게되면 누구나 쉽게 디코딩이 가능하기 때문에 암호화는 아니라는 것입니다.
❗️ 위 내용을 작성하면서 내가 부족하다고 느낀 부분
multipart/form-data를 통해 파일을 전송하는 것을 사용했지만 해당 내용에 대해 거의 아무것도 모르고 있었다.
어떤 방식으로 파일을 전송하는게 좀 더 효율적일지에 대해 고민하지 않고 그동안 코드를 작성한 점.
Base64 인코딩에 대해 너무 두루뭉실하게 알고 있었다.
지금까지 구현한 것에 대해 궁금점을 가지지 않고 그냥 코드를 작성하고 있었다. => 깊이가 없는 상태
* 공부를 하기 위해 작성한 글입니다. 더 자세한 내용은 위 링크들을 참고하시길 바랍니다.
관계 연산자(Comparison Operators)
연관된 서로의 값들이 같은지 비교하거나, 넓게는 그 값보다 크거나 작음을 서로 비교하는 연산이다.
두 가지 타입을 비교하게 된다면 결과 값은 true, false를 반환하게 된다.
연산자
기능
a < b
a 가 b 보다 작은가?
a > b
a 가 b 보다 큰가?
a <= b
a 가 b 보다 작거나 같은가?
a >= b
a 가 b 보다 크거나 같은가?
a == b
a 와 b 가 같은가?
a != b
a 와 b 가 다른가?
'==' 연산자 (Equals Operators)
프리미티브 타입에 한해서 두 피연 사자의 값이 같으면 true, 아니면 false를 리턴한다. => 이는 프리미티브 타입에 한해서 'Value(값)'이 서로 같은지 비교를 한다. => 만약 프리미티브 타입이 아닌 '레퍼런스 타입'은 각 객체의 참조 주소를 비교하게 된다. 결국 두 개의 값이 같은지 판단하는 게 아니라, 두 개의 주소가 같은지 판단하는 것이다. => 두 개의 String 변수가 있다면 '==' 연산자를 사용하면 주소가 같은지 비교하게 된다.
'!=' 연산자 (Not Equals Operators)
'!=' 연산자는 '==' 연산의 반개 개념이다.
프리미티브 타입이 2개 있다면, 이 두 개의 값(Value)이 서로 다르다면 true를 반환하게 된다. => 반대로 같으면 false를 반환하게 됩니다.
웹 클라이언트가 요청을 보낼 때, HTTP 프로토콜의 바디 부분에 데이터를 여러 부분으로 나눠서 보내는 것입니다.
웹 클라이언트가 서버에게 파일을 업로드할 때, http 프로토콜의 바디 부분에 파일정보를 담아서 전송을 하는데, 파일을 한번에 여러개 전송을 하면 body 부분에 파일이 여러개의 부분으로 연결되어 전송됩니다. 이렇게 여러 부분으로 나뉘어서 전송되는 것은 Multipart data라고 합니다.
보통 파일을 전송할 때 사용합니다.
Multipart/form-data란?
일반적으로 폼 데이터를 전송하면 application/x-www-form-urlencoded 의 형식으로 전송됩니다. => HTTP body 에 바로 전송하고자 하는 데이터가 들어가는 형태입니다. => 예시로 name=lim&age=25 과 같은 key-value 쌍이 body에 들어가는 것입니다. 이렇게 동일한 타입의 문자 데이터를 전송하는 것은 전혀 무리가 없습니다.
key-value 형태의 문자데이터와 바이너리 형태의 파일 데이터가 함께 전송되는 것은 다릅니다. => application/x-www-form-urlencoded 타입으로는 전송이 어렵습니다. => 여기서 multipart/form-data로 지정되고 정해진 형식에 따라 메시지를 인코딩해 전송합니다. => 이를 처리하기 위해 서버는 멀티파트 메시지에 대해서 각 파트별로 분리하여 개별 파일의 정보를 얻게 됩니다. * 이미지 파일도 문자로 이뤄져 있어 HTTP request body에 담아 서버로 전송합니다.
MultipartResolver
MultipartResolver의 경우 사용자의 파일업로드 요청에 대한 처리를 하는 인터페이스입니다.
MultipartResolver의 경우에는 개발자가 별도의 Bean을 등록하지 않는다고해도 별도로 Spring에서 등록해 주지 않습니다. 하지만 Spring Boot를 사용한다면 기본 구현체가 등록이 됩니다.
springMVC에서는 파일 업로드 처리 시 DispatcherServlet에서 사용할 MultipartResolver의 Bean을 등록을 해주어야 합니다.
MultipartFile 이란?
사용자가 업로드한 File을 핸들러에서 손쉽게 다룰 수 있게 도와주는 매개변수 중 하나입니다.
매개변수를 사용하기 위해서는 MultipartResolver Bean이 등록되어 있어야 합니다.
이는 springBoot에서는 자동 등록을 지원하지만, springMVC에서 기본으로 등록해주지 않으므로 꼭 확인해야 합니다.
MultipartFile 인터페이스는 스프링에서 업로드 한 파일을 표현할 때 사용되는 인터페이스입니다.
MultipartFile 인터페이스를 이용해서 업로드한 파일의 이름, 실제 데이터, 파일 크기 등을 구할 수 있습니다.
File 클래스란?
기본적이면서도 가장 많이 사용되는 입출력 대상이기 때문에 중요하다.
자바에서는 File클래스를 통해서 파일과 디렉토리를 다룰 수 있도록 하고 있습니다. => 그래서 File인스턴스는 파일 일 수도 있고 디렉토리일 수도 있습니다.