package ch2;

import java.io.Serializable;

public class ArrayEx2 {
	String[] arrayOfStrings = new String[] {"1", "2"};
	String[][] arrayOfarraysOfStrings;
	
	Object[] o = arrayOfStrings;
	Cloneable c = arrayOfStrings;
	Cloneable c2 = arrayOfarraysOfStrings;
	Object o2 = arrayOfStrings;
	
	public static void main(String[] args) {
		String[] arrayOfStrings2 = new String[] {"1", "2", "3"};
		String[] arrayOfStrings3 = {""};
		String[][] arrayOfStrings4 = new String[3][];
		Object[][] o4 = arrayOfStrings4;
		Object o5 = arrayOfStrings4;
		
		System.out.println(arrayOfStrings2 instanceof Object);
		System.out.println(arrayOfStrings3 instanceof Object);
		System.out.println(arrayOfStrings4 instanceof Object);
		
		System.out.println(arrayOfStrings2 instanceof Cloneable);
		System.out.println(arrayOfStrings3 instanceof Cloneable);
		System.out.println(arrayOfStrings4 instanceof Cloneable);
		
		System.out.println(arrayOfStrings2 instanceof Serializable);
		System.out.println(arrayOfStrings3 instanceof Serializable);
		System.out.println(arrayOfStrings4 instanceof Serializable);
		
		System.arraycopy(arrayOfStrings2, 1, arrayOfStrings2, 0, 2);
		for(String e : arrayOfStrings2) {
			System.out.println(e);
		}
		
		int[][] products1 = new int[10][10];
 		int[][] products2 = new int[10][];
		for(int i=0; i<10; i++)
			products2[i] = new int[10];
		int[][] products3 = { {} };
		
		float[][][] globalTemperatureData1 = new float[360][180][100];
		float[][][] globalTemperatureData2 = new float[360][180][];
		float[][][] globalTemperatureData3 = new float[360][][];
		
	}
}

 

package ch2;

public class Array3 {
	public static void main(String[] args) {

		int[] one = new int[] {1,2,3};
		int[][] two = new int[][] {
				{1,2,3},
				{4,5,6},
				{7,8,9}
		};
		int[][][] three = new int[][][] {
			{	
				{1,2,3},
				{4,5,6},
				{7,8,9}
			},
			{
				{10,11,12},
				{13,14,15},
				{16,17,18}
			},
			{
				{19,20,21},
				{22,23,24},
				{25,26,27}
			}
		};
		
		int[][][][] four = new int[][][][] {
			{
				{	
					{1,2,3},
					{4,5,6},
					{7,8,9}
				},
				{
					{10,11,12},
					{13,14,15},
					{16,17,18}
				},
				{
					{19,20,21},
					{22,23,24},
					{25,26,27}
				}
			},
			{
				{	
					{28, 29, 30},
					{31, 32, 33},
					{34, 35, 36}
				}
			}
		};
		
		for(int k=0; k<three.length; k++) {
			for(int i=0; i<three[k].length; i++) {
				for(int j=0; j<three[k][i].length; j++) {
					System.out.printf("%2d", three[k][i][j]);
				}
				System.out.println();
			}
		}
	}
}

깃: https://github.com/isfpcat/web-application-server/commit/fa9fb0010607ac417b23de8b8c1a10483fd2f132

책에 나와있는 리팩토링 1단계 힌트를 참고하여 요청 데이터를 처리하는 로직을 별도의 HttpRequest 클래스로 작성했다.
해당 클래스는 InputStream으로 전달받은 Http 메시지를 다음 4가지 필드로 파싱하고 저장한다.  
1. 서버의 동작 내용을 판단하는 "Method"
2. 어떤 리소를를 읽을지 판단하는 "Path"
3. 리소스의 타입, 로그인 상태 등의 속성을 가지고 있는 "Header"
4. 추가적으로 필요한 내용이 담긴 "Body"

우선 내가 이해한 리팩토링은 프로그램에 필요한 기능을 잘 나누고 (예- Http 요청 메시지를 method, path, header, body로 나누어 파싱하자) 비슷한 기능들을 묶어 (Http 요청 메시지 관련한 기능들) 하나의 클래스가 처리할 수 있도록 구성해야 하는 것 같았다.
또한 클래스를 만들기 전에 성격에 따라 필드를 그룹핑하고 구분 짓는 것도 필요하겠구나 싶었다.
+ 동작이 잘 되는지 최소한의 테스트코드도 같이 작성해야 한다.




+ WebServer에서도 해당 클래스를 사용하도록 메시지를 전달하도록 수정을 같이 해 주었는데 응답하지 않는 에러가 발생한다. 이 부분은 강사님이 구현한 내용과 비교해보며 어떤 부분을 잘못 쓰고 있는지 다시 확인해 봐야할 것 같다.

기본적으로 자바는 변수와 변수에 들어가는 리터럴 값에 타입이 지정되어 있다. 즉 변수에 맞는 타입의 값을 저장해줘야 한다. 
그러나 다른 타입에 값을 저장하고 싶으면 어떻게 해야할까? 이 경우에는 타입 변환 즉, 캐스팅을 사용해 변환이 가능하다. 

캐스팅도 크게 기본형 내에서 boolean을 제외한 나머지 타입끼리만 가능하다. 참조형 타입 내에서도 자손 관계에 있어야만 가능하다. 또한 기본형과 참조형 간에서는 서로 캐스팅이 불가능하다.

 

더 큰 타입의 값을 작은 타입의 변수에 저장할 경우 컴파일 에러가 발생한다!

 

하지만 () - 캐스팅 연산자를 사용해 값을 지정할 순 있다. 이때는 값 손실이 발생할 수 있다.

 

참조형에서의 캐스팅 예

프리미티브 타입 종류와 공간 크기, 값의 범위, 그리고 기본 값
실수형 저장 방식 - 10^45까지 표현 가능하나 10^7값까지만 정확한 값을 제공한다.

  •  프리미티브 타입과 레퍼런스 타입
    • 종류의 차이 : 프리미티브 타입의 경우 8가지이나 레퍼런스 타입의 경우 개발자가 다양한 종류의 타입을 지정할 수 있다. 
    • 저장 공간의 차이 : 프리미티브 타입의 경우 1byte ~ 8byte까지 지정된 저장 공간을 가지지만, 레퍼런스 타입의 경우 포함된 데이터의 영역에 따라 공간의 크기가 결정된다.
    • 파라미터로 전달 되는 방식의 차이 : 프리미티브 타입의 경우는 변수가 복사되어 전달되지만, 레퍼런스 타입의 경우 주소가 전달된다.
  • 리터럴
    • 자바 코드에 직접적으로 표현되는 값들이다.
    • 1, 1.0, '1', "one", true, false, null 등이 있다.
    • 리터럴에도 타입이 있다.
  • 변수 선언 및 초기화하는 방법
    • 기본적으로 자바는 타입을 가지고 있는 언어이기에 사용하기 전에 반드시 선언을 해 주어야 한다.
    • 변수 선언은 [타입] [변수명] 이다.
    • 선언과 동시에 다음과 같이 초기화가 가능하며 아래의 두 변수에 final 키워드를 맨 앞에 붙여 다른 값으로 수정하지 못하도록 설정이 가능하다. 
      • String str1 = "hello";
      • String str2 = readLine(); 
    • 지역변수는 사용 전에 반드시 초기화를 해 주어야 한다.
      (JAVAC 컴파일러에서 <The local variable a may not have been initialized> 에러를 뱉는다.) 
  • 변수의 스코프와 라이프타임 (잘 모르겠는 부분)
  • 타입 변환, 캐스팅 그리고 타입 프로모션

 

  • 1차 및 2차 배열 선언하기
  • 타입 추론, var (잘 모르겠는 부분)
    • JAVA 10 부터의 기능으로써, 컴파일러가 변수를 초기화하는 값에 의해 타입을 추론하는 것

깃 : https://github.com/isfpcat/web-application-server/commit/1d98fa8545262e13e00d12fc65cdb7c6d9b826f6

HTTP 전송, 응답 메시지를 보면 다음과 같이 다양한 방식으로 구성되어 있음을 알 수 있다.
1. queryString 형식 : field1=value1&field2=value2 (URL 중 ? 뒤에 붙거나 form 내용으로 전달되는 형식)
2. Header 필드 형식 : field1: value1
3. Header 필드의 value의 형식 :
- Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7
-
Cookie: logined=true; path=/

강사님이 제공하는 소스를 보면 각각의 메시지 중 필요한 key와 value값을 파싱하기 위한 클래스를 따로 정의해서 제공해주셨다. 
나는 해당 클래스에 몇가지 메서드 및 각 메서드가 잘 작동하는지 테스트 케이스를 추가했다.
그리고 강사님이 작성한 테스트 케이스와 비교해 보았다. 

내가 작성한 버전 

@Test
    public void parseQueryStringTest() {
        String userId = "test";
        String password = "pwd";
        String name = "jylee";
        String email = "a%40naver.com";

        String query = "userId=" + userId + "&password=" + password + "&name=" + name+ "&email=" + email;
        Map<String, String> result = HttpRequestUtils.parseQueryString(query);

        assertTrue(userId.equals(result.get("userId")));
        assertTrue(password.equals(result.get("password")));
        assertTrue(name.equals(result.get("name")));
        assertTrue(email.equals(result.get("email")));
    }

 

강사님이 작성한 버전 

@Test
    public void parseQueryString() {
        String queryString = "userId=javajigi";
        Map<String, String> parameters = HttpRequestUtils.parseQueryString(queryString);
        assertThat(parameters.get("userId"), is("javajigi"));
        assertThat(parameters.get("password"), is(nullValue()));

        queryString = "userId=javajigi&password=password2";
        parameters = HttpRequestUtils.parseQueryString(queryString);
        assertThat(parameters.get("userId"), is("javajigi"));
        assertThat(parameters.get("password"), is("password2"));
    }


먼저 강사님이 작성한 내용을 보면 훨씬 한눈에 읽기가 쉽다. 동작 확인할 메서드를 실행한 후 결과값에서 원하는 키를 꺼내고,
이 값이 실제로 내가 원하는 것이 맞는지?의 방식으로 테스트가 되고 있다. 결과값이 기대값과 같은지? 의 방식으로 테스트가 되고 있어서 문맥상으로도 훨씬 이해하기 쉽다. (나는 반대의 방식으로 했었다.) 그리고 없는 키를 찾는 경우 결과값이 null인지? 에 대한 코드도 있음을 알 수 있다. (이 부분은 Map 자체의 특성이기도 해서 해당 클래스의 기능에 대해서도 잘 이해하고 있어야겠구나 싶었다.)

그리고 null, "", " ", 유효한 속성 및 유효하지 않은 속성이 전달되는 경우 빈 map객체가 리턴되는지에 대한 테스트 코드도 같이 작성되어 있음을 알 수 있다.  

@Test
    public void parseQueryString_null() {
        Map<String, String> parameters = HttpRequestUtils.parseQueryString(null);
        assertThat(parameters.isEmpty(), is(true));

        parameters = HttpRequestUtils.parseQueryString("");
        assertThat(parameters.isEmpty(), is(true));

        parameters = HttpRequestUtils.parseQueryString(" ");
        assertThat(parameters.isEmpty(), is(true));
    }

    @Test
    public void parseQueryString_invalid() {
        String queryString = "userId=javajigi&password";
        Map<String, String> parameters = HttpRequestUtils.parseQueryString(queryString);
        assertThat(parameters.get("userId"), is("javajigi"));
        assertThat(parameters.get("password"), is(nullValue()));
    }

 

+ 빈 map객체 및 null 리턴은 다음 코드에서 알 수 있다. String, Map, Arrays.stream ... 도 계속 사용해서 익혀야 할 것 같다.

private static Map<String, String> parseValues(String values, String separator, String valueSeperator) {
        if (Strings.isNullOrEmpty(values)) {
            return Maps.newHashMap();
        }

        String[] tokens = values.split(separator);
        return Arrays.stream(tokens).map(t -> getKeyValue(t, valueSeperator)).filter(p -> p != null)
                .collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()));
    }

 

먼저 해당 웹 서버가 어떤 식으로 동작하는지 내가 이해한 만큼 그림으로 그려보았다. 또한 코드 중 사용자의 요청을 동시에 받아 처리할 수 있도록 RequestHandler가 Thread를 기반으로 상속되어 있는 것은 100% 이해하진 못한 것 같아, 다시 코드를 작성하며 공부를 해야할 것 같다. 그리고 한가지 헷갈렸던 부분은 java.util.stream.Stream과는 전혀 다른 Stream 기반 (java.io)을 사용한다는 것이다. Socket이 제공하는 java.io 패키지의 InputStream으로는 사용자가 요청한 데이터를 읽고, OutputStream으로 데이터를 전송하는 것을 알게 되었다. Stream도 간단한 코드를 작성해서 정리해봐야겠다.


원래 구현은 사용자가 요청한 URI, Method별로 if 분기를 하였고 이때마다 response를 각각 하게 해 주어 중복적인 코드가 많았다. 
RequestHandler와 비슷하게 ResponseHandler로 한번에 응답을 처리할 수 있도록 클래스를 분리하는 작업을 해 주었다. RequestHandler와 마찬가지로 ResponseHandler도 Thread를 상속하도록 처리했는데 이 부분은 굳이 그렇게 안 해줘도 될 것 같기도 하다.. 잘 모르겠다!
git : https://github.com/isfpcat/web-application-server/commit/e85c4a860426011c9b56545cc16d0f96dd094731

 

 

HTTP 웹 서버 구현을 통해 HTTP 이해하기 1 - 요구사항 구현 내용 중 강사님께서 try () 안에 Closeable이 상속된 객체를 생성하면 굳이 close를 안해도 java compiler가 알아서 삭제를 해준다는 언급을 해 주셨는데 관련해서 어떤 내용인지 알고 싶고, java.io.inputStream 관련해서도 모르는 부분이 많은 것 같아 찾아보게 되었다.

 

예를 들어 파일에서 내용을 읽어와 다른 파일에 복사하는 코드가 있다고 하자.
복사할 때 반드시 bw.close() 를 호출해야 내용이 쓰여지고 (쓰여지는 건 flush라는 호출만으로 가능하다) 관련된 리소스가 삭제된다.

		File sourceFile = new File("recipe_src.txt");
		File copiedFile = new File("recipe_dst.txt");
        
        try {
			FileInputStream is = new FileInputStream(sourceFile);
			InputStreamReader ir = new InputStreamReader(is);
			BufferedReader br = new BufferedReader(ir);
			
			FileWriter fw = new FileWriter(copiedFile);
			BufferedWriter bw = new BufferedWriter(fw);

			String source = "";
			String line = "";
			while ((line = br.readLine()) != null) {
				source += line + "\n";
			}
			
			bw.write(source);
			bw.close();
			br.close();
			
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

하지만 try 사이에 () 괄호문을 넣어주고 그 안에서 객체를 생성한다면 close를 굳이 안해줘도 된다. java 7버전에서 제공되는 기법으로 try-with-resources 이라고 한다. java compiler가 생성된 리소스를 알아서 삭제해준다고 하니 훨씬 편한 것 같다. 그런데 자바는 굳이 생성된 객체 관리를 안해도 되지 않나? (가비지 콜렉터가 있기 때문) 이 경우는 왜 자동으로 삭제가 안되고 직접 close()를 호출해야 하는지 잘 모르겠다.

		File sourceFile = new File("recipe_src.txt");
		File copiedFile = new File("recipe_dst.txt");

		try (InputStreamReader ir = new InputStreamReader(new FileInputStream(sourceFile));
				BufferedReader br = new BufferedReader(ir);
				BufferedWriter bw = new BufferedWriter(new FileWriter(copiedFile));) {
			String source = "";
			String line = "";
			while ((line = br.readLine()) != null) {
				source += line + "\n";
			}
			
			bw.write(source);
//			bw.close(); 안해줘도 된다.
//			br.close();
			
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

<자바 웹 프로그래밍 - Next Step의 HTTP 웹 서버 구현을 통해 HTTP 이해하기> 에 나와있는 요구사항들을 토대로
HTTP  요청이 오면 서버에서 각 리소스별로 어떻게 응답할지에 대한 구현을 무작정 시도해 보았었는데 오늘은 유투브 영상과 책을 참고하며 더 나은 방법과 잘 이해하지 못했던 부분을 다시 이해하며 적용해 보았다.

오늘 작업한 내용
https://github.com/isfpcat/web-application-server/commit/09004e04922262e0cd3ba99b19b28e9f43ba172b

- 알게 된 것
1. HTTP에서 교환하는 정보는 다음과 같이 정해진 표준 규칙에 따라 구성된다. (시작줄과 헤더는 필수 구성요소이다.)
시작줄 : 메서드, URI, 버전
헤더 : <Key> : <Value> 와 같은 형태로 다음과 같은 값들이 들어갈 수 있다. (Host부터 시작)

바디 : 전송되는 데이터로 헤더와 \r\n (빈줄 공백) 다음에 위치한다.
예를 들어 서버측에서 Stream을 통해 클라이언트에서 요청된 바디를 읽을 때 read()를 사용하는데,
이때 데이터의 길이가 어느 정도 되는지를 헤더의 Content-Length를 통해 알아내어 파라미터로 전달해 파싱할 수 있도록 한다.

2. 302 Redirect의 의미는 브라우저에게 서버가 보내는 URI 다시 보내달라고 요청하는 의미이며 (이것 역시 표준으로 정해져 있다.)
다음과 같이 작성할 수 있다.

    private void response302Header(DataOutputStream dos) {
   	    try {
        	dos.writeBytes("HTTP/1.1 302 Found \r\n");
            dos.writeBytes("Location: http://localhost:8080/index.html\r\n");
            dos.writeBytes("\r\n");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

 

다음에 작업할 것들이 정말 수두룩하다.
우선은 코드적으로 동작확인을 할 수 있는 junit을 작성하고 중복을 줄이는 리팩토링 단계를 먼저 실행해보면 좋겠다.

유투브 링크: https://youtu.be/qgFVj916nX8?si=WfmWzXAiWOA8uaiF

HTTP 웹 서버 실습 설명 및 개발 환경 세팅

 

+ Recent posts