[Medium] 클린 코드: Null 반환을 피하는 법
NullPointerException 위험을 줄이려면 메서드에서 null 값을 반환하지 않도록 노력하세요. 값이 없는 경우를 전달하기 위해서는 Optional, 빈 컬렉션, 또는 에러 핸들링을 사용하는 것과 같은 대체 전략을 사용하십시오.
📌 null을 반환하는 대신, Optional을 사용하여 값의 부재를 명시적으로 표현하는 것을 고려하세요. 이를 통해 클라이언트는 값이 있는 경우와 없는 경우를 모두 처리할 수 있습니다.
import java.util.Optional;
// A service class that may or may not find a user
class UserService {
// Simulating a method that may or may not find a user by ID
public Optional<User> findUserById(int userId) {
// In a real-world scenario, this method might interact with a database or another data source
// Here, we'll just return an Optional based on a simple condition
if (userId > 0 && userId <= 100) {
return Optional.of(new User(userId, "John Doe"));
} else {
return Optional.empty();
}
}
}
// User class representing a user
class User {
private final int id;
private final String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
public class OptionalExample {
public static void main(String[] args) {
UserService userService = new UserService();
// Attempting to find a user with a valid ID
Optional<User> userOptional = userService.findUserById(42);
// Handling the presence or absence of the user
if (userOptional.isPresent()) {
User user = userOptional.get();
System.out.println("User found: " + user.getName());
} else {
System.out.println("User not found.");
}
// Using the orElse method to provide a default value if the user is not found
User defaultUser = userService.findUserById(101).orElse(new User(0, "Default User"));
System.out.println("Default User: " + defaultUser.getName());
}
}
📌 컬렉션을 처리할 때, null 대신 빈 컬렉션을 반환하세요. 이렇게 하면 클라이언트 코드가 간소화되어 null 검사가 필요하지 않고 일관된 인터페이스를 제공할 수 있습니다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// A service class that may or may not return a list of items
class ItemService {
// Simulating a method that may or may not find items
public List<String> findItems() {
// In a real-world scenario, this method might interact with a database or another data source
// Here, we'll just return an empty list based on a simple condition
if (Math.random() > 0.5) {
List<String> items = new ArrayList<>();
items.add("Item 1");
items.add("Item 2");
items.add("Item 3");
return items;
} else {
return Collections.emptyList(); // Returning an empty list instead of null
}
}
}
public class EmptyCollectionExample {
public static void main(String[] args) {
ItemService itemService = new ItemService();
// Attempting to find items
List<String> foundItems = itemService.findItems();
// No need for null checks - directly iterate over the collection
for (String item : foundItems) {
System.out.println("Found item: " + item);
}
// Handling an empty list without the need for explicit null checks
if (foundItems.isEmpty()) {
System.out.println("No items found.");
} else {
System.out.println("Total items found: " + foundItems.size());
}
}
}
📌 값이 없는 상태를 나타내는 특정한 객체를 생성하여 Null Object Pattern을 구현하세요. 이 객체는 실제 객체와 동일한 인터페이스 또는 클래스를 준수해야 합니다.
// Interface representing a shape
interface Shape {
void draw();
}
// Concrete implementation of Shape for Circle
class Circle implements Shape {
private final int radius;
public Circle(int radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing Circle with radius " + radius);
}
}
// NullObject representing the absence of a Shape
class NullShape implements Shape {
@Override
public void draw() {
// Do nothing or provide a default behavior for the absence of a shape
System.out.println("No shape to draw.");
}
}
// Client code using the Shape interface
public class NullObjectPatternExample {
public static void main(String[] args) {
// Drawing a Circle
Shape circle = new Circle(5);
circle.draw();
// Attempting to draw a null shape (traditional approach)
Shape nullShape = null;
if (nullShape != null) {
nullShape.draw();
} else {
System.out.println("Null shape encountered.");
}
// Using Null Object Pattern to represent the absence of a shape
Shape nullObjectShape = new NullShape();
nullObjectShape.draw();
}
}
📌 값의 부재가 예외적인 조건을 나타내는 경우 null을 반환하는 것보다 예외를 던지는 것을 고려하십시오.
import java.util.HashMap;
import java.util.Map;
// Custom exception for representing the absence of a value
class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
// Service class that may throw NotFoundException
class ItemService {
private Map<String, String> itemMap;
public ItemService() {
this.itemMap = new HashMap<>();
itemMap.put("item1", "Item 1");
itemMap.put("item2", "Item 2");
itemMap.put("item3", "Item 3");
}
// Method that throws NotFoundException if the item is not found
public String getItemByName(String itemName) {
String item = itemMap.get(itemName);
if (item == null) {
throw new NotFoundException("Item not found: " + itemName);
}
return item;
}
}
public class ExceptionInsteadOfNullExample {
public static void main(String[] args) {
ItemService itemService = new ItemService();
try {
// Trying to get an item by name
String item = itemService.getItemByName("item2");
System.out.println("Item found: " + item);
// Trying to get a non-existent item (throws NotFoundException)
String nonExistentItem = itemService.getItemByName("item4");
System.out.println("This line won't be reached."); // This won't be printed
} catch (NotFoundException e) {
System.out.println("Exception caught: " + e.getMessage());
}
}
}
📌 값이 없음을 나타내기 위해 특정 반환 값을 지정합니다. 예를 들어, 오류 조건을 나타내기 위해 특수 문자열이나 상수를 반환합니다.
// Service class that returns a special string to indicate errors or absence of a value
class DataService {
private static final String ERROR_STRING = "Error: Unable to retrieve data";
// Simulating a method that may return data or an error string
public String getDataById(int id) {
// In a real-world scenario, this method might interact with a database or another data source
// Here, we'll simulate a condition where data is not found
if (id >= 1 && id <= 100) {
return "Data for ID " + id;
} else {
return ERROR_STRING; // Special string to indicate an error or absence of data
}
}
}
public class ReturnValueForErrorExample {
public static void main(String[] args) {
DataService dataService = new DataService();
// Trying to get data by ID
String data1 = dataService.getDataById(42);
if (data1.equals(dataService.ERROR_STRING)) {
System.out.println("Error: Data not found.");
} else {
System.out.println("Data found: " + data1);
}
// Trying to get data for a non-existent ID
String data2 = dataService.getDataById(150);
if (data2.equals(dataService.ERROR_STRING)) {
System.out.println("Error: Data not found.");
} else {
System.out.println("Data found: " + data2);
}
}
}
📌 null을 반환하는 대신 기본값을 제공합니다. 이를 통해 클라이언트는 항상 의미 있는 결과를 얻을 수 있습니다.
// Service class that provides default values
class ConfigurationService {
private static final String DEFAULT_COLOR = "White";
private static final int DEFAULT_FONT_SIZE = 12;
// Simulating a method that may return a configuration value or a default value
public String getColor(String elementId) {
// In a real-world scenario, this method might read configuration values
// Here, we'll simulate a condition where the color is not configured
// Instead of returning null, we provide a default color
if ("header".equals(elementId)) {
return "Blue"; // Configured color for the header
} else {
return DEFAULT_COLOR; // Default color when the configuration is not available
}
}
// Simulating a method that may return an integer configuration value or a default value
public int getFontSize(String elementId) {
// In a real-world scenario, this method might read configuration values
// Here, we'll simulate a condition where the font size is not configured
// Instead of returning null, we provide a default font size
if ("body".equals(elementId)) {
return 16; // Configured font size for the body
} else {
return DEFAULT_FONT_SIZE; // Default font size when the configuration is not available
}
}
}
public class DefaultValueInsteadOfNullExample {
public static void main(String[] args) {
ConfigurationService configurationService = new ConfigurationService();
// Getting color for the header
String headerColor = configurationService.getColor("header");
System.out.println("Header Color: " + headerColor);
// Getting color for a non-existent element (returns default color)
String unknownElementColor = configurationService.getColor("unknownElement");
System.out.println("Unknown Element Color: " + unknownElementColor);
// Getting font size for the body
int bodyFontSize = configurationService.getFontSize("body");
System.out.println("Body Font Size: " + bodyFontSize);
// Getting font size for a non-existent element (returns default font size)
int unknownElementFontSize = configurationService.getFontSize("unknownElement");
System.out.println("Unknown Element Font Size: " + unknownElementFontSize);
}
}
📌 메서드가 특정 상황에서 null을 반환할 수 있는 경우 문서에 이 동작을 명확하게 기록합니다. 이는 개발자가 잠재적인 null 값을 이해하고 처리하는 데 도움이 됩니다.
📌 null을 피할 수 없는 경우 Objects.requireNull 또는 조건부 검사를 사용해서 안전한 null 처리 방법을 사용하여 NullPointerException 위험을 최소화합니다.
import java.util.Objects;
// Service class that may return null in certain cases
class UserService {
// Simulating a method that may return a user object or null
public User findUserById(int userId) {
// In a real-world scenario, this method might interact with a database or another data source
// Here, we'll simulate a condition where the user is not found
if (userId > 0 && userId <= 100) {
return new User(userId, "John Doe");
} else {
return null;
}
}
}
// User class representing a user
class User {
private final int id;
private final String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
public class SafeNullHandlingExample {
public static void main(String[] args) {
UserService userService = new UserService();
// Attempting to find a user by ID
User user = userService.findUserById(42);
// Encouraging safe null handling practices
if (user != null) {
// Performing actions with the user object
System.out.println("User found: " + user.getName());
// Using Objects.requireNonNull for additional safety
processUser(Objects.requireNonNull(user));
} else {
System.out.println("User not found.");
}
}
// Example method that processes a user object (additional functionality)
private static void processUser(User user) {
System.out.println("Processing user with ID " + user.getId());
}
}
📌 만약 null이 예기치 않은 프로그래밍 오류를 나타내는 경우, NullPointerException을 허용하여 "Fail Fast" 원칙을 받아들입니다. 이는 개발 중 문제를 식별하고 해결하는 데 도움이 됩니다.
// Service class that throws NullPointerException for unexpected null values
class AccountService {
// Simulating a method that expects a non-null account object
public void processAccount(Account account) {
// Using Objects.requireNonNull to fail fast if the account is null
Objects.requireNonNull(account, "Account cannot be null");
// Processing the account (example functionality)
System.out.println("Processing account with ID " + account.getId());
}
}
// Account class representing an account
class Account {
private final int id;
public Account(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
public class FailFastExample {
public static void main(String[] args) {
AccountService accountService = new AccountService();
// Attempting to process an account (non-null case)
Account validAccount = new Account(123);
accountService.processAccount(validAccount);
// Attempting to process a null account (programming error)
Account invalidAccount = null;
try {
accountService.processAccount(invalidAccount);
} catch (NullPointerException e) {
// Handling the NullPointerException (printing or logging the error)
System.err.println("NullPointerException caught: " + e.getMessage());
}
}
}
📌 최신 프로그래밍 언어를 사용하는 경우, Kotlin의 nullable type 이나 Swift의 optionals와 같이 더 안전한 널 핸들링 기능을 탐색합니다.
null을 반환하는 것에 대한 대안을 채택함으로써 코드의 명확성, 안전성 및 유지보수성을 향상합니다. null을 피하는 것은 optional, 에러 핸들링 또는 기타 패턴을 사용하는 것을 통해 NullPointerException을 방지합니다.
출처: https://medium.com/@bubu.tripathy/clean-code-avoid-returning-null-9353498d6a01