본문 바로가기
개발/Java

일급 컬렉션 사용하기 [Java]

by Mingvel 2022. 4. 22.

First Class

 

우아한 테크캠프 Pro 프리코스를 진행하며 일급 컬렉션에 대한 내용을 접하게 되었다.

 

일급 컬렉션특정 Collection을 포장하며, 그 외 다른 멤버 변수가 없는 상태를 말한다.

 

가장 쉽게 예를 들면

public class Names {
	private List<String> names;
}

혹은

public class Cars {
	private Set<Car> cars;
}

와 같은 형태로 이해하면 된다.

 

그런데 여기서 자연스러운 의문이 생긴다.

 

비즈니스 로직에서 

public class XXXSerivce {
	public void method() {
    	List<String> names = new ArrayList<>();
        names.add("xxx");
    }
}

위와 같이 컬렉션을 선언해서 사용하면 될 것을, 

 

굳이 한 번 더 공을 들여서 포장하는 이유가 무엇일까? 그리고 그렇게 함으로써 얻을 수 있는 이점은 무엇일까?

 

필자가 일급 컬렉션을 사용하며 크게 와닿았던 이점두 가지였다.

 

  1. 비즈니스 로직의 역할과 책임을 Service Layer에서 해당 객체의 Domain으로 가져올 수 있다.
  2. 기술적으로 컬렉션의 불변성을 보장해 줄 수 있다.

 

위에서 언급한 두 가지 이점을 예제를 통해 자세히 알아보자

 

아래와 같은 요구 사항이 있다.

'김'씨 성을 가진 사람들의 이름 목록과, '이'씨 성을 가진 사람들의 이름 목록이 있다.
각각의 목록에 해당 성씨가 아닌 사람이 포함될 경우, 예외를 발생시킨다.


위 사항을 구현한 일반적인 서비스 로직은 아래와 같다.

 

public class NameService {

    public void names() {
        List<Name> kimsName = Arrays.asList(new Name("Kim ming"), new Name("Kim hi"));
        kimsValidate(kimsName);
        List<String> kimNameList = kimsGetNames(kimsName);

        List<Name> leesName = Arrays.asList(new Name("Lee ming"), new Name("Lee hi"));
        leesValidate(leesName);
        List<String> leesNameList = leesGetNames(leesName);
    }

    private void kimsValidate(List<Name> names) {
        names.forEach(name -> {
            if (name.notContains("Kim")) {
                throw new IllegalArgumentException("반드시 'Kim' 이라는 단어를 포함하여아 합니다.");
            }
        });
    }

    private void leesValidate(List<Name> names) {
        names.forEach(name -> {
            if name.notContains("Lee")) {
                throw new IllegalArgumentException("반드시 'Lee' 라는 단어를 포함하여아 합니다.");
            }
        });
    }
    
    private List<String> kimsGetNames(List<Name> kimsName) {
        return kimsName.stream()
                .map(Name::getName)
                .collect(Collectors.toList());
    }
    
    private List<String> leesGetNames(List<Name> leesName) {
        return leesName.stream()
                .map(Name::getName)
                .collect(Collectors.toList());
    }    
}

위 코드를 보고 왠지 모를 답답함이 느껴지는가? 

 

왠지 모를 답답함의 이유를 정리해보았다.

 

  1. 현재는 '김'씨와 '이'씨 두 가지 요구사항이 있지만, '박'씨 등등.. N개의 요구사항이 추가로 발생할 경우, 그에 따른 유효성 검사 method 또한 N개가 생성될 것이다. (getNames 관련 메소드 또한 N개가 생성된다)
  2. '김'씨, '이'씨와 같은 객체의 생성과 사용에 대한 책임은 Service에 있고, 해당 객체의 유효성 검사에 대한 책임 또한 Service에 있다.
  3. 시간이 흘러 코드가 흩어지고 기존의 요구사항이 흐려질 때 쯤이면, 동일한 List <Name> 형 변수인 'kimsName' 이라는 녀석과 'leesName'이라는 녀석의 차이가 무엇이고, 'kimsValidate', 'kimsGetNames' 메소드가 무엇을, 왜 하는지 조차 희미해질 수 있다.
  4. 그리고 그 결과는, 중복 코드의 구현으로 이어지거나 해당 Service가 어떤 역할을 해야하는지 조차 알 수 없을 정도로 잡동사니가 되어버린다. (실무에서도 빈번하게 일어나는 일이다)

 

앞에서 나열한 사항들은 일급 컬렉션을 사용하여 해소할 수 있는데, 

이 부분이 바로 위에서 언급한 일급 컬렉션의 두 가지 이점첫 번째 이다

비즈니스 로직의 역할과 책임을 Service Layer에서 해당 객체의 Domain으로 가져올 수 있다.

 

일급 컬렉션을 사용하여 앞선 답답한 상황을 해소해보자.

 

먼저 '김'씨 목록'이'씨 목록을 각각의 일급 컬렉션으로 포장해주고, 

해당 객체의 생성과 동시에 유효성 검사에 대한 로직을 객체 내부에서 수행함으로서, 객체를 캡슐화한다.

 

KimsName.class

public class KimsName {

    private final List<Name> names;

    public KimsName(List<Name> names) {
        validate(names);
        this.names = names;
    }

    public List<String> getNames() {
        return names.stream()
                .map(Name::getName)
                .collect(Collectors.toList());
    }

    private void validate(List<Name> names) {
        names.forEach(name -> {
            if (name.notContains("Kim")) {
                throw new IllegalArgumentException("반드시 'Kim' 이라는 단어를 포함하여아 합니다.");
            }
        });
    }
}

 

LeesName.class

public class LeesName {

    private final List<Name> names;

    public LeesName(List<Name> names) {
        validate(names);
        this.names = names;
    }

    public List<String> getNames() {
        return names.stream()
                .map(Name::getName)
                .collect(Collectors.toList());
    }

    private void validate(List<Name> names) {
        names.forEach(name -> {
            if (name.notContains("Lee")) {
                throw new IllegalArgumentException("반드시 'Lee' 라는 단어를 포함하여아 합니다.");
            }
        });
    }
}

 

NameService.class

public class NameService {

    public void names() {
    	KimsName kimsName = new KimsName(Arrays.asList(new Name("Kim ming"), new Name("Kim hi")));
        LeesName leesName = new LeesName(Arrays.asList(new Name("Lee ming"), new Name("Lee hi")));
    }
}

 

필자는 일급 컬렉션의 상태와 행위에 대한 책임각 일급 컬렉션 내부로 옮겼고

그 결과 실제 Service Layer에서는 그저 객체를 사용하기만 하는 구조로 변경되었다.

당연하게도, Service Layer에 난잡하게 존재하던 메소드 무리도 삭제되었다.

더불어, 'kimsName'은 앞선 예제의 List <Name> 타입의 무언가가 아닌, KimsName 타입의 인스턴스본인을 명확하게 표현하고 있다.

 

이제야 비로소, 각자의 위치에서 본인의 책임을 다하는 구조가 만들어진 것이다.

 

 

 

우리는 바쁘다. 숨 쉴 틈도 없이 일급 컬렉션의 두 번째 이점을 알아보자

기술적으로 컬렉션의 불변성을 보장해 줄 수 있다.

 

눈치챘을지 모르겠지만, 두 번째 이점은 첫 번째 이점의 예시에서도 확인할 수 있었다.

설명을 위해 KimsName을 다시 데려오자

 

KimsName.class

public class KimsName {

    private final List<Name> names;

    public KimsName(List<Name> names) {
        validate(names);
        this.names = names;
    }

    public List<String> getNames() {
        return names.stream()
                .map(Name::getName)
                .collect(Collectors.toList());
    }

    private void validate(List<Name> names) {
        names.forEach(name -> {
            if (name.notContains("Kim")) {
                throw new IllegalArgumentException("반드시 'Kim' 이라는 단어를 포함하여아 합니다.");
            }
        });
    }
}

 

개발적 관점에서 '불변'이라 함은, 특정 변수가 값을 가진 이후에, 그 값이 변하지 않음을 기술적으로 명시한 것을 말한다.

 

간단한 예로, 불변의 String 변수인 name에 특정 값을 할당하고, 재 할당을 시도할 경우 에러 메세지를 만날 수 있다.

private void someMethod() {
        final String name = "밍블";
        // name = "다른이름"; //error message : Cannot be assign a value to final variable 'name'
    }

 

 

그렇다면 다시 이 글의 주제로 돌아와 보자

private void someMethod() {
        final List<String> names = Collections.singletonList("밍블");
    }

위 예시의 'names' 컬렉션은 불변인가?

 

 

예상과 달리 정답은

반은 맞고 반은 틀리다.

 

무엇이 맞고 무엇이 틀린가요?

 

final로 선언한 컬렉션은 두 가지 특징이 있다.

1. 재 할당이 금지된다. --> 불변이다!

2. 내부 변수의 값은 조작할 수 있다. --> 불변이 아니다!

 

우리는 컬렉션을 불변으로 만들기 위해 일급 컬렉션을 사용함으로써 위 한계를 극복할 수 있다.

 

그것은 바로 

일급 컬렉션의 생성자내부 변수를 할당 및 생성하는 것.

그리고 그 이후 해당 컬렉션의 내부 데이터에 조작을 가하려는 외부의 움직임을 차단할 것.

public class KimsName {

    private final List<Name> names;

    public KimsName(List<Name> names) {
        validate(names);
        this.names = names;
    }

    public List<String> getNames() {
        return names.stream()
                .map(Name::getName)
                .collect(Collectors.toList());
    }

    private void validate(List<Name> names) {
        names.forEach(name -> {
            if (name.notContains("Kim")) {
                throw new IllegalArgumentException("반드시 'Kim' 이라는 단어를 포함하여아 합니다.");
            }
        });
    }
}

 

KimsName 은 생성자로 내부 names 컬렉션에 값을 할당하고, 

names에 값이 할당된 후에는 names 컬렉션에 변경을 가할 수 없게 기술적으로 접근을 막아버렸다.

 

단지 getNames 메서드처럼, 비즈니스 요구사항에 맞는 값을 알맞게 포장해서 외부로 보여줄 뿐이다.

 

이로서, KimsName은 기술적으로 내부 names 컬랙션의 불변성을 보장할 수 있는 구조가 완성되었다.

 

 

 

마치며..


지금까지 일급 컬렉션을 사용함으로써 얻을 수 있는 이점들 대해서 살펴보았다

 

이러한 이점만 보고 일급 컬렉션이 만능이라고 생각할 수도 있는데,

 

모든 기술의 채택은 트레이드오프의 산물이다

 

완벽한 기술은 없고

 

장점이 있으면 단점도 당연히 존재한다

 

내가 사용하고자 하는 상황에 적합한 기술인지 항상 고민해보고 사용하길 바란다

반응형

댓글