정리

타입 안정성을 지키고 컴파일 에러를 보장하기 위해 로 타입이 아닌 제네릭 타입을 사용하라!

제네릭 타입이란?

클래스/인터페이스에 <타입 매개변수>를 붙인 타입을 의미한다. 제네릭 클래스는 클래스<타입 매개변수>, 제네릭 인터페이스는 인터페이스<타입 매개변수> 형태이다.

  • 제네릭 예시
    • List<E> → E는 정규 타입 매개변수
    • List<String> → String은 실제 타입 매개변수

 

제네릭 타입 사용 시 이점

private final Collection<Stamp> stamps = ...;
  • 타입 안정성
    • 컴파일러가 stamps에는 Stamp 인스턴스만 넣어야함을 인지함
    • stamps에 다른 타입의 인스턴스를 넣으려할 때 컴파일 에러 발생
  • 표현력

 

로 타입

List와 같이 타입 매개변수가 없는 제네릭 타입을 의미

  • 로 타입 사용을 하면 안되는 이유
    • 타입 안정성과 표현력이 없음
    • 런타임 에러 가능성 존재
  • 예시 코드
    • 에러 발견 시기가 컴파일 시점이 아닌 런타임 시점이다.
    // 로 타입
    private final Collection stamps = {...};
    stamps.add(new Coin(...)); 
    
    // 위 코드에서 발생할 수 있는 문제점
    add() 하는 시점에는 에러가 발생하지 않지만
    추후에 런타임 에러 가능성이 있고, 원인 코드를 추적하기가 어려워짐
    
    // 해결책
    private final Collection<Stamp> stamps = {...}; -> 제네릭으로 타입 안정성 확보
    stamps.add(new Coin(...)); -> 컴파일 에러 발생
    

 

로 타입이 만들어진 이유

제네릭이 나오기 전 코드와의 호환성을 위해 로 타입이 만들어졌다.

List vs. List<Object>

  • List
    • 제네릭 타입을 배제한다는 것을 의미
    • List 타입 매개변수에 List<String> 넘길 수 있음 → 타입 안정성 보장되지 않음
public static void main(String[] args) {
		List<String> strings = new ArrayList<>();

		// List 에 List<String> 넘길 수 있음
		unsafeAdd(strings, Integer.valueOf(42));
		strings.get(0); // 컴파일러가 자동 형변환
}

// 매개변수에 로 타입 List 존재
private static void unsafeAdd(List list, Object o) {
		list.add(o);
}
  • List<Object>
    • 모든 타입(Object)을 허용한다는 의사를 컴파일러에 명확히 전달한다는 것을 의미
    • List<Object> 타입 매개변수에 List<String> 넘길 수 없음 → 타입 안정성 보장, 무공변성?
      • List<Object>, List<String>
    public static void main(String[] args) {
    		List<String> strings = new ArrayList<>();
    
    		// List<Object> 에 List<String> 넘길 수 없음
    		safeAdd(strings, Integer.valueOf(42)); -> 컴파일 에러 발생
    }
    
    // 매개변수에 제네릭 타입 List<Object> 존재
    private static void safeAdd(List<Object> list, Object o) {
    		list.add(o);
    }
    

→ 로 타입 말고 제네릭 타입 사용해라

비한정적 와일드 카드 <?>

제네릭 타입을 쓰고 싶지만, 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않을 때 사용하는 타입이다.

Set<?>과 같은 형태로 사용하면 된다. 비한정적 와일드 카드를 사용하면 타입 안정성을 보장하고, 유연한 코드를 작성할 수 있다.

 

 

로 타입을 사용하는 예외 케이스

  1. class 리터럴에는 로 타입을 사용하라
    1. List.class, String[].class, int.class 사용 가능
    2. List<String>.class, List<?>.class 사용 불가능
    Class<List> listClass = List.class; // 가능
    List<?>.class; // 불가능
    
  2. instanceof 연산자
    1. 런타임에는 제네릭 타입 정보가 지워지므로 <?>(비한정적 와일드카드 타입) 외에 매개변수 타입은 instanceof 사용에 적용할 수 없다.
    if (o instanceof Set) { // 로 타입
    	Set<?> s = (Set<?>) o; // (로 타입 -> 와일드 카드 타입) 형변환
    }
    

요약

클래스 내부에 클래스를 만드는 방법은 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스가 있다.

상황에 맞춰서 가장 적절한 중첩 클래스를 사용하도록 하자. 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어서 사용하자.

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길고, 바깥 클래스를 참조한다 → 비정적 멤버 클래스

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길고 바깥 클래스를 참조하지 않음 → 정적 멤버 클래스

중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한곳이고, 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다 → 익명 클래스로 만들자.

아니면 지역 클래스로 만들자.

 

중첩 클래스란?

다른 클래스 안에 정의된 클래스이고, 자신을 감싼 바깥 클래스에서만 사용되어야 한다.

종류로는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스가 있다.

정적 멤버 클래스를 제외한 모든 중첩 클래스는 내부 클래스(inner class)이다.

  • 중첩 클래스를 왜 쓰는가?
    • 클래스 간 논리적 그룹을 나타낼 때 사용한다.
      • model 객체에서 상위 모델과 하위 모델이 있을 때 static nested class 사용
    • 향상된 캡슐화
    • 좋은 가독성과 유지보수성

 

1. 정적 멤버 클래스

클래스 내부에 있는 static 클래스이다.

// 모양새
class Outer {
    변수;
    메소드;
		private 멤버;

    public static class Inner {

    }
}

// 객체 생성
Outer.Inner 객체 = new Outer.Inner();
  • 특징
    • 다른 클래스 내부에서 선언된다
    • 바깥 클래스의 private 멤버에도 접근할 수 있다
    • 정적 멤버와 같은 접근 규칙을 적용받는다 (ex. private 선언시 바깥 클래스에서만 접근 가능)
  • private 정적 멤버 클래스
    • 바깥 클래스가 표현하는 객체의 한 부분(구성 요소)을 나타낼 때 주로 사용한다.
    • Map 인터페이스 구현체는 엔트리 객체를 private static 멤버 클래스로 만든다?

→ 바깥 클래스 인스턴스와 독립적으로 존재해야하는 경우 사용해야 한다.

 

2. 비정적 멤버 클래스

클래스 내부에 있는 non-static 중첩 클래스이다.

비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없다.

비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다(숨은 외부참조).

즉, 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this(**바깥 *클래스명.this*** 형태)로 바깥 인스턴스 메서드 호출 하거나 바깥 인스턴스의 참조를 가져올 수 있다.

// 모양새
class Outer {
    변수;
    메소드;
		this.~~~
		
		메서드1 {
			new Inner(); // 내부 클래스 생성자 호출
    }

    public class Inner {
		
		이너 메서드 {
			Outer.this.메서드1
		}
  }
}
  • 바깥 인스턴스와 비정적 멤버 인스턴스 관계 형성 방법
    • 자동
      • 바깥 클래스 메서드 안에 비정적 멤버 클래스 생성자가 호출되고 멤버 클래스 인스턴스 생성 시 자동으로 관계 형성
    • 수동
      • 바깥 인스턴스 [클래스.new](<http://클래스.new>) Member Class(args) 호출로 수동 생성도 가능
  • 숨은 외부 참조 특징
    • 한 번 형성된 관계는 변경 불가능하다
    • 관계 정보는 비정적 멤버 클래스 인스턴스 안에 만들어진다
      • 메모리 공간 차지
      • 생성 시간 더 걸림
    • GC에서 멤버 클래스 인식이 안돼 메모리 누수 발생 가능
    • 참조가 눈에 보이지 않아 문제 원인을 찾기 어려워짐
  • 사용 예시 - 어댑터(Adapter)
    • 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰를 사용할 때

→ 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

 

3. 익명 클래스

이름이 없는 클래스를 의미한다.

  • 장점
    • 사용되는 시점에 선언과 동시에 인스턴스 생성
    • 코드 어디에서든 만들 수 있다
    • 비정적 익명 클래스
      • 바깥 클래스 인스턴스 참조 가능
    • 정적 익명 클래스
      • 상수표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다
  • 단점
    • 선언한 지점에서만 인스턴스 생성 가능
    • instanceof 검사나 클래스 이름이 필요한 작업 수행 불가능
    • 인터페이스 구현 및 상속 불가능
    • 코드가 길어질 경우 가독성 저하

→ 람다 출시 이후에는 람다가 익명 클래스를 거의 대체할 수 있다.

 

4. 지역 클래스

지역 변수를 선언할 수 있는 곳에 어디든 만들 수 있는 클래스를 의미한다. 유효 범위도 지역변수와 같다. 메서드 내부에서 new로 생성한 다음에 메서드 내부에서만 사용이 가능하다.

class Outer {
    변수;
    메소드1;

    메소드2() {
        지역변수;

        class Inner {

        }
    }
}
  • 특징
    • 이름이 있고, 반복해서 사용 가능
    • 비정적 문맥에서 사용될 때만 바깥 인스턴스 참조 가능
    • 정적 멤버 가질 수 없다
    • 가독성을 위해 짧게 작성해야 한다

본 포스트는 책 '이펙티브 자바' 에서 학습한 내용을 기반으로 작성되었습니다.

요약

자바 8부터 디폴트 메서드의 도입으로 기존의 인터페이스를 문제없이 수정할 수 있게 되었지만, 되도록이면 신중하게 디폴트 메서드를 추가하자.

 

디폴트 메서드

디폴트 메서드는 자바 8부터 추가된 내용으로, 기존 인터페이스에 메서드를 추가할 수 있는 새로운 방법이다. 인터페이스의 기존 구현체에서 디폴트 메서드를 재정의 하지 않아도 디폴트 구현이 자동으로 쓰이기 때문에 웬만하면 문제없이 인터페이스에 추가할 수 있다고 한다. (100%는 아님)

자바 8에서는 람다를 활용하기 위한 목적으로 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었음.

 

예시 : removeIf()

// 예시
arrayList.removeIf('람다?')

// 디폴트 메서드 안에 함수형 인터페이스를 활용해 람다를 사용할 수 있다? 
default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean result = false;
        for (Iterator<E> it = iterator(); it.hasNext();) {
            if (filter.test(it.next())) {
                it.remove();
                result = true;
            }
        }
        
        return result;
    }
  • 반복자를 이용해 순회하면서 각 원소를 인수로 넣어 프레디키트를 호출
  • 프레디키드가 반환값이 true라면 반복자의 remove 메서드를 호출
  • 해당 원소 제거

 

문제

  • 디폴트 메서드 추가로 문제를 안 일으킨다는 보장이 없음
    • 변경 전 인터페이스를 사용하는 클라이언트에서 문제 발생 가능성 존재
    • 컴파일 에러가 발생하지 않더라도, 런타임 에러의 위험성
    • 예기치 못한 충돌/API 재앙, 사용자 불편 상황 발생 가능성 존재

→ 되도록이면 기존 인터페이스에는 디폴트 메서드를 추가하지 말자.

 

문제 해결 방법

  1. 인터페이스 구현체에서 디폴트 메서드 재정의 강제
  2. 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업 강제
  3. 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 하지 말자
  4. 기존 인터페이스가 아닌 새로운 인터페이스를 설계하는 경우라면 위험성 적음

 

인터페이스 설계 후 해야할 일들

바로 잡을 기회가 아직 남았을 때 결함을 테스트로 찾아내자

  1. 새로운 인터페이스인 경우 릴리스 전에 반드시 테스트를 거쳐야 함
  2. 인터페이스의 서로 다른 구현 케이스를 생각해 테스트 해야함
  3. 인터페이스를 사용하는 다양한 클라이언트도 만들어서 테스트 해야함

정적 팩터리 메서드란?

public 생성자 외에 인스턴스를 반환하는 방법 중 하나로, 해당 클래스의 인스턴스를 반환하는 단순한 정적 메서드. 디자인 패턴의 팩터리 메서드(Factory Method)와는 다른 개념

  • 예시 
  • public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
  • boolean 값을 받아 Boolean 객체 참조로 변환하는 함수

 

장점

  1. 이름을 가질 수 있다.
    1. public 생성자
      1. 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못함
      2. 하나의 시그니처에 하나의 생성자만 만들 수 있음
    2. 정적 팩터리 메서드
      1. 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사 가능
      2. BigInteger(int, int, Random) BigInteger.probablePrime -> 소수인 BigInteger 반환을 더 잘 설명함
      3. 동일한 시그니처의 생성자가 여러 개 필요한 경우
      4. 각각의 특성을 잘 나타내는 네이밍으로 함수를 만든다면 동일한 시그니처의 생성자를 만들 때 발생하는 문제 해결 가능
  2. 호출 될 때마다 인스턴스를 새로 생성하지 않아도 된다.
    1. 불변 클래스는 재사용하면 되므로 불필요한 객체 생성을 피할 수 있음
      1. 생성 비용이 큰 동일한 객체가 자주 요청되는 상황에서 용이함 → 성능 향상
    2. 인스턴스 통제(instance-controlled)
      1. 반복되는 요청에 같은 객체를 반환하는 방식으로 언제 어느 인스턴스를 살아 있게 할지 통제 가능
      2. 인스턴스를 통제하는 이유?
        1. 클래스를 싱글턴 클래스, 인스턴스화 불가로 만들 수 있음
        2. 동치인 인스턴스가 단 하나뿐임을 보장 가능
  3. Boolean.valueOf(boolean) -> 객체를 아예 생성하지 않음
  4. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    1. 구현 클래스를 공개하지 않고도 객체를 반환하기에 반환할 객체의 클래스를 자유롭게 선택 가능
    2. API를 작게 유지 가능
    3. 인터페이스 기반 프레임워크를 만드는 핵심 기술
  5. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    1. 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없음
    • 예시 - EnumSet
      1. 생성자 없이 정적 팩터리로만 제공
      2. OpenJDK에서는 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스 반환
        1. 원소 개수 < 65 : RegularEnumSet 반환
        2. 원소 개수 ≥ 65 : JumboEnumSet 반환
      3. 클라이언트는 두 클래스의 존재를 모르기에 클래스의 제거 및 추가가 용이함
    • EnumSet
  6. 정적 팩터리 메서드를 작성하는 시점엔 반환할 객체의 클래스가 존재하지 않아도 된다.
    1. 서비스 제공자 프레임워크의 근간
    2. 방법1) 리플렉션에 동적 클래스 로딩을 적용해서 반환 타입 가져옴
    3. 방법2) 인터페이스만 만들어 놓고 그거를 반환하게 함
    • 서비스 제공자 프레임 워크
      • 서비스 인터페이스
        • 구현체의 동작을 정의
      • 제공자 등록 API
        • 제공자가 구현체를 등록할 때 사용
      • 서비스 접근 API
        • 클라이언트가 서비스의 인스턴스를 얻을 때 사용
    • 3개의 핵심 컴포넌트로 이루어져 있고, 예시로는 JDBC(Java Database Connectivity)가 있음

 

 

단점

  1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    1. 정적 팩터리 메서드만 사용한 경우, 생성자가 없기 때문에 해당 메서드는 상속할 수 없음
    2. private 접근 제어자는 불가능
    3. 컴포지션 + 불변 타입으로 만들 시 이 특징은 오히려 장점이 될 수 있음
  2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
    1. 정적 팩터리 메서드는 생성자처럼 API 설명에 나와있지 않으므로 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 함
      1. 현재로서는 이 문제는 다음 방식으로 해결할 수 있음
        1. API 문서를 잘 쓰기
        2. 메서드 이름도 널리 알려진 규약을 따라 짓기
        • 예시
          • from
            • 입력 매개변수 : 1개
            • 해당 타입의 인스턴스 반환하는 형변환 메서드
            Date d = Date.from(instant);
            
          • of
            • 입력 매개변수 : 여러 개
            • 적합한 타입의 인스턴스를 반환하는 집계 메서드
            • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING)0;
          • valueOf
            • from과 of의 더 자세한 버전
            • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
          • instance/getInstance
            • 인스턴스 반환
            • 매개변수를 받는 경우 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스 보장하지 않음
            • StackWalker luke = StackWalker.getInstance(options);
          • create/newInstance
            • instance/getInstance와 같음
            • 매번 새로운 인스턴스를 생성 후 반환을 보장
            • Object newArray = Array.newInstance(classObject, arrayLen);
          • get{Type} → getType
            • getInstance와 동일
            • 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용
            • {Type} : 팩터리 메서드가 반환할 객체의 타입
            FileStore fs = Files.get{FileStore}(path)
            -> Files에 FileStore를 반환하는 팩터리 메서드 정의
            
          • new{Type} → newType
            • newInstance와 동일
            • 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용
            • {Type} : 팩터리 메서드가 반환할 객체의 타입
            BufferedReader br = Files.new{BufferedReader}(path);
            
          • type
            • getType과 newType의 간결한 버전
            List<Complaint> litany = Collections.list(legacyLitany);
            

 

 

정리

정적 팩터리 메서드와 public 생성자는 각각 장단점이 있으므로 상황에 맞게 적절하게 사용.

그래도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로, 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

 

추천 함수

  • Integer.valueOf() → -126 ~ 126 숫자 생성 → 성능 향상

+ Recent posts