JSON 데이터를 라이브러리 없이 파싱해보자
우선 json이란 무엇일까요?
파싱해보는게 주제이므로 json에 대해서는 아래글을 참조해주세요.
https://notavoid.tistory.com/29
restfull 통신할때 많이 사용하는 MIME(Content-type)타입인 application/json 타입 데이터를 파싱 해볼겁니다.
파싱할때 로직이 복잡해서, 보통 jackson 라이브러리나 gson 라이브러리를 많이 씁니다.
얼마나 복잡하냐?
- 중첩된 객체, 배열, 특수 문자 처리, 타입 변환 등 복잡한 JSON 구조나 데이터 타입을 처리를 내부적으로 처리해줘야합니다.
하지만 저는 중첩된 객체, 배열 정도만 처리하는걸로해서 예제를 작성해 볼 예정입니다.
물론 jackson 라이브러리를 써서 처리하는것도 작성해 볼 겁니다.
우선 백엔드쪽에서 application/json 형태가 어떻게 들어오는지 예제의 구조부터 봐봅시다.
프론트쪽에서 ajax 통신으로 아래와 같이 던진다고 예를 들어봅시다.
{
"name": "Sumin Kim",
"age": 30,
"skills": ["Java", "Spring", "Docker"],
"address": {
"city": "seoul",
"country": "KOREA"
}
}
해당 요청 json을 파싱하는 코드를 작성해봤습니다.
public class JsonParser {
public static Map<String, Object> parseJson(String json) {
Map<String, Object> data = new HashMap<>();
json = json.trim().substring(1, json.length() - 1); // 기본 JSON 문자열 전처리
while (!json.isEmpty()) {
int colonIndex = json.indexOf(':');
if (colonIndex == -1) break;
String key = json.substring(0, colonIndex).trim().replaceAll("^\"|\"$", "");
json = json.substring(colonIndex + 1).trim();
Object value;
if (json.startsWith("[")) {
int endIndex = findClosingBracketIndex(json, '[', ']') + 1;
String arrayString = json.substring(0, endIndex);
value = parseArray(arrayString);
json = endIndex == json.length() ? "" : json.substring(endIndex + 1).trim();
} else if (json.startsWith("{")) {
int endIndex = findClosingBracketIndex(json, '{', '}') + 1;
String objectString = json.substring(0, endIndex);
value = parseJson(objectString);
json = endIndex == json.length() ? "" : json.substring(endIndex + 1).trim();
} else {
int commaIndex = json.indexOf(",");
int endIndex = (commaIndex == -1) ? json.length() : commaIndex;
String stringValue = json.substring(0, endIndex).trim().replaceAll("^\"|\"$", "");
value = parseValue(stringValue);
json = (commaIndex == -1) ? "" : json.substring(endIndex + 1).trim();
}
data.put(key, value);
if (!json.isEmpty() && json.startsWith(",")) json = json.substring(1).trim();
}
return data;
}
private static List<Object> parseArray(String arrayString) {
List<Object> list = new ArrayList<>();
arrayString = arrayString.trim().substring(1, arrayString.length() - 1);
if (!arrayString.isEmpty()) {
String[] items = arrayString.split(",");
for (String item : items) {
list.add(parseValue(item.trim().replaceAll("^\"|\"$", "")));
}
}
return list;
}
private static Object parseValue(String value) {
if (value.matches("-?\\d+")) {
return Integer.parseInt(value);
} else if (value.matches("-?\\d+\\.\\d+")) {
return Double.parseDouble(value);
} else {
return value; // 문자열 값 반환
}
}
private static int findClosingBracketIndex(String str, char open, char close) {
int depth = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == open) depth++;
else if (str.charAt(i) == close) {
if (--depth == 0) return i;
}
}
return -1; // 일치하는 닫는 괄호를 찾지 못한 경우
}
}
ㅗㅜㅑ.... 느껴지십니까? 중첩된 객체, 배열 정도만 처리만 한 로직입니다.
라이브러리에서 얼마나 많은 일을 해주는지 느껴지시죠?
우선 Junit5로 테스트까지 진행해볼게요.
class JsonParserTest {
// {
// "name": "Sumin Kim",
// "age": 30,
// "skills": ["Java", "Spring", "Docker"],
// "address": {
// "city": "seoul",
// "country": "KOREA"
// }
// }
@Test
void parseJson() {
String json = "{\"name\":\"Sumin Kim\",\"age\":30,\"skills\":[\"Java\",\"Spring\",\"Docker\"],\"address\":{\"city\":\"seoul\",\"country\":\"KOREA\"}}";
Map<String, Object> result = JsonParser.parseJson(json);
System.out.println(result);
assertEquals("Sumin Kim", result.get("name"));
assertEquals(30, result.get("age"));
assertTrue(result.get("skills") instanceof List);
@SuppressWarnings("unchecked")
List<Object> skills = (List<Object>) result.get("skills");
assertTrue(skills.contains("Java"));
assertTrue(skills.contains("Spring"));
assertTrue(skills.contains("Docker"));
assertTrue(result.get("address") instanceof Map);
@SuppressWarnings("unchecked")
Map<String, Object> address = (Map<String, Object>) result.get("address");
assertEquals("seoul", address.get("city"));
assertEquals("KOREA", address.get("country"));
}
}
JSON 데이터를 Jackson 라이브러리로 파싱해보자
1. maven 의존성추가 (jackson, lombok)
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
2. 자바 객체생성
@Getter
@Setter
@ToString
public class Person {
private String name;
private int age;
private List<String> skills;
private Address address; // 내부 클래스 또는 별도 클래스로 정의 필요
}
@Getter
@Setter
@ToString
public class Address {
private String city;
private String country;
// Getter와 Setter
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
}
3. ObjectMapper로 역직렬화 후 테스트작성해서 테스트
public class JsonParsingTest {
@Test
void parseJsonWithJackson() throws IOException {
String json = "{\"name\":\"Sumin Kim\",\"age\":30,\"skills\":[\"Java\",\"Spring\",\"Docker\"],\"address\":{\"city\":\"seoul\",\"country\":\"KOREA\"}}";
ObjectMapper objectMapper = new ObjectMapper();
Person person = objectMapper.readValue(json, Person.class);
System.out.println(person);
assertEquals("Sumin Kim", person.getName());
assertEquals(30, person.getAge());
assertTrue(person.getSkills().contains("Java"));
assertTrue(person.getSkills().contains("Spring"));
assertTrue(person.getSkills().contains("Docker"));
assertEquals("seoul", person.getAddress().getCity());
assertEquals("KOREA", person.getAddress().getCountry());
}
}
결론
알고리즘 공부 차원에서 파싱을 해보는 것은 개념을 익히는데 좋다.
해보면 라이브러리를 쓰는 이유가 뭔지 감이 올 것이다.
왜 쓰는지 알고 쓰도록 하자
'자바' 카테고리의 다른 글
[자바] Form 데이터 파싱후 JUnit5로 테스트 (x-www-form-urlencoded) (2) | 2024.02.17 |
---|---|
[자바]Java Reflection: ModelMapper를 활용한 효율적인 객체 매핑 기법 (1) | 2024.02.11 |
[자바] 레코드(Records) : 코드 간결성과 효율성을 높이는 새로운 방법 (0) | 2024.01.28 |
[자바] Try-with-resources in Java: 코드를 깔끔하게 유지하는 현대적 방법 (0) | 2024.01.21 |
[자바] 클래스 파일 구조, VM명세서 (0) | 2023.11.14 |