순간을 성실히, 화려함보단 꾸준함을

[리뷰] 14주차 : 제네릭 본문

백기선님과 함께 하는 자바 스터디

[리뷰] 14주차 : 제네릭

폭발토끼 2021. 7. 31. 22:20

중복되는 코드를 줄여보자!!!!

과거 스프링 JAP(아직 나도 이 개념이 뭔지는 모르지만 요즘 JPA 모르면 바보라니깐 천천히라도 공부하려는 의지를 갖자!!!)가 나오기 이전에는 데이터베이스에 접근하려는 방식은 DAO(Data Access Object) 를 사용하였다.
그러나 이런 방식은 중복된 코드를 양상시키는 주된 이유였으며, 단순 노동으로 치부되기도 하였다.

//Apple
public class Apple {
    private Integer id;

    public Integer getId(){
        return this.id;
    }
}
public class Banana {
    private Integer id;
    public Integer getId(){
        return this.id;
    }
}

Apple 과 Banana 클래스가 존재하고 있다.

public class AppleDao {

    private Map<Integer,Apple> datasource = new HashMap<>();

    public void save(Apple apple){
        datasource.put(apple.getId(), apple);
    }
    public void delete(Apple apple){
        datasource.remove(apple.getId());
    }
    public void aVoid(Integer integer){
        datasource.remove(integer);
    }
    public List<Apple> findAll(){
        return new ArrayList<>(datasource.values());
    }
    public Apple findById(Integer id){
        return datasource.get(id);
    }
}
public class BananaDao {
    private Map<Integer,Banana> datasource = new HashMap<>();

    public void save(Banana banana){
        datasource.put(banana.getId(), banana);
    }
    public void delete(Banana banana){
        datasource.remove(banana.getId());
    }
    public void aVoid(Integer integer){
        datasource.remove(integer);
    }
    public List<Banana> findAll(){
        return new ArrayList<>(datasource.values());
    }
    public Banana findById(Integer id){
        return datasource.get(id);
    }
}

각각의 Dao 가 정의되어져 있다. 위의 소스만 봐도 Dao 에 너무나도 같은 역할을 하는 메소드들이 중복되어있는 것을 볼 수 있다. 이를 제네릭을 사용해서 한번 중복된 코드들을 해결해 보자.

그 전에 먼저 변경된 소스와 비교해 주기 위해 main메소드를 선언해 주어서 실행시켜 보자.

public class Store {
    public static void main(String[] args) {
        AppleDao appleDao = new AppleDao();
        appleDao.save(Apple.of(1));
        appleDao.save(Apple.of(2));

        List<Apple> list = appleDao.findAll();
        list.forEach(System.out::println);
    }
}
//결과값
generic.Apple@2e0fa5d3
generic.Apple@5010be6

먼저 GenericDao 라는 클래스를 하나 선언해주고 KeyEntity 제네릭 키워드로 변경을 시켜주자

public class GenericDao<K,E> {
    private Map<K,E> datasource = new HashMap<>();

    public void save(E entity){
        datasource.put(entity.getId(), entity);    //Compile Error
    }
    public void delete(E entity){
        datasource.remove(entity.getId());    //Compile Error
    }
    public void aVoid(K id){
        datasource.remove(id);
    }
    public List<E> findAll(){return new ArrayList<>(datasource.values()); }
    public E findById(K id){return datasource.get(id); }
}

그러면 너무나도 당연스럽게 getId() 에 컴파일 에러가 발생하는 것을 확인할 수 있을 것이다.
그 이유는 현재 getId() 메소드를 찾지 못하고 있기 때문이다. 이유는 entity 에는 getId()가 정의되어 있지 않은 상태이기 때문이다.

getId()라는 메소드가 존재한다고 알려주기 위해 Entity 라는 클래스를 추가해주고 id 맴버변수와 getId() 메소드를 추가해준 후 AppleBanana 모두에게 상속을 받게 해주자.(이때 Apple 클래스와 Banana클래스에선 idgetId() 메소드를 삭제해도 된다.) 또한, GenericDaoE extends Entity 으로 E에게 상속받게 해주자

public class Entity {
    protected Integer id;

    public Integer getId(){
        return id;
    }
}
public class GenericDao<K,E extends Entity> {
    private Map<K,E> datasource = new HashMap<>();

    public void save(E entity){
        datasource.put(entity.getId(), entity);        //Compile Error
    }
    public void delete(E entity){
        datasource.remove(entity.getId());
    }
    public void aVoid(K id){
        datasource.remove(id);
    }
    public List<E> findAll(){return new ArrayList<>(datasource.values()); }
    public E findById(K id){return datasource.get(id); }
}

그러면 여전히 컴파일 에러가 발생하지만, 아까와는 다르게 getId()를 찾을 수 있는 것을 확인할 수 있다.
일단 getId()를 찾았으니 지금까진 잘하고 있는 것이다.

그러나 컴파일에러는 아직도 발생하고 있는데 그 이유는 바로 타입이 일치하지 않기 때문이다.

위의 소스에서는 entity의 타입은 E라고 정의를 해놓았지만, 클래스 Entity 에서는 현재 리턴타입을 Integer로 정의한 것을 확인할 수 있다. 이것 때문에 컴파일 에러가 발생하는 것이다.

이를 해결해주기 위해 타입을 일치시켜주자
Entity 클래스에 K 타입을 정의해 주고 나머지 클래스들에서도 추가해주자.

public class Entity<K> {
    protected K id;

    public K getId(){
        return id;
    }
}
public class Apple extends Entity<Integer>{
    public static Apple of(Integer id){
        Apple apple = new Apple();
        apple.id=id;
        return apple;
    }
}
public class Banana extends Entity<Integer>{
    public static Banana of(Integer id){
        Banana banana = new Banana();
        banana.id=id;
        return banana;
    }
}
public class GenericDao<K,E extends Entity<K>> {
    private Map<K,E> datasource = new HashMap<>();

    public void save(E entity){
        datasource.put(entity.getId(), entity);
    }
    public void delete(E entity){
        datasource.remove(entity.getId());
    }
    public void aVoid(K id){
        datasource.remove(id);
    }
    public List<E> findAll(){return new ArrayList<>(datasource.values()); }
    public E findById(K id){return datasource.get(id); }
}

그럼 여기까지 제네릭한 클래스를 만든 것이다. 마지막 단계만 남았다. AppleDaoBananaDao 의 코드들을 싹다 밀어주자!!

public class AppleDao extends GenericDao<Integer,Apple>{

}
public class BananaDao extends GenericDao<Integer,Banana>{

}

그럼 Stroe 클래스에서 실행한 결과가 이전값과 같은지 확인해 주자

generic.Apple@2e0fa5d3
generic.Apple@5010be6

동일한 것을 확인할 수 있다.

여기서 더 나아가 곰곰이 생각을 해보면 AppleDaoBananaDao가 굳이 존재할 필요성이 있을까??라는 의문점이 생길 수 있다.
사실은 필요없다.

public class Store {
    public static void main(String[] args) {
        //AppleDao appleDao = new AppleDao();
        GenericDao<Integer,Apple> appleDao = new GenericDao<>();
        appleDao.save(Apple.of(1));
        appleDao.save(Apple.of(2));

        List<Apple> list = appleDao.findAll();
        list.forEach(System.out::println);
    }
}

이렇게 GenericDao 클래스를 사용하여 접근할 수 있다.

정말 제네릭 타입은 런타임시 알아낼 수 있는 방법이 아에 없는 것 인가???

지난 포스팅에서 컴파일러가 컴파일시 제네릭 타입을 Erasure 한다고 했다. 그러면 당연하게 런타임시에 제네릭의 타입을 알아낼 수 있는 방법은 없어야 한다.
그러나 정말 없는 것일까??

만약 정말로 타입 정보가 필요한 경우가 생긴다면 super 토큰을 사용하여 타입 정보를 알아낼 수 있는 방법이 존재한다.

public class Store {
    public static void main(String[] args) {
        AppleDao appleDao = new AppleDao();
        //GenericDao<Integer,Apple> appleDao = new GenericDao<>();
        appleDao.save(Apple.of(1));
        appleDao.save(Apple.of(2));

        List<Apple> list = appleDao.findAll();
        list.forEach(System.out::println);

        System.out.println(appleDao.getEntityClass());
    }
}
public class AppleDao extends GenericDao<Integer,Apple>{
    public AppleDao(){
        super(Apple.class);
    }
}
public class GenericDao<K,E extends Entity<K>> {

    private Class<E> entityClass;

    public GenericDao(Class<E> entityClass){
        this.entityClass = entityClass;
    }

    public Class<E> getEntityClass(){
        return entityClass;
    }

    private Map<K,E> datasource = new HashMap<>();

    public void save(E entity){
        datasource.put(entity.getId(), entity);
    }
    public void delete(E entity){
        datasource.remove(entity.getId());
    }
    public void aVoid(K id){
        datasource.remove(id);
    }
    public List<E> findAll(){return new ArrayList<>(datasource.values()); }
    public E findById(K id){return datasource.get(id); }
}

이렇게 생성자에 메타데이터의 정보를 사용하여 타입의 정보를 알아낼 수 있다.
그러나 이 방법은 매우매우 귀찮다...그래서 우린 리플렉션으로 타입을 추론할 수 있는 방법이 있다.

public class GenericDao<K,E extends Entity<K>> {

    private Class<E> entityClass;

    public GenericDao(){
        this.entityClass = (Class<E>)((ParameterizedType)this.getClass().getGenericSuperclass())
                .getActualTypeArguments()[1];
    }

    public Class<E> getEntityClass(){
        return entityClass;
    }

    private Map<K,E> datasource = new HashMap<>();

    public void save(E entity){
        datasource.put(entity.getId(), entity);
    }
    public void delete(E entity){
        datasource.remove(entity.getId());
    }
    public void aVoid(K id){
        datasource.remove(id);
    }
    public List<E> findAll(){return new ArrayList<>(datasource.values()); }
    public E findById(K id){return datasource.get(id); }
}
public class AppleDao extends GenericDao<Integer,Apple>{

}

이렇게 생성자의 소스를 변경해 주고 AppleDao 의 소스를 전부 지워줘 보자
그리고 실행을 하면

generic.Apple@25f38edc
generic.Apple@1a86f2f1
class generic.Apple

Apple이 잘 나오는 것을 확인 할 수 있다.

'백기선님과 함께 하는 자바 스터디' 카테고리의 다른 글

15주차 과제: 람다식  (0) 2021.08.07
[번외] final 과 static 키워드  (0) 2021.08.01
14주차 과제: 제네릭  (0) 2021.07.31
[리뷰] 13주차 : I/O  (0) 2021.07.25
13주차 과제: I/O  (0) 2021.07.21