동작 파라미터화 : 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록, 코드 블록의 실행은 나중으로 미뤄진다.
변화하는 요구사항에 유연하게 대응할 수 있게 코드를 구현하는 방법
리스트의 모든 요소에 '어떤 동작'을 수행할 수 있음
리스트 관련 작업을 끝낸 다음에 '어떤 다른 동작'을 수행할 수 있음
에러가 발생하면 '정해진 어떤 다른 동작'을 수행할 수 있음
1. 변화하는 요구사항에 대응하기
DRY(don't repeat yourself) : 같은 것을 반복하지 말것.
문자열, 정수, 불린 등의 값으로 메서드를 파라미터화 할 때 => 한줄이 아니라 메서드 전체 구현을 바꾸어야 한다.
녹색 사과 필터링 filterGreenApples(List<Apple> inventory) -> 색에 따른 필터링 filterApplyByColor(List<Apple> inventory, String color) -> 속성 필터링 filterApples(List<Apple> inventory, String color, int wieght, boolean flag)
별로다
2. 동작 파라미터화
프레디케이트 Predicate : 선택 조건을 결정하는 인터페이스 => 어떤 속성에 기초해서 불린값을 반환
전략 디자인 패턴(strategy design pattern)
각 알고리즘(전략이라 불리는)을 캡슐화하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법
객체가 할 수 있는 행위를 전략으로 만들고 동적으로 행위의 수정이 필요한 경우 전략으로 바꾸는 것만으로도 행위의 수정이 가능하도록 만든 패턴
public interface ApplePredicate{
boolean test(Apple apple);
}
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return "green".equals(apple.getColor());
}
}
public class AppleHeavyWeightPredicate implements ApplePredicate{
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
동작 파라미터화 : 메서드가 다양한 동작(전략)을 받아서 내부적으로 다양한 동작을 수행한다.
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple))
result.add(apple);
}
return result;
}
List<Apple> heavyApples = FilteringApple.filterApples(inventory, new AppleHeavyWeightPredicate());
List<Apple> greenApples = FilteringApple.filterApples(inventory, new AppleGreenColorPredicate());
내가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정된다.
한 메서드가 다른 동작을 수행하도록 재활용이 가능하다 : 한개의 파라미터 다양한 동작
3. 간소화 하기
a. 익명 클래스
익명클래스(anonymous class) 기법 : 클래스의 선언과 인스턴스화를 동시에 수행할 수 있는 기법
클래스 선언과 인스턴스화를 동시에 할 수 있다 == 즉석에서 필요한 구현을 만들어서 사용할 수 있다.
Predicate 인터페이스를 구현하는 클래스 만들어서 인스턴스 화 해야한다 (귀찮음)
List<Apple> redApples = FilteringApple.filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return "red".equals(apple.getColor());
}
});
assertThat(redApples.size()).isEqualTo(2);
그러나 장황하다 : 나쁘다!
람다 표현식으로 코드를 더 간결하게 정리할 수 있다.
b. 람다 표현식
List<Apple> redApples = FilteringApple.filterApples(inventory, apple -> "red".equals(apple.getColor()));
유연함과 간결함 두가지를 모두 가져간다!
c. 형식 파라미터를 이용한 추상화
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T e: list){
if(p.test(e))
result.add(e);
}
return result;
}
d. 종합: 동작 파라미터화의 3가지 방법
클래스
익명 클래스
람다
5. 실전 예제
a. Comparator 정렬
sort 동작을 파라미터화 할 수 있다.
// 익명클래스 이용
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});
// 람다식 이용
inventory.sort((a1, a2)->a1.getWeight().compareTo(a2.getWeight()));
b. Runnable 코드 블록 실행
// 익명클래스 이용
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("helloWorld");
}
});
t1.run();
// 람다식 이용
Thread t2 = new Thread(() -> System.out.println("helloWorld"));
t2.run();
계속 써야지 써야지 하면서 미루다가 인턴십이 끝난 지 거의 2달가량이 돼서 사실 시간이 꽤 많이 지났지만, 아무래도 첫 면접, 첫 실무의 기억이기 때문에 아직도 생생해서 기록으로 남긴다 :)
재택이 끝나는 날 받았던 웰컴 키트
인턴십 준비 과정
1학년 때는 대학 개발 동아리를 위주로 2~3학년 때는 각종 회사 연계 대외활동을 위주로 내 개발 실력을 한 단계씩 높이려는데 목적이 있었다. 하지만 더 이상 대학생들 사이가 아닌 더 많은 현직자분들을 만나고 나도 현직자로 나아가고 싶다는 생각을 가지게 되었다.
그래서 3학년 2학기 말에는 네이버 핵 데이, 우아한 프리코스부터 시작해서 대학생 프로젝트에서 인턴십 및 취업으로 나아갈 수 있는 활동에 몰두했었다. 하지만 두 프로그램 모두 인턴십, 테크 코스까지는 이어지지 못했고, 안 좋은 결과에 기가 죽기보다는 오히려 시선을 돌려서 인턴십에 직접 지원을 하자고 마음을 먹게 되었다.
아무래도 네이버 핵데이를 2번이나 참가하다 보니 네이버라는 회사는 나에게 시작점이 되고 싶은 회사였다. 직접 작성한 레주메를 바탕으로 마침 기회가 생겨서 의도치 않게 네이버 인턴십에 여러 프로세스로 지원을 하게 되었는데
1. 클로바에 12월 경 교수님 추천으로 인턴십 지원을 하게 되었고, 서류 -> 코딩 테스트 후에 결과를 기다리고 있었다.
2. 클로바 코딩 테스트 결과가 약 한 달 동안 안 나와서 떨어졌다 생각했고, 네이버 웹툰 체험형 인턴십을 지원했다. 그 결과 서류 통과 후 면접에 오라는 메일을 받았다.
3. 네이버 웹툰에 지원서를 낸 상태였는데 우연히 아폴로 CIC 쪽에 지인 추천으로 서류를 낼 수 있는 기회가 생겨, 서류 통과 후 면접을 보았다 (당시 매우 프로세스가 빨랐다)
4. 아폴로 CIC 쪽에서 가장 빠르게 합격 결과를 알려주어서 아폴로 CIC 백엔드 인턴십을 진행하게 되었다. (의도치 않게 3개 조직에 민폐를 끼친 건 아닌가 싶어서 당시 매우 조마조마하면서 웹툰 면접을 거절했다) (클로바에서도 면접에 오라는 연락을 받았지만 아폴로 입사가 확정이라 거절했다)
조직마다 달랐던 결과 메일 템플릿
1차 면접
아폴로 CIC에서 본 면접이 내 인생 첫 인턴십 면접이었다. 그래서 당시 매우 떨리는 마음으로 면접을 봤던 기억이 난다. 당시에 무슨 깡이었는지ㅋㅋ 면접에서 물어볼 법한 컴퓨터 공학 지식에 대한 공부를 하나도 안 하고 갔다. (패기 봐라) 당시 외주 프로젝트 기한 때문에 면접 준비를 많이 못했었고, 같이 스터디를 했었던 자바봄(http://javabom.tistory.com/) 멤버들이 한번 봐줬던 모의 면접이 끝이었는데🧐그래서 잠을 못 자서 밤을 꼴닥 새우고 면접을 보러 갔었다.
나는 주로 기존에 알고 있던 Java와 관련된 지식, Cleancode, 자주 사용하던 test 관련 개념, 사용했던 디자인 패턴 위주 복습하고, 이전에 했던 프로젝트에서 사용했던 기술의 간단한 특징 정도를 리마인드하고 갔다.(queryDSL을 사용했을 때 장점 같은 것들!)
면접을 준비할 때는 기술에 대한 키워드만 적어두고 그 키워드에 대한 내용을 보기만 하기보다는, 실제로 다른 사람에게 전달한다 생각하고 말을 하면서 준비를 했었다. 이렇게 하다 보니 이 키워드에 대한 질문이 나왔을 때 어떤 흐름으로 대답을 해야겠다는 노하우가 생겨서 수월했다.👍
면접 질문은 크게 2가지 종류였다.
1. 종이에 문제가 2가지 적혀있었고, 30분 동안 각 문제에 대한 해결법을 말로 풀어내는 것이었다. 문제 중 기억에 남는 문제가 있었는데 당시 복기해서 적어놨었다.
25마리의 말이 있습니다. 이중에 가장 순서대로 빠른 말 3마리를 찾아내고 싶습니다. 말의 절대 속도는 알 수가 없으며, 한 경주에 최대 5마리의 말이 참여할 수 있고, 그들의 상대적인 순위만을 알 수 있습니다 최소한의 횟수로 찾아내려면 어떤 방법을 써야 할까요?
이 문제를 풀 때 종이에 적어가며 설명을 드렸었는데, 처음에 낸 정답에 대해 "여기서 더 최적화할 수 있을 것 같아요!", "이런 경우엔 어떻게 해야 할까요?" 등의 질문을 면접관님이 넌지시 던져주셔서 정답에 도달할 수 있었다. 내 생각을 기반으로 면접관 님이랑 소통하면서 같이 정답에 도달한 느낌이라 아직도 기억에 남을 정도로 재미있었다👍
2. 두 번째로는 약 1시간 반 동안 자바에 대한 기술 질문이 이어졌다. JVM의 구조, GC의 동작 방식과 같은 기본적인 질문을 하시기도 했고, 내 깃허브에 들어가서 했던 프로젝트에서 dto와 domain 폴더링의 이유를 여쭤보셨던 기억도 난다. 외에도 테스트 코드를 짜면 좋은 점이나 테스트 코드를 짤 때 어떤 부분을 생각하며 짜는지를 여쭤보셨다.(자바 봄 스터디원들이 해준 모의 면접에 나왔던 질문이 많이 나와서 정말 좋았다)
그 결과 합격!!! 3월로 입사일이 정해지게 되었다.
당시 원래는 휴학을 하려 했었는데, 채용형 인턴십이었기 때문에 빠른 졸업이 필요해 4학년 1학기 학점을 인턴십으로 대체할 수 있도록 교수님과 조율하면서 정신없이 입사를 기다렸었다.
코로나로 인한 원격 입사
그렇지만 3월에 코로나 확진자 수가 급격하게 늘어나면서 입사일이 미뤄지는 사태가 발생했다. 많이 기다리던 입사일이었는데 코로나로 인해 걱정을 하면서 회사 측의 연락을 기다렸었다.
그 결과 입사일이 일주일이 미뤄졌었고, 미뤄진 입사일 조차도 네이버에서 풀 재택근무 체계로 들어가다 보니 원격 입사를 하게 되었다. OT 진행도 화상으로 했고, 각종 계약서도 모두 메일로 받았다.
그래서 입사일에 각종 회사 장비가 집으로 배송이 되었고, 당시 생활멘토, 기술멘토 멘토님이두 분이 계셨는데 웍스로만 연락을 드리게 되었다.
그래서 입사일부터 약 한 달 반 정도 풀 재택근무를 했었다. 그리고 남은 한 달 반은 주 2회 출근을 했었어서 그때 팀원분들이나 멘토님들을 볼 수 있었다.
인턴 과제
입사 2일 후에 멘토님과 앞으로 인턴십 기간 동안 어떤 과제를 진행할지에 대한 부분을 이야기했었다. 나는 모먼트 팀 소속이었는데, 모먼트는 4월에 런칭된 서비스라서 한 달 동안 다른 분들께 내 부서를 설명할 때 둘러 대며 말했던 기억이 난다.
인턴 기간 동안 내가 했던 job은 3가지였다.
1. 이펙티브 자바와 클린 코드를 읽고 정리하기
2. 현재 모먼트 서버와 비슷한 환경의 토이 프로젝트를 진행하기 2-1. 네이버 서비스를 연동하는 서버 만들기 2-2. 만든 서버를 k8s를 이용하여 배포하기 2-3. jenkins pipeline을 작성하여 해당 서버의 CI/CD 구축하기 2-4. sonarlint를 사용하여 코드 리팩터링 하기
책 읽는 과제를 하면서 내가 집중했던 건 모르는 구절이 나와도 넘어가지 않는 것이었다. 자바 봄 스터디에서 이 방식으로 책을 읽고 있어서 이 과제를 할 때도 영향을 줬었다.
다른 사람 블로그를 찾아보기도 하고, oracle docs를 찾아보기도 하고, 책에 있는 내용을 코드로 작성해보기도 했었다. 당시 prallel stream 부분을 읽으면서 forkjoinpool에 대한 이야기가 나왔었는데, JDK 코드를 보면서 까지 책을 읽었던 기억이 난다.
ForkJoinPool JDK 코드를 보고 참조관계에 대한 정리를 해놓은 필기
두 책이 인턴 과제였던 점은 아직까지도 영향을 미치고 있다. 사실 나는 개발 서적 읽는걸 별로 안 좋아했다.대학원 생각이 없는 이유가 문서를 많이 읽어야 하는 게 싫어서였을 정도로그런데, 당시 매주 정해진 분량에 대한 계획을 세우고 책을 읽었었는데 이 습관이 지금까지 이어지고 있다 (요즘은 하루에 한 챕터씩 자바 8 인 액션을 읽고 있다.)
생각보다 내가 잘 알고 있다고 생각했던 자바에는 공부를 계속해도 궁금한 점이 나왔었고, 실제로 내가 모르던 부분에 대한 키워드를 책에서 얻어서 몇 번 포스팅한 적이 있다.
그래서 이 과제 이후로 부터 책을 읽는다는 것에 거부감이 덜 생기게 되었고, 앞으로 읽고 싶은 책이 정말 많다. (실제로 많이 사뒀다..ㅎㅎ)
2. 현재 모먼트 서버와 비슷한 환경의 토이 프로젝트를 진행하기
주로 삽질속에서 성장을 하는 과정이었다ㅋㅋ 아무래도 당시 모먼트의 런칭이 얼마 안 되었을 때였고, 나의 집념도 한몫해서 멘토님께는 정말 해결이 안 될 때만 여쭤봤었다. 해결이 안 될 때에는 "어떤 부분을 하고 있는데, 이런 부분이 해결이 안돼서 이렇게 에러를 해결해 보려고 몇 가지 방법을 찾아보았습니다 제가 생각할 때는 이 부분이 문제인 것 같아 이 부분을 다시 찾아보려고 하는데 올바른 방향이 맞나요?" 이런 식의 현재의 상황을 브리핑하면서 조언을 얻는 방향으로 여쭤봤었다.
아무래도 구글링을 해서 바로 나오는 내용이면 좀 민망하기도 하고, 한심해 보이지 않을까 싶어서 그랬는데, 후에 멘토님 피드백으로 뭔가 혼자 알아서 해결하고 혼자 잘 정리해오는 스타일이었다고...ㅋㅋㅋㅋ
2-1. 연동 서버 구현
먼저 네이버 api를 연동하여 여러 서비스의 response 값을 조회하고, 이 내용을 디비, 캐시에 CRUD 하는 서버를 만들었다. 사실 구현은 어렵지 않았지만 이 부분에서는 멘토님과의 커뮤니케이션을 많이 할 수 있었다. 요구사항에 대해 구체화하고, 나에게는 접근권한이 없는 docs 페이지의 경우엔 멘토님께 도움을 요청해야 구현까지 할 수 있었기 때문이다.
코드를 구현하면서 가장 내가 신경 썼던 것은 테스트 코드 작성이었다. 한 가지 기능을 구현할 때 그 기능과 대응하는 TC를 작성하려고 의식적으로 생각했었다. 그래서 구현 중간에 test line coverage를 측정해봤을 때 구현을 마치고 측정했을 때 모두 80프로 대가 나왔었다.
혼자 하는 과제이지만 테스트 코드는 내가 작성한 프로젝트의 요구사항을 알 수 있는 커뮤니케이션 수단이라고 생각했기 때문에, @DisplayName까지 꼼꼼하게 작성했었다 (미래의 내가 봤을 때 못 알아보는 경우를 그나마 방지하기 위해)
2-2. 컨테이너 환경에서의 배포
이렇게 완성한 서버를 k8s를 이용해서 배포해야 했었는데, 당시 나는 docker, k8s에 대한 지식이 전무했다. (대충 환경을 저장하는 게 도커구나 만 알고 있었음) 과제를 하면서 어떻게든 공부를 해야 하다 보니 익히게 되었는데 요즘 개발할 때 정말 유용하게 써먹고 있다ㅋㅋㅋ (얼마 전에 나간 엔젤핵의 배포 시스템이 dockerfile 기반이라 수월했다)
당시 helm chart를 이용해서 pod 오브젝트 스펙들을 관리했었고, 사내 컨테이너 배포 시스템을 이용해 배포 파이프라인까지 모두 잘 작성하였다. 당시 과제 기한이 3주였는데 여기까지 다 했을 때 1주가 남아서, 추가적으로 HPA 설정, ELK 설정까지 도전해 볼 수 있었다.
이 부분에 대한 인턴 발표를 진행했을 때, 사내 컨테이너 배포 시스템의 불편한 점에 대해 언급했었다. 그때 한 개발자 님이 본인 팀에도 도입하려 했었는데 이런 불편함을 참고해서 개발해야겠다고 발표 내용이 인상적이라고 피드백 주셨었는데 진짜 뿌듯했었다.
다시 해보자. (단호한 온점)
배포를 하면서 여러 Devops적 지식을 알게 된 점이 있었다. 그래서 매일매일 위 사진처럼 트러블 슈팅에 대한 문서를 작성했었다. 근데 대부분의 말이 "다시 해보자." "왜 안되지" "으아아아ㅏㄱ!!" 이런 거였음ㅋ
2-3. jenkins pipeline을 이용한 CI/CD
jenkins도 인턴 과제 덕분에 처음 사용해봤었다 (그동안은 팀원들이 해주던 CI/CD를 사용했었으니..)
역시나 삽질은 많이 했었는데, 삽질속에 깨달음이 많았음.. jenkins는 앞으로 사용할 일이 많으니 좋은 경험이었다 생각 중이다ㅋㅋ
아무래도 처음 jenkinsFile을 작성하다 보니 문법 오류도 나에겐 퍼포먼스 저하의 하나의 원인이었다. 그때 사내 github에 있는 소스코드를 가져와서 해당 소스 폴더에 있는 jenkinsFile을 읽어 jenkins pipeline을 실행하도록 했었는데 문제가 많았다.
1. jenkinsFile 문법이 맞는지 확인하려고 한 줄을 수정해도 그때그때 github에 push 했었다.
2. 후에 commit 내역이 더러워지고 있음을 깨닫고 force push를 막 했었다 그래도 github에 push를 계속해보면서 jenkinsFile을 테스트한다는 사실이 올바른 방법 같진 않았다.
3. 똑똑한 방법으로 발전! : jenkins안에서 jenkinsFile 동작을 확인할 수 있게 script 테스트 jenkins의 build 내역에 들어가서 replay 버튼을 이용해 작성한 스크립트의 재 빌드를 할 수 있었다. 이렇게 하니 그때그때 빌드 파이프라인이 잘 작동하는지 여부를 확인할 수 있었다. (앞으로 종종 써먹을 듯하다.)
2-4. sonarlint를 사용하여 코드 리팩터링 하기
마지막 1주에는 리팩터링 및 코드 품질을 향상하는 작업을 했었는데, 이때 테스트의 중요성을 다시 한번 느꼈다. 소나린트에 따라 소스를 리팩터링 할 때, 패키지 명을 바꾸는 간단한 리팩터링이더라도, 사람의 실수로 혹은 테스트 스코프가 벗어나서 등 다양한 이유로 소스가 안 돌아갈 수 있었다.
코드 악취가 146에서 13으로 줄어드는 걸 보면서도, 리팩터링 과정에서 에러가 없음을 보장받을 수 있는 수단이 TestCase 였다. 테스트의 중요성은 아무리 말해도 부족한 듯.
3. 모먼트 팀 소속 인턴
당시 남들보다 먼저 사용해 볼 수 있었던 모먼트!!
모먼트 팀의 소속으로 각종 회의에 참석하고 팀원 분들과 개발에 대한 이야기부터 앞으로 계획에 대한 이야기도 하면서 실무에 대한 이해를 높일 수 있었다.
인턴 시작 3일 때 모먼트 프로세스 문서를 보고 궁금한 점을 정리했었는데, 실무가 아니면 몰랐을 사실들이 있었다. (예를 들면 DB설계 문서에서 Auto Increasement가 빠져있었는데, 분산 DB를 사용하기 때문이라는 사실이 너무 놀라웠다ㅋㅋㅋ)
팀원들과 일과를 보낼 때 나와 같은 인턴도 똑같은 개발자라는 마음으로 대해주셔서 좋았다. 일부 이슈 해결에 대한 일정이 있었음에도, 페어 프로그래밍으로 팀원들과 같이 도메인 지식의 수준을 동등하게 맞춰나가는 시니어 개발자님의 모습이 좋았다. 인턴임에도 옆에서 같이 보고 의견을 낼 수 있었고 환상으로만 있었던 이런 개발자 회사의 문화에 감동했었다.
그리고 코로나 시기 인턴으로서 팀원들 그리고 멘토님께 감사했던 점은 활발한 비대면 커뮤니케이션이었다! 사실 인턴십을 하면서 모르는 점을 물어볼 때 메신저로 갑자기 연락을 드리기가 죄송했었다(이건 내 성격이다. 다들 친절하셨음). 그래서 초반에는 차라리 직접 만나서 대면 인턴십을 진행했다면 좀 달랐을까 싶었는데, 뒤로 갈수록 비대면으로도 너무 의사소통이 잘돼서 재택을 사랑하게 되었다..ㅎㅎ
팀 내 시니어 개발자님이 나를 만나고 얼마 안 되었을 때, 갑자기 웍스로 "민정님 이 코드의 문제점이 뭘까요?" 하고 먼저 말을 걸어주셨는데, 코드에 대한 이야기를(Optional과 Null을 같이 사용할 때 문제점에 대한 이야기를 했었다.) 30분 내내 웍스로 집중해서 하다 보니까, 얼굴을 뵌 적이 없는 분임에도 내적 친밀감이 마구 상승했었다ㅋㅋㅋ
외에도, 과제를 하다가 의존성 때문에 문제가 생긴 적이 있었는데, 이 의존성 문제를 해결하려고 역시 웍스로 1시간 내내 커뮤니케이션하면서 해결했었다.
또 팀원 분들과 많은 커피타임을 가지면서 운동의 중요성에 대한 부분을 정말 많이 들었었는데ㅋㅋ (사실 지금도 엄청 듣고 있다) 개발 외적으로도 자기 계발에 힘쓰는 분들과 일을 할 수 있어서 감사하다는 마음이 들었다.
인턴십 끝
배운 것들 부터해서 좋은 팀원 분들까지 얻은 게 너무 많은 인턴십이었지만, 아쉽게도 채용까지 이어지지는 못했다.
인턴 평가는 좋았으나, 전환면접 (2차 면접)에서 (다시 말하지만 나는 이 인턴십이 첫 기술면접이었다.) 너무 긴장한 나머지 나의 장점을 말하기보다는 내가 못하는 부분, 나의 단점을 더 드러내는 면접을 했었다.
항상 내 사진은 왜 화장실 찍...ㅋㅋㅋㅋ
어느 때보다 치열하게 살았던 3개월이었지만, 인턴십 자체만으로도 좋은 경험임을 알고 있기에 결과보다는 과정 위주로 기억이 남았다. (좋은 얘기만 있어서 포장이라 할 수 있는데, 포장아니다 ㄹㅇ 재밌었음) 아까운 결과이지만, 2달이 지났음에도 좋은 기억으로 남은 걸 보니 후기를 안 쓰면 후회할 것 같아서 기록으로 남긴다!
끝!
I'd like to write about my Naver internship experience, which lasted about 3 months from March to June of this year.
I kept telling myself I'd write it up but kept putting it off, and now it's been almost 2 months since the internship ended, so quite a bit of time has passed. But since it was my first interview and first real work experience, the memories are still vivid, so I'm putting them down in writing :)
The welcome kit I received on the day remote work ended
Preparing for the Internship
During my freshman year, I focused on university dev clubs, and in my sophomore and junior years, I focused on various company-affiliated extracurricular activities — all with the goal of leveling up my development skills step by step. But I started wanting to meet more industry professionals rather than just staying among college students, and I wanted to step into the professional world myself.
So toward the end of the second semester of my junior year, I dove into activities that could lead from student projects to internships and employment — starting with Naver Hack Day and Woowa Pre-Course. Unfortunately, neither program led to an internship or tech course, but rather than feeling discouraged by the bad results, I shifted my focus and decided to apply directly for internships.
Having participated in Naver Hack Day twice, Naver was a company I wanted as my starting point. Based on a resume I'd written myself, an opportunity came up and I unexpectedly ended up applying to Naver internships through multiple channels:
1. Around December, I applied for an internship at Clova through a professor's recommendation. I was waiting for results after the document screening -> coding test.
2. The Clova coding test results didn't come for about a month, so I assumed I'd been rejected and applied for the Naver Webtoon experiential internship. As a result, I passed the document screening and received an email inviting me for an interview.
3. While my Naver Webtoon application was still pending, I happened to get a chance to submit my resume to Apollo CIC through someone I knew. After passing the document screening, I had the interview (the process was super fast at the time).
4. Apollo CIC was the fastest to let me know I'd been accepted, so I went ahead with the Apollo CIC Backend Internship. (I felt terrible that I might have inconvenienced all three teams, and I was super nervous when I had to decline the Webtoon interview) (I also got an interview invitation from Clova, but I declined since my Apollo onboarding was already confirmed)
Each team had different result email templates
First Interview
The interview at Apollo CIC was the very first internship interview of my life. So I remember being incredibly nervous. Looking back, I don't know where I got the nerve lol — I went in without studying any of the computer science knowledge they might ask about. (The audacity!) I couldn't prepare much for the interview because of a freelance project deadline, and the only prep I had was a mock interview that my Java Bom (http://javabom.tistory.com/) study group members put me through 🧐 So I ended up pulling an all-nighter and went to the interview without any sleep.
I mainly reviewed Java-related knowledge I already knew, Clean Code concepts, testing concepts I frequently used, and design patterns I'd worked with. I also reminded myself of the key characteristics of technologies I'd used in previous projects (like the advantages of using queryDSL!).
When preparing for the interview, rather than just jotting down keywords and reading about them, I practiced actually speaking as if I were explaining to someone else. Doing this gave me a sense of how to structure my answers when questions about those keywords came up, which was really helpful. 👍
The interview questions fell into two main categories.
1. There were 2 problems written on paper, and I had 30 minutes to verbally explain my solutions. One problem that stuck with me — I actually wrote it down from memory at the time:
You have 25 horses. You want to find the top 3 fastest horses in order. You can't measure their absolute speed, and each race can have a maximum of 5 horses, where you can only see their relative rankings. What's the minimum number of races needed to find the top 3?
When solving this problem, I explained while writing on paper. When I gave my initial answer, the interviewer gently nudged me with questions like "I think you could optimize this further!" and "What would you do in this case?" — which helped me arrive at the correct answer. It felt like we reached the answer together through communication based on my ideas, and it was so fun that I still remember it vividly. 👍
2. The second part consisted of about an hour and a half of technical questions about Java. They asked basic questions like JVM structure and how GC works, and I also remember them going to my GitHub and asking about why I structured my DTO and domain folders the way I did. They also asked about the benefits of writing test code and what I think about when writing tests. (A lot of the questions from the mock interview my Java Bom study group gave me actually came up, which was really great!)
The result: I passed!!! My start date was set for March.
At the time, I was originally planning to take a leave of absence, but since it was a conversion-type internship, I needed to graduate quickly. So I coordinated with my professor to have my senior year first semester credits replaced by the internship, and I waited anxiously for my start date.
Remote Onboarding Due to COVID
However, as COVID cases surged in March, my start date got pushed back. I'd been looking forward to it so much, but I waited for the company's update while worrying about COVID.
As a result, the start date was pushed back by a week, and even on the delayed start date, Naver had transitioned to a full remote work system, so I ended up onboarding remotely. The orientation was conducted via video call, and all contracts were received by email.
So on my start date, all the company equipment was delivered to my home, and I had two mentors at the time — a life mentor and a tech mentor — and I could onlycontact them through Works (the company messenger).
So from my start date, I worked fully remote for about a month and a half. For the remaining month and a half, I went to the office twice a week, which is when I got to meet my team members and mentors in person.
Intern Tasks
Two days after joining, I discussed with my mentor what tasks I'd be working on during the internship. I was part of the Moment team, and since Moment was a service that launched in April, I remember having to talk around it when explaining my department to others for the first month.
During the internship, I had 3 main jobs:
1. Read and summarize Effective Java and Clean Code
2. Build a toy project in an environment similar to the current Moment server 2-1. Build a server that integrates with Naver services 2-2. Deploy the server using k8s 2-3. Set up CI/CD for the server by writing a jenkins pipeline 2-4. Refactor the code using sonarlint
3. Analyze the Moment service and attend meetings as a Moment team member
1. Reading and Summarizing Effective Java and Clean Code
What I focused on while doing this reading task was not skipping over any passages I didn't understand. I'd been reading books this way in my Java Bom study group, and that approach carried over to this task.
I'd look things up on other people's blogs, check the Oracle docs, and try coding out examples from the book. I remember reading the section on parallel streams, which led to a discussion about ForkJoinPool — I even ended up reading through the JDK source code while studying that part.
Notes I took on the reference relationships after looking at the ForkJoinPool JDK code
Having those two books as intern tasks still has a lasting impact on me. Honestly, I wasn't a big fan of reading dev books.I disliked reading documents so much that it was one of the reasons I didn't want to go to grad school.But back then, I'd set a plan each week for how much to read, and that habit has stuck with me to this day. (These days I'm reading a chapter a day of Java 8 in Action.)
I was surprised to find that Java, which I thought I knew well, kept raising new questions no matter how much I studied. I actually got keywords for things I didn't know from the books and wrote a few blog posts about them.
So after this task, I became much less resistant to reading books, and there are so many books I want to read now. (I've actually bought a bunch already..haha)
2. Building a Toy Project in an Environment Similar to the Moment Server
This was mostly a process of growing through trial and error lol. Since Moment had just recently launched at the time, and partly due to my own stubbornness, I only asked my mentor when I truly couldn't solve something. When I was stuck, I'd ask in this format: "I'm working on this part, and I'm stuck on this issue. I've looked into a few approaches to resolve it, and I think the problem is in this area, so I'm going to look into it further — am I heading in the right direction?" It was all about briefing the current situation and getting guidance.
I was a bit embarrassed to ask questions that could be easily Googled, and I didn't want to look clueless. Later, in my mentor's feedback, they said I was the type to figure things out on my own and come back with everything neatly organized... lol
2-1. Building the Integration Server
First, I built a server that integrated with Naver APIs to query response data from various services and perform CRUD operations with a database and cache. The implementation itself wasn't that difficult, but this part allowed me to communicate a lot with my mentor. I needed to flesh out the requirements, and for docs pages I didn't have access to, I had to ask my mentor for help before I could implement anything.
What I focused on most while coding was writing test code. When implementing a feature, I consciously made an effort to write corresponding test cases. So when I measured the test line coverage mid-implementation and after finishing, it was in the 80% range both times.
Even though it was a solo project, I considered test code as a communication tool that conveys the project's requirements, so I was thorough about writing @DisplayName annotations too. (To at least prevent my future self from not understanding what's going on)
2-2. Deploying in a Container Environment
I had to deploy the finished server using k8s, but at the time, I had zero knowledge of docker or k8s. (I vaguely knew that Docker was something that saves environments) Since I had to learn it for the task, I picked it up along the way, and I'm actually using it all the time these days lol (The deployment system for Angel Hack, which I recently participated in, was Dockerfile-based, so it went smoothly!)
At the time, I managed pod object specs using helm charts and successfully set up the entire deployment pipeline using the company's internal container deployment system. The task deadline was 3 weeks, and when I finished all of this with 1 week to spare, I was able to additionally try setting up HPA and ELK configurations.
When I gave my intern presentation on this part, I mentioned some pain points about the internal container deployment system. A developer there gave me feedback saying they'd been planning to adopt it for their team too and would keep these issues in mind — they said the presentation content was impressive. That made me really proud.
Let's try again. (Firm period.)
I learned a lot of DevOps knowledge while doing deployments. So every day I wrote troubleshooting documentation like the screenshot above. But most of it was like "Let's try again." "Why isn't this working?" "AAAARGH!!" lol
2-3. CI/CD Using Jenkins Pipeline
Jenkins was also something I used for the first time thanks to this intern task. (I'd always just used the CI/CD that teammates set up..)
There was plenty of trial and error as expected, but I learned so much through the struggle.. Jenkins is something I'll use a lot going forward, so I consider it a great experience lol
Since I was writing a JenkinsFile for the first time, even syntax errors were a source of performance slowdowns. I was pulling source code from the company's internal GitHub and running the Jenkins pipeline from the JenkinsFile in that source folder, but there were many issues.
1. To check if the JenkinsFile syntax was correct, I'd push to GitHub every time I changed even a single line.
2. Later I realized the commit history was getting messy and started force pushing carelessly. Still, testing the JenkinsFile by continuously pushing to GitHub didn't feel like the right approach.
3. Evolved to a smarter approach: Testing scripts directly within Jenkins I found that I could go into Jenkins build history and use the replay button to rebuild with a modified script. This way, I could verify whether the build pipeline was working correctly on the spot. (I'll definitely use this trick going forward.)
2-4. Refactoring Code Using SonarLint
In the last week, I worked on refactoring and improving code quality, and this is when I once again felt the importance of testing. When refactoring based on SonarLint suggestions, even simple refactoring like changing a package name could break things — due to human error, tests going out of scope, and various other reasons.
Watching the code smells drop from 146 to 13, the thing that guaranteed there were no errors during refactoring was TestCase. You can never say enough about the importance of testing.
3. Being an Intern on the Moment Team
Moment, which I got to try before everyone else!!
As a member of the Moment team, I attended various meetings and talked with team members about everything from development to future plans, which helped deepen my understanding of real-world work.
On my third day as an intern, I reviewed the Moment process documentation and wrote down my questions. There were things I never would have known without actual work experience. (For example, I was surprised to find that Auto Increment was missing from the DB design docs — turns out it was because they were using a distributed DB lol)
I appreciated that when spending time with team members, they treated me as a fellow developer, even though I was just an intern. Even when there were deadlines for resolving certain issues, it was great to see a senior developer doing pair programming with team members to bring everyone's domain knowledge up to the same level. Even as an intern, I could sit alongside them, watch, and share my opinions. I was moved by this kind of developer culture that I'd only dreamed about.
And as a COVID-era intern, one thing I was really grateful to my team and mentors for was their active remote communication! Honestly, during the internship, I felt bad about suddenly messaging people on the company messenger whenever I had questions (that's just my personality — everyone was actually very kind). So early on, I wished the internship had been in person instead. But as time went on, communication worked so well even remotely that I fell in love with working from home..haha
Shortly after I first met a senior developer on the team, they suddenly messaged me on Works saying, "Minjeong, what do you think the problem with this code is?" We ended up having an intense 30-minute conversation about code on Works (we discussed the issues with using Optional and Null together), and even though I'd never seen this person's face, my sense of closeness shot through the roof lol
On another occasion, I ran into a dependency issue while working on a task, and we spent an entire hour communicating through Works to resolve it.
I also had lots of coffee chats with team members, and I kept hearing about the importance of exercise lol (I'm honestly still hearing about it constantly). It made me grateful to work with people who invest in self-improvement beyond just development.
End of the Internship
From everything I learned to the great team members I gained, it was an internship that gave me so much. Unfortunately, it didn't lead to a full-time offer.
My intern evaluation was good, but in the conversion interview (2nd interview) — (again, this internship was my very first technical interview experience) — I was so nervous that instead of highlighting my strengths, I ended up revealing the things I couldn't do and my weaknesses.
Why are my photos always taken in the bathroom... lol
It was 3 months of living more intensely than ever, but since I know the internship itself was a great experience, the memories are more about the journey than the outcome. (It might sound like I'm sugarcoating things since it's all positive, but I'm not — it was genuinely fun) It's a bittersweet result, but the fact that it remains a good memory even 2 months later makes me think I'd regret not writing this up, so I'm leaving it as a record!
Notes from my struggles trying to connect MySQL to a GCP compute engine.
1. Installing MySQL on Compute Engine
sudo apt-get update
sudo apt-get install mysql-server
sudo mysql_secure_installation // mysql 설정
Log in using the password you set during mysql_secure_installation.
How to connect: Log in as the root user with a password
sudo mysql -u root -p
2. Opening Firewall Ports for External IP Access
Go to VPC network > firewall rules menu
traffic : ingress
protocols and port : 3306
Port 3306 will be opened in the firewall.
Customize the source filters and targets as you need. I set it to all instances for general-purpose use.
3. Changing the Daemon Address
To allow MySQL access via IP, the MySQL daemon needs to be listening on 0.0.0.0 (the default is 127.0.0.1) To do this, you need to modify the conf file in the compute engine's shell.
cd /ect/mysql/mariadb.conf.d
sudo vi 50-server.cnf
The config file is located at this path. Go in and change the bind-address.
# bind-address = 127.0.0.1
bind-address = 0.0.0.0
After making this change, you need to restart the daemon.
You could use service restart, but since it wasn't installed on the VM, I used a different method.
sudo /etc/init.d/mysql restart
4. Granting User Permissions
Earlier, we connected to the DB using sudo privileges. So let's add a dedicated user for external access and grant permissions to that user.
Check User List
SELECT user, host FROM mysql.user;
Add a User Account
CREATE USER 'jyami'@'%' IDENTIFIED BY 'password';
Username: jyami
Access: % - allows access from external sources
Password: password
User Access Scope
localhost
Allows access only from local
%
Allows access from all IPs
xxx.xxx.xxx.xxx
Allows access only from a specific IP
xxx.xxx.%
Allows access from a specific IP range
Grant DB Privileges
GRANT ALL PRIVILEGES ON *.* TO 'jyami'@'%' WITH GRANT OPTION;
This grants all privileges to the user jyami connecting from anywhere.
*.*
Privileges to do everything
[database_name].*
Privileges to manage a specific DB
[database_name].[tablename]
Privileges to manage a specific table in a specific DB
View Granted Privileges
SHOW GRANTS FOR jyami@%
5. Connecting from Spring Boot
When I accessed the GCE, I definitely installed MySQL following the commands above, but when I actually connected to MySQL, it showed MariaDB [DB]>. In this case, the MySQL connection didn't work, but the MariaDB connection did.
알아두면 언젠간 깨달을 도커지식 2 - 도커 네트워크 | Docker Knowledge You'll Eventually Appreciate 2 - Docker Network
쟈 미
728x90
이글은 야매로 작성된 글이며 필자가 깨달아갈 수록 추가되는 글입니다 필자가 도커를 공부하며 깨달은 지극히 주관적인 관점일 수 있으니 이상한 점은 친절한 댓글 부탁드립니다! 그리고 항상 도커의 길로 인도해주는 영우찡 감사여!
가상 네트워크
아이피의 갯수는 제한적이다 우리가 사용하는 공유기는 주로 192.168.x.x 아이피 대역으로 내 컴퓨터에 아이피를 할당해준다. 그치만 이 아이피는 내 공유기 밖에서는 접근 할 수 없다.
그럼 이 때 외부에서 공유기 안의 특정 서버에 접근하기 위해서는 포트 포워딩을 사용한다. 포트포워딩이 필요한 이유는 공유기는 하나인데 비해(ip는 하나인데 비해) 내부 서버는 여러대일 수 있으니 어느 서버의 포트로 연결을 해주어야 하는지 몰라서 포트 포워딩을 사용하는 것이다. 외부에서 8080포트를 요청할 때 공유기 하위에 있는 서버들이 모두 8080을 쓰고 있을 때 어느 서버의 8080인지를 몰라서 서버를 지정하는게 목적이다. 추가로 서버를 지정하면서 포트도 바꿀수 있게 되었고!
주로 사용하는 http와 tcp 요청은 ip와 포트로 타겟을 지정하는데, 외부에서 요청할 때
ip는 우리집 공유기가 가진 공인 아이피를 지정할 테고, port는 공유기 아래 있는 여러대의 서버중에 어떤 곳에 이 요청을 전달해야 할 지 모르기 때문에 포트포워딩으로 미리 정해두고 전달하는 것이다.
도커의 포트포워딩
도커도 마찬가지로 -p 플래그를 사용해서 포트포워딩을 지정할 수 있다. 도커도 하나의 서버에 여러개의 컨테이너가 있어서, 서버의 포트에 컨테이너를 지정할지가 애매해서, 위에서 공유기에서 서버를 지정할 때 애매했던 것과 동일한 이유로 포트포워딩이 필요하다.
도커환경에서는 OS가 설치된 host machine이 공유기의 역할을 하고, 도커가 가상 망을 이루고 있는 것이다.
ip는 도커가 설치된 머신을 의미하고 port는 도커 망안에서 어떤 컨테이너로 연결을 할지에 대한 의미로 생각하자 (-p)
도커 안에서는 모든 컨테이너가 전부 다른 서브넷을 가진다.
도커 컨테이너 끼리는 통신을 하지 못한다 -> 컨테이너끼리 연결해주는 작업이 도커 안에 네트워크를 다는 것
사실 컨테이너 가상화가 각자 분리된 영역을 만들어 주기 위함이었으니 서로 통신도 분리를 해둔 것이라고 생각하면 편하다.(필요할 경우에 통신을 연결하면 되도록 만든 것)
도커 네트워크
하지만 컨테이너가 분리가 되어있다 하더라도 통신은 필요하다. 예를 들자면 내 springboot 서비스에 대한 컨테이너 A와 DB에 대한 컨테이너 B 둘사이에서 통신이 일어나는 경우를 생각할 수 있을 것이다.
따라서 컨테이너끼리 통신을 위해 도커 네트워크를 만들어서 여러개의 컨테이너를 하나의 가상의 망 아래에 묶을 수 있다.
이런식으로 네트워크를 만들고
docker network create test_network
만들어진 네트워크에 컨테이너를 등록하는 형태로 지정하면 컨테이너간 통신이 가능해진다.
docker network connect web_server_container
이때 문제는 컨테이너의 아이피가 랜덤으로 만들어진다는 점이다. (아이피가 컨테이너마다 부여된다) 컨테이너가 뜰 때마다 아이피가 랜덤이다.
아이피가 랜덤으로 부여되면, 아이피를 이용해서 통신을 할 수 없다. 따라서 통신을 하기위해서는 컨테이너 네임으로 지정하는 방식을 이용한다.
컨테이너 네임이 Container network interface(CNI)이라는 도커 내부의 가상망을 만들어주는 곳에 등록되어 있어 ip 대신 컨테이너 네임을 DNS 서버에 등록한다.
도커 네트워크에 컨테이너가 등록될 때 컨테이너의 ip는 랜덤하게 부여되지만, CNI 덕분에 컨테이너 네임을 이용할 수 있다.
결국 도커에서 네트워크 통신을 할 때 DNS 검색 우선 순위에서 CNI가 1순위이기 때문에 컨테이너 이름으로 호출이 가능한 것이다.
컨테이너 이름을 아이피 대신 사용한다.
그래서 웹앱 컨테이너 하나랑 db 컨테이너하나 띄우고 두 컨테이너를 하나의 네트워크로 묶어서 앱에서 DB를 호출할 때.
네임스페이스
포트
웹앱 컨테이너
web_app_container
9090
DB 컨테이너
DB_container
3306
다음과 같은 정보를 갖고있다면, 그저 springboot 에서 DB 컨테이너에 연결을 하고자 한다면 DB_container:3306 이런식으로 적어도 작동이 된다는 것이다.
이해를 위한 예제
Q. 집에 공유기가 있고, 그 안에 서버가 있고, 그안에 컨테이너 한경에서 9090 포트로 서비스하는앱이 있다. 이때 포트 포워딩은 몇 번 일어날까?
A. 2번일어난다 : 공유기에서 한번, 도커에서 한번
Q. 클라우드 환경에서 nginx를 사용한 포트포워딩을 할 때, nginx는 springboot 로 연결 연결, springboot에서는 db로 연결할 때 도커 네트워크 통신은 몇번 일어날까?
A. 2번 일어난다 : nginx의 proxy_pass 1번, spring에서 mysql로의 1번
container_name:portNumber 의 규칙을 사용하여 도커 네트워크 통신을 한다
This post is written in a rough-and-ready style and gets updated as I learn more This may reflect a highly subjective perspective gained while studying Docker, so please leave a kind comment if anything seems off! And thanks as always to Youngwoo for guiding me down the path of Docker!
Virtual Network
The number of IP addresses is limited. The router we use typically assigns an IP to our computer in the 192.168.x.x range. However, this IP is not accessible from outside our router.
So, to access a specific server behind the router from the outside, we use port forwarding. The reason port forwarding is needed is that while there's only one router (only one IP), there can be multiple internal servers, and there's no way to know which server's port to route the connection to — that's why we use port forwarding. When an external request comes in on port 8080, if all the servers behind the router are using 8080, there's no way to tell which server's 8080 it should go to — so the purpose is to specify the server. As a bonus, you can also change the port while specifying the server!
Commonly used HTTP and TCP requests specify targets using IP and port. When making a request from the outside:
The IP will point to the public IP of our home router, and since there's no way to know which of the multiple servers behind the router should receive the request, we use port forwarding to predetermine and route it.
Port Forwarding in Docker
Docker works the same way — you can specify port forwarding using the -p flag. Since Docker also has multiple containers on a single server, it's ambiguous which container should be assigned to the server's port. For the same reason it was ambiguous when specifying servers on a router, port forwarding is needed.
In a Docker environment, the host machine with the OS installed acts as the router, and Docker forms its own virtual network.
Think of the IP as referring to the machine where Docker is installed, and the port as indicating which container to connect to within the Docker network (-p).
Inside Docker, every container has a completely different subnet.
Docker containers cannot communicate with each other -> The process of connecting containers is adding a network within Docker
Since container virtualization was designed to create isolated areas in the first place, it makes sense that communication is also separated. (It's built so that you connect communication only when needed.)
Docker Network
However, even though containers are isolated, communication is still necessary. For example, you can think of a case where container A running your Spring Boot service needs to communicate with container B running the DB.
So, for inter-container communication, you can create a Docker network to group multiple containers under a single virtual network.
You create a network like this:
docker network create test_network
Then you register containers to the created network, and communication between containers becomes possible.
docker network connect web_server_container
The problem here is that container IPs are assigned randomly. (Each container gets its own IP.) Every time a container starts up, the IP is random.
If IPs are assigned randomly, you can't rely on IPs for communication. Therefore, to communicate, we use the container name instead.
The container name is registered in Docker's internal virtual network manager called the Container Network Interface (CNI), and the container name is registered in the DNS server instead of the IP.
When a container is registered to a Docker network, its IP is assigned randomly, but thanks to CNI, you can use the container name instead.
Ultimately, when Docker handles network communication, the CNI has the highest priority in DNS lookup, which is why you can call containers by their name.
Container names are used in place of IPs.
So when you spin up one web app container and one DB container, group them under a single network, and the app calls the DB:
Namespace
Port
Web App Container
web_app_container
9090
DB Container
DB_container
3306
If you have the information above, then to connect from Spring Boot to the DB container, you can simply write DB_container:3306 and it will work.
Examples for Understanding
Q. You have a router at home, a server behind it, and inside that server, an app serving on port 9090 in a container environment. How many times does port forwarding occur?
A. It happens 2 times: once at the router, once at Docker
Q. In a cloud environment using nginx for port forwarding, where nginx connects to Spring Boot, and Spring Boot connects to the DB — how many times does Docker network communication occur?
A. It happens 2 times: once for nginx's proxy_pass, once for Spring to MySQL
Docker network communication follows the rule of container_name:portNumber
This post is written in a rough-and-ready style and will be updated as I learn more This may reflect a highly subjective perspective from my experience studying Docker, so please leave a kind comment if anything seems off! And shoutout to Youngwoo for always guiding me down the path of Docker!
Server Virtualization
There are two types of virtualization.
Hypervisor Virtualization
Container Virtualization
Hypervisor Virtualization
Hypervisor-based virtualization works by installing a host OS on your server, partitioning resources as needed to create virtual machines, then installing a Guest OS on each VM to run applications on top of it.
However, with hypervisor-based virtualization, each VM uses its own separately allocated resources — and when there are duplicate resources across VMs, it essentially wastes unnecessary capacity (resources).
Container Virtualization
So what came along to replace this OS-based virtualization technology is Container technology. This container technology is called LXC (Linux Containers), and Docker is something built by leveraging this LXC technology really well.
In a Docker environment, as shown in the diagram above, a Guest OS is not needed. Let's walk through an example for easier understanding.
The Host OS is CentOS, and you want to run an application on Ubuntu.
1. With a hypervisor, you install Ubuntu as the Guest OS and run the app on top of it. Then the kernel in the Guest OS passes commands to the hypervisor, and the hypervisor relays them to the CentOS kernel.
Just looking at this, there are already two kernels and two OSes (consuming a lot of resources).
Here, the hypervisor's role is to logically separate CPU and memory resources.
2. On the other hand, the Docker engine can deliver app commands directly to the host OS (CentOS) kernel without needing the Guest OS (Ubuntu) or Ubuntu's kernel.
This is because the Docker engine translates the commands from apps running on Ubuntu (Guest OS) into something the CentOS (host OS) kernel can understand.
Compared to a hypervisor, resource consumption is significantly lower.
Unlike a hypervisor, Docker doesn't install a kernel, so even the base image has no kernel. Therefore, Docker images (even base images) are much lighter compared to the Windows or Linux image files (ISO) used when setting up servers in a hypervisor.
Being lightweight means installation is incredibly fast!!! (Container environments are lightweight)
LXC (Linux Containers)
If you think about how a container environment is actually stored in memory,
you can think of it as processes separated by different namespaces in Linux. It creates isolated zones in Linux so things don't get tangled up, and simply runs processes within those zones.
This is what LXC does for us.
LXC is a userspace interface for the Linux kernel containment features. It lets Linux users easily create and manage system and application containers.
According to the official documentation, it has the following features. (Though honestly, I still don't fully get it even after reading it)
So, running code on top of Docker in a container environment basically means:
Through LXC, isolated zones are created inside the Linux kernel by combining identifiers called Cgroups and Namespaces.
The Docker engine uses LXC to run programs inside those isolated zones.
In other words, when the Docker engine runs on Linux, it uses LXC to translate commands into host OS kernel commands (as mentioned above) and isolate the zones.
As a side note, Docker on Windows in the old days
used to install Linux as a Guest OS on top of a hypervisor and run the Docker engine on that (so there were no resource savings).
Nowadays it just works natively, they say. (Apparently a Linux kernel [WSL] was recently added to Windows..I'm a Mac user so I didn't really care)
volatile을 사용한 쓰레드간 통신 동기화 | Thread Synchronization for Inter-Thread Communication Using volatile
쟈 미
728x90
이펙티브 자바를 읽으면서 짜릿한 단일검사(racy single-check)에 대해 찾아보던 중, 알게된 내용이다.
동기화의 기능
자바의 쓰레드 프로그래밍을 해보았다면 synchronized 키워드를 몇번 접해볼 수 있을 것이다. 동기화에서 synchronized 를 이용해 한가지 자원을 동시에 접근할 때 thread safe하게 자원의 내용을 변경할 수 있어, 이 기능만 동기화의 기능이라고 보기 쉽다. 즉 synchronized 가 걸려있는 블록 혹은 메서드에서 한번에 한 쓰레드씩 수행하도록 한다.
그러나 사실 동기화의 기능은 총 2가지이다.
a. 배타적 실행
위에 말한 대로 한 쓰레드가 변경하는 중이라서, 상태가 일관되지 않는 (공유하는 자원의) 객체를 현재 사용중인 쓰레드만 접근이 가능하고, 다른 쓰레드가 보지 못하게 막는 용도를 말한다.
이때 락에 대한 개념이 나온다. 락을 건 메서드에서 객체의 상태를 확인하고 필요하면 수정하도록 작성했을 때, 한 쓰레드에서 해당 메서드를 사용하게 되면 객체에 락이 걸리게 되고, 해당 객체는 다른 쓰레드에서 동시에 접근이 불가능하다.
즉 배타적 실행은 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화시키는 것이다.
b. 쓰레드 사이의 안정적 통신
나는 이 a번만 이전에 알고있었는데, 동기화의 중요한 기능이 하나 더 있다.
동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다. 동기화덕분에 한 스레드에서 락의 보호하에 수행된 수정사항을 다른 쓰레드에서 최종 결과를 볼 수 있다.
자바 언어에서 long과 double을 제외한 변수를 읽고 쓰는 동작은 원자적이다. 여러 쓰레드가 primitive 타입의 변수를 동기화 없이 수정하더라도, 각 스레드에서는 정상적으로 그 값을 온전하게 (연산중간에 끼어들지 않고 온전히) 읽어온다
원자적 연산
위에서 읽고 쓰는 동작이 원자적이라 했는데, 원자적 연산은 중단이 불가능한 연산을 이야기한다 여러 자바의 연산은 바이트코드로 이루어져 있는데, 하나의 연산을 수행하기 위해 바이트코드가 수행될 때 중간에 다른 쓰레드가 끼어들어서 연산의 결과가 올바르지 않게 변한다면 해당 연산은 원자적 연산이 아니다.
원자적이지 않은 동작의 예시로는 a++(증가 연산자)이 있다. cleancode책의 동시성 부록에서는 아래와 같은 설명이 나온다
lastId값이 42였다고 가정하자. 다음은 getNextId 메서드의 바이트 코드다. 예를 들어 첫째 스레드가 ALOAD 0, DUP, GETFIELD lastId까지 실행한 후 중단 되었다고 가정하자. 둘째 스레드가 끼어들어 모든 명령을 실행했다. 즉, lastId를 하나 증가해 43을 얻어갔다. 이제 첫째 스레드가 중단했던 실행을 재개한다. 첫째 스레드가 GETFIELD lastId를 실행한 당시 lastId 값은 42였다. 그래서 피연산자 스택에도 42가 들어있었다. 여기에 1을 더해 43을 얻은 후 결과를 저장한다. 첫째 스레드가 반환하는 값 역시 43이다. 둘째 스레드가 증가한 값은 잃어 버린다. 둘째 쓰레드가 첫째 스레드를 중단한 후 다시 실행된 첫째 스레드가 둘째스레드의 작업을 덮어썼기 때문이다.
즉, 여기서의 문제는 연산을 수행할 때 JVM에서 사용하는 프레임, 지연변수, 피연산자 스택에 저장하는 과정에서 원자적 연산이 아닌경우, 연산 중간 과정이 덮어씌워져 올바르지 않은 값을 갖는다는 것이다.
좀더 쉽게는 두개의 쓰레드에서 ++ 연산을 했으니 +2가 되어야하는데, ++ 연산이 원자적이지 않아 +1만 되었다는 것이다.
원자적 데이터에서의 동기화
위의 원자성에 대한 이야기를 들으면 원자적 데이터를 읽고 쓸 때는 (할당 연산은 원자적이다) 동기화를 하지 않아도 괜찮다고 생각 할 수 있다. (중단이 불가능하기 때문에!)
하지만 원자적 데이터라도 동기화가 필요하다
Java언어에서 스레드가 (원자적 데이터 값을 가지더라도) 필드를 읽을 때 '수정이 완전히 반영된' 값을 얻는다고 보장하지 않는다. 즉 A 쓰레드에서 필드를 수정했더라도, B 쓰레드에서 수정된 필드를 반드시 볼 수 있는 것은 아니라는 것이다.
따라서 한 쓰레드에서 수정이된 필드값을 다른 쓰레드에서 '잘 읽기' 위해서라도 동기화의 안정적인 통신이 필요하다 이는 자바 메모리 모델 때문이다.
동기화의 관점에서의 자바의 메모리 모델
동기화를 하지 않으면 스레드가 변수를 읽어올 때 각 쓰레드가 변수를 cached한 영역에서 읽어오게 된다. 그래서 한 쓰레드로 인해 해당 변수가 값이 변화해도, 다른 쓰레드에서는 이전에 읽었던 cached된 변수의 값을 읽기 때문에 변경된 사항을 볼 수 없다.
따라서 각 쓰레드에서 변경한 값을 값을 통신하기 위해 동기화가 필요하며. 이때 통신을 위한 동기화를 사용하기 위해서는 volatile 한정자를 사용하는 방법이 있다. (Synchronized는 배타적수행, 안정적 통신을 모두수행하는 것이고, volatile은 안정적 통신만을 수행한다고 생각하면 편하다)
즉, 여러 스레드가 공유하는 변수값을 읽어오기 위해서 volatile 키워드를 붙이면 그 변수를 읽어올때 각 쓰레드의 cached한 영역이 아닌 메인 메모리에서 직접 읽어오기 때문에 안정적인 통신을 보장할 수 있다.
공식문서에 있는 자바 메모리 모델에 대한 설명
volatile 변수의 경우에는 inter-thread action에 해당하여, synchronized된 경우의 자바 메모리 모델 reordering 규칙이 적용된다. (Reordering은 다른 쓰레드의 변수값을 읽어오기 위한 작업으로, 한 쓰레드의 변경사항이 다른 쓰레드에 표시될 수 있게 하기 위한 작업이라 생각하자.) 이 규칙은 volatile 변수가 쓰기가 일어날 경우에는, 항상 임의의 읽기 쓰레드에 의해서 동기화가 되도록 reordering되는 것을 의미하며, reordering이 된다는 것은 다른 쓰레드에서 변수를 읽을 때 최신 변경사항을 읽을 수 있다는 것이다.
volatile이 설정되지 않은 long, doulbe에 대한 쓰기는 두번에 이루어진다 => 먼저 첫번째 32비트를 쓰고 다음 쓰기에서 두번째 32비트를 쓴다.
volatile이 설정된 long, double이라면 항상 원자적이다
프로그래머는 shared 되는 62bit 값은 volatile이나 synchronize 로 선언하는게 좋다. complication을 피하기 위해!
즉 첫 비트 32비트 값을 할당한 직후에, 즉 둘째 32비트를 할당하기 직전에 다른 쓰레드가 끼어들어 두 32비트 값중 하나를 변경할 수 있기 때문에 long, double은 원자적 연산이 될 수 없다.
하지만 volatile을 사용을 한다는 것은 여러 쓰레드에서 하나의 변수가 같은 값을 읽도록 보장하는 것이기 때문에, 메모리를 2번 접근을 하더라도 같은 값을 읽도록하는. 변수에 접근하는 연산을 원자적으로 수행하게 보장한다는 것이다.
in which case the Java memory model ensures that all threads see a consistent value for the variable
따라서 long, double 변수를 원자적으로 사용하고 싶다면 volatile로 선언하는게 좋다.
I came across this while reading Effective Java and looking into the racy single-check idiom.
Functions of Synchronization
If you've done any thread programming in Java, you've probably encountered the synchronized keyword a few times. In synchronization, synchronized lets you safely modify a shared resource when multiple threads access it simultaneously, so it's easy to think that's all synchronization does. In other words, it ensures that only one thread at a time can execute a synchronized block or method.
But in fact, synchronization has two functions in total.
a. Mutual Exclusion
As mentioned above, this refers to preventing other threads from seeing a shared object while one thread is modifying it and its state is inconsistent — only the thread currently using the object can access it.
This is where the concept of a lock comes in. When you write a method that acquires a lock to check and potentially modify an object's state, once a thread enters that method, the object becomes locked, and other threads cannot access it simultaneously.
In short, mutual exclusion is about transitioning an object from one consistent state to another consistent state.
b. Reliable Communication Between Threads
I previously only knew about point (a), but there's another important function of synchronization.
Without synchronization, changes made by one thread may not be visible to other threads. Thanks to synchronization, modifications performed under the protection of a lock in one thread can be seen as the final result by other threads.
In the Java language, reading and writing variables is atomic for all types except long and double. Even if multiple threads modify a primitive variable without synchronization, each thread will read the value correctly and completely (without being interrupted mid-operation).
Atomic Operations
I mentioned above that read and write operations are atomic. An atomic operation is one that cannot be interrupted. Many Java operations are composed of bytecode instructions, and if another thread can intervene during the bytecode execution of an operation and cause incorrect results, then that operation is not atomic.
A classic example of a non-atomic operation is a++ (the increment operator). The concurrency appendix of the Clean Code book explains it like this:
Assume lastId had a value of 42. Here is the bytecode for the getNextId method. For example, suppose the first thread executes up to ALOAD 0, DUP, GETFIELD lastId and then gets interrupted. The second thread cuts in and executes all the instructions — it increments lastId and gets 43. Now the first thread resumes execution from where it was interrupted. When the first thread executed GETFIELD lastId, the value of lastId was 42. So 42 was on the operand stack. It adds 1 to get 43 and stores the result. The first thread also returns 43. The value incremented by the second thread is lost. This happened because the second thread interrupted the first thread, and when the first thread resumed, it overwrote the second thread's work.
The problem here is that when performing operations, if the operation is not atomic during the process of storing values in the JVM's frame, local variables, and operand stack, intermediate results can get overwritten, leading to incorrect values.
To put it more simply: two threads each performed a ++ operation, so the result should have been +2, but since the ++ operation is not atomic, only +1 was applied.
Synchronization with Atomic Data
After hearing about atomicity above, you might think that you don't need synchronization when reading and writing atomic data (since assignment operations are atomic). (Because they can't be interrupted!)
However, even with atomic data, synchronization is necessary.
The Java language does not guarantee that when a thread reads a field (even if the data is atomic), it will get a 'fully updated' value. In other words, even if thread A modifies a field, thread B is not guaranteed to see the modified value.
Therefore, reliable communication through synchronization is needed even just to ensure that a field value modified by one thread is 'properly read' by another thread. This is due to the Java Memory Model.
Java Memory Model from a Synchronization Perspective
Without synchronization, when a thread reads a variable, it reads from its own cached area. So even if one thread changes the variable's value, other threads still read the previously cached value and cannot see the change.
Therefore, synchronization is needed for threads to communicate changed values. One way to achieve communication-only synchronization is by using the volatile modifier. (Think of it this way: synchronized provides both mutual exclusion and reliable communication, while volatile only provides reliable communication.)
In other words, when you add the volatile keyword to a shared variable, reading that variable goes directly to main memory instead of each thread's cached area, which guarantees reliable communication.
Java Memory Model Explained in the Official Documentation
Volatile variables are considered inter-thread actions, so the Java Memory Model's reordering rules for synchronized cases apply. (Think of reordering as the mechanism for reading variable values from other threads — it's what makes changes in one thread visible to other threads.) This rule means that writes to volatile variables are always reordered so that they are synchronized by any reading thread through subsequent reads (as defined by synchronization order). Being reordered means that other threads can read the latest changes when they access the variable.
The article above explains Java's memory model and demonstrates how unsynchronized programs can produce surprising results.
Java's memory model works by examining each read in an execution and checking whether the write being observed by the read is valid according to specific rules.
The behavior of each isolated thread operates in a manner controlled by that thread's semantics, except when the values it sees are determined by the memory model (intra-thread semantics).
In other words, when the behavior of an isolated thread is determined by the memory model, it needs to be understood based on values visible in a multithreaded context.
intra-thread semantics: In a single thread, the thread's behavior is predictable based on values visible only within that thread. inter-thread action: An action performed by one thread that can be directly detected or affected by another thread.
Reordering in the Java Memory Model with Synchronization
Left: Before reordering / Right: After reordering
In the left case, it seems impossible for r2 == 2 and r1 == 1. However, the compiler can reorder instructions in both threads if it doesn't affect each thread's execution (as shown in the right image).
Why it seems impossible: If instruction 1 comes first, it can't see the write result from instruction 4. If instruction 3 comes first, it can't see the write result from instruction 2.
But in the right case, there is no synchronization.
One thread is writing
Another thread is reading the same variable
The writes and reads are not ordered by synchronization: the explanation of synchronization ordering is in section 17.4.4.
Reordering in the Java Memory Model with Synchronization
A write to a volatile variable is synchronized with any subsequent read (a read defined by synchronization order) of that variable by any thread. → Changes to a volatile variable are always visible to other threads.
long and double
I mentioned above that "in the Java language,reading and writing variables is atomic for all types except long and double." So why aren't read and write operations atomic for long and double?
It's related to the JVM's bit width. According to the Java memory model, assigning a value to 32-bit memory is an uninterruptible operation (i.e., atomic). However, long and double occupy 64-bit memory space.
Reading through that article, it can be summarized in three points:
Writes to non-volatile long and double are done in two steps. => The first 32 bits are written first, then the second 32 bits are written next.
If long or double is declared volatile, the operation is always atomic.
Programmers should declare shared 64-bit values as volatile or synchronized to avoid complications.
In other words, right after assigning the first 32 bits — just before assigning the second 32 bits — another thread can cut in and modify one of the two 32-bit values. That's why long and double cannot be atomic operations.
However, using volatile guarantees that multiple threads read the same value from a single variable. So even though memory is accessed twice, it ensures the same value is read — meaning the variable access operation is guaranteed to be performed atomically.
in which case the Java memory model ensures that all threads see a consistent value for the variable
Therefore, if you want to use long or double variables atomically, it's best to declare them as volatile.
댓글
Comments