Header Banner
GG Logo

Future Engineering

기술의 최전선을 기록합니다.

기술 자료/BackEnd/Java 기초 제네릭(Generics)이란?

Java 기초 제네릭(Generics)이란?

BackEnd11개월 전

1. 제네릭(Generics)이란?

제네릭은 클래스, 인터페이스, 메서드에서 사용할 데이터 타입을 컴파일 시에 미리 지정하지 않고, 실제 사용 시점에 타입을 지정할 수 있게 하는 기능입니다. 이를 통해 코드 재사용성을 높이고, 컴파일 시 타입 안전성을 보장할 수 있습니다.

 

2. 제네릭 클래스와 메서드

제네릭 클래스

타입 매개변수를 정의하여 다양한 타입을 다룰 수 있습니다.

public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

제네릭 클래스는 여러 타입을 유연하게 처리할 수 있는 데이터 구조나 기능이 필요할 때 사용됩니다. 예를 들어, List, Map, Set과 같은 컬렉션 클래스들은 제네릭을 사용해 다양한 타입의 객체를 처리할 수 있습니다.

Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println(stringBox.getItem());

Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
System.out.println(integerBox.getItem());

제네릭 메서드

메서드 수준에서 타입 매개변수를 정의합니다.

public static <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.print(element + " ");
    }
    System.out.println();
}

클래스 전체가 아닌 특정 메서드에서만 제네릭 타입이 필요할 때 사용됩니다. 같은 기능을 하는 메서드가 여러 타입의 인자를 받아야 할 경우 유용합니다.

 

Integer[] intArray = {1, 2, 3};
String[] stringArray = {"Hello", "World"};
printArray(intArray);
printArray(stringArray);

 

3. 와일드카드와 경계 설정

  • 상한 경계 (<? extends T>): T 또는 그 하위 타입만 허용

  • 하한 경계 (<? super T>): T 또는 그 상위 타입만 허용

  • 무제한 와일드카드 (<?>): 모든 타입 허용

List<? extends Number> list = new ArrayList<Integer>();

4. 제네릭의 제한사항

  • 기본 타입(primitive type) 사용 불가 (int 대신 Integer 사용)

  • 정적(static) 멤버에 제네릭 타입 매개변수 사용 불가

  • 타입 소거(Type Erasure)로 인해 런타임에 제네릭 타입 정보가 유지되지 않음

  • 제네릭 타입의 배열 생성 불가 (new T[] 형태 사용 불가)

 

5. 예시

DAO (Data Access Object)

DAO 계층에서 제네릭을 사용하면 여러 엔티티 타입에 대해 재사용 가능한 데이터 접근 메서드를 구현할 수 있습니다.

public interface GenericDAO<T, ID> {
    T findById(ID id);
    List<T> findAll();
    void save(T entity);
    void update(T entity);
    void delete(ID id);
}

public class UserDAO implements GenericDAO<User, Long> {
    @Override
    public User findById(Long id) {
        // 구현 내용
    }

    @Override
    public List<User> findAll() {
        // 구현 내용
    }

    @Override
    public void save(User entity) {
        // 구현 내용
    }

    @Override
    public void update(User entity) {
        // 구현 내용
    }

    @Override
    public void delete(Long id) {
        // 구현 내용
    }
}

Service

서비스 계층에서 제네릭을 사용하면 다양한 비즈니스 엔티티에 대해 일관된 서비스 인터페이스를 제공할 수 있습니다.

public interface CRUDService<T, ID> {
    T findById(ID id);
    List<T> findAll();
    T save(T entity);
    T update(T entity);
    void delete(ID id);
}

public class UserService implements CRUDService<User, Long> {
    private final UserDAO userDAO;
    public UserService(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

    @Override
    public User findById(Long id) {
        return userDAO.findById(id);
    }

    @Override
    public List<User> findAll() {
        return userDAO.findAll();
    }

    @Override
    public User save(User entity) {
        userDAO.save(entity);
        return entity;
    }

    @Override
    public User update(User entity) {
        userDAO.update(entity);
        return entity;
    }

    @Override
    public void delete(Long id) {
        userDAO.delete(id);
    }
}

RestController

RestController에서 제네릭을 사용하면 다양한 타입의 요청과 응답을 처리하는 범용 컨트롤러를 구현할 수 있습니다.

@RestController
@RequestMapping("/api")
public class GenericController<T, ID> {

    private final CRUDService<T, ID> service;

    public GenericController(CRUDService<T, ID> service) {
        this.service = service;
    }

    @GetMapping("/{id}")
    public ResponseEntity<T> getById(@PathVariable ID id) {
        T entity = service.findById(id);
        return ResponseEntity.ok(entity);
    }

    @GetMapping
    public ResponseEntity<List<T>> getAll() {
        List<T> entities = service.findAll();
        return ResponseEntity.ok(entities);
    }

    @PostMapping
    public ResponseEntity<T> create(@RequestBody T entity) {
        T savedEntity = service.save(entity);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedEntity);
    }

    @PutMapping("/{id}")
    public ResponseEntity<T> update(@PathVariable ID id, @RequestBody T entity) {
        T updatedEntity = service.update(entity);
        return ResponseEntity.ok(updatedEntity);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable ID id) {
        service.delete(id);
        return ResponseEntity.noContent().build();
    }
}

// 사용 예시
@RestController
@RequestMapping("/api/users")
public class UserController extends GenericController<User, Long> {
    public UserController(UserService userService) {
        super(userService);
    }
}

 

각 계층에서 제네릭을 적절히 활용하면 더 유연하고 확장 가능한 코드를 작성할 수 있습니다. 하지만 예시코드와 같이 제네릭의 사용은 코드의 복잡성을 증가시킵니다.