HJW's IT Blog

일급 객체와 일급 컬렉션 본문

카테고리 없음

일급 객체와 일급 컬렉션

kiki1875 2024. 11. 14. 15:15

들어가며

개발에 있어 일급 객체와 컬렉션은 OOP 와 함수형 프로그래밍 둘 다에서 매우 중요한 개념이다. 일급 객체/컬렉션은 단순히 기술적인 개념을 넘어, 코드의 가독성, 유지보수성, 확장성, 협업의 생산성 등을 높일 수 있다. 객체의 책임을 분리 하거나, SOLID 원칙 준수, 로직의 명확성 등의 유지 보수성이 향상될 수 있다.

일급 객체란 무엇인가?

일급 객체의 3가지 조건

First-Class Citizen 은, 다음 3가지 특성을 가지는 객체를 말한다 :

  1. 변수나 데이터 구조에 할당할 수 있어야 한다.
  2. 함수의 매개변수로 전달할 수 있어야 한다
  3. 함수의 반환값으로 사용할 수 있어야 한다.

JAVA의 일급 객체

JAVA에선 함수가 독립적인 일급 객체로 존재하지 않았었다. 이들은 메서드를 변수에 할당하거나 인자로 전달할 수 없었다. 하지만, JAVA 8 부터 도입된 람다 표현식, 함수형 인터페이스 등을 통해 메서드를 일급 객체처럼 다룰 수 있게 되었다.

그렇다면 어떻게 람다 표현식, 혹은 인터페이스를 이용해 메서드를 일급 객체처럼 다룰 수 있을까?

이걸 이해하기 위해서 우선 Wrapper Class 에 대해 이해해야 한다. Java의 int, string, char 과 같은 자료형은 primitive type 이라 부른다. 이러한 자료형들은 객체가 아니기 때문에 객체의 특성을 사용할 수 없다. 이러한 자료형들은 type 변환, 컬렉션 프레임워크에 사용할 수 없는 등의 제약 사항이 있다.

이러한 점을 극복하기 위해 Wrapper Class 를 사용한다. Wrapper Class 는 기본 타입의 값을 내부에 두고 “포장” 하는 형태로, 객체이다. Wrapper Class 를 사용함으로써, null 에 대한 처리, 유틸리티 메서드, 컬렉션 프레임워크 등을 사용할 수 있다.

람다 표현식이 일급 객체로 작동하는 원리

자바의 람다 표현식은 함수형 인터페이스의 구현체로 취급된다. 함수형 인터페이스는 단 하나의 추상 메서드만을 가지는 인터페이스이다. 예를 들어,

@FunctionalInterface
interface LambdaFunction {
    int sum(int a, int b);
}

LambdaFunction lam = (int x, int y) -> x + y;

여기서, 람다 표현식 (int x, int y) -> x + y 는 LamdaFunction의 sum 메서드를 구현한다.

또한 람다표현식은 변수에 할당될 수 있으며, 다른 함수의 인자로 전달될 수 있는데, 이는 아까 언급했던 일급객체의 주요 특성이다.

Consumer<String> c = (t) -> System.out.println(t);

public static void print(Consumer<String> c, String str){
    c.accept(str);
}

print((t)->System.out.println(t), "Hello World");

마지막으로, 함수의 반환값으로 사용할 수 있다

public static Consumer<String> hello() {
    return (t) -> System.out.println(t);
}

일급 컬렉션

위에서 일급 객체의 특성에 대해 알아보았다. 일급 컬렉션이란, 이러한 일급 객체를 컬렉션으로 감싸 관련된 로직을 한 곳에 응집시키는 디자인 패턴이다. 이를 함으로, 개발자는 캡슐화를 만족시키고, 컬렉션을 사용하는 코드의 책임을 명확하게 분리하여 SRP 를 만족할 수 있다. 특히 일급 컬렉션은, 불변성을 유지하면서도 데이터의 상태를 관리하는데 유리하다.

일급 컬렉션의 특징

  1. 컬렉션을 감싸는 단일 클래스
    • 일급 컬렉션은 컬렉션을 감싸는 단일 클래스로 구성된다. 예를 들어 List 혹은 Set 과 같은 컬렉션 자체를 그대로 사용하지 않고, 이를 포장하는 클래스를 만들어 해당 컬렉션에 관련된 모든 로직을 캡슐화 한다.
  2. 컬렉션과 관련된 로직의 응집
    • 컬렉션을 다루는 로직은 일급 컬렉션 내부에 응집된다. 이렇게 하면 비즈니스 로직이 한군데에 모여있고, 컬렉션의 관리와 책임을 명확하게 정의할 수 있다.
  3. 불변성 유지
    • 일급 컬렉션은 기본적으로 불변 객체로 설계하는것이 권장되는데, 이는 내부 데이터를 외부에 노출하지 않음으로써 컬렉션의 상태 병경을 방지하고, 데이터의 일관성을 유지할 수 있게 도와준다.
  4. 도메인 중심 설계
    • 컬렉션을 단순 데이터 구조로 사용하는 대신, 컬렉션을 도메인 모델의 일부로써 의미 있는 동작을 포함시켜, 가독성과 유지보수성을 향상 시킬 수 있다.

다음은 예시이다

사용자 도메인 클래스

public class User {
    private final String name;
    private final boolean active;

    public User(String name, boolean active) {
        this.name = name;
        this.active = active;
    }

    public boolean isActive() {
        return active;
    }

    public String getName() {
        return name;
    }
}

사용자 목록 관리

public class Users {
    private final List<User> users;

    public Users(List<User> users) {
        validate(users);
        this.users = new ArrayList<>(users); // 불변성 유지
    }

    private void validate(List<User> users) {
        if (users == null || users.isEmpty()) {
            throw new IllegalArgumentException("Users list cannot be null or empty");
        }
    }

    public List<User> getActiveUsers() {
        return users.stream()
                    .filter(User::isActive)
                    .collect(Collectors.toUnmodifiableList());
    }

    public int count() {
        return users.size();
    }
}