[이펙티브 자바] Effective Method와 주의사항
by rowing0328※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.
아이템 49 - 매개변수가 유효한지 검사하라
매개변수 검사의 필요성
API나 메서드 호출 시 매개변수(parameter)가 올바른 값인지 검사하는 것은 매우 중요하다.
잘못된 값이 들어오면 예기치 않은 동작이나 오류가 발생할 수 있기 때문이다.
매개변수의 유효성을 사전에 검증하면 서비스의 안정성과 예측 가능성을 확보할 수 있다.
이를 통해 코드가 예상치 못한 상황에서도 안전하게 동작할 수 있도록 보장할 수 있다.
예제 코드 - 장바구니 아이템 추가
@RestController
public class ParameterCheckController {
@Data
class AddItemForm {
private long itemId;
private int count;
}
@PostMapping(value = "/{userId}/add")
public ResponseEntity getBasketInfo(
@PathVariable long userId,
@RequestBody AddItemForm addItemForm
) {
// 매개변수 검사가 필요함
}
}
- 이처럼 매개변수를 받는 API에서는 유효성 검사 로직이 포함되지 않으면 예상치 못한 문제가 발생할 수 있다.
검사 위치와 TPO(Time, Place, Occasion)의 중요성
매개변수 검사는 메서드의 시작 부분에서 명시적으로 수행하는 것이 좋다.
특히 외부에 공개된 API에서는 엄격한 검사가 필수적이다.
예상치 못한 값이 들어올 가능성이 높기 때문이다.
내부 코드에서 사용되는 메서드라 하더라도, 예외 상황을 고려해 충분한 검사를 수행하는 것이 바람직하다.
이는 서비스 안정성과 예측 가능성을 높이는 데 기여한다.
예제 코드
/**
* 사용자 포인트를 계산하는 메소드
* @param userId 사용자 ID
* @param cardId 사용자의 카드 ID
* @param orderItemInfoVoList 주문 아이템 정보
* @return 계산된 포인트 값
*/
public static double calculateUserPoint(
long userId,
long cardId,
List<OrderItemInfoVo> orderItemInfoVoList
) {
// 매개변수에 대한 검사가 필요
}
검사 기준의 문서화
매개변수 검사를 수행할 때는 검사 규칙을 문서화해 두는 것이 좋다.
이를 통해 팀원 간의 이해를 높이고, 코드 유지보수를 용이하게 만들 수 있다.
물론 실제로 코드에 모든 규칙을 적용하는 것은 쉽지 않다.
하지만 문서와 테스트 코드를 통해 이러한 부분을 보완할 수 있다.
검사를 지나치게 강하게 하는 것도 문제지만, 기본적인 검사를 통해 코드의 품질과 안정성을 높이는 것이 중요하다.
아이템 50 - 적시에 방어적 복사본을 만들라
방어적 복사의 필요성
Java의 Date 객체는 설계상의 문제로 인해 원본 데이터가 쉽게 변조될 위험이 있다.
이로 인해 의도하지 않은 값 변경이 발생할 수 있으며, 코드의 예측 가능한 동작이 깨질 수 있다.
이를 방지하기 위해 방어적 복사(Defensive Copying)를 통해 데이터를 보호하는 것이 중요하다.
필요한 경우 객체의 복사본을 사용함으로써 원본 데이터의 안전성을 보장할 수 있다.
방어적 복사는 특히 외부에서 전달받은 객체나 반환값을 다룰 때 필수적인 안전 장치다.
예제 코드 - Date 객체의 문제점
public Date getNearbyMonth(Date time, int month) {
if (time.getMonth() > month) {
time.setYear(time.getYear() + 1); // 원본이 변경됨
}
time.setMonth(month);
return time;
}
- 위 코드처럼 Date 객체를 직접 조작하는 경우, 원본 객체가 의도치 않게 변경될 수 있다.
- 이런 상황을 방지하기 위해 방어적 복사를 적용하는 것이 좋다.
방어적 복사의 적용 방법
방어적 복사는 메서드가 매개변수로 받은 객체나 반환하는 객체를 새로운 복사본으로 만들어,
원본이 손상되지 않도록 보호하는 기법이다.
이를 통해 외부에서 전달된 객체가 변경될 위험을 줄이고,
코드의 안정성과 예측 가능성을 높일 수 있다.
방어적 복사 적용 예시
public Date getNearbyMonth(Date time, int month) {
// 원본 객체의 복사본을 생성하여 조작
Date copiedTime = new Date(time.getTime());
if (copiedTime.getMonth() > month) {
copiedTime.setYear(copiedTime.getYear() + 1);
}
copiedTime.setMonth(month);
return copiedTime; // 복사본을 반환하여 원본 보호
}
실무에서의 적용과 주의사항
방어적 복사는 필요한 상황에서만 적용하여 성능과 메모리 사용을 최적화한다.
테스트 코드로 원본 객체의 손상 여부를 검증하여 코드의 안정성을 높인다.
아이템 51 - 메서드 시그니처를 신중히 설계하라
메서드 이름의 중요성
메서드 이름은 코드 가독성과 유지보수에 큰 영향을 미친다.
- 표준 명명 규칙을 따르고 너무 긴 이름은 피해야한다.
- 예시로, 아래와 같은 지나치게 긴 메서드 이름은 혼란을 초래할 수 있다.
예제 코드 - Bad Pratice
findByIdAndItemNameAndItemTargetSubIdAndCouponGroupIdOrderByCreatedDateDesc
올바른 메서드 명명 규칙 요약
- 메서드 이름은 명확하고 간결하게 작성한다.
- 길어질 경우, 메서드가 지나치게 많은 기능을 수행하고 있는지 점검한다.
편의 메서드 남용 방지
Util 클래스나 Helper 메서드는 자주 사용될 때만 제공하는 것이 좋다.
- 불 필요한 편의 메서드가 많아지면 코드 복잡도가 증가하고 유지보수가 어려워질 수 있다.
- 확신이 없다면 메서드를 private로 선언한 후 필요에 따라 점진적으로 리팩토링하는 방식이 권장된다.
Parameter 목록을 짧게 유지하라
파라미터가 너무 많을 경우 메서드를 이해하기 어렵고 실수할 가능성이 커진다.
특히 같은 타입의 여러 파라미터가 존재할 경우 더 혼란스럽다.
해결 방법
- 메서드가 너무 장황하다면 분리하거나 구조를 단순화한다.
- 파라미터를 **Value Object(VO)**로 묶어 사용한다.
예제 코드
class UserVo {
private long id;
private int age;
private int footSize;
private float weight;
private float height;
}
// 메서드 호출 예시
public static void expandParam(long id, int age, int footSize, float weight, float height) {}
public static void voParam(UserVo userVo) {} // 간결해진 형태
Parameter 타입은 Interface로
- 메서드 파라미터로 클래스보다 인터페이스 타입을 사용하는 것이 좋다.
예를 들어, ArrayList<String>이 아닌 List<String>으로 선언하여 유연성을 확보할 수 있다.
예제 코드
public static void example() {
List<String> aList = new ArrayList<>();
}
Boolean 파라미터는 Enum으로 대체하라
Boolean 같은 참/거짓의 두가지 상태만 표현할 수 있다.
필요에 따라 Enum을 사용하여 의미를 명확히 하는 것이 좋다.
예제 코드
enum SmartPhoneChargeType {
FIVE_PIN, LIGHTNING, C_TYPE
}
boolean isFivePin; // 불명확한 표현
아이템 52 - 다중 정의는 신중히 사용하라
Overloading의 위험성
메서드 오버로딩(Overloading)은 동일한 이름의 메서드를 여러 개 정의하는 기능이다.
하지만 매개변수가 같은 오버로딩은 코드의 혼란과 버그를 유발할 수 있다.
예를 들어, 아래와 같은 코드가 있다고 가정해보자.
public List<E> sort(ArrayList<E> arrayList) {
// 알고리즘 1번
return arrayList;
}
public List<E> sort(LinkedList<E> arrayList) {
// 알고리즘 2번
return arrayList;
}
- 메서드 이름은 같지만 매개변수 타입만 다르다.
- 호출자가 매개변수 타입을 혼동하거나, 의도와 다른 메서드가 호출될 위험이 있다.
해결 방법
1. 매개 변수 타입이 다른 경우에도 메서드 이름을 명확히 구분한다.
public List<E> sortFromArrayList(ArrayList<E> arrayList) {
// 알고리즘 1번
return arrayList;
}
public List<E> sortFromLinkedList(LinkedList<E> arrayList) {
// 알고리즘 2번
return arrayList;
}
2. 생성자와 같이 형변환이 발생할 수 있는 경우에도 오버로딩을 피하는 것이 좋다.
public class OverloadingExample {
public OverloadingExample(int value) {
System.out.println("int 생성자 호출");
}
public OverloadingExample(double value) {
System.out.println("double 생성자 호출");
}
public static void main(String[] args) {
OverloadingExample example1 = new OverloadingExample(10); // int 생성자 호출
OverloadingExample example2 = new OverloadingExample(10.0); // double 생성자 호출
OverloadingExample example3 = new OverloadingExample(10L); // long -> double로 자동 형변환, double 생성자 호출
}
}
아이템 53 - 가변인수는 신중히 사용하라
가변 인수의 문제점
가변 인수만으로 메서드를 구성하면 필수 인수가 있는 경우에도 호출자가 실수할 가능성이 높아진다.
호출자가 필수 인수를 제공하지 않아도 메서드가 정상적으로 호출되지만,
실행 중에 예기치 않은 오류가 발생할 수 있다.
잘못된 예시
static int min(int... args) {
int min = Integer.MAX_VALUE;
for (int arg : args) {
if (arg < min)
min = arg;
}
return min;
}
- 위 메서드는 인수를 0개로 호출해도 컴파일 에러가 발생하지 않지만, 실행 시 빈 배열로 인해 잘못된 결과를 반환할 수 있다.
가변 인수와 필수 인수를 분리하라
필수 인수는 반드시 가변인수 외부에 명시적으로 선언하여, 호출자가 실수하지 않도록 해야 한다.
올바른 예시
static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs) {
if (arg < min)
min = arg;
}
return min;
}
- 첫 번째 인수를 필수로 지정했기 때문에 최소한 하나의 인수가 반드시 전달된다.
- 호출 시점에 명확한 사용법을 제공하며, 코드 안정성이 향상된다.
프레임워크에서의 가변인수 사용 예시
Java의 List.of() 메서드는 가변인수를 활용하되, 다양한 오버로드를 통해 유연성을 제공한다.
다음과 같은 메서드 시그니처들이 제공된다.
예제 코드
List.of();
List.of(E e1);
List.of(E e1, E e2);
List.of(E e1, E e2, E e3, ...);
- 이처럼 필요한 경우 적절한 메서드 오버로드를 함께 사용하여 가변인수의 사용을 보완할 수 있다.
아이템 54 - null이 아닌 빈 컬렉션이나 배열을 반환하라
null 반환의 문제점
메서드가 null을 반환하는 경우, 호출자는 추가로 null 체크를 해야 한다.
이는 코드가 복잡해지고, NullPointerException과 같은 예외가 발생할 위험을 증가시킨다.
잘못된 예시
public List<String> getNameList() {
return null; // 빈 리스트 대신 null 반환
}
- 호출자는 매번 null 여부를 확인해야 하며, 이를 놓칠 경우 런타임 예외가 발생할 수 있다.
빈 컬렉션이나 배열을 반환하라
null 대신 빈 컬렉션이나 배열을 반환하면 코드가 더욱 안전해지고 간결해진다.
올바른 예시
@GetMapping
public List<String> getNameList() {
return Collections.emptyList(); // 빈 리스트 반환
}
- Collections.emptyList()는 불변 객체로 성능에 영향이 없으며, 메모리 낭비도 최소화된다.
API 응답의 경우
API에서 JSON 응답을 반환할 때에도 null 대신 빈 배열을 반환하는 것이 좋다.
예를 들어, 특정 API 호출이 성공했으나 반환할 데이터가 없는 경우, 다음과 같이 빈 배열로 응답한다.
{
"data": []
}
- null 대신 빈 배열을 반환하면 클라이언트가 불필요한 null 체크를 하지 않아도 된다.
- 이는 API 응답의 일관성을 유지하고, 오류 가능성을 줄여준다.
아이템 55 - Optional 반환은 신중히 하라
Optional<T>는 값이 존재할 수도 있고 존재하지 않을 수도 있는 컨테이너 객체이다.
주로 메서드가 값이 없을 수 있는 상황에서 null 대신 안전하게 반환값을 처리하기 위해 사용된다.
예제 코드 - Optional 사용
Optional<Laptop> optionalLaptop = laptopRepository.findById(id);
Laptop laptop = optionalLaptop.orElse(null); // 권장되지 않음
API 노트에 따르면, Optional은 메서드의 반환값으로 사용하도록 설계되었다.
- 변수 자체가 Optional 타입일 때 null로 설정하지 말고 항상 Optional 객체를 반환해야 한다.
- null 반환이 예상되는 경우 Optional로 감싸서 명시적으로 처리한다.
올바른 Optional 사용법
Optional은 값이 없을 경우 기본값을 제공하거나, 예외를 던질 때 유용하다.
예시 코드 - 기본값 사용
public int maxPositiveIntegerValue(List<Integer> integerList) {
return getMaxInteger(integerList).orElse(0); // 기본값 0 반환
}
private OptionalInt getMaxInteger(List<Integer> integerList) {
return integerList.stream()
.mapToInt(Integer::intValue)
.filter(integer -> integer > 0)
.max();
}
예제 코드 - 예외 처리
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
Optional을 사용하지 말아야 할 경우
Optional이 항상 적합한 것은 아니다. 다음과 같은 경우에는 Optional 사용이 권장되지 않는다.
- 컨테이너 타입 (예: 컬렉션, 배열)
빈 컬렉션 또는 배열을 반환하는 것이 더 적절한다.
public List<String> getNameList() {
return Collections.emptyList();
}
- 기본 타입 (Primitive Type)
OptionalInt, OptionalDouble과 같은 전용 클래스를 사용해야 한다. - 잘못된 예시
public ItemInfo getItemInfo() {
return itemInfoRepository.findById(1L).orElse(null); // Optional을 무시하고 null 반환
}
아이템 56 - 공개된 API 요소에는 항상 문서화 주석을 작성하라
문서화 주석의 중요성
공개된 API는 다양한 개발자가 사용할 수 있기 때문에, 명확한 문서화 주석이 필수적이다.
주석은 사용자가 API를 올바르게 이해하고 사용할 수 있도록 도와준다.
현실적인 접근 방법
모든 API에 완벽한 주석을 작성하는 것은 현실적으로 어려운 일이다.
따라서 아래와 같은 방식으로 주석을 점진적으로 보완하는 것이 좋다.
- 가장 중요한 요소부터 시작
메서드의 반환값, 매개변수, 예외 처리 등 핵심 요소에 우선적으로 주석을 작성한다. - 점진적으로 개선
시간이 허락될 때마다 주석을 추가하거나 기존 주석을 개선한다.
예제 코드 - 주석 작성 예시
/**
* 사용자의 이름 목록을 반환합니다.
*
* @return 사용자의 이름이 포함된 리스트 (비어 있을 수 있음)
* @throws DataAccessException 데이터 접근 중 오류가 발생한 경우
*/
public List<String> getUserNames() {
// 메서드 구현
}
마무리
메서드 시그니처와 주석의 중요성을 다시 한 번 느꼈다.
명확한 이름과 매개변수 설계를 통해 코드의 의도를 전달하고,
협업을 위해 주석과 API 문서화를 꼼꼼히 작성해야 한다는 점을 깨달았다.
또한 사이드 프로젝트에서 null로 인한 문제를 많이 겪었던 만큼,
Optional을 활용해 null 체크의 복잡함을 줄이고 더 안전한 코드를 작성하는 방법이 인상 깊었다.
'📚Book Archive > Effective Java' 카테고리의 다른 글
[이펙티브 자바] Basic한 프로그래밍 원칙 - Part 2 (0) | 2025.02.07 |
---|---|
[이펙티브 자바] Basic한 프로그래밍 원칙 - Part 1 (0) | 2025.02.05 |
[이펙티브 자바] 람다의 우아함과 Stream의 주의사항 (0) | 2025.01.17 |
[이펙티브 자바] Enum과 Annotaion의 Effective한 사용 (0) | 2025.01.12 |
[이펙티브 자바] Generic method와 Generic의 주의사항 (0) | 2025.01.08 |
블로그의 정보
코드의 여백
rowing0328