null이란?
null 참조(null reference) 또는 null 포인터(null pointer)란 유효한 객체(Object)를 포인터(또는 참조) 하지 않고 있음을 가리키기 위한 저장된 값을 말한다.
null은 왜 나쁜가?
- Ad-hoc(임시방편, 이것을 위해서만 만들어진) 에러 핸들링
- 객체를 받을 경우 항상 null인지 유효한 객체인지 체크해야 한다.
- 모호한 의도(또는 의미)
- 모호성을 없애려면 항상 실제 객체를 반환하거나, null 객체 반환 또는 exception을 던져야한다.
- 컴퓨터적 사고 vs 객체적 사고
- - Hello, is it a software department?
- 안녕하세요. 소프트웨어 부서인가요?
- Yes.
- 네.
- Let me talk to your employee "Jeffrey" please.
- Jeffrey라는 직원 좀 바꿔주세요.
- Hold the line please...
- 잠시만 기다려주세요.
- Hello.
- (제프리 일수도 있고 null일 수도 있는 객체)안녕하세요.
- Are you NULL?
- 당신은 NULL 입니까?
- - Hello, is it a software department?
- 느린 실패
- null을 사용한 코드는 빠른 실패가 아니라 천천히 실패하게 만든다. 뭔가 잘못되었을 때 모두에게 알리고 즉시 에러 핸들링을 하는 대신에, null을 사용한 코드는 클라이언트에게 이러한 실패들을 숨기게 된다.
- 가변적이고 불완전한 객체
- null은 타입을 파괴한다.
- null은 엉성(지저분)하다. (Sloppy)
- 예를 들어 java에서는 String에 대해 늘 이런 처리를 해줘야 한다.
- if (str == null || str.equals("")) {}
- 예를 들어 java에서는 String에 대해 늘 이런 처리를 해줘야 한다.
- null은 빈약한 api를 만든다.
null 창시자가 스스로 인정한 null 탄생의 실수
우선 null이라는 개념은 언제 누구에 의해 만들어졌을까?
null 참조는 1965년에 Tony Hoare라는 영국의 컴퓨터 과학자에 의해서 처음으로 고안되었다.
당시 그는 "존재하지 않는 값"을 표현할 수 있는 가장 편리한 방법이 null 참조라고 생각했다고 한다.
하지만 나중에 그는그 당시 자신의 생각이 "10억불 짜리 큰 실수" 였고, null 참조를 만든 것을 후회한다고 토로하였다.
NPE (NullPointerException)
null 참조로 인해 자바 개발자들이 가장 골치아프게 겪는 문제는 그 악명높은 널 포인터 예외(소위, NPE)일 것이다.
자바 초보이든 고수이든 객체를 사용하여 모든 것을 표현하는 자바 개발자에게 NPE는 코드 베이스 곳곳에 깔려있는 지뢰같은 녀석이다.
컴파일 타임에는 조용히 잠복해있다가 런타임 때 펑펑 터지는 NPE의 스택 트레이스에 자바 개발자들은 속수무책으로 당할 수 밖에 없었다.
java.lang.NullPointerException
at seo.dale.java.practice(OptionalTest.java:26)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
null 처리가 취약한 코드
null 처리가 취약한 코드에서는 NPE 발생 확률이 높다.
예를 들어 어떤 쇼핑몰에서 다음과 같은 구조의 데이터 모델들이 있다고 가정해보자.
/* 주문 */
public class Order {
private Long id;
private Date date;
private Member member;
// getters & setters
}
/* 회원 */
public class Member {
private Long id;
private String name;
private Address address;
// getters & setters
}
/* 주소 */
public class Address {
private String street;
private String city;
private String zipcode;
// getters & setters
}
Order 클래스는 Member 타입의 member 필드를 가지며 Member 클래스는 다시 Address 타입의 address 필드를 가진다.
그리고 "어떤 주문을 한 회원이 어느 도시에 살고 있는지 알아내기" 위해서 다음과 같은 메소드가 있다고 가정해보자.
/* 주문을 한 회원이 살고 있는 도시를 반환한다 */
public String getCityOfMemberFromOrder(Order order) {
return order.getMember().getAddress().getCity();
}
위 메소드가 얼마나 NPE 위험에 노출된 상태인지 보이는가? (안보인다면 평소에 null 처리를 열심히 하시지 않는 분으로..)
NPE 발생 시나리오
위 메소드에서 구체적으로 어떤 상황에서 NPE가 발생할까?
여러 단계로 이루어진 객체 탐색의 과정을 짚어보면 다음과 같이 NPE 위험 포인트를 도출할 수 있다.
order 파라미터에 null 값이 넘어옴
order.getMember()의 결과가 null임
order.getMember().getAdderss()의 결과가 null임
order.getMember().getAddress().getCity()의 결과가 null 임
4번째 경우에는 엄밀히 얘기하면 이 메소드 내부에서 NPE가 발생하는 케이스는 아니다.
하지만 null을 리턴함으로써 호출부에 NPE 위험을 전파시키는 케이스이다.
호출부에서 적절히 null 처리를 해주지 않으면, 다음과 같이 호출부에서 NPE를 발생시킬 수 있다.
String city = getCityOfMemberFromOrder(order); // returns null
System.out.println(city.length()); // throws NPE!
전통적인(?) NPE 방어 패턴
Java8 이전에는 이렇게 NPE의 위험에 노출된 코드를 다음과 같은 코딩 스타일로 회피하였다.
중첩 null 체크하기
public String getCityOfMemberFromOrder(Order order) {
if (order != null) {
Member member = order.getMember();
if (member != null) {
Address address = member.getAddress();
if (address != null) {
String city = address.getCity();
if (city != null) {
return city;
}
}
}
}
return "Seoul"; // default
}
정말 끔찍하지만 실무에서 심심치 않게 볼 수 있었던 코드이다.
객체 탐색의 모든 단계마다 null이 반환되지 않을지 의심하면서 null 체크를 한다.
들여쓰기 때문에 코드를 읽기가 매우 어려우며 핵심 비즈니스 파악이 쉽지 않다.
사방에서 return 하기
public String getCityOfMemberFromOrder(Order order) {
if (order == null) {
return "Seoul";
}
Member member = order.getMember();
if (member == null) {
return "Seoul";
}
Address address = member.getAddress();
if (address == null) {
return "Seoul";
}
String city = address.getCity();
if (city == null) {
return "Seoul";
}
return city;
}
첫 번째 코드를 조금 개선해서 (악화라고 볼 수도 있음) 전반적으로 코드 읽기가 쉬워지긴 했지만 결과를 여러 곳에 리턴하기 때문에 유지 보수하기가 난해해졌다.
2가지 방법 모두 기본적으로 객체의 필드나 메소드에 접근하기 전에 null 체크를 함으로써 NPE를 방지하고 있다. 하지만 안타깝게도 이로 인해 초기 버전의 메소드보다 코드가 상당히 길어지고 지저분해졌음을 볼 수 있다.
이 밖에도 null object 패턴 등 NPE 문제를 해결하기 위한 다양한 시도들이 있었지만 만족스러운 대안은 없었다.
null의 저주
코드가 이 지경에 이르면 과연 문제의 원인이 개발자의 무능함 때문인지 다른 곳에서 근본 원인을 찾아야 하는지 혼란스러워진다. 애초에 getCityOfMemberFromOrder() 메소드에 대한 우리의 요구 사항은 명확하고 간단했다.
"어떤 주문을 한 회원이 어느 도시에 살고 있는지 알려주시오!"
하지만 우리의 코드는 중첩된 if 조건문과 사방에 return 문으로 도배되고 말았다. 유지보수 기간이 길어질수록 비즈니스 로직은 점점 null 체크에 가려지곤 했다. 이쯤되면 애초에 우리가 하려던 것이 null체크인지 비즈니스 로직인지 헷갈리기까지 한다.
NPE 때문에 시스템이 다운되서 한 두번 데어보신 분이라면, 위와 같은 코드를 작성하고 있는 자신을 발견할 것이다. 장애를 겪을 바에 코드 가독성과 유지 보수성을 희생하는 게 현실적인 선택이기 때문이다.
자바 언어는 (대부분의 다른 언어들처럼) "값의 부재"를 나타내기 위해 null을 사용하도록 설계되었다. 하지만 null 창시자가 이도 했던 바와 다르게 null은 자바 개발자들에게 NPE 방어라는 끝나지 않는 숙제를 남겼다.
2편
https://jmdwlee.tistory.com/36
ref.
'Study > WEB' 카테고리의 다른 글
[WEB] 프록시(Proxy)란? (0) | 2023.02.12 |
---|---|
[Java] 빠져나올 수 없는 null 처리의 늪 - 2 (0) | 2023.02.08 |
[JAVA] 예외 던지기(throw) & 예외 연결(Chained Exception) (0) | 2023.01.29 |
[WEB] HTTP Method (0) | 2023.01.26 |
좋은 객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2023.01.24 |