[마소콘 2018] 2080 라이프 강연 내용 정리 및 느낀점.

|

2018년 12월 15일에 와이프랑(개발자다..!) 마이크로소프트웨어 콘퍼런스에 다녀왔다. (줄여서 마소콘)

  • 관련 링크 : https://www.imaso.co.kr/masocon2018/

전반적인 강연 내용은 기술부채를 개선하는 방법에 대한 내용들이었다.
기술부채란 개선해야 할 것을 알지만, 묵혀두고 있는 기술적 빚을 뜻한다.

일단 글을 쓰기전에 중간중간에 놓친 부분도 있고 (사실 집중을 잘 못한다..)
정리를 잘한다고 생각하지 않기에 강연자가 전달하고자 하는 부분을 제대로 짚었는지는 모르겠다.
하지만 강연 내용을 기억으로만 남기기에는 나중에 기억이 나지 않을 것 같아 정리를 해보려 한다.

2080 라이프 강연 내용 정리.

설계 또는 개발 그리고 안정화 과정에서 시간이 부족하다는 이유로 돌아가게만 짜는 경우가 있다. 또한 정해진 컨벤션을 무시하고 본인만 만족하는 코드로 개발하는 개발자들도 있다보니 기술부채가 쌓이고 그 빚을 값는데에 더 많은 시간을 쓰게되며 야근 철야를 하게 된다. 그런 기술부채들을 줄이기 위한 방안으로 강연자는 코드리뷰를 강조했다.

코드 리뷰란?

개발자가 작성한 코드를 다른 개발자가 정해진 방법을 통해 검토하는 일을 말한다. 서로가 검토를 통해 코드가 안고 있는 잠재적인 결함을 찾고 개선해가는 과정이다.

코드리뷰는 어떠한 방법으로 하는가?

코드리뷰시엔 Gerrit이란 툴을 사용하는것을 추천하였다.

그럼 Gerrit은 무엇인가?

Gerrit은 코드 리뷰 기능과 Git 서버 저장소 관리 기능을 제공하는 웹 기반 코드 리뷰 시스템이며. 일정 점수 이상의 코드 리뷰 점수를 얻어야 코드의 변경 사항을 적용할 수 있게 해 코드 리뷰를 강제하는 도구이다. 정리하면 Gerrit은 애초에 코드 리뷰를 자동화하고 강제하는 목적에 맞춰 만들어진 시스템이다.

  • Gerrit은 참고글 : http://ithub.tistory.com/112 , https://d2.naver.com/helloworld/6033708

강연자가 다니고 있던 회사에서는 매일 커밋을 하고 싶을때마다 Gerrit에 적고 한명 이상의 리뷰어가 그 코드에 승인을 하자마자 커밋을 할 수 있다고 하였다. 그리고 코드리뷰는 하는게 좋다고 재차 강조하였다.

하지만 코드리뷰를 하면 시간이 많이걸리므로 다른 일정들이 영향을 받아버린다. 그래서 주저할 수 있는데 강연자는 리뷰 시 많이 보이는 문제들의 20%를 해결하면 80%의 문제는 해결할 수 있다고 하였다.

코드리뷰를 단축하기 위한 몇가지 가이드도 제시하였다.

  1. 빌드타임은 짧은게 좋다.
  2. 브랜치를 잘 쓴다. 브랜치마다 무슨 역할을 하는지 분류를 하여 관리한다. 코드리뷰할때 범위를 좁힐 수 있음. (merge를 했을 대 어떤일이 나올지 알게 되므로)
  3. Style : 가독성을 높이면 코드리뷰를 빨리 볼 수 있고 리뷰시간이 빨라진다.
    • gerrit, commit 시 로직이 100줄이 넘어가지 않도록 되도록이면 짧게 커밋한다.
  4. 코드 리뷰 관련 기록을 계속 남겨 히스토리를 쌓는다.
  5. Refactor : // TODO(name) Refactor 주석 달기.
  6. VSCode 시 확장팩 사용 : CodeMetrics -> 이걸로 분석하여 리팩토링 포인트를 찾음. 리팩토링은 시간이 날때마다 하는게 코드리뷰 시간을 줄이는 방법임.
  7. TEST코드 작성.
  8. 빌드테스트 자동화.

중요한건 커뮤니케이션

  • 조직 전체가 코드 리뷰 문화를 자연스럽게 받아들이도록 하는 것도 중요하다
  • 다른사람이 안한다고 하면 다 할 수 없다.
  • 규모가 있는 회사같은 경우는 커뮤니티가 있어서 문서에다가 정리.. 새로 입사한 사람들한테 문서를 주면서 이렇게 하고 있으니 그 이유는 이러니 해달라고 부탁…
  • 셋업하는게 시간이 많이 걸리므로 부담이 들고 의구심이 들지만 결국엔 품질이 좋아지고 버그잡는데 시간을 줄일 수 있으므로 결국엔 개발시간이 줄어듬
  • 코드리뷰를 하지 않거나 가끔씩만 받게되면 언젠가는 본인몫으로 들어온다. (새벽근무 밤샘 등등..) 중시하는 문화는 결국엔 팀에 좋은 영향을 미친다.

느낀점

사실 이전 솔루션 회사를 다닐 때(좀 오래됐다..) Gerrit을 써볼려고 팀 차원에서 시도해본적은 있었으나.. 사실 잘 되지 않았다.

코드리뷰를 해본 경험에서 확실한 단점이라고 생각되는 것은 리뷰를 하는 부분은 다른 개발자도 어려워 하는 부분이거나 명확하지 않는 부분이라는것..
또한 커밋 시 계속 반려가 되면 (사실 자괴감이 든다.) 생산 의욕을 꺾을 때도 있다는 것이다.

기존 솔루션이 SVN을 사용하였고 당시 팀원들이 GIT에 그렇게 익숙하지는 않았다. (그리고 당시 이클립스를 개발툴로 썼는데 그 당시 EGit 은 그닥 좋지 못했다.) 또한.. 코드리뷰를 이미 어느정도는 구두로 하긴 했어서(대부분 팀장님이 했었고 팀장님 퇴사 후엔 리뷰가 필요할 것 같은 코드만 같이 공유) 그게 더 빨랐기에 그닥 필요성을 느끼진 못했던거 같다.

하지만 지금 생각하면 아쉬움이 많이 남는다. 다들 단점들에 대해서도 익숙해지는데 시간이 필요했는데 그 기간만 어느정도 확보되었으면 좀 더 좋은 품질의 제품이 되지 않았을까? 라는 생각이다. (사실 솔루션이 시간이 지나면서 매우 덩치가 커졌고 그러면서 에러를 잡는데 시간을 많이 쓰게 되었다.) 코드리뷰가 기술부채를 반드시 해결할 것이라고 보장할 순 없지만 개인의 발전 및 제품의 코드 컨벤션을 지키기 위해서라도 필요하다고는 생각한다. (결국엔 그것이 기술부채를 조금씩 갚아나가는 과정이라고 생각한다.)

다른 강연 후기들은 내일부터 천천히 써야겠다. 벌써 잘 시간이네..

관련자료

  • https://www.imaso.co.kr/archives/4481
  • http://it.chosun.com/site/data/html_dir/2018/12/15/2018121500832.html

자바에서 클라이언트 IP 주소를 얻는 방법

|

기본적으로는 Java 서블릿에서 HttpServletRequest.getRemoteAddr()을 사용하여 Java 웹 프로그램에 접근하는 클라이언트의 IP 주소를 가져올 수 있다.


import javax.servlet.http.HttpServletRequest;

protected String getRemoteAddr(HttpServletRequest request){
    return request.getRemoteAddr();
}

하지만 프록시 환경 또는 클라우드 위에 있는 웹 응용 프로그램의 경우 HTTP 요청 헤더 X-Forwarded-For(XFF)를 통해 클라이언트 IP 주소를 가져와야 한다.
WAS 는 보통 2차 방화벽 안에 있고 Web Server 를 통해 client 에서 호출되거나 cluster로 구성되어 load balancer 에서 호출된다. 이럴 경우에서 getRemoteAddr() 을 호출하면 접근한 클라이언트의 외부아이피가 아닌 웹서버나 load balancer의 IP 가 나오면서 같은아이피가 계속 찍히게 된다.
위와 같은 문제를 해결하기 위해 사용되는 HTTP Header인 X-Forwarded-For 값을 확인해서 있으면 해당 키값을 사용하고 없으면 getRemoteAddr() 를 사용한다.


import javax.servlet.http.HttpServletRequest;

protected String getRemoteAddr(HttpServletRequest request){
    return (null != request.getHeader("X-FORWARDED-FOR")) ? request.getHeader("X-FORWARDED-FOR") : request.getRemoteAddr();
}

참고

  • https://stackoverflow.com/questions/29910074/how-to-get-client-ip-address-in-java-httpservletrequest

github jekyll blog를 구글에서 검색되도록 설정하기. (naver, daum 포함)

|

네이버나 티스토리 같은 블로그 서비스들은 자동으로 검색이 노출되어 구글에서 바로 검색이 가능하도록 되어있다.
하지만 github와 같은 플렛폼을 사용하여 블로그를 만들거나 직접 사이트를 구성해서 만들경우엔 구글에서 검색이 되지 않는다고 한다.
아래는 내가 만든 블로그가 검색이 되도록 했던 방법이다.
참고로 이 블로그는 jekyll을 사용하여 제작하였다.

절차

  1. sitemap.xml 파일 작성.
  2. RSS feed 생성.
  3. robots.txt 만들기.
  4. 검색엔진에 등록하기.

차례대로 등록해보자.

1. sitemap.xml 파일 작성

블로그의 /root 경로에 /sitemap.xml 파일을 만들고 아래의 내용을 복사해 넣는다. 반드시 root 디렉토리에 넣어야 한다.

sitemap.xml



---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    {% for post in site.posts %}
    <url>
        <loc>{{ site.url }}{{ post.url | remove: 'index.html' }}</loc>
    </url>
    {% endfor %}

    {% for page in site.pages %}
    {% if page.layout != nil %}
    {% if page.layout != 'feed' %}
    <url>
        <loc>{{ site.url }}{{ page.url | remove: 'index.html' }}</loc>
    </url>
    {% endif %}
    {% endif %}
    {% endfor %}
</urlset>


제대로 작성 했을 시 주소로 접근하면 아래와 같이 나온다.

2. RSS feed 생성

네이버와 다음에 검색이 노출되게 하기 위해 rss feed 파일을 생성한다.
sitemap.xml과 마찬가지로 root 디렉토리에 /feed.xml파일을 생성하고 아래의 코드를 복사한다.

feed.xml


---
layout: null
---
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ site.title | xml_escape }}</title>
    <description>{{ site.description | xml_escape }}</description>
    <link>{{ site.url }}{{ site.baseurl }}/</link>
    <atom:link href="{{ "/feed.xml" | prepend: site.baseurl | prepend: site.url }}" rel="self" type="application/rss+xml"/>
    <pubDate>{{ site.time | date_to_rfc822 }}</pubDate>
    <lastBuildDate>{{ site.time | date_to_rfc822 }}</lastBuildDate>
    <generator>Jekyll v{{ jekyll.version }}</generator>
    {% for post in site.posts limit:30 %}
      <item>
        <title>{{ post.title | xml_escape }}</title>
        <description>{{ post.content | xml_escape }}</description>
        <pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
        <link>{{ post.url | prepend: site.baseurl | prepend: site.url }}</link>
        <guid isPermaLink="true">{{ post.url | prepend: site.baseurl | prepend: site.url }}</guid>
        {% for tag in post.tags %}
        <category>{{ tag | xml_escape }}</category>
        {% endfor %}
        {% for cat in post.categories %}
        <category>{{ cat | xml_escape }}</category>
        {% endfor %}
      </item>
    {% endfor %}
  </channel>
</rss>


제대로 작성 했을 시 주소로 접근하면 아래와 같이 나온다.

3. robots.txt 생성

  • 필자는 모든 검색엔진을 허용하도록 추가하였다.
  • 해당내용 나무위키 : https://namu.wiki/w/robots.txt
User-agent: *
Allow: /

Sitemap: https://jmlim.github.io/sitemap.xml

4. 구글에 검색되도록 하기

  1. 구글 웹 마스터 도구 접속 [https://www.google.com/webmasters/#?modal_active=none]
  2. 구글 서치 콘솔 클릭 후 블로그 사이트 주소 추가. (ex: https://jmlim.github.io/)
  3. 추가 후 소유권을 확인하기 위해 html 확인(googlexxxxxx.html) 파일을 받아 root 경로에 추가.
  4. 소유권이 확인되면 Google Search Console 화면으로 전환되어 나오는데 색인에 Sitemaps 에 사이트맵의 경로를 추가해준다.
  5. 추가한 후 색인이 접수중임을 확인할 수 있다.

5. 네이버에 검색되도록 하기.

  1. 네이버 웹마스터 도구 접속 [https://webmastertool.naver.com/]
  2. 네이버에도 블로그 사이트 주소 추가. (ex: https://jmlim.github.io/)
  3. 사이트 소유 확인을 위해 html 확인 파일 다운로드 후 root 경로에 추가.
  4. html 확인 파일이 블로그에 업로드 되어있는지 확인 후 확인버튼 클릭.
  5. 위 블로그의 웹마스터 도구에 접속한다.
  6. 요청 -> RSS 제출 메뉴를 클릭 한 후 URL을 포함한 주소인 blogurl/feed.xml을 입력 후 확인한다. ( ex: https://jmlim.github.io/feed.xml)
  7. 요청 -> 사이트맵제출로 들어가서 사이트맵을 등록한다. (ex : https://jmlim.github.io/sitemap.xml)

6. 다음에 검색되도록 하기.

  1. 다음 검색등록 사이트 접속 [https://register.search.daum.net/index.daum]
  2. 등록 탭에서 블로그 등록을 선택하고 주소를 입력한다.

검색엔진에 노출되기까지는 시간이 어느정도 걸린다고 한다. 얼마나 걸릴지는 지켜봐야겠다.

참고자료:

  • http://jinyongjeong.github.io/2017/01/13/blog_make_searched/
  • https://wayhome25.github.io/etc/2017/02/20/google-search-sitemap-jekyll/
  • https://devmjun.github.io/archive/addSearch

객체가 들어있는 컬렉션(ArrayList or HashSet 등) 목록 깊은 복사(Deep copy) 하기.

|

Java의 Collection은 기본적으로 얕은 복사(shallow copy)는 제공하나 깊은 복사기능은 제공하지 않는다. 즉, 원본 목록과 복제된 목록에 저장된 객체가 동일하고 그렇다는것은 Java 힙 공간에서 동일한 메모리 위치를 가리켜 문제가 될 수 있다.

예를 들면 직원 정보가 들어있는 ArrayList의 생성자를 사용하여 복제시엔 직원 객체 정보는 그대로 있으므로 복제된 list의 객체를 수정시에 같이 영향을 받게 된다.

아래 예제이다.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class CollectionCloningTest {
    public static void main(String args[]) { 
        List<Employee> org = new ArrayList<>();
        org.add(new Employee("MA", "Manager"));
        org.add(new Employee("AH", "Developer"));
        org.add(new Employee("JM", "Developer"));

        // 생성자를 통해 copy list 생성.
        List<Employee> copy = new ArrayList<>(org);

        System.out.println("ORG LIST : " + org);
        System.out.println("COPY LIST : " + copy);

        Iterator<Employee> itr = org.iterator();
        while (itr.hasNext()) {
            itr.next().setDesignation("Specialist");
        }

        //org list 에 해당하는 값을 바꿨으나 copy list 에 들어있는 객체값도 같이 바뀌게 된다.
        System.out.println("ORG LIST : " + org);
        System.out.println("COPY LIST : " + copy);
    }
}

class Employee {
    private String name;
    private String designation;

    public Employee(String name, String designation) {
        this.name = name;
        this.designation = designation;
    }

    public String getDesignation() {
        return designation;
    }

    public void setDesignation(String designation) {
        this.designation = designation;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return String.format("%s: %s", name, designation);
    }
}
  • 출력값 : org list 에 해당하는 값을 바꿨으나 copy list 에 들어있는 객체값도 같이 바뀌게 된다.

    ORG LIST : [MA: Manager, AH: Developer, JM: Developer]
    COPY LIST : [MA: Manager, AH: Developer, JM: Developer]
    ORG LIST : [MA: Specialist, AH: Specialist, JM: Specialist]
    COPY LIST : [MA: Specialist, AH: Specialist, JM: Specialist]

복제본이 단순 복사이고 힙의 동일한 Employee 개체를 가리 키기 때문에 원본 컬렉션의 Employee 개체 ( “Specialist”로 변경된 지정)가 복제 된 컬렉션에도 반영되어 있음을 확인할 수 있다.
이 문제를 해결하려면 Collection을 반복할 시 Employee 객체를 깊게 복제해야하며 그 전에 Employee 객체의 복제 메소드를 재정의해서 넣어야한다.

해결방법

1) 객체 복사를 할 수 있도록 하는 인터페이스 구현 (Employee가 Cloneable 인터페이스를 구현하도록 한다.) 2) Employee 클래스에 clone() 메소드 추가

/** 1. 기존 Employee 클래스에 Cloneable을 구현하도록 추가. 2. clone 메소드 구현. */

class Employee implements Cloneable {
    private String name;
    private String designation;

    public Employee(String name, String designation) {
        this.name = name;
        this.designation = designation;
    }

    public String getDesignation() {
        return designation;
    }

    public void setDesignation(String designation) {
        this.designation = designation;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return String.format("%s: %s", name, designation);
    }

    @Override
    public Employee clone() {
        Employee clone = null;
        try {
            clone = (Employee) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
        return clone;
    }
}

생성자를 사용하여 List를 복사하는 대신 다음 코드를 사용하여 Java에서 콜렉션 전체 복사

public class CollectionCloningTest {
    public static void main(String args[]) { // deep cloning Collection in Java
		....위와 같은 로직...
			
		//deep copy 로직
		List<Employee> deepCopy = new ArrayList<>(org.size());
		Iterator<Employee> iterator = org.iterator();
		while (iterator.hasNext()) {
			deepCopy.add(iterator.next().clone());
		}

		//deepCopy list 에 해당하는 값을 바꿨으나 org list 에 있는 객체값이 영향을 받지 않는다..
		deepCopy.get(0).setDesignation("Expert");

		System.out.println("ORG LIST : " + org);
		System.out.println("DEEP COPY LIST : " + deepCopy);
	}
}

참고자료

  • https://javarevisited.blogspot.com/2014/03/how-to-clone-collection-in-java-deep-copy-vs-shallow.html#axzz5ZXZJA1NR

스프링 부트에서 JSP view 설정하기.

|

기본적으로 spring-boot-starter-web 에 포함된 tomcat은 JSP를 포함하지 않는다.
하지만 간단한 설정만으로도 JSP view를 사용 가능하다.

일단 dependency를 pom.xml 에 추가한다.

pom.xml 에 아래 구문 추가.

....
<!-- JSP 쓰기 -->
<dependency>
	<groupId>org.apache.tomcat.embed</groupId>
	<artifactId>tomcat-embed-jasper</artifactId>
</dependency>

<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>jstl</artifactId>
</dependency>
....

application.properties 에 추가할 내용 (jsp 파일이 들어갈 경로 및 확장자 지정.)


spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

jsp 페이지 추가.


ex) signin.jsp

<!DOCTYPE html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>로그인</title>
</head>
<body>
로그인 페이지가 될 부분.
</body>
</html>

컨트롤러 작성.


import java.util.Date;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class SignController {
  
  @GetMapping("/")
  public String index(Map<String, Object> model) {
    model.put("time", new Date());
    return "index";
  }
  
  @GetMapping("/signin")
  public String siginin(Map<String, Object> model) {
      model.put("time", new Date());
      return "signin";
  }
  
  @GetMapping("/signup")
  public String siginup(Map<String, Object> model) {
    model.put("time", new Date());
    return "signup";
  }
}

url 호출

  • http://localhost:8080
  • http://localhost:8080/siginin
  • http://localhost:8080/siginup

위 url 을 차례대로 실행 시 각각 index.jsp, siginin.jsp, siginup.jsp 파일이 띄워짐을 확인할 수 있다.