배열로 구현한 Stack 

import java.util.Arrays;
import java.util.Stack;

public class MyStack {
	Stack stack = new Stack();
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	private int[] element;
	private int size;
	
	MyStack() {
		element = new int[10];
	}
	
	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
	}
	
	private void resize(int minCapacity) {
		int oldCapacity = element.length;
		int newCapacity = oldCapacity * 2;
		
		if (newCapacity - minCapacity < 0)
			newCapacity = minCapacity;
		if (newCapacity - MAX_ARRAY_SIZE > 0)
			newCapacity = hugeCapacity(minCapacity);
		element = Arrays.copyOf(element, newCapacity);
	}
	
	void push(int data) {
		if (size + 1 - element.length > 0)
			resize(size + 1);
		element[size++] = data;
	}
	
	int pop() {
		int result = -1;
		if (size < 0) return result;
		else {
			result = element[--size];
			System.arraycopy(element, size+1, element, size, 1);
			element[size] = -1;
			return result;
		}
	}
	
	int getSize() {
		return this.size;
	}
}

 

Node로 구현한 Stack 

import java.util.Arrays;

public class MyStack2<E> {

	private Node<E> head;
	private Node<E> tail;
	private int size;
	
	static class Node<E> {
		E item;
		Node<E> next;
		Node<E> prev;
		
		Node(E element) {
			this.item = element;
		}
		
		Node(Node<E> prev, E element, Node<E> next) {
			this.item = element;
			this.next = next;
			this.prev = prev;
		}
	}
	
	MyStack2() {
	}
	
	void push(int data) {
		Node<E> prev = tail;
		Node<E> node = new Node(prev, data, null);
		tail = node;
		
		if (prev == null)
			head = node;
		else
			prev.next = node;
		
		size++;
	}
	
	int pop() {
		if (size == 0) return -1;
		else {
			int result = (int) tail.item;
			final Node<E> prev = tail.prev;
			
			Node<E> beforeTail = tail;
			beforeTail.item = null;
			beforeTail.prev = null;
			
			tail = prev;
			if (tail == null) {
				head = null;
			} else {
				prev.next = null;
			}
			size--;
			return result;
		}
	}
	
	int getSize() {
		return this.size;
	}
}

 

테스트 코드

import static org.junit.Assert.assertEquals;

import org.junit.jupiter.api.Test;

class TestMyStack {

	MyStack stack = new MyStack();
	MyStack2<Integer> stack2 = new MyStack2<Integer>();
	
	@Test
	void pushTest() {
		stack.push(1);
		stack.push(2);
		stack.push(3);
		
		assertEquals(stack.getSize(), 3);
		assertEquals(stack.pop(), 3);
		assertEquals(stack.pop(), 2);
	}
	
	@Test
	void pushTest2() {
		stack2.push(1);
		stack2.push(2);
		stack2.push(3);
		
		assertEquals(stack2.getSize(), 3);
		assertEquals(stack2.pop(), 3);
	}
}

 

Node로 구현한 List

public class NewLinkedList {
	private int size;
	Node head;
	Node tail;
	
	static class Node {
		int item;
		Node prev;
		Node next;
		
		Node(int item){
			this.item = item;
		}
		
		Node(Node prev, int item, Node next){
			this.prev = prev;
			this.item = item;
			this.next = next;
		}
	}
	
	Node getNode(int position) {
		Node node;
		if (position < size >> 1) {
			node = head;
			for(int i=0; i<position; i++) {
				node = node.next;
			}
		} else {
			node = tail;
			for(int i=size-1; i>position; i--) {
				node = node.prev;
			}
		}
		return node;
	}
	
	void add(int data, int position) {
		if (position == size) { // add node to last.
			Node node = new Node(tail, data, null);
			final Node prev = node.prev;
			tail = node;
			if (head == null) {
				head = node;
			} else {
				prev.next = node;
			}
		} else { // add node to first or before.
			Node afterNode = getNode(position);
			final Node prev = afterNode.prev;
			Node node = new Node(prev, data, afterNode);
			afterNode.prev = node;
			if (prev == null) {
				head = node;
			} else {
				prev.next = node;
			}
		}
		size++;
	}
	
	void delete(int position) {
		if (position == size - 1) { // delete node to last.
			Node node = getNode(position);
			final Node prev = node.prev;
			tail = prev;
			if (tail == null) {
				head = null;
			} else {
				prev.next = null;
			}
		} else { // delete node to first or before.
			Node node = getNode(position);
			final Node next = node.next;
			final Node prev = node.prev;
			if (prev == null) {
				head = node.next;
			} else {
				prev.next = next;
				next.prev = prev;
			}
			node.next = null;
			node.prev = null;
		}
		size--;
	}
	
	boolean contains(Node node) {
		for (Node x = head; x != null; x = x.next) {
			if (node.item == x.item)
				return true;
		}
		return false;
	}
	
	int getSize() {
		return this.size;
	}
}

 

테스트 코드

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class TestNewLinkedList {

	NewLinkedList list = new NewLinkedList();
	
	@Test
	void addTest() {
		list.add(1, 0);
		list.add(2, 1);
		list.add(3, 2);
		list.add(4, 0);
		list.add(5, 1);
		
		assertEquals(list.getSize(), 5);
	}
	
	@Test
	void deleteTest() {
		list.add(1, 0);
		list.add(2, 1);
		list.add(3, 2);
		list.add(4, 3);
		list.add(5, 4);
		list.delete(0);
		list.delete(2);
		
		assertEquals(list.getSize(), 3);
	}

	@Test
	void containsTest() {
		list.add(1, 0);
		list.add(2, 1);
		list.add(3, 2);
		list.add(4, 3);
		list.add(5, 4);
		
		assertEquals(list.getSize(), 5);
		assertEquals(list.contains(new NewLinkedList.Node(5)), true);
	}
}
  • 깃헙 이슈 1번부터 18번까지 댓글을 순회하며 댓글을 남긴 사용자를 체크 할 것.
  • 참여율을 계산하세요. 총 18회에 중에 몇 %를 참여했는지 소숫점 두자리가지 보여줄 것.
  • Github 자바 라이브러리를 사용하면 편리합니다.
  • 깃헙 API를 익명으로 호출하는데 제한이 있기 때문에 본인의 깃헙 프로젝트에 이슈를 만들고 테스트를 하시면 더 자주 테스트할 수 있습니다.

최종으로 작성한 코드를 그려보면 다음과 같다.

 

GitHub Java Library를 이용하기 위해 mvn 환경이 제공되는 Legacy Java Project를 생성해주었다.

library를 pom.xml에 추가해 주었다.

<!-- https://mvnrepository.com/artifact/org.kohsuke/github-api -->
		<dependency>
		    <groupId>org.kohsuke</groupId>
		    <artifactId>github-api</artifactId>
		    <version>1.318</version>
		</dependency>


GitHubManager (라이브러리를 사용해 깃 관련 api를 호출하는 클래스) 소스는 다음과 같다.
참고로 오류 발생시 그저 에러 메시지를 호출하기만 하는데, 이 부분이 아쉽다.
(아직은 어떻게 처리해야 하는 것인지 잘 모르겠다.)

package com.whitship.dashboard;

import java.io.IOException;
import java.util.List;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHIssueComment;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;

public class GitHubManager {
	
	private final String TOKEN = "";
	private final String REPO_URL = "";
	private GHRepository repo;
	
	public GitHubManager() {
		connect();
	}
	
	public void connect() {
		try {
			GitHub github = new GitHubBuilder().withOAuthToken(TOKEN).build();
			this.repo = github.getRepository(REPO_URL);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public List<GHIssue> getOpenIssue() {
		if (repo != null) {
			try {
				return repo.getIssues(GHIssueState.OPEN);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
	
	public List<GHIssueComment> getComment(GHIssue issue) {
		if (issue != null) {
			try {
				return issue.getComments();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
}


Dashboard (GitHubManager를 이용해 데이터를 가져온뒤 가공하여 print하는 클래스) 소스는 다음과 같다.

package com.whitship.dashboard;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHIssueComment;

public class Dashboard {
	static private final float TOTAL = 18;
	private GitHubManager github;
	
	public Dashboard() {
		github = new GitHubManager();
	}
	
	public Map<Integer, Set<String>> getUserTimes() throws IOException {
		Map<Integer, Set<String>> userMap = new HashMap<Integer, Set<String>>();
		int number = 1;
		List<GHIssue> issue = github.getOpenIssue();
		
		for (GHIssue is : issue) {
			Set<String> users = new HashSet<String>();
			for (GHIssueComment comment : github.getComment(is)) {
				String userName = comment.getUserName();
				users.add(userName);
			}
			userMap.put(number++, users);
		}
		
		return userMap;
	}
	
	public Map<String, Integer> getUserParticipationTimes() throws IOException {
		Map<String, Integer> board = new HashMap<String, Integer>();
		Map<Integer, Set<String>> userMap = getUserTimes();
		
		for (int i = 1; i <= TOTAL; i++) {
			Set<String> users = userMap.get(Integer.valueOf(i));
			if (users != null && users.size() > 0) {
				for (String user : users) {
					if (!board.containsKey(user)) {
						board.put(user, 1);
					} else {
						board.put(user, board.get(user)+1);
					}
				}
			}
		}
		
		return board;
	}
	
	public void printDashboard() throws IOException {
		Map<String, Integer> board = getUserParticipationTimes();
		if (board.size() > 0) {
			for (String user : board.keySet()) {
				System.out.printf("%s - %.2f%c%n", 
									user,
									board.get(user)/TOTAL*100,
									'%');
			}
		}
	}
}
  • 산술 연산자
    • 종류 : +, -, /, *, % (+의 경우 String 타입의 변수가 먼저 오는 경우 우측 피연산자와 연결하는 역할을 한다.)
    • 알고 가면 좋아요 - 산술 변환 : 앞선 2주차에서 공부했듯이 자바는 타입 기반의 언어이다. (프리미티브, 참조형 타입)
                      두 피연산자의 타입이 다른 경우 동일하게 맞춘 후 연산을 한다.
                      보통 작은 타입이 큰 타입으로 변환되며 int형 보다 작은 경우 무조건 int로 변환되어 연산된다.
                      그렇기에 byte의 경우 int로 선언하는 것이 좋겠다.
  • 비트 연산자 
    • ~ (단항 연산자) &, ^, |, >>, << (이항 연산자)
    • 기본적으로 컴퓨터는 2진체계로 구성되어 있다. 비트 단위로 연산이 필요한 경우 비트 연산을 해 준다. 
    • ~ : 0인 경우 1로, 1인 경우 0으로 변환한다. (단항 연산자이며, 양수의 경우 음수로 변환된다. 1의 보수)
    • & : 둘다 1인 경우에만 1로 변환한다. (특정 위치의 값을 알아낼 때 사용)
    • ^ : 두 값이 다른 경우에만 1로 변환한다. (간단한 암호 알고리즘에서 사용)
    • | : 두 값중 하나라도 1인 경우 1로 변환한다. (특정 위치에 값을 셋팅할 때 사용)
    • >> : 각 비트의 값을 우측의 값으로 이동한다. (음수의 경우 1을 유지하기 위해 왼쪽의 값이 비어있는 경우 1로 셋팅된다.)
    • << : 각 비트의 값을 좌측의 값으로 이동한다. (음수의 경우 양수가 될 수 있다. 오른쪽의 값이 비어있는 경우 0로 셋팅된다.)
    • (>>, << 는 비트의 값만 이동하기에 산술변환이 일어나지 않는다.)

  • 관계 연산자 
    • 두 값의 대소, 같음, 다름 혹은 자손 및 구현(?) 관계가 참인지 거짓인지 판단하여 true 혹은 false를 리턴한다. 
    • >, <, >=, <=, instanceof, ==, != 
  • 논리 연산자
    • &&: 두 피연산자의 결과가 모두 true여야만 true를 리턴한다. 아닌 경우는 false를 리턴한다.
    • || : 두 피연산자의 결과가 하나라도 true인 경우만 true를 리턴한다. 아닌 경우는 false를 리턴한다.
    • ! (단항 연산자) 비트의 값을 반대로 변경한다.
  • instanceof 
    • 좌측에는 참조형 변수를 우측에는 참조형 타입을 넣어 사용한다. 
    • 좌측 참조형 변수가 우측 참조형 타입으로 변환할 수 있으면 true를 리턴한다.
      (필자는 자손에서 상위 클래스를 상속받거나 인터페이스의 경우 구현을 한 경우라고 이해했다.)

 

  • assignment (=) operator
    • 우측의 결과값을 좌측에 저장할 수 있는 공간이 있는 변수에 할당하는 연산자이다.
int i = 0;
//3 = i + 3; 
//i + 3 = 3;
//왼쪽 피연산자는 반드시 저장할 수 있는 공간이어야 한다.
//The left-hand side of an assignment must be a variable 발생
  • 화살표 (->) 연산자
    • 함수형 인터페이스의 메소드를 구현할 때, 코드를 깔끔하게 만드는 연산자인 것 같다.
    • 두번째에서 세번째 버전으로 어떻게 저렇게 코드를 작성해 사용할 수 있는 것인지 잘은 모르겠다.
    • 어떤 경우 화살표 연산자를 사용하는지 코드를 사용해서 확인하자.
// 첫번째 버전
public class OperatorEx10 implements Foo{
	
	@Override
	public int calc(int x, int y) {
		return x + y;
	}
	
	public static void main(String[] args) {
		OperatorEx10 oe = new OperatorEx10();
		System.out.println(oe.calc(1, 2));
	}
	
}

// 두번째 버전 
public class OperatorEx10 {
	
	public static void main(String[] args) {
		Foo foo = new Foo() {
			@Override
			public int calc(int x, int y) {
				return x + y;
			}
		};
		
		System.out.println(foo.calc(1, 2));
	}
}

// 세번째 버전 
public class OperatorEx10 {
	
	public static void main(String[] args) {
		Foo foo = (x, y) -> x + y;

		System.out.println(foo.calc(1, 2));
	}
}

https://catch-me-java.tistory.com/30 참고하여 작성해 보았다.

  • 3항 연산자
    • 간단한 if, else 문을 한 줄에 사용할 수 있는 연산자이다. 
//		if(x == 10) {
//			x = 100;
//		} else {
//			x = 0;
//		}

x = x == 10 ? 100 : 0;

 

  • 연산자 우선 순위 

이번 작업은 각 url 에 대한 처리 로직을 requestHandler에서 직접해주는 것이 아닌, Controller 클래스로 역할을 위임해주는 것이었다.
먼저 책에 나온 힌트를 보고 리팩토링을 시도하였다. 

한가지 신기했던 것은 service 라는 메소드 하나만을 가진 controller 최상위 인터페이스를 생성하고 해당 클래스를 상속하는 추상 클래스를 만들어 각각의 메소드별 처리를 doPost(),  doGet()으로 하라는 것이었다. 각 controller별 구현체에서의 공통적인 메소드 처리하는 부분을 묶어 추상클래스로 올린 듯 하였다. 사실 이 부분은 알 것 같으면서도 잘 와닿지 않았다.

이전 코드는 다음과 같다. uri별 method별 처리 로직이 있다.

if ("/user/create".equals(path) && "POST".equals(method)) {
        String userId = request.getParameter("userId");
        String password = request.getParameter("password");
        String name = request.getParameter("name");
        String email = URLDecoder.decode(request.getParameter("email"), "utf-8");

        Collection<String> param = new ArrayList<String>(Arrays.asList(userId, password, name, email));
        if (Util.isNullOrEmpty(param)) {
            log.debug("Create user fail. Required userId, password, name, email");
        } else {
            User user = new User(userId, password, name, email);
            DataBase.addUser(user);
            log.debug("Create user information = " + user.toString());
        }
        response.redirect("index.html");
    } else if("/user/login".equals(path) && "POST".equals(method)) {
        String userId = request.getParameter("userId");
        String password = request.getParameter("password");

        Collection<String> param = new ArrayList<String>(Arrays.asList(userId, password));
        if (Util.isNullOrEmpty(param)) {
            log.debug("Login fail. Required userId, password");
        } else {
            User user = DataBase.findUserById(userId);
            String cookie = "";
            if (user != null && password.equals(user.getPassword())) {
                loginState = LOGIN.SUCCESS;
                cookie = "logined=true";
                log.debug("Login success.");
            } else {
                loginState = LOGIN.FAIL;
                cookie = "logined=false";
                log.debug("Login failed.");
            }
            response.addHeaderProperty(new Pair("Set-Cookie", cookie));
        }
        response.redirect("index.html");
    } else if("/user/list".equals(path) && "GET".equals(method)) {
   		...
    }


강사님의 힌트대로 구현한 버전은 다음과 같다. 각 url별 처리 로직을 담당하는 클래스에서 직접해주는 식으로 구현하였다.

if (!Strings.isNullOrEmpty(path)) {
    if ("/user/create".equals(path)) {
        Controller controller = controllerMap.get(path);
        controller.service(request, response);
    } else if("/user/login".equals(path)) {
        Controller controller = controllerMap.get(path);
        controller.service(request, response);
    } else if("/user/list".equals(path)) {
        Controller controller = controllerMap.get(path);
        controller.service(request, response);
    } else {
        response.forward("/".equals(path) ? "/index.html" : path);
    }
}


코드가 정말 깔끔해졌다. 아 이렇게 리팩토링을 할 수도 있구나! 감명을 받았고, 조금 더 수정을 해 본후에 강사님의 모범답안과 비교해서 보면 좋을 것 같다.

 

 

 

이번 리팩토링은 Http메시지를 전송하는 역할을  HttpResponse로 분리하는 작업이었다.
먼저 힌트만 보고 혼자서 진행을 했는데, 응답 관련 메시지 미리 만들고 HttpResponse에 전달하는 식으로 구현을 했었다.
그래서 ResponseHandler코드에는 응답 관련한 코드들이 산재되어 있었다.
예를 들자면 응답시 사용할 코드(200, 302)라던가, 헤더의 속성값 같은 것들말이다. 

else if("/user/login".equals(uri) && "POST".equals(method)) {
            String userId = request.getParameter("userId");
            String password = request.getParameter("password");
            statusCode = 302;

            Collection<String> param = new ArrayList<String>(Arrays.asList(userId, password));
            if (Util.isNullOrEmpty(param)) {
                log.debug("Login fail. Required userId, password");
            } else {
                User user = DataBase.findUserById(userId);
                String cookie = "";
                if (user != null && password.equals(user.getPassword())) {
                    loginState = LOGIN.SUCCESS;
                    cookie = "logined=true";
                    log.debug("Login success.");
                } else {
                    loginState = LOGIN.FAIL;
                    cookie = "logined=false";
                    log.debug("Login failed.");
                }
                responseHeaderProperty.add(new Pair("Set-Cookie", cookie));
            }

        }


하지만 책의 코드를 보니 http 응답을 전송하는 것 뿐만 아니라, 전송 메시지를 만드는 기능, 이와 관련된 데이터들도
다 HttpResponse 클래스로 분리하는 것을 알 수 있었다. 

else if("/user/login".equals(path) && "POST".equals(method)) {
        String userId = request.getParameter("userId");
        String password = request.getParameter("password");

        Collection<String> param = new ArrayList<String>(Arrays.asList(userId, password));
        if (Util.isNullOrEmpty(param)) {
            log.debug("Login fail. Required userId, password");
        } else {
            User user = DataBase.findUserById(userId);
            String cookie = "";
            if (user != null && password.equals(user.getPassword())) {
                loginState = LOGIN.SUCCESS;
                cookie = "logined=true";
                log.debug("Login success.");
            } else {
                loginState = LOGIN.FAIL;
                cookie = "logined=false";
                log.debug("Login failed.");
            }
            response.addHeaderProperty(new Pair("Set-Cookie", cookie));
        }
        response.redirect("index.html");
    }


응답 관련한 속성과 기능들을 모조리 HttpResponse에 넣으니 ResponseHandler 코드가 훨씬 깔끔해진 것을 알 수 있었다. 
예를 들어 ResponseHandler 입장에서는 redirect 할 때 302 코드가 필요하구나까지 알 필요 없다는 말이다.
이는 HttpResponse에서만 알고 있으면 되고, 필요시 사용하면 되는 것이다.

 

+ Recent posts