이번 포스트는 지난 포스트에서 스트림의 groupingBy에 대해 공부하던 도중 알게된 것들을 다룬다.
(지난포스트: https://mason-lee.tistory.com/65)
사건의 발단
사건의 발단은 스트림의 groupingBy를 구현해보다가 코드를 좀더 깔끔하게 보여지기를 원해서 새로운 클래스에 스태틱 메서드를 작성하게 되었다. 이후 해당 클래스를 콜렉션 프레임워크 처럼 필요할때마다 꺼내쓰면 어떨까? 라는 의문이 들어 실험을 하게 되었다.
public class Main {
public static void main(String[] args) {
List<Human> list = new ArrayList<>();
list.add( new Human("김철수",23,"남자"));
list.add( new Human("이영희",24,"여자"));
list.add(new Human("개똥이",21,"남자"));
list.add( new Human("박대기",23,"남자"));
list.add( new Human("김세희",23,"여자"));
Map<Integer,List<Human>> groupingBy = Mapping.groupingBy(list, Mapping.Options.NAME);
Set<Map.Entry<Integer,List<Human>>> entrySet = Mapping.entrySet(groupingBy);
Mapping.print(entrySet);
}
}
class Mapping{
enum Options {AGE,NAME,GENDER}
private static Options category;
public static Map groupingBy(List<Human> list, Options option){
category = option;
return list.stream().collect(Collectors.groupingBy((a)->{
if(option.equals(Options.NAME)) return a.getName();
if(option.equals(Options.GENDER)) return a.getGender();
return a.getAge();
}));
}
public static Set entrySet(Map<Integer,List<Human>> groupingBy){
return groupingBy.entrySet();
}
public static void print(Set<Map.Entry<Integer,List<Human>>> entrySet){
System.out.printf("Map groupingBy %s {\n",category);
entrySet.stream().forEach((entry)->{
System.out.printf("Entry { Key = %s, Values = ",entry.getKey());
entry.getValue().forEach((a)->a.print());
System.out.println("}");
});
System.out.println("}");
}
}
class Human {
private String name;
private int age;
private String gender;
public Human(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() { return name;}
public String getGender() { return gender; }
public int getAge() { return age; }
public void print(){
System.out.printf("[ name: %s, age: %d, gender: %s ], ", this.name, this.age, this.gender);
}
}
전체 적인 코드는 이러하다. Human클래스의 객체들을 생성후 리스트에 담아 Mapping이라는 클래스의 스태틱 메서드들에 최소한의 요구 사항(매개변수)만 보내주어 호출하게되면 스트림의 groupingBy를 통해서 매핑시켜주고 Set으로 전환후 출력까지 해주게 되는 것이다.
문제 인식
일단 컬렉션 프레임워크처럼 언제라도 필요한때에 꺼내쓰기위해서 해결해야 할것은 범용성이었다. 해당 코드를 보면 데이터 List에 한정하여 작업을 수행해주었기에 이것들을 바꿔주어야 됬다.
class Mapping<T>{
enum Options {AGE,NAME,GENDER}
private static Options category;
public static Map groupingBy(Collection<Human> list, Options option){
//모든 List 매개변수 타입을 Collection으로 바꿔줌
category = option;
return list.stream().collect(Collectors.groupingBy((a)->{
if(option.equals(Options.NAME)) return a.getName();
if(option.equals(Options.GENDER)) return a.getGender();
return a.getAge();
}));
}
public static Set entrySet(Map<Integer,Collection<Human>> groupingBy){
return groupingBy.entrySet();
}
public static void print(Collection<Map.Entry<Integer,Collection<Human>>> entrySet){
System.out.printf("Map groupingBy %s {\n",category);
entrySet.stream().forEach((entry)->{
System.out.printf("Entry { Key = %s, Values = ",entry.getKey());
entry.getValue().forEach((a)->a.print());
System.out.println("}");
});
System.out.println("}");
}
}
위와 같이 인자로 받는 모든 매개변수의 타입을 List에서 Collection을 바꾸니 해당 메서드들을 데이터 Set을 넣고 호출해도 잘 작동하게 되었다.
두번째 문제는 Human객체였다. 방금 바꿔 주었던 Collection과 마찬가지로 list안의 구성이 Human객체에만 의존하고 있기때문에 이를 바꿔주려 타입매개변수를 사용해보았지만 처참하게 실패했다.
문제는 Human클래스의 메서드들을 사용하는곳에서 컴파일에러가 나는데 당연한 결과였다. 타입매개변수를 통해 받은 인자는 최상위 클래스인 Object 클래스의 메서드들을 제외하고는 사용할수가 없기 때문이다.
사실 이부분에서 많은것들을 깨우첬다. 만약 위의 내용대로 수정을하고 컴파일에러가 안났을때의 시나리오를 생각해보면 groupingBy를 호출할때 어떠한 임의의 객체를 매개변수로 받게된다면 getName()과 같이 Human객체에만 존재하는 메서드들을 호출하지못하므로 런타임 에러가 발생할 것이다.
해결방안
생각해본 해결 방안으로는 두가지가 존재했다.
첫번째, 내가 만든 Mapping클래스의 groupingBy메서드에(다른 메서드들도 마찬가지로) 함수형 인터페이스를 설정하여 람다식으로 만드는 방식이다. 예를들면 어떤 데이터를 grouping하는데 기준점으로 세울것인가, 를 람다식으로 받는것이다. 하지만 이렇게되면 스트림의 groupingBy와 다를게없고 오히려 코드만 더 복잡해질것이다.
두번째, 특정한 최상위 인터페이스를 만들어 모든 클래스들은 그 인터페이스를 구현하게 하는것이다. 즉, 타입매개변수를 받으면 Object의 메서드만 사용할수 있듯이 매개변수의 타입을 Human같은 특정한 객체가아닌 최상위 인터페이스로 설정해주고 모든 객체들이 생성된 최상위 인터페이스에 맞게 생성해주면 될 것이다.
interface Highest<T> { // 최상위 인터페이스 생성
T getBase1();
T getBase2();
T getBase3();
void print();
}
class Car implements Highest{ // 다른 새로운 클래스를 생성하면서 최상위 인터페이스 Highest 구현
private String facturer;
private int serialNum;
private String sort;
public Car(String facturer, int serialNum, String sort) {
this.facturer = facturer;
this.serialNum = serialNum;
this.sort = sort;
}
//인터페이스 Highest를 위해 구현해야하는 메서드들
public String getBase1() { return facturer; }
public String getBase2() { return sort; }
public Integer getBase3() {return serialNum;}
public void print(){
System.out.printf("[ facturer: %s, serialNum: %d, sort: %s ], ",
this.facturer, this.serialNum, this.sort);
}
}
public class Main {
public static void main(String[] args) {
Collection<Highest> list = new HashSet<>();
//리스트 안의 데이터들은 Highest타입을 부여
list.add( new Car("BMW",34121,"Suv"));
list.add( new Car("KIA",765432,"Suv"));
list.add(new Car("BENZ",1235623,"Sedan"));
list.add( new Car("KIA",763377,"Sedan"));
list.add( new Car("KIA",894566,"Truck"));
Map<Integer,Collection<Highest>> groupingBy = Mapping.groupingBy(list, Mapping.Options.BASE2);
Set<Map.Entry<Integer,Collection<Highest>>> entrySet = Mapping.entrySet(groupingBy);
Mapping.print(entrySet);
}
}
class Mapping<T>{
enum Options {BASE1,BASE2,BASE3}
private static Options category;
public static Map groupingBy(Collection<Highest> list, Options option){
//Highest를 구현하는 객체들만을 받음
category = option;
return list.stream().collect(Collectors.groupingBy((a)->{
// 모든 Highest의 하위 클래스들은 getBase()메서드들을 가지고 있기 때문에 이를 이용해줌
if(option.equals(Options.BASE1)) return a.getBase1();
if(option.equals(Options.BASE2)) return a.getBase2();
return a.getBase3();
}));
}
public static Set entrySet(Map<Integer,Collection<Highest>> groupingBy){
return groupingBy.entrySet();
}
public static void print(Collection<Map.Entry<Integer,Collection<Highest>>> entrySet){
System.out.printf("Map groupingBy %s {\n",category);
entrySet.stream().forEach((entry)->{
System.out.printf("Entry { Key = %s, Values = ",entry.getKey());
entry.getValue().forEach((a)->a.print());
System.out.println("}");
});
System.out.println("}");
}
}
위와 같이 어떠한 클래스를 새로 생성해주더라도 최상위 인터페이스인 Highest를 구현한다면 어떠한 객체든 Mapping클래스의 스태틱 메서드들을 사용할수 있게 되었다.
물론 몇가지 문제점들이 아직 남아있지만 확실히 범용성만은 크게 향상 된것을 볼수 있다.
마무리
사실 객체지향 프로그래밍을 배우면서 추상화의 의미와 존재의 이유에 대해 의문을 가졌었다. 하지만 이번 실험으로인해 추상화의 중요성을 알게되었다. 또한 같은 이야기 이지만 Stream이라는 타입을 통해 어떠한 데이터들을 받아오더라도 어떻게 같은 메서드를 공유할수 있는지 조금은 잘 이해할수 있게 되었고, 스트림의 groupingBy메서드가 추상클래스나 인터페이스를 통해 묶어 사용하지 않고 람다식을 통해 해당 메서드의 사용에 자율성을 높이게 되었는지도 알수 있게 되었다.
여담으로 자바의 람다식과 자바스크립트의 arrow function의 차이를 포스팅하고 싶은데.... 아직 배워야할게 산더미라
너무 시간이없다...언젠간 포스팅하리....
'Personal Research' 카테고리의 다른 글
SecurityContextHolder (0) | 2023.05.18 |
---|---|
BindException과 MethodArgumentNotValidException (0) | 2023.05.10 |
Mysql, safe update mode (0) | 2023.03.31 |
MySQL 서브쿼리 (0) | 2023.03.31 |