코드의 여백

[이펙티브 자바] Basic한 프로그래밍 원칙 - Part 2

by rowing0328

※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.

 

아이템 64 - 객체는 인터페이스를 사용해 참조하라

 

왜 인터페이스가 클래스보다 나은가

  • 유연성 (Flexibility)
    인터페이스는 구현체에 의존하지 않기 때문에 다양한 구현체로 대체가 가능하다.
    반면, 클래스는 고정된 형태로 설계되어 있어 변경과 확장이 어렵다.
  • 구현체의 불투명성
    인터페이스를 사용할 경우, 내부 구현이 무엇인지 알 필요가 없다. 이는 추상화의 장점을 극대화한다.
    클래스는 명확한 구현을 포함하고 있어 내부 구조를 알 수 있지만, 이는 때로 불필요한 의존성을 초래할 수 있다.

 

예제 코드 - 인터페이스 활용의 중요성

public class EmailUtil {

    private static final Pattern EMAIL = Pattern.compile(
        "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$"
    );

    static boolean isEmailValid(String s) {
        return EMAIL.matcher(s).matches();
    }

    public static List<String> filterEmail(List<String> stringList) {
        return stringList.stream()
                         .filter(EmailUtil::isEmailValid)
                         .collect(Collectors.toList());
    }

    public static ArrayList<String> filterEmail(ArrayList<String> stringArrayList) {
        return stringArrayList.stream()
                              .filter(EmailUtil::isEmailValid)
                              .collect(Collectors.toCollection(ArrayList::new));
    }
    
}
  • 위 코드에서 `filterEmail` 메서드는 `List` 와 `ArrayList` 두 가지 버전으로 작성되었다.
  • 하지만 인터페이스 `List` 만 사용했다면 중복된 코드를 줄이고 더 유연한 설계가 가능했을 것이다.

 

적합한 인터페이스가 없는 경우

인터페이스 사용이 항상 가능한 것은 아니다. 다음과 같은 경우에는 클래스를 사용하는 것이 더 적합할 수 있다.

 

  • 값을 나타내는 클래스
    예 : `String`, `BigInteger`
  • 클래스 기반 프레임워크
    예 : `OutputStream` 과 같은 클래스
  • 특수한 메서드를 제공하는 클래스
    예 : `PriorityQueue` 는 `Queue` 에 없는 `Comparator` 메서드를 제공한다. 

 

 

아이템 65 - 리플렉션보다는 인터페이스를 사용하라

 

Reflection이란 무엇인가

Java 프로그래밍에서 리플렉션은 런타임에 프로그램이 스스로 검사하거나 수정할 수 있는 강력한 기능이다.

이 기능을 통해 클래스의 구조를 확인하고, 메서드 호출이나 필드의 값을 가져오는 등의 작업을 수행할 수 있다.

예를 들어, 특정 클래스의 모든 멤버 이름을 가져오고 출력하는 것도 가능하다.

 

Reflection은 Spring, Hibernate, Jackson 등 다양한 프레임워크에서 활용되며,

동적인 객체 생성 및 메서드 호출을 가능하게 한다. 하지만 이러한 강력함에는 단점도 존재한다.

Reflection의 주요 특징

  • 런타임 정보 접근
    클래스, 메서드, 필드 등의 정보를 런타임에 확인할 수 있다.
  • 특정 객체의 모든 메서드를 출력하는 예제 코드
public static void main(String[] args) {
    List<String> a = new ArrayList<>();
    dumpMethod(a);
}

public static void dumpMethod(Object arg) {
    Class c = arg.getClass();
    Method m[] = c.getDeclaredMethods();
    for (Method method : m) {
        System.out.println(method.toString());
    }
}

 

  • 동적 객체 생성 및 메서드 호출
    컴파일 시점에 알 수 없는 클래스나 메서드를 런타임에 동적으로 사용할 수 있다.

 

Reflection의 단점

  • 컴파일타임 타입 검사 불가
    Reflection을 사용하면 컴파일 시점에 타입 검사가 이루어지지 않아 런타임 오류가 발생할 가능성이 높아진다.
  • 코드 복잡성 증가
    Reflection을 사용한 코드는 가독성이 떨어지고 유지보수가 어려워질 수 있다.
  • 성능 저하
    Reflection은 일반적인 메서드 호출보다 느리다.
    따라서 성능이 중요한 애플리케이션에서는 주의해야 한다.

 

Reflection을 사용해야 할 때

Reflection은 강력하지만 남용해서는 안 된다. 다음과 같은 경우에만 사용하는 것이 좋다.

 

  • 컴파일 시점에 알 수 없는 클래스 사용
    런타임에 동적으로 클래스를 로드하거나 객체를 생성해야 할 때 유용하다.
  • 객체 생성 시에만 사용
    Reflection으로 생성한 객체는 인터페이스나 상위 클래스를 통해 참조하여 사용하는 것이 바람직하다.

 

 

아이템 66 - 네이티브 메서드는 신중히 사용하라

 

네이티브 메서드란 무엇인가

네이티브 메서드는 Java 코드가 아닌 네이티브 프로그래밍 언어(C, C++ 등)로 작성된 코드를 호출하기 위해 사용된다.

이를 통해 Java가 기본적으로 지원하지 않는 플랫폼 특화 기능이나 기존 네이티브 라이브러리를 사용할 수 있다.

 

네이티브 메서드를 사용해야 경우

플랫폼 특화 기능 활용
특정 플랫폼에서만 제공되는 기능(예: Windows 레지스트리 접근)을 활용해야 할 때,

Java만으로 구현하기 어려운 작업을 수행하기 위해 네이티브 코드를 사용한다.

 

기존 네이티브 라이브러리 사용
이미 작성된 C/C++ 기반의 라이브러리를 재사용해야 하는 경우,

특히 고성능 데이터 처리나 오래된 시스템과의 연동 등에서 네이티브 메서드가 유용하다.

 

성능 개선을 위한 분리
Java 코드로 작성했을 때 성능에 결정적인 영향을 주는 부분을 네이티브 코드로 분리하여 최적화할 때 사용한다.

다만, 현대 JVM은 대부분의 상황에서 최적화가 잘 되어 있어 이 경우는 드물다.

 

네이티브 메서드 사용 시 주의사항

유지보수성 저하
네이티브 코드는 플랫폼에 종속적이고,

디버깅이 어려워 코드 가독성과 유지보수성이 떨어질 수 있다.

 

성능 오버헤드
JNI 호출에는 오버헤드가 존재하며,

잘못 설계하면 성능 개선보다 오히려 저하될 수 있다.

 

현대 JVM의 발전
현재 JVM이 성능과 기능 면에서 크게 발전하여
대부분의 작업을 순수 Java 코드로 충분히 구현할 수 있으므로,
네이티브 메서드는 정말 필요한 경우에만 사용하는 것이 바람직하다.

 

 

아이템 67 - 최적화는 신중히 하라

 

빠른 프로그램보다 좋은 프로그램이 더 중요하다

최적화가 문제 해결의 방법이 될 수 있지만, 지나치게 최적화에 집착하면 프로그램의 구조가 손상될 수 있다.

구조가 무너지면 유지 보수와 수정이 어려워질 위험이 있다.

 

따라서 설계를 먼저 하고, 최적화는 나중에 진행하는 것이 중요하다.

탄탄한 설계를 기반으로 해야 최적화도 효과적으로 이루어질 수 있다.

 

성능을 제한하는 설계를 피해야 한다

설계 단계에서 성능에 제약을 주는 요소들을 조심해야 한다.

외부 시스템과의 과도한 통신이나 컴포넌트 간 의존성이 복잡할 경우, 나중에 변경이 어렵거나 확장이 불가능해질 수 있다.

 

이러한 문제는 설계 초기부터 충분히 고려하는 것이 바람직하다.

 

API 설계에서도 성능을 고려해야 한다

API 설계를 할 때는 성능에 미치는 영향을 신중하게 검토해야 한다.

잘못된 API 설계는 시스템의 전반의 성능을 저하시키고, 이후 수정이 어려운 문제로 발전할 수 있다.

 

API는 다양한 사용자와 시스템이 소통하는 핵심 요소이기 때문에, 처음부터 성능과 확장성을 함께 고려해야 한다.

 

 

아이템 68 - 일반적으로 통용되는 명명 규칙을 따르라

 

패키지(Package) 명명 규칙

  • 외부에 사용되는 경우 조직의 인터넷 도메인을 역순으로 작성한다.
    예시 : com.google
  • 각 요소는 8글자 이하의 짧은 단어를 사용한다.
    예시 : GlobalPositioningSystem -> gps
  • 필요한 경우 계층을 나누어 명명한다.
    예시 : deliveryDriver 대신 delivery.driver
  • 합성어가 필요한 상황에서는 _(underline)을 사용할 수 있지만, 일반적으로 권장되지 않는다.
    예시 : Google의 가이드에서도 잘 사용하지 않는다.

 

클래스(Class)와 인터페이스(Interface) 명명 규칙

  • 하나 이상의 단어로 구성되며, 통용되는 줄임말을 제외하고는 줄여 쓰지 않는다.
    예시 : passenger, deliveryDriver
  • 약어는 첫 글자를 대문자로 하거나 전체를 대문자로 표기한다.
    예시 : HTTPServer, URLParser

 

메서드(Method)와 필드(Field) 명명 규칙

  • 메서드는 첫 글자를 소문자로 시작한다.
    예시 : calculateTotal, getData
  • 상수 필드는 전체를 대문자로 표기하고, 단어 사이에는 _를 사용한다.
    예시 : static final int MAX_VALUE
  • 지역 변수는 약어를 사용할 수 있다.

 

문법 규칙

  • 단순명사나 명사구로 이름을 짓는다.
    객체를 생성할 수 없는 클래스의 이름은 복수형으로 작성한다.
    예시 : PatternUtils
  • 인터페이스 이름은 able이나 ible로 끝나는 형용사를 사용한다.
    예시 : Serializable, Comparable
  • 동작을 수행하는 메서드는 동사나 동사구로 명명한다.
    예시 : append, add
  • Boolean 타입의 메서드는 is나 has를 사용한다.
    예시 : isAvailable, hasData

 

메서드 반환 및 타입 변경 규칙

  • 반환 타입이 Boolean, void가 아닌 경우 속성을 반환하는 메서드에는 get~을 붙인다.
    예시 : getName, getAge
  • 객체의 타입을 변경하는 메서드는 to~접두사를 사용한다.
    예시 : toArray, toList, toString
  • 정적 팩터리 메서드는 상황에 따라 from이나 valueOf 접두사를 사용한다.
    예시 : fromString, valueOf

 

 

마무리

프로그래밍은 단순히 코드를 작성하는 것을 넘어, 문제 해결 능력과 설계 철학을 함께 요구한다.

이번에 다룬 내용들은 모두 개발자로서 반드시 익히고 적용해야 할 기본기들이다.

시간이 지나면서 더욱 중요하게 느끼게 되는 부분이기도 하다.

 

나 역시 프로젝트 경험을 통해 하나씩 배워가고 있으며, 앞으로도 이러한 원칙들을 실천하며 성장하고자 한다.

기본에 충실한 개발자가 되기 위해 꾸준히 배우고 적용해 나가야 할 것이다.

블로그의 정보

코드의 여백

rowing0328

활동하기