독서

[이펙티브 코틀린] 6장. 클래스 설계

오렌지색 귤 2024. 7. 28. 22:23
반응형

아이템 36. 상속보다는 컴포지션을 사용하라

 

생략

 

 

아이템 37. 데이터 집합 표현에 data 한정자를 사용하라.

 

생략

 

 

아이템 38. 연산 또는 액션을 전달할 때는 인터페이스 대신 함수 타입을 사용하라

 

결론 : 자바 등에 제공하는 api가 아니라면 SAM(Single-Abstract Method), 즉 @FunctionalInterface 정의할 필요가 없다.

 

 

 

아이템 39. 태그 클래스보다는 클래스 계층을 사용하라

 

p. 266

 

태그 클래스와 상태 패턴의 차이

 

 

태그 클래스

 

태그 클래스는 객체의 상태나 동작을 구분하기 위해 사용되는 빈 클래스를 의미한다.


주로 다형성을 이용하여 코드를 보다 명확하고 유지보수하기 쉽게 만드는 데 사용된다.


태그 클래스 자체에는 데이터나 로직이 포함되지 않으며, 단순히 상태를 표시하거나 구분하는 역할만 한다.

 

 

예제 코드

sealed class AnimalState

class Running : AnimalState()
class Sleeping : AnimalState()
class Eating : AnimalState()

class Animal(var state: AnimalState)

fun main() {
    val animal = Animal(Running())

    when (animal.state) {
        is Running -> println("The animal is running")
        is Sleeping -> println("The animal is sleeping")
        is Eating -> println("The animal is eating")
    }
}

 

위 예제에서 Running, Sleeping, Eating은 각각의 상태를 나타내는 태그 클래스이다.


Animal 클래스는 현재 상태를 나타내는 state 변수를 가지며, 이 변수를 통해 동물의 상태를 구분할 수 있다.

 

 

태그 클래스 예제의 클래스 계층 적용

 

태그 클래스보다는 클래스 계층을 사용하라 격언을 적용해보자.

 

sealed class AnimalState {
    abstract fun handle()
}

class Running : AnimalState() {
    override fun handle() {
        println("The animal is running")
    }
}

class Sleeping : AnimalState() {
    override fun handle() {
        println("The animal is sleeping")
    }
}

class Eating : AnimalState() {
    override fun handle() {
        println("The animal is eating")
    }
}

class Animal(var state: AnimalState)

fun main() {
    val animal = Animal(Running())

    animal.state.handle() // The animal is running

    animal.state = Sleeping()
    animal.state.handle() // The animal is sleeping

    animal.state = Eating()
    animal.state.handle() // The animal is eating
}

 

 

상태 패턴

 

상태 패턴은 객체의 상태에 따라 동작을 변경하는 디자인 패턴이다.


상태 패턴을 사용하면 객체가 상태에 따라 다른 동작을 수행할 수 있도록 상태를 객체로 캡슐화하고, 상태 전환을 쉽게 관리할 수 있다.

 

 

구성 요소

  1. Context : 상태를 가지고 있으며, 상태 전환을 관리
  2. State : 인터페이스 또는 추상 클래스로, Context의 다양한 상태를 정의
  3. Concrete States : State 인터페이스를 구현한 구체적인 상태 클래스들로, 각 상태에 따른 동작을 정의

 

예제 코드

interface DoorState {
    fun open(door: Door)
    fun close(door: Door)
}

class OpenState : DoorState {
    override fun open(door: Door) {
        println("The door is already open.")
    }

    override fun close(door: Door) {
        println("Closing the door.")
        door.state = ClosedState()
    }
}

class ClosedState : DoorState {
    override fun open(door: Door) {
        println("Opening the door.")
        door.state = OpenState()
    }

    override fun close(door: Door) {
        println("The door is already closed.")
    }
}

class Door(var state: DoorState)

fun main() {
    val door = Door(ClosedState())

    door.open()
    door.close()
    door.close()
}

 

위 예제에서 DoorState 인터페이스는 openclose 메서드를 정의한다.


OpenStateClosedState는 각각 문이 열려 있는 상태와 닫혀 있는 상태를 나타내며, 각 상태에 따른 동작을 구현한다.


Door 클래스는 현재 상태를 가지고 있으며, 상태에 따라 다른 동작을 수행한다.

 

 

 

 

 

아이템 40. equals의 규약을 지켜라

 

생략

 

아이템 41. hashCode의 규약을 지켜라

 

생략

 

아이템 42. compareTo의 규약을 지켜라

 

생략

 

 

 

아이템 43. API의 필수적이지 않는 부분을 확장 함수로 추출하라

 

p. 297

 

확장 함수는 어노테이션 프로세서가 따로 처리하지 않습니다.

 

어노테이션 프로세서의 동작 원리

 

어노테이션 프로세서는 자바 컴파일러(javac)와 코틀린 컴파일러(kotlinc)에 의해 지원되며, 컴파일 시간에 어노테이션을 처리하여 소스 코드에 대한 추가 작업(코드 생성, 검사 등)을 수행한다.

 

 

어노테이션 프로세서의 정의

 

어노테이션 프로세서는 javax.annotation.processing 패키지의 AbstractProcessor 클래스를 상속하여 구현된다.


이 클래스는 process 메서드를 통해 어노테이션을 처리한다.

 

어노테이션 탐색


어노테이션 프로세서는 roundEnv.getElementsAnnotatedWith 메서드를 사용하여 특정 어노테이션이 적용된 요소(클래스, 메서드, 필드 등)를 탐색합니다.

 

어노테이션 적용 대상


어노테이션은 특정 타겟(AnnotationTarget)을 가진다.


예를 들어, AnnotationTarget.CLASS는 클래스에, AnnotationTarget.FUNCTION는 함수에 적용될 수 있다.

 

 

확장 함수와 클래스 멤버

 

확장 함수는 클래스의 멤버가 아니며, 정적 메서드로 컴파일된다.


이는 확장 함수가 클래스 외부에 정의되기 때문이다.

 

 

확장 함수의 정의 : 확장 함수는 기존 클래스에 메서드를 추가하는 것처럼 보이지만, 실제로는 정적 메서드로 정의된다.

 

예를 들어, 다음과 같은 확장 함수는:

fun String.customLength(): Int {
    return this.length
}

 

이는 실제로 다음과 같이 정적 메서드로 컴파일된다:

public static int customLength(String receiver) {
    return receiver.length();
}

 

 

클래스 멤버가 아님

 

확장 함수는 클래스 내부에 정의되지 않기 때문에, 어노테이션 프로세서가 클래스의 멤버를 탐색할 때 확장 함수는 포함되지 않는다.


어노테이션 프로세서는 클래스의 멤버를 탐색할 때 클래스의 내부 구조만을 살펴보므로, 확장 함수는 탐색 대상에서 제외된다.

 

 

 

lombok 코드 살펴보기

 

Lombok의 @Getter 어노테이션을 처리하는 HandleGetter 클래스의 주요 부분

@Provides
public class HandleGetter extends JavacAnnotationHandler<Getter> {

    @Override
    public void handle(AnnotationValues<Getter> annotation, JCAnnotation ast, JavacNode annotationNode) {
        handleFlagUsage(annotationNode, ConfigurationKeys.GETTER_FLAG_USAGE, "@Getter");

        Collection<JavacNode> fields = annotationNode.upFromAnnotationToFields();
        deleteAnnotationIfNeccessary(annotationNode, Getter.class);
        deleteImportFromCompilationUnit(annotationNode, "lombok.AccessLevel");
        JavacNode node = annotationNode.up();
        Getter annotationInstance = annotation.getInstance();
        AccessLevel level = annotationInstance.value();
        boolean lazy = annotationInstance.lazy();
        if (lazy) handleFlagUsage(annotationNode, ConfigurationKeys.GETTER_LAZY_FLAG_USAGE, "@Getter(lazy=true)");

        if (level == AccessLevel.NONE) {
            if (lazy) annotationNode.addWarning("'lazy' does not work with AccessLevel.NONE.");
            return;
        }

        if (node == null) return;

        List<JCAnnotation> onMethod = unboxAndRemoveAnnotationParameter(ast, "onMethod", "@Getter(onMethod", annotationNode);
        if (!onMethod.isEmpty()) {
            handleFlagUsage(annotationNode, ConfigurationKeys.ON_X_FLAG_USAGE, "@Getter(onMethod=...)");
        }

        switch (node.getKind()) {
            case FIELD:
                createGetterForFields(level, fields, annotationNode, true, lazy, onMethod);
                break;
            case TYPE:
                if (lazy) annotationNode.addError("'lazy' is not supported for @Getter on a type.");
                generateGetterForType(node, annotationNode, level, false, onMethod);
                break;
        }
    }
}

 

주요 포인트

 

애초에 @Getter 어노테이션은 클래스나 필드에 적용되도록 설계되었다는 점을 기억하자.

 

  1. 어노테이션 노드 탐색:
Collection<JavacNode> fields = annotationNode.upFromAnnotationToFields();
JavacNode node = annotationNode.up();

 

 

annotationNode.upFromAnnotationToFields()annotationNode.up() 메서드는 애노테이션이 적용된 클래스의 필드나 타입 노드로 이동한다.


이 과정에서 현재 애노테이션이 적용된 위치(필드 또는 타입)를 탐색한다.

 

 

  1. 노드 종류에 따른 처리:
switch (node.getKind()) {
    case FIELD:
        createGetterForFields(level, fields, annotationNode, true, lazy, onMethod);
        break;
    case TYPE:
        if (lazy) annotationNode.addError("'lazy' is not supported for @Getter on a type.");
        generateGetterForType(node, annotationNode, level, false, onMethod);
        break;
}

 

 

node.getKind()를 통해 현재 노드의 종류를 확인한다.


FIELD인 경우 필드에 대해 getter 메서드를 생성하고, TYPE인 경우 클래스에 대해 getter 메서드를 생성한다.


확장 함수는 클래스의 멤버가 아니기 때문에 여기서 처리되지 않는다.

 

 

아이템 44. 멤버 확장 함수의 사용을 피하라

 

생략

반응형