본문 바로가기

Kotlin

코틀린, 코틀린답게 사용하기 - 3. Class

 

이번 포스팅에서는 클래스를 다루는 측면에서 자바와 같은 기능을 하지만 더 효율적으로 수행하고있는 부분에 대해서 알아보려고합니다.

 

1. 생성자

@Service
public class AccountService {
  private final AccountRepository accountRepository;
  private final PasswordEncoder passwordEncoder;

  @Autowired
  public AccountService(AccountRepository accountRepository, PasswordEncoder passwordEncoder) {
    this.accountRepository = accountRepository;
    this.passwordEncoder = passwordEncoder;
  }
}

 

자바와 스프링에 익숙한 분들이라면 위와 같은 생성자를 통한 Dependency Injection(DI) 코드가 익숙하실겁니다.

 

Spring 4.3 부터는 @Autowired 애노테이션도 생략이 가능하고, 여기에 private final로 선언된 프로퍼티에대한 생성자를 만들어주는 Lombok 기능이 더해져서 다음과 같이 간단하게 DI를 수행할 수 있습니다.

 

@Service
@RequiredArgsConstructor
public class AccountService {
  private final AccountRepository accountRepository;
  private final PasswordEncoder passwordEncoder;
}

 

프로퍼티 선언에 val/var 키워드만으로 게터/세터를 제공하는 코틀린은 Lombok 의 @Getter/@Setter 애노테이션을 완벽하게 대체할 수 있었습니다. 이와 비슷하게 코틀린에서는 위와같은 생성자 초기화방법을 간단하게 할 수 있습니다.

 

 

@Service
class AccountService consturctor(
    _accountRepository: AccountRepository,
    _passwordEncoder: PasswordEncoder
) {
  val accountRepository: AccountRepository
  val passwordEncoder: PasswordEncoder

  init {
    accountRepository = _accountRepository
    passwordEncoder = _passwordEncoder
  }
}

 

우선 롬복을 적용하지 않았던 자바코드와 비슷하게, 코틀린에서는 init 키워드를 사용하여 초기화 블록을 만들 수 있습니다.

 

@Service
class AccountService (
    _accountRepository: AccountRepository,
    _passwordEncoder: PasswordEncoder
) {
  val accountRepository = _accountRepository
  val passwordEncoder = _passwordEncoder
}

 

또는 프로퍼티를 선언하는 코드에 초기화코드를 포함하여 초기화블록을 생략할 수도있습니다.

 

여기서 주의할 점은, AccountService 클래스를 선언할 때 지정한 주 생성자 파라미터인 _accountRepository, _passwordENcoder

에 대하여 위에서 보여드린 초기화블록과 프로퍼티 초기화를 제외하고는 참조할 수 없습니다.

 

즉, 초기화블록 이후에는 _accountRepository 를 사용할 수 없습니다.(사용하는게 더 이상하겠네요)

 

 

하지만 이렇게 단순히 주 생성자 파라미터로 초기화하는 코드는 val 키워드만으로 매우 간단해질 수 있습니다.

 

@Service
class AccountService @Autowired (
    val accountRepository: AccountRepository,
    val passwordEncoder: PasswordEncoder
) {
  ...
}

 

롬복의 @RequiredArgsConstructor 를 적용한 코드와 매우흡사해졌습니다!

다만 Spring IoC 를 통한 DI라면 @Autowired 애노테이션을 붙여주도록합니다. (주 생성자가 하나일 때는 생략가능)

 

 

 

2. 정적 팩토리 메서드

이펙티브자바를 읽어보신분이라면 누구나 알고있는 내용이 있습니다. 대부분 첫장은 자세히 읽거든요^^
바로 '아이템 1: 생성자대신 정적 팩토리 메서드를 사용하라' 입니다. 코틀린에서는 어떤식으로 구현하는지 알아보겠습니다.

 

많은 경우에 생성자를 호출하여 인스턴스화하는 방법은 바람직하지 않습니다. 명시적이지도 않고, 싱글톤, 하위타입 리턴과 같은 추가적인 기능을 더하기가 어렵기 때문입니다.

 

따라서 생성자를 private으로 선언하여 호출을 막고 static 키워드와 of/valuOf 등의 네이밍으로 정적 팩토리 메서드를 만드는 관례가있습니다. 정적 팩토리 메서드를 만들어보지 않은 분들이라도, List.of(), Integer.valuOf() 등을 호출하여 사용해보셨을겁니다.

 

static 키워드가 없는 코틀린에서는 어떻게 정적 팩토리 메서드를 만들 수 있을까요~?

패키지 레벨에 최상위 함수를 만든다면 정적 메서드를 만들 수 있겠지만, 이런 최상위 함수는 private 멤버에 대한 접근을 할 수가없습니다.

 

이럴때 사용하는 것이 바로 Companion Object(동반 객체)입니다.

 

class User private constructor(
    val username: String,
    val email: String
) {
  companion object {
    fun of(username: String, email: String): User {
      return User(username, email)
    }
  }
}

 

이렇게  companion object 를 통해 of() 라는 메서드를 제공해주면, 인스턴스화 하지않고 User 클래스 내부에 있는 메서드에 접근할 수 있습니다. 이 동반객체는 private 으로 선언한 생성자에 접근할 수 있고, 그렇게 호출한 결과를 리턴합니다. 

 

User("jjeda","jjeda@naver.com") // X
User.of(username = "jjeda", email = "jjeda@naver.com") // O

 

이제 User 클래스는 외부에서 생성자를 통한 호출이 불가능하고 새롭게 정의한 of라는 팩토리 메서드를 통해서만 인스턴화할 수 있습니다~

 

 

 

3. 데코레이터

스프링 트라이앵글 중 하나인 AOP, 3대 핵심요소인 이 AOP는 두 가지 패턴으로 시작합니다.
바로 데코레이터 패턴과 프록시 패턴입니다. 쓰임새가 다를 뿐, 구현하는 방법은 두 패턴이 상당히 비슷합니다.
다만, 이 구현방법이 상당히 귀찮은데, 코틀린에서는 Class Delegation(클래스 위임)이라는 방법으로 매우 간단하게 해결할 수 있습니다.  

 

먼저 Class Delegation 을 사용하지 않고 데코레이터를 만들어보겠습니다.

 

학생 클래스가 있고, 내부에 enter() 메서드와 leave() 메서드가 있습니다.

class Student(val name: String) {
  fun enter() {
    ...
  }
  fun leave() {
    ...
  }
}

 

여기에 학생이 들어올 때 반갑게 인사를 해주는 기능을 추가한다고 가정해보죠.

간단하게 enter() 메서드가 시작하는 부분에 코드한줄 추가하면 쉽게 가능해보입니다~

 

하지만, 쉽게 한줄 추가하는 기능이 아니고 복잡한 기능을 추가하거나, 혹은 아예 이 메서드를 수정할 수 없으면 어떻게할까요?

이럴때  쉽게 생각하는 방법이 상속입니다! 상속은 많은 것들을 가능하게해주죠~

자식클래스에서 enter() 메서드를 오버로딩하면 간단해보입니다.

 

그런데.. 코틀린의 클래스는 기본적으로 final 이라서 이것도 힘들어보이네요.

이런상황에서 기존 클래스를 수정하지않고 부가기능을 더하거나 객체에 접근을 제어하는 방법이 있는데,

데코레이터 패턴과 프록시 패턴입니다.

 

interface Person {
  fun enter()
  fun leave()
}

 

다음과 같이 기존 Student 클래스의 메서드들을 모두 포함하고있는 Person 인터페이스를 하나 생성하고 Student 클래스는 이 인터페이스를 구현하는 클래스로 변경해줍니다.

 

class Student(val name: String): Person {
  override fun enter() {
    ...
  }
  override fun leave() {
    ...
  }
}

 

또 다른 클래스인 HelloStudent 정의하여 마찬가지로 Person 인터페이스를 구현합니다.

 

class HelloStudent (
    val targetStudent: Person = Student("jjeda")
) : Person {
  override fun enter() {
    targetStudent.enter()
  }
  
  override fun leave() {
    targetStudent.leave()
  }
}

 

HelloStudent 는 기존의 Student 클래스(타겟)를 주입받아서 메서드 실행을 타겟에게 넘깁니다.

이때 타겟메서드를 실행하기 직전에 부가적인 코드를 실행하면!

 

override fun enter() 
  println("Hello $targetStudent.name !") // Decorator!!
  targetStudent.enter()
}

 

새로운 기능을 실행하고 타겟메서드를 실행해줄수 있겠네요~

 

이처럼 효율적으로 기존 코드에 부가적인 기능을 더할 수 있지만, 인터페이스를 만들고 모든 메서드를 타겟에게 넘기도록 오버로딩 하는일은 상당히 귀찮은 일입니다.. 메서드가 10개라면 단순히 넘겨주는 오버로딩만 10번해야겠네요.

 

스프링 프레임워크에서는 다이나믹 프록시와 ProxyFactoryBean 등을 통해 이런것들이 자동으로 되고있지만,

수동으로 데코레이터를 만들거나 프록시를 만들때는 여전히 까다롭습니다. 

 

하지만! 코틀린의 Class Delgation 기능을 사용하면

 

// Class Delegation
class HelloStudent (
    val targetStudent: Person = Student("jjeda")
) : Person by targetStudent {
  override fun enter() {
    println("Hello $targetStudent.name !")
    targetStudent.enter()
  }
}

 

by 키워드를 사용하여 클래스를 위임하고,

부가기능을 수행할 메서드만 재정의 하여 쉽게 만들 수 있습니다.