- 0
- 네모
- 조회 수 2667
○ 시작하기 전
'아무튼 대충 알아보는 NPE 예방법!' 강좌를 작성한 네모입니다.
본 강좌는 웹스택 팁 게시판과 한국마인크래프트포럼 디벨로퍼 강의 게시판에도 업로드 됩니다.
잘 부탁드리겠습니다 :-)
○ NPE란?
NPE란,
NullPointerException의 줄임말으로, null 값을 가진 객체를 참조하려고 했을 때 일어나는 Exception 입니다.
(Java 에서의 null 은 값이라고 부를 수 없는 존재이기는 합니다만, 작성자 편의상 그렇게 부르도록 하겠습니다.)
String 처리나 FileIO 처리, 서버 송수신 처리시 자주 발생하고, 이외의 많은 부분에서도 쉽게 일어날 수 있는 문제이지요.
Java를 즐겨 사용하지는 않습니다만, NPE는 예방하려면 충분히 예방할 수 있는 오류입니다.
그럼에도 개인적으로 원천봉쇄 하려고 하지는 않는 오류이기도 합니다.
충분히 예방을 해 둔 상태라면, 오류의 원인을 쉽게(물론 다른 것들에 비해) 발견할 수 있고,
오류의 원인을 알고 있다면 그 이후엔 원인에 따라 문제를 해결하기만 하면 되니까요.
그럼 왜 원천봉쇄를 하지 않느냐 하면...
NPE 하나 보지 않겠다고 꼼꼼히 예외처리 하다가 오류가 전혀 발생하지 않는 더 끔찍한 오류를 대면할지도 모르기 때문이지요.
차라리 귀찮음 무릅쓰고 NPE 처리하면, 다른 오류들을 모르고 넘기는 일은 없을테니까요. 큼큼.
○ NPE 예방법 1 - 의미없는 null 파라미터 사용하지 않기
의미없는 null 남발은 NPE의 적입니다. 아무리 다음 로직에서 NPE 방어로직을 추가해 두었다고 했더라도요.
애초에 null 전달을 하지 않으면 될 일을 굳이 방어로직까지 써 가면서 저런걸 사용할 이유가 없지요.
여러모로 파생 오류가 발생할 수도 있구요. 가능하면 사용하지 않는게 좋겠네요.
Integer TestFunction(){ return null; } int testNum = TestFunction(); finalNum = (int) (2020 / testNum);
이런 코드를 작성하게 된다면(TestFunction 의 반환값이 너무 노골적이죠?ㅋㅋㅋ) ArithmeticException 이 발생하게 됩니다.
testNum은 int 형식이고, null값을 전달받은 int 변수의 값은 0이 되어버리지요.
그러면 무슨 문제가 있냐? 2020 을 0 으로 나누게 되어버리는 끔찍한 문제가 생기지요.
다음달 월급을 이번달 월급에서 0으로 나눈 값으로 준다고 하는것보다 끔찍한 이야기지요.
아무튼 null 전달은 사용하지 않는게 맞습니다.
○ NPE 예방법 2 - 메소드 체이닝 사용 자제하기
객체의.메소드().를호출().하고또().호출했().더니널().포인터().가일어().나버림();
같은 구조의 패턴을 메소드 체이닝이라고 부릅니다.
작성해야 하는 코드량이 확 줄어들어서 작업시간이 줄고, 일과시간때 웹툰도 보고 할 수 있을 것 같은 꿈을 꾸게 해주는 패턴이지요.
그렇지만 다들 알고 계실겁니다. 꿈은 높은데 현실은 시궁창이라고.
저렇게 썼다가 NPE 한번 발생하면 디버깅하기 정말 힘들고, 오류 찾느라 일과시간을 넘어서 야근을 달리고, 가정이 무너지고, 사회가 ㅁ...
String MCPathURI = Minecraft.getInstallationPath().toURI();
위 코드의 Minecraft.getInstallationPath() 에서 null 을 반환해 버린다면?
우리의 toURI는 대체 어디서 호출해야 할까요. null.toURI() 가 되는걸까요? 아, NPE 네요. 망했어요 GG.
이러한 문제를 방지하기 위해서는, 체이닝을 자제해야 합니다. nullable 하지 않은 메서드들만 골라서 사용합시다.
저라면 nullable 검증할 시간에 몇줄 더 써서 나누겠습니다ㅡ 큼큼.
String MCPath = Minecraft.getInstallationPath(); String MCURI = MCPath.toURI();
물론 이 방식이 NPE를 아예 막아주는 것은 아닙니다. 단지 디버깅 하기 편하게만 해 줄 뿐이죠.
체이닝을 사용했을 경우, getInstallationPath 에서 오류가 났는지 toURI 에서 오류가 났는지 확인이 불가능한 반면,
체이닝을 사용하지 않았을 경우, 둘 중 어디에서 문제가 발생한건지 확실하게 알 수 있지요.
아무튼 메소드 체이닝은 사용을 자제하는게 맞습니다.
○ NPE 예방법 3 - null 여부 비교 처리하기
제일 확실한 방법이기도 하지요.
조그마한 두개의 박스가 있습니다. 각 박스에는 소고기 한덩이와 돼지고기 한덩이가 있구요.
하나는 소고기 하나는 돼지고기인걸 아는데, 둘중에 뭔지 눈으로 봐서는 전혀 알수가 없네요.
그럴때 두 고기를 구분하려면 어떻게 해야 할까요? 답은 직접 구워먹어 보는겁니다. 암냠냠.
String pork = null; System.out.println(pork.indexOf("beef"));
이와 같은 코드를 작성한다면 indexOf 에서 오류가 발생할 수 밖에 없지요.
애초에 존재하지 않는 문자열인데요. 어떻게 문자열의 위치를 파악할 수 있겠습니까.
String pork = null; if(pork != null){ System.out.println(pork.indexOf("beef")); }
이렇게 하면 일단 돼지고기를 누가 먼저 구워먹었는지 아닌지 구분할 수 있겠지요?
3항 연산자를 사용해서 구분하는 방법도 있습니다. 저렇게 길게 나누기 귀찮은 경우에 사용하면 될 것 같네요.
String baka = null; System.out.println((baka == null) ? "0" : baka.length());
아무튼 null 여부를 비교해 주는게 맞습니다.
○ NPE 예방법 4 - 객체 초기화는 빈값으로
객체 초기화를 굳이 null 으로 할 필요가 있나요?
요즘 컴퓨터 사양이 얼마나 좋은데 메모리 걱정해서 null 로 초기화 하는건 아닐테고.
String stringA = ""; String[] stringArr = new String[0]; stringArr[0] = ""; List listA = new ArrayList();
이렇게 하면 얼마나 좋아. 참조해도 NPE 같은 오류는 발생할 일이 없는데요.
물론 임베디드니 뭐니 하는 소형기기 대상의 프로젝트에서는 null 로 초기화 하는게 맞지요. 메모리 아껴야하니...
아무튼 웬만해선 객체 초기화는 빈값으로 하는게 맞습니다.
○ NPE 예방법 5 - 문자열 비교는 Literal을 기준으로 사용하자.
String 문자열은 객체형이기 때문에 비교시 관련 메서드를 사용해야 합니다. 등호같은거 사용안된다 이말입니다.
보통 equals 메서드를 사용하는데요. 비교 기준을 무엇으로 잡느냐에 따라 NPE 여부가 달라집니다.
String nemo = new String("NEMO"); if(nemo == "NEMO"){ }
를 사용할 경우, 애초에 nemo 라는 객체와 "NEMO" 라는 리터럴값을 비교할 수 없으므로 오류가 납니다.
그래서 equals 함수를 사용하죠.
String nemo = new String("NEMO"); if(nemo.equals("NEMO")){ }
이런 방식으로 개선하였지만, 또 다른 문제가 남아있지요.
String nemo = null; if(nemo.equals("NEMO")){}
같은 방식을 사용하면, nemo 가 null 이므로 자연스럽게 NPE가 발생할 수 밖에 없지요.
그래서 문자열 비교는 Literal 기준의 비교가 필요한 듯 합니다.
String nemo = null; if("NEMO".equals(nemo)){ }
아무튼 문자열 비교는 Literal 기준이어야 합니다.
○ NPE 예방법 6 - toString() 대신 valueOf() 사용하기
toString()
은 객체를 참조해야만 하므로, 객체가 null 일 경우 NPE를 발생시킬 수 밖에 없지요.
Integer num = null; System.out.println(num.toString());
이와 같이 toString 이 참조되는 객체가 null 일 경우, Java는 에베벱 NPE! 를 외칠 수 밖에 없으니...
Integer num = null; System.out.println(String.valueOf(num));
과 같은 방식으로 valueOf()
로 문자열 변환을 해 주면 됩니다. 그래봐야 null 값의 valueOf 변환값은 "null" 이지만요.
애초에 null 값을 전달하지 않는게 중요합니다.
아무튼 문자열 변환은 valueOf 를 사용해야 합니다.
○ NPE 예방법 7 - Optional 사용하기
Java8 부터 도입된 Optional 이라는 Wrapper이 있습니다. 어떤 객체타입이든 Optional< >
이 씌워질 수 있지요.
사용법이 어렵지는 않습니다.
Optional<String> opt = Optional.ofNullable(null);
이렇게 Optional 래퍼를 사용한 객체를 생성할 수 있습니다.
Nullable 하지 않은 객체의 경우 Optional.of 를 통해 생성할 수 있으나, 여기서는 다루지 않겠습니다.
개인적으로 이렇게 길쭉길쭉하게 작성되어야 하는 코드들은 선호하지 않기 때문에, Optional 도 잘 사용하지 않습니다.
물론, 애초에 Java를 사용할 일이 별로 없고, 사용하더라도 소형 프로젝트로만 사용한다는 점이 더 큰 이유지만요.
이렇게 Optional 객체를 만들었다면, null 값을 받은 Optional 객체는 빈 객체(null과는 다름)를 반환하고,
이외의 경우에는 잘 래핑된 객체를 반환받을 수 있겠지요.
Optional은 이 이외에도 중간처리, 종단처리를 함께 할 수 있습니다. 이 점은 의외로 나쁘지 않은 것 같아요.
여러 처리들 중 정말 자주 사용될만한 것들만 몇개 소개해 보겠습니다.
Optional.of("Hello").filter((val) -> "Hello".equals(val)).orElse("Im SAD"); // Hello Optional.of("Bye").filter((val) -> "Hello".equals(val)).orElse("Im SAD"); // Im SAD
filter()
입니다. 진술부가 참이라면 필터를 통과하고, 거짓이라면 통과하지 못하고 빈 객체를 반환합니다.
String s = null; String opt = Optional.ofNullable(s).orElse("No Data"); System.out.println(opt); // "No Data" 출력.
orElse()
입니다. 최종 연산 이후 Optional 객체가 비어 있다면 반환할 값을 설정해 줄 수 있지요.
위 예제에서는 따로 연산이 없었으니, Nullable 체크 이후 계속 객체가 비어있었을 것이고, 그러니 최종 연산값도 빈 객체겠지요.
필연적으로 orElse 처리도 진행될 수 밖에 없구요.
Optional 은 Java9에 와서 한차례 더 강화되기도 하였죠. or 연산이나 stream 메서드 등이 추가되었는데.
자세한 내용은 직접 검색 해 보시면 될 것 같습니다. 여기서 다룰 내용은 아니니.
아무튼 Optional 이라는 래퍼를 사용하는 방법도 있습니다.
○ 마치며
NPE는 문제점을 발견하기만 하면 해결은 매우 쉬운 오류입니다. 그 문제점을 찾기 어려울 때가 있다는 점이 문제지만요.
다들 NPE 만나지 않도록 조심하시고, 행복한 JAVA 인생 만들어 나가세요.
아무튼, NPE는 열받는 오류입니다.