-
Notifications
You must be signed in to change notification settings - Fork 102
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
Changes from all commits
3377aec
93d75ff
aeb4fce
043cdc7
bfe9b65
151fffd
a5966b1
aa69360
45637b5
2bb7c36
063d138
735251f
1b14a49
638df1c
fc9cfd8
7747125
26b0673
20dd100
ef67b49
0b84d73
08efd59
ba33952
992f82c
9a27b45
787759a
0f49a5a
34b519d
ab74c96
ddddde8
8cbddd8
548b1ce
e7af7e8
b4f1a46
26b157d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()); | ||
} | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 삼항 연산자로 중복되는 코드를 |
||
map.put(key , value); | ||
} | ||
return map; | ||
} | ||
|
||
public String getContent(Matcher matcher){ | ||
if(!matcher.find()) | ||
return ""; | ||
return matcher.group().replace("\"" , ""); | ||
} | ||
} |
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; | ||
} |
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); | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에 있는 |
||
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(); | ||
} | ||
} |
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); | ||
} | ||
|
||
} |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 검증 코드도 추가해 주세요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 테스트 코드를 처음 작성해봐서 어떻게 해야할까... 고민하다가 종립님이 말씀하신 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 너무 잘 하셨어요. 이건 몇 주 후에 구체적으로 알려드리려 했는데 스스로 먼저 시도하셨네요. 이런 방식의 테스트 코드를 작성할 때 핵심 아이디어는 바로 우리가 어떤 함수/메소드를 만나건 간에 "뭐가 들어가서 뭐가 리턴되는지만 구체적으로 알 수 있다면" 코드를 아주 빠르게 이해할 수 있다는 것입니다. 이건 어떤 언어로 작업하건 간에 거의 공통이죠. 심지어 컴퓨터나 프로그래밍이 아닌 경우에도 통합니다. 은행 창구에 가서 새로운 계좌를 만들겠다고 하면 어떤 결과가 리턴될지 우리가 잘 알고 있는 것과 비슷합니다. 그래서 이런 쪽에 집중하는 방식에서는 입력과 리턴값에 대한 설명을 특히 중요하게 여깁니다. 좀 이른 감이 있긴 하지만 이 문서도 한번 읽어보세요. 제가 작성한 문서입니다. |
||
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"); | ||
} | ||
|
||
|
||
} |
There was a problem hiding this comment.
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
에 작성해 보았습니다There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다. 빠른 구현을 목표로 한 작업이라는 것이 눈에 보입니다. 이 정도로도 과제의 목표는 잘 달성하신 것 같습니다. 만약 여기에서 한 단계 더 어려운 것을 해보고 싶다면 (직접 코딩을 하지 않고) task에 프로퍼티가 하나 더 추가되었을 때 / 100개가 더 추가되었을 때 어떻게 대응할지에 대해 상상해 보는 연습을 해보세요.