벌써 이제 7월의 마지막주가 다가왔다. 왜 벌써 8월이 다가온지 모르겠지만.. 23년도의 8월을 후회없게 보내기 위해 잘 준비해야겠다.
지난18일에는 아버지 생신파티로 공부하다가 글 작성하는 것을 깜빡했는데, 토요일날 해야지 했던게, 왜 벌써 일요일이 됬는지.. 그리고또 하루가 거의 다 갔는지... 의문이다.....
내가 정한 공부를 쉬는날인 토요일은, 한주동안 정말 열심히 공부하고 일했다는 전제하에 토요일은 공부하지않고 쉬는날로 정한것인데, 결과물을 보면 나 정말 열심히 한게 맞나 싶다. 공부를 더 열심히 해서 토요일날 쉬는데 죄책감(?)이 들지 않도록 해야겠다.
독서: 이펙티브자바 아이템7~ 9
매일 꾸준하게 보고 있는 이펙티브자바, 이번에 본 아이템들은 해당 부분을 생각해본 적이 없어서인지 나에게는 와닿지 못하는 이야기들이었다.
아이템 7: 다 쓴 객체 참조를 해제해라
나는 지금까지 개발을 하면서, 메모리를 크게 고민해본 적이 없다. 이는 내가 파이썬과 PHP를 주된 언어로 개발을 해서인지는 모르겠지만, 해당 언어로 알고리즘 문제를 풀이할 때말고는 메모리 문제를 고민해보거나 경험해본적이 없는듯하다.
하지만 자바 영역(혹은 다른 언어의 영역)에서는 메모리에 대한 부분에 상당히 민감한듯하다. 그리고 지금보면 당연한것인데 지금까지 내가 잘못코드를 작성해왔겠구나 하는 생각이 들었다.
자바에는 가비지 컬렉터가 존재한다. 이는 내가 자바로 개발을 하지 않아도, 들어보았었다. 가비지 컬렉터는 다쓴 객체를 회수하는 것을 말하는데, 나는 가비지 컬렉터를 통해 메모리가 잘 관리 되는 줄 알았다.(그냥 있는 동작 그대로 훌륭할 것이라고 생각했다..)
하지만 이것은 잘못된(?) 생각이었다. 먼저, 나는 언제 가비지 컬렉터를 통해 가비지 컬렉팅이 되는가? 에 대한 생각이 없었다. 그리고 이를 고려하여 말한다면, 가비지 컬렉터를 통해 메모리가 효과적으로 관리된다. 라고 무조건적으로 말할 수는 없을 것이다. 가비지 컬렉션 언어에서는 (의도치 않게객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다고한다. 그 이유로, 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체)를 회수해가지 못하기 때문이다.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = 0;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
// 원소를 위한 공간을 적어도 하나 이상 여유를 두며, 늘려야하는 경우 두배 이상 늘린다.
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
위 스택은 스택이 늘었다가 주는 경우에 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수 하지 않는다. 이 스택이 그 객체들의 다 쓴 참조(obsolete reference: 앞으로 다시 쓰지 않을 참조)를 여전히 가지고 있기 때문이다. 이를 올바르게 작성한다면?
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
null 처리 를 통해 해당 참조 해제 처리를 하여 해결할 수 있다. 다 쓴 참조를 null 처리하고서, 실수로 해당 참조를 사용하려고 하면 시스템은 즉시 NullPointException 을 던지며 종료된다. 그리고 당연하듯 프로그램 오류는 가능한 초반에 발견하는 것이 좋다.
하지만 모든 객체를 다 쓰자마자 null 처리를 할 필요는 없고, 이는 필요 이상으로 프로그램을 지저분하게 만들 수 있다고 한다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다쓴 참조를 해제하는 가장 좋은 방법으로 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것을 제안했다.
하지만 null 처리를 통해 관리를 해야할 때가 존재하기도 하는데, 바로 Stack 클래스와 같이 자기 메모리를 직접 관리하는 경우가 이에 해당한다. 가비지 컬렉터가 보기에는 비활성 역역에서 참조하는(예시코드와 같은 상황) 객체도 똑같이 유요한 객체로 바라본다. 비활성 영역의 객체가 더이상 쓸모없다는 것은 프로그래머만 알 수 있는 사실이기 때문에, 프로그래머는 비활성 영역이 되는 순간 Null 처리를 통해 해당 객체는 더이상 사용하지 않는다는 것을 가비지 컬렉터에게 알려야 한다. 이와 관련된 예시 상황으로는 자기 메모리를 직접관리하는 클래스, 캐시를 사용할 경우, 리스너 혹은 콜백을 사용할 경우을 이야기 할 수 있다.
정리를 한다면, 가비지 컬렉션언어에서는 (의도치않게 객체를 살려두는) 메모리 누수를 찾기 어렵다. 이를 해결하기 위해서는 1. 객체참조를 null 처리한다. 2. 다쓴 참조를 담은 변수를 유효범위(Scope) 밖으로 밀어낸다. 3. 약한 참조를 사용한다.
이번 아이템은 평소 개발하면서 고민하지못한 이야기를 고민하게하는 부분이었다. 그리고 리스너와 콜백을 사용하는 경우가 현 프로젝트에서 카프카와 관련된 부분으로, 잠재적으로 문제가 될 수 있는 부분이라는 것을 인지하게되어, 이 부분을 다른 사례에서는 어떻게 하는지에 대해 알아봐야겠다는 생각이 들었다.
아이템 8 : finalizer 와 cleaner 사용을 피하라
나는 개발을 하면서 finalizer 와 cleaner 의 단어를 생전 처음 들어봤다. 사용에 대한 부분은 말해뭐해.. 그래서 이번 아이템의 내용은 그냥 교양(?)처럼 읽었다.
finalizer 와 cleaner 는 객체 소멸자 라고 한다. 객체소멸자.. 이 단어조차 낯설다. finalizer와 cleaner는 GC가 더 이상 사용하지 않는 자원에 대한 정리작업을 진행하기 위해 사용된다고 한다.
- 예측이 불가능하며, 상황에 따라 위험할 수 있어 일반적으로 불필요
- 오동작, 낮은 성능, 이식성 문제의 원인
- 예측 불가능
- 느림
- 일반적으로 불필요
finalizer 와 cleaner 의 특징을 보았을 때, 이것을 왜 만들었는지 이해가 안됬다.
사용을 지양하는 이유
- finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 알 수 없다.
- finalizer와 cleaner는 심각한 성능 문제도 동반될 수 있다.
- finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다.
그러면서, 대안방법을 제안해주었는데, AutoCloseable을 구현해주고, close 메서들르 호출하면된다고 한다. 사실 이 부분도 들어본적이 없어 그렇구나 하고 넘어가게되었는데, 관련된 이야기를 나눠보거나, 실제로 사용해볼 경험이 생겼으면 좋겟다.
아이템 9 : try-finally 보다는 try-with-resources 를 사용하라
이 아이템은 들어는 봤지만, 내가 알고 있는 내용과 많이(?) 달랐다. 내가 알고있던 try 구문은 예외 처리를 위해서만 사용하던 것인데, try-with-resources..? 이건 내가 다른 것으로 알고있는 것같은데.. 뭔지 아직 잘 모르겠다.
이번 아이템에서는 try-finally 가 전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 사용되었다고한다. 내가 알고있던 예외가 발생하는 경우를 포함해서 말이다. 자원이 제대로 닫힘을 보장 을 생각해볼 때, 이 또한 예외가 발생했을 때 자원을 닫는 다는 동작을 보장해주기 위한 것이라는 생각이 들었다.(결국에는 같은말이라는 것)
그렇다면, close 메서드를 통해 자원을 닫아줘야하는(관리해줘야하는) 자원의 예시는 무엇이 있을까?
- InputStream
- OutputStream
- java.sql.Connection
DB 연결의 경우, 비싼 자원이라는 것을 알고 있었지만, InputStream, OutputStream 은 또 처음 알게 되었다.
그럼 이제, 왜 try-finally 가 아닌 try-with-resources 를 사용해야하는 것일까?
static String firstLineOfFile(String path) throws IOException{
BufferedReader br = new BufferedReader(new FileReader(path));
try{
return br.readLine();
}finally {
br.close();
}
}
firstLineOfFile 메서드 안의 readLine 메서드가 문제가 생긴다면 readLine() 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패할 것이다. 이러한 상황에서 두번째 예외가 첫번째 예외를 삼켜, 스택 추적 내역에 첫번째 예외에 대한 정보는 남지 않게 된다. 두 번째 예외 대신 첫 번째 예외를 남길 수는 있지만, 코드가 너무 지저분해져서 실제로 그렇게 하는 경우는 거의 없을것이라고 한다.
진짜일까..? 그냥 readLine 메서드를 try 구문 안에 넣으면 해결되는것이 아닌가..? 하는 생각이 들었다.
무튼 이러한 문제들은 java 7의 try-with-resources 덕에 모두 해결되었다고 한다. try-with-resources 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야하는데, 수 많은 인터페이스가 이미 AutoCloseable을 구현하거나 확장해두었다.
static String firstLineOfFile(String path) throws IOException{
// try - with - resources
try(BufferedReader br = new BufferedReader(new FileReader(path)){
return br.readLine();
}
}
readLine()과 close() 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고, readLine에서 발생한 예외만 기록이 된다. 또한, 숨겨진 예외들도 스택추적 내역에 suppressed라는 꼬리표를 달고 출력되며, Throwable에 추가된 getSuppressed 메서드를 이용해 프로그램 코드에서도 가져올 수 있다.
try-with-resources
try (SomeResource resource = getResource()) {
use(resource);
} catch(...) {
...
}
public interface AutoCloseable {
void close() throws Exception;
}
또한 try 안에 복수의 자원 객체를 전달할 수 있다고 한다.
try(Something1 s1 = new Something1();
Something2 s2 = new Something2()) {
} catch(...) {
...
}
static void copy(String src, String dst) throws IOException{
try(InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst))
{
byte[] buf = new byte[BUFFER_SIZE];
int n;
while((n = in.read(buf))>= 0)
out.write(buf, 0, n);
}
}
따라서 꼭 회수해야 하는 자원을 다룰 때에는 try-finally말고 try-with-resources를 사용하도록 해야겠다. 코드는 더 짧고 분명해지며, 만들어지는 예외정보도 훨씬 유용하다고한다.
참고
Try-with-resources를 이용한 자원해제 처리 | Integerous DevLog
코드 리뷰를 받던 중 Try-with-resources를 알게 되어 정리 코드 리뷰를 받기 전 코드는 아래와 같았다. public static String getHtml(String url) { try{ URL targetUrl = new URL(url); BufferedReader reader = new BufferedReader(new I
ryan-han.com
문제 풀이
계속해서 매일 꾸준하게 알고리즘 문제를 풀이하고있다. 코틀린으로 문제를 풀며, 코틀린에 익숙해지려고하고있는데, 생각보다 코틀린이 문제가 아니라, 문제 해결에 대한 아이디어에서 문제를 겪고있다.
난이도가 3, 4 라면 어려움에 당연하다 생각하겠지만, 난이도 0 문제를 보는데... 아니 어째 이것들이 더 어려운것같다.. 분명 간단하게 문제가 해결될 것인데, 방법이 생각이 안나거나.. 불필요하게 복잡하다거나.. 그런 식이다.. 그래도 지금 이 문제들이 분명 난이도 2,3 문제를 푸는데 도움이 될 것 같다.
주말에는 SQL 문제를 풀이하고 있는데, 여전히 GROUP BY 를 활용한 문제 해결에서 내가 약한 것 같다. 그래도 의도를 가지고 쿼리를 작성한다는 것, 약간의 부분만 수정하면 원하는 쿼리를 얻을 수 있었다는 점에서 포기하지 않고 꾸준하게 부족한 부분을 채워나가야겠다.
그리고 이제서야 문제해결을 기록하기 시작했는데, 역시 시간이 지나고서 풀었던 문제를 기록하니, 내 풀이를 기록 할 지언정, 해당 문제를 풀면서 고민했던 고민은 기록할 수 없었다. 무엇을 고민했고, 새로 알게된 메서드나 다른 사람의 풀이를보고 느낀점등.. 앞으로는 문제를 풀자마자 기록을 해야겠다.
항상 그렇듯, 공부한 것을 정리하는것이 어렵다. 특히 생각을 기록하는 것이 재밌으면서도 어렵고, 시간이 정말 순식간에 지나간다. 프로젝트도 해야하고, 프로젝트 관련 작업도 해야하고, 보고싶은 책도 봐야하고, 문제도 풀어야하고, 이젠 이력서도 반영해야한다. 이렇게 보니깐 나 욕심이 없는 줄 알았는데, 욕심쟁이인것같다. 근데 다른것과는 다르게 이것들을 포기하고 싶지 않다.
'회고 > TIL' 카테고리의 다른 글
웹 계층 테스트, 알고리즘 문제풀이 (0) | 2023.07.25 |
---|---|
알고리즘 문제풀이, 이슈 피드백(?), 클린 아키텍처 정리 (0) | 2023.07.24 |
이펙티브 자바 독서, 클린 아키텍처 정리, 프로젝트 템플릿 작성 (0) | 2023.07.20 |
Product Messaging 개발, 독서, 문제풀이 (0) | 2023.07.19 |
PR 리뷰-피드백, 알고리즘 문제풀이 (1) | 2023.07.18 |