[Java]Generic(Interface에서의 활용)
**제네릭(Generic)**은 인터페이스에서도 유용하게 사용됩니다. 제네릭을 사용하면 인터페이스를 구현하는 클래스들이 구체적인 데이터 타입을 명시할 수 있어, 보다 유연하고 타입 안전한 코드를 작성할 수 있습니다. 이 방식은 특히 다양한 타입을 처리해야 하는 클래스를 작성할 때 많이 사용됩니다.
1. 제네릭을 사용한 인터페이스 정의
제네릭을 사용한 인터페이스는 타입 매개변수를 통해 다양한 타입의 객체를 처리할 수 있습니다. 제네릭 타입을 인터페이스 선언부에 추가하면, 인터페이스를 구현하는 클래스는 그 타입을 명시하여 구체화할 수 있습니다.
예시: 제네릭을 사용한 인터페이스
// 제네릭 인터페이스 선언
public interface Repository<T> {
T findById(int id);
void save(T entity);
}
• 여기서 **T**는 타입 매개변수로, 구체적인 데이터 타입을 지정하지 않았습니다. **T**는 나중에 인터페이스를 구현하는 클래스에서 결정됩니다.
2. 제네릭 인터페이스 구현
제네릭 인터페이스를 구현할 때, 구현 클래스에서 구체적인 타입을 지정할 수 있습니다. 제네릭 인터페이스를 통해 다양한 데이터 타입을 처리할 수 있게 됩니다.
예시: 제네릭 인터페이스 구현
// User 엔티티
public class User {
private int id;
private String name;
// 생성자, getter, setter 생략
}
// UserRepository는 User 타입의 데이터를 처리하는 인터페이스 구현체
public class UserRepository implements Repository<User> {
@Override
public User findById(int id) {
// 데이터베이스에서 User를 찾는 로직 (가상 로직)
return new User(id, "User" + id);
}
@Override
public void save(User user) {
// 데이터베이스에 User를 저장하는 로직 (가상 로직)
System.out.println("User saved: " + user.getName());
}
}
• UserRepository는 **Repository<User>**로 제네릭 타입을 User로 지정하였습니다.
• Repository<T>에서 사용된 모든 T 타입이 User로 구체화됩니다. 즉, findById와 save 메서드는 User 객체를 처리하게 됩니다.
3. 다양한 타입을 처리하는 인터페이스 구현
같은 제네릭 인터페이스를 다양한 타입으로 구현할 수 있습니다. 예를 들어, Product라는 클래스가 있고, 같은 제네릭 인터페이스를 사용하여 Product를 처리할 수 있습니다.
예시: 다른 타입을 사용한 제네릭 인터페이스 구현
// Product 엔티티
public class Product {
private int id;
private String name;
// 생성자, getter, setter 생략
}
// ProductRepository는 Product 타입의 데이터를 처리하는 인터페이스 구현체
public class ProductRepository implements Repository<Product> {
@Override
public Product findById(int id) {
// 데이터베이스에서 Product를 찾는 로직 (가상 로직)
return new Product(id, "Product" + id);
}
@Override
public void save(Product product) {
// 데이터베이스에 Product를 저장하는 로직 (가상 로직)
System.out.println("Product saved: " + product.getName());
}
}
• 이번에는 **ProductRepository**가 **Repository<Product>**로 구체화되었습니다.
• 동일한 인터페이스 **Repository<T>**를 사용하지만, 각각 **User**와 **Product**에 대해 다른 데이터 타입을 처리할 수 있습니다.
4. 제네릭 타입 경계 설정 (Bounded Generics)
때로는 제네릭 인터페이스에서 타입에 제한을 두고 싶을 때가 있습니다. 이때는 제네릭의 상한 경계(Upper Bounded Generics)를 설정하여, 특정 클래스나 인터페이스를 상속한 타입만 사용하도록 제한할 수 있습니다.
예시: 타입 경계를 설정한 제네릭 인터페이스
// Comparable<T> 인터페이스를 상속한 타입만 처리할 수 있도록 경계 설정
public interface ComparableRepository<T extends Comparable<T>> {
void compare(T entity1, T entity2);
}
// Comparable을 상속한 String 타입을 처리하는 구현체
public class StringRepository implements ComparableRepository<String> {
@Override
public void compare(String entity1, String entity2) {
int result = entity1.compareTo(entity2);
System.out.println("Comparison result: " + result);
}
}
• **T extends Comparable<T>**로 타입 경계를 설정했습니다. 이제 Comparable을 상속한 클래스만 제네릭 인터페이스로 사용할 수 있습니다.
• **StringRepository**는 String을 처리하는 제네릭 인터페이스의 구현체입니다.
5. 와일드카드를 사용한 제네릭 인터페이스
때로는 제네릭 인터페이스를 사용할 때 타입의 범위를 유연하게 만들고 싶을 때가 있습니다. 이때는 **와일드카드(?)**를 사용하여 타입 경계를 설정할 수 있습니다.
예시: 와일드카드를 사용한 제네릭 인터페이스
public interface Printer<T> {
void print(T item);
}
// 와일드카드를 사용한 메서드
public class WildcardPrinter {
// Printer의 하위 타입을 모두 처리할 수 있음
public void printItems(Printer<? extends Number> printer) {
printer.print(123);
}
}
• **? extends Number**는 Number 타입과 그 하위 타입만 허용합니다. 이를 통해 유연한 타입 처리가 가능합니다.
• 와일드카드로 인해 Integer, Double 등 다양한 타입의 Number를 처리할 수 있습니다.
6. 제네릭 인터페이스의 실용적 예시 (스프링의 Repository)
스프링 프레임워크에서는 제네릭 인터페이스가 자주 사용됩니다. 예를 들어, 스프링 데이터 JPA의 **CrudRepository<T, ID>**는 제네릭 인터페이스로 정의되어 있어, 다양한 엔티티에 대해 CRUD 기능을 제공합니다.
스프링 데이터 JPA에서의 제네릭 인터페이스 예시
import org.springframework.data.repository.CrudRepository;
// CrudRepository 인터페이스는 제네릭으로 다양한 타입을 처리할 수 있음
public interface UserRepository extends CrudRepository<User, Long> {
}
• **CrudRepository<T, ID>**는 제네릭 인터페이스로, T는 엔티티 클래스, ID는 엔티티의 ID 타입을 나타냅니다.
• UserRepository는 User 엔티티에 대해 CRUD 기능을 제공하며, ID 타입은 **Long**으로 설정되어 있습니다.
7. 제네릭을 사용한 인터페이스의 장점
1. 재사용성: 하나의 제네릭 인터페이스로 다양한 타입의 객체를 처리할 수 있습니다.
2. 타입 안전성: 제네릭을 사용하면 컴파일 시점에 타입을 체크하므로, 런타임 타입 오류를 방지할 수 있습니다.
3. 유연성: 제네릭 인터페이스는 다양한 데이터 타입을 처리할 수 있으며, 필요에 따라 타입 경계를 설정할 수 있습니다.
결론
• 제네릭 인터페이스는 타입에 유연성을 제공하면서도 타입 안전성을 보장하는 강력한 기능입니다.
• 제네릭을 사용하면 하나의 인터페이스로 다양한 데이터 타입을 처리할 수 있으며, 이는 특히 재사용성과 유연성이 중요한 시스템에서 매우 유용합니다.
• 타입 경계를 설정하거나 와일드카드를 사용하여 더 세밀하게 타입을 관리할 수 있습니다.
• 제네릭 인터페이스는 특히 스프링 프레임워크와 같은 라이브러리에서 데이터 처리에 많이 사용됩니다.