Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1주차 과제 - ToDo REST API 만들기 #115

Merged
merged 34 commits into from
Aug 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3377aec
feat: HttpServer를 사용하여 로컬 서버 테스트
jdalma Aug 1, 2022
93d75ff
feat: Task 모델 추가
jdalma Aug 1, 2022
aeb4fce
refactor: method , path 상수 선언
jdalma Aug 1, 2022
043cdc7
feat: ToDo REST 추가
jdalma Aug 2, 2022
bfe9b65
refactor: 응답 상태 enum 적용 및 import 세분화
jdalma Aug 3, 2022
151fffd
feat: Path 클래스 추가
jdalma Aug 3, 2022
a5966b1
refactor: 응답 코드 추가
jdalma Aug 3, 2022
aa69360
refactor: setter 제거 , 필드 별 final 키워드 추가
jdalma Aug 3, 2022
45637b5
feat: Path 경로변수 커스텀 예외 추가
jdalma Aug 3, 2022
2bb7c36
fix: Jackson은 빈 생성자가 필요하다
jdalma Aug 3, 2022
063d138
feat: TaskConverter 클래스 추가
jdalma Aug 3, 2022
735251f
feat: 응답 상태 반환
jdalma Aug 3, 2022
1b14a49
feat: Path 클래스 resourceEquals 메서드 추가
jdalma Aug 4, 2022
638df1c
feat: HttpMethod enum 추가
jdalma Aug 4, 2022
fc9cfd8
feat: Path Class - fullPath 문자열 필드 추가
jdalma Aug 4, 2022
7747125
refactor: ObjectMapper 제거
jdalma Aug 4, 2022
26b0673
docs: HttpMethod JavaDoc 작성
jdalma Aug 5, 2022
20dd100
docs: HttpResponse JavaDoc 작성
jdalma Aug 5, 2022
ef67b49
fix: HttpMethod DELETED → DELETE 수정
jdalma Aug 5, 2022
0b84d73
feat: ObjectMapper 제거
jdalma Aug 5, 2022
08efd59
refactor: 임시 try-catch 제거
jdalma Aug 5, 2022
ba33952
refactor: HttpMethod equals메서드 제거
jdalma Aug 5, 2022
992f82c
feat: Path resource null 체크 추가 및 메서드 추가
jdalma Aug 5, 2022
9a27b45
docs: TODO 주석 수정
jdalma Aug 5, 2022
787759a
fix: 응답 헤더 Content-Type 설정
jdalma Aug 5, 2022
0f49a5a
test: TaskConverter jsonToMap 메서드 테스트 코드
jdalma Aug 5, 2022
34b519d
refactor: hasPathVariable메소드 추가로 인해 커스텀 예외 제거
jdalma Aug 6, 2022
ab74c96
refactor: 풀이 영상을 통한 메소드 추출 작업
jdalma Aug 6, 2022
ddddde8
test: TaskConverter의 jsonToMap메소드 테스트 코드 추가
jdalma Aug 6, 2022
8cbddd8
refactor: TaskHandler 경로에 대한 Return Early
jdalma Aug 6, 2022
548b1ce
docs: @DisplayName 표시 내용 수정
jdalma Aug 6, 2022
e7af7e8
Revert "refactor: TaskHandler 경로에 대한 Return Early"
jdalma Aug 6, 2022
b4f1a46
refactor: TaskHandler 경로에 대한 Return Early
jdalma Aug 6, 2022
26b157d
refactor: Path 클래스 필드 private → public으로 변경
jdalma Aug 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/src/main/java/com/codesoom/assignment/App.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
package com.codesoom.assignment;

import com.codesoom.assignment.handler.TaskHandler;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

public class App {
public String getGreeting() {
return "Hello World!";
}

public static void main(String[] args) {
System.out.println(new App().getGreeting());
InetSocketAddress address = new InetSocketAddress(8000);

try {
HttpServer server = HttpServer.create(address , 0);
HttpHandler handler = new TaskHandler();
server.createContext("/" , handler);
server.start();

} catch (IOException e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.codesoom.assignment.converter;

import com.codesoom.assignment.models.Task;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TaskConverter {

public static TaskConverter getInstance(){
return LazyHolder.INSTANCE;
}

private static class LazyHolder{
public static final TaskConverter INSTANCE = new TaskConverter();
}

public Task newTask(String content , Long taskId){
Map<String , String> map = jsonToMap(content);
String title = map.get("title");
return new Task(taskId , title);
}

public String convert(Task task){
return String.format("{\"id\":%d,\"title\":\"%s\"}" , task.getId() , task.getTitle());
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task → JSON으로 변경하는 메서드를 Task모델에 넣을지 TaskConverter에 넣을지 고민해 보았습니다
Task에 대한 Converter가 이미 존재하니 TaskConverter에 작성해 보았습니다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다. 빠른 구현을 목표로 한 작업이라는 것이 눈에 보입니다. 이 정도로도 과제의 목표는 잘 달성하신 것 같습니다. 만약 여기에서 한 단계 더 어려운 것을 해보고 싶다면 (직접 코딩을 하지 않고) task에 프로퍼티가 하나 더 추가되었을 때 / 100개가 더 추가되었을 때 어떻게 대응할지에 대해 상상해 보는 연습을 해보세요.

}

public String convert(Map<Long , Task> tasks){
StringBuilder sb = new StringBuilder();
StringJoiner sj = new StringJoiner(",");
sb.append("[");
for(Task task : new ArrayList<>(tasks.values())){
sj.add(convert(task));
}
sb.append(sj);
sb.append("]");
return sb.toString();
}

public Map<String , String> jsonToMap(String content){
Map<String , String> map = new HashMap<>();
String[] contents = content.split(",");
Pattern pattern = Pattern.compile("\"(.*?)\"");
for(String test : contents){
Matcher matcher = pattern.matcher(test);
String key = getContent(matcher);
String value = getContent(matcher);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

삼항 연산자로 중복되는 코드를 getContent()로 분리해 보았습니다

map.put(key , value);
}
return map;
}

public String getContent(Matcher matcher){
if(!matcher.find())
return "";
return matcher.group().replace("\"" , "");
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/codesoom/assignment/enums/HttpMethod.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.codesoom.assignment.enums;

/**
* HTTP Method를 관리한다 <a href="https://datatracker.ietf.org/doc/html/rfc7231#section-4.3">RFC7231</a>
*/
public enum HttpMethod {
johngrib marked this conversation as resolved.
Show resolved Hide resolved
GET,
POST,
PUT,
PATCH,
DELETE;
}
24 changes: 24 additions & 0 deletions app/src/main/java/com/codesoom/assignment/enums/HttpResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.codesoom.assignment.enums;

/**
* HTTP Response Status Code를 관리한다 <a href="https://datatracker.ietf.org/doc/html/rfc9110#section-15">RFC9110</a>
*
* @see HttpResponse#getCode() Response Status Code를 반환한다
*/
public enum HttpResponse {
OK(200),
CREATED(201),
BAD_REQUEST(400),
NO_CONTENT(204),
NOT_FOUND(404);

private final int code;

HttpResponse(int code) {
this.code = code;
}

public int getCode(){
return code;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.codesoom.assignment.exceptions;

public class ParameterNotFoundException extends Exception{
public ParameterNotFoundException(String msg){
super(msg);
}
}
114 changes: 114 additions & 0 deletions app/src/main/java/com/codesoom/assignment/handler/TaskHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.codesoom.assignment.handler;

import com.codesoom.assignment.converter.TaskConverter;
import com.codesoom.assignment.enums.HttpMethod;
import com.codesoom.assignment.enums.HttpResponse;
import com.codesoom.assignment.exceptions.ParameterNotFoundException;
import com.codesoom.assignment.models.Path;
import com.codesoom.assignment.models.Task;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

public class TaskHandler implements HttpHandler {

public static Map<Long , Task> tasks = new ConcurrentHashMap<>();
public static AtomicLong taskId = new AtomicLong(1L);
public TaskConverter taskConverter = TaskConverter.getInstance();

@Override
public void handle(HttpExchange exchange) throws IOException {
final HttpMethod method = HttpMethod.valueOf(exchange.getRequestMethod());
final Path path = new Path(exchange.getRequestURI().getPath());
System.out.printf("[method] : %s , [path] : %s%n", method , path.fullPath);
if(!path.hasResource() || !path.resourceEquals("tasks")) {
send(exchange, HttpResponse.OK.getCode() , "");
}

if(path.hasPathVariable()){
handleUsingTaskId(exchange , method , Long.parseLong(path.pathVariable));
}
else{
handleTask(exchange , method);
}
}

private void handleUsingTaskId(HttpExchange exchange, HttpMethod method , Long id) throws IOException {
String content = "";
Task task = tasks.get(id);
if(task == null){
send(exchange , HttpResponse.NOT_FOUND.getCode(), content);
return;
}

HttpResponse response = HttpResponse.OK;
if ("GET".equals(method.name())) {
content = taskConverter.convert(task);
} else if ("PUT".equals(method.name()) || "PATCH".equals(method.name())) {
content = updateTask(exchange, id);
} else if ("DELETE".equals(method.name())) {
Comment on lines +56 to +60
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기에 있는 GET, PUT, DELETE도 enum을 활용할 수 있겠죠.

tasks.remove(id);
response = HttpResponse.NO_CONTENT;
}

send(exchange , response.getCode(), content);
}

private void handleTask(HttpExchange exchange, HttpMethod method) throws IOException {
String content = "";
HttpResponse response = HttpResponse.OK;
if ("GET".equals(method.name())) {
content = taskConverter.convert(tasks);
} else if ("POST".equals(method.name())) {
content = createTask(exchange);
response = HttpResponse.CREATED;
}
send(exchange, response.getCode() , content);
}

private String createTask(HttpExchange exchange) throws IOException {
long taskId = getNextId();
Task task = taskConverter.newTask(getBody(exchange.getRequestBody()) , taskId);
tasks.put(taskId, task);
return taskConverter.convert(task);
}

private void send(HttpExchange exchange, int code, String content) throws IOException {
Headers responseHeaders = exchange.getResponseHeaders();
responseHeaders.set("Content-Type" , "application/json; charset=UTF-8");
exchange.sendResponseHeaders(code , content.getBytes().length);
OutputStream outputStream = exchange.getResponseBody();
outputStream.write(content.getBytes());
outputStream.flush();
outputStream.close();
}

private String updateTask(HttpExchange exchange, Long id) {
String content;
Task newTask = taskConverter.newTask(getBody(exchange.getRequestBody()) , id);
tasks.replace(id, newTask);
content = taskConverter.convert(newTask);
return content;
}

private String getBody(InputStream inputStream){
return new BufferedReader(new InputStreamReader(inputStream))
.lines()
.collect(Collectors.joining("\n"));
}

private long getNextId(){
return taskId.getAndIncrement();
}
}
28 changes: 28 additions & 0 deletions app/src/main/java/com/codesoom/assignment/models/Path.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.codesoom.assignment.models;

import com.codesoom.assignment.exceptions.ParameterNotFoundException;

public class Path {
public final String fullPath;
public final String resource;
public final String pathVariable;

public Path(String path){
this.fullPath = path;
String[] pathArr = path.split("/");
this.resource = pathArr.length >= 2 ? pathArr[1] : null;
this.pathVariable = pathArr.length >= 3 ? pathArr[2] : null;
}

public boolean hasPathVariable(){
return this.pathVariable != null;
}
public boolean hasResource(){
return this.resource != null;
}

public boolean resourceEquals(String resource){
return this.resource.equals(resource);
}

}
30 changes: 30 additions & 0 deletions app/src/main/java/com/codesoom/assignment/models/Task.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.codesoom.assignment.models;

public class Task {
private Long id;
private String title;

public Task(){}

public Task(Long id , String title){
this.id = id;
this.title = title;
}

public Long getId() {
return id;
}

public String getTitle() {
return title;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Task{");
sb.append("id=").append(id);
sb.append(", title='").append(title).append('\'');
sb.append('}');
return sb.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.codesoom.assignment.converter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Map;

public class TaskConverterJsonToMapTest {

@Test
@DisplayName(" JSON형식에 맞지 않아도 [\"]로 감싸준다면 Map에 키 또는 값으로 저장된다")
void notJson1(){
// given
String json = "[\"title1\"=\"테스트1\" ,\"title2\"=\"테스트2\"]";
TaskConverter converter = new TaskConverter();

// when
Map<String , String> map = converter.jsonToMap(json);

// then
Assertions.assertEquals(map.get("title1") , "테스트1");
Assertions.assertEquals(map.get("title2") , "테스트2");
}

@Test
@DisplayName(" [,]로 키:값 Entry가 구분되어 있지 않다면 첫 번째 키와 값만 가져온다")
void notJson2(){
// given
String json = "{\"title1\":\"테스트1\" \"title2\":\"테스트2\"}";
TaskConverter converter = new TaskConverter();

// when
Map<String , String> map = converter.jsonToMap(json);

// then
Assertions.assertEquals(map.get("title1") , "테스트1");
}

@Test
@DisplayName("JSON문자열에 키가 중복되지 않는다면 Map에 각각 저장된다")
void jsonhasMultiKey(){
// given
String json = "{\"title1\": \"테스트1\",\"title2\": \"테스트2\"}";
TaskConverter converter = new TaskConverter();

// when
Map<String , String> map = converter.jsonToMap(json);

// then
Assertions.assertEquals(map.get("title1"), "테스트1");
Assertions.assertEquals(map.get("title2"), "테스트2");
}

@Test
@DisplayName("JSON문자열에 숫자 값이 있을 때 구분하지 못 한다")
void jsonInNumeric(){
// given
String json = "{\"id\": 1,\"title\": \"테스트1\"}";
TaskConverter converter = new TaskConverter();

// when
Map<String , String> map = converter.jsonToMap(json);

// then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검증 코드도 추가해 주세요.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 코드를 처음 작성해봐서 어떻게 해야할까... 고민하다가 종립님이 말씀하신 "이런 입력이 들어가면 이런 것이 나온다"에 초점을 맞추어서 작성해 보았습니다 ㅎㅎ

ddddde8

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 너무 잘 하셨어요. 이건 몇 주 후에 구체적으로 알려드리려 했는데 스스로 먼저 시도하셨네요. 이런 방식의 테스트 코드를 작성할 때 핵심 아이디어는 바로 우리가 어떤 함수/메소드를 만나건 간에 "뭐가 들어가서 뭐가 리턴되는지만 구체적으로 알 수 있다면" 코드를 아주 빠르게 이해할 수 있다는 것입니다. 이건 어떤 언어로 작업하건 간에 거의 공통이죠. 심지어 컴퓨터나 프로그래밍이 아닌 경우에도 통합니다. 은행 창구에 가서 새로운 계좌를 만들겠다고 하면 어떤 결과가 리턴될지 우리가 잘 알고 있는 것과 비슷합니다.

그래서 이런 쪽에 집중하는 방식에서는 입력과 리턴값에 대한 설명을 특히 중요하게 여깁니다.

좀 이른 감이 있긴 하지만 이 문서도 한번 읽어보세요. 제가 작성한 문서입니다.

https://johngrib.github.io/wiki/junit5-nested/

Assertions.assertEquals(map.get("id"), "");
}

@Test
@DisplayName("JSON객체가 여러 개일 때 title은 덮어씌워진다")
void jsons(){
// given
String jsons = "[{\"id\": 1,\"title\": \"테스트1\"},{\"id\": 2,\"title\": \"테스트2\"}]";
TaskConverter converter = new TaskConverter();

// when
Map<String , String> map = converter.jsonToMap(jsons);

// then
Assertions.assertEquals(map.get("title") , "테스트2");
}


}