코드 리뷰 중 final을 언제 붙일까? 라는 고민이 들었습니다.

프로젝트 구성원과 논의한 내용은 ‘일단 다 붙이자’, ‘마음대로 하자’, ‘그게 뭐임? 신경 써야함?‘와 같이 역시 다양했습니다.

개인적으로는 가능하다면 ‘일단 다 붙이자’라는 생각이었습니다. IntelliJ IDEA에서 변수 추출 시 자동으로 final을 붙여주기 때문에 별도의 타이핑도 필요 없고 일단 붙인다면 적어도 컴파일 시점에 개발자의 실수를 최소화 할 수 있다고 생각하였습니다. 하지만 불필요하게 final을 사용하는 경우도 있었습니다.

현재까지 final 사용에 대한 고민은 다음과 같습니다. 정답은 아니지만 사람들이 고민하고 결정에 참고 할 수 있도록 정리해 보았습니다.

Kotlin, Scala 등의 다른 언어에서는 final을 의미하는 val와 같은 지시어가 있어서 더 편리합니다.

final 은 무엇인가?

위키백과[참고1]에서 설명하는 final (Java) 정의와 예제는 다음과 같습니다.

the final keyword is used in several contexts to define an entity that can only be assigned once.

final 키워드는 엔티티를 한 번만 할당합니다. 즉, 두 번 이상 할당하려 할 때 컴파일 오류가 발생하여 확인이 가능합니다.

Final classes

public final class MyFinalClass {...}

public class ThisIsWrong extends MyFinalClass {...} // forbidden

Final class는 final 지시어를 통해 클래스 상속을 제한

MyFinalClass는 final 지시어를 통해 상속하지 못하도록 정의합니다. ThisIsWrong에서 MyFinalClass를 상속 받으려 하면 컴파일 오류가 발생합니다. 그리고 IDE를 이용하는 경우 실시간으로 코드에 빨간 줄이 표시되어 빌드가 실패하였다는 사실을 알 수 있습니다.

대표적인 final class는 String이 있습니다. 왜 String을 final로 정의하였는지는 StackOverFlow[참고2]을 참고하면 Immutable object로 얻을 수 있는 이점 때문이라고 설명합니다.

  1. Immutable objects
    • You can share duplicates by pointing them to a single instance.
  2. Security
    • The system can hand out sensitive bits of read-only information without worrying that they will be altered
  3. Performance
    • Immutable data is very useful in making things thread-safe.

위의 기준은 클래스 디자인 시 final로 선언할지 결정하는데 고려 요소로 생각됩니다.

Final methods

public class Base {
    public       void m1() {...}
    public final void m2() {...}

    public static       void m3() {...}
    public static final void m4() {...}
}

public class Derived extends Base {
    public void m1() {...}  // OK, overriding Base#m1()
    public void m2() {...}  // forbidden

    public static void m3() {...}  // OK, hiding Base#m3()
    public static void m4() {...}  // forbidden
}

Final methods 는 final 지시어를 통해 메소드 오버라이드를 제한

final 이 선언된 메소드는 자식 클래스에서 재정의하려 할 때 컴파일 오류가 발생합니다. 클래스를 구현 시 명시적으로 Override Method를 막고 싶을 때 사용하면 좋습니다.

Final variables

final long ONE_MINUTE = 60000;

Final variables 는 final 지시어를 이용해 Immutable 선언

Java 에서는 상수를 선언할 때 final을 이용하여 Read-Only 로 설정할 수 있습니다. 즉 한 번 선언한 뒤 변하지 않는 Immutable 형식이라는 것을 명시적으로 표현할 수 있습니다.

그래서 final 언제 사용할까?

사실 final을 안 쓰더라도 잘 기존 코드 이해하고 작성하면 문제없이 코딩이 가능합니다. 하지만 이런 일은 전설 속의 선배들만 가능한 일이고 언제 final을 써야 다른 사람들과 오해를 최소화하고 도움을 줄 수 있을지를 고민하면 좋을 것 같습니다.

클린 코드[참고3]에서는 기본적으로 코드 가독성을 해치지 않고 명시적으로 final 선언이 필요한 부분에 사용하라고 정의되어 있습니다. 역시 가장 중요한 것은 같이 일하는 사람들과 합의가 필요하도 생각됩니다. 코드 리뷰를 진행하며 자연스럽게 final 사용에 대한 논의를 하고 적절한 합의점을 찾아가야한다고 생각합니다. (개인 프로젝트라면 나 혼자 결정 👌).

그리고 전반적인 final 사용에 관한 지침은 Is that your final answer? [참고4]에 매우 잘 정리되어 있습니다. 이러한 정보들을 바탕으로 개인적으로 생각하는 final 사용 기준은 다음과 같이 정리해보았습니다.

  • 개발 의도(변수, 함수, 클래스의 명시적 제한)
  • 코드 가독성

개발 의도(변수, 함수, 클래스의 명시적 제한)

개발 의도는 반드시 본 변수, 함수, 클래스는 final로 제한되어야 함을 보여줍니다. 클래스와 메소드를 제한함으로써 Override로 인한 실수를 최소화 하고 버그를 줄이기 위해 선언합니다.

개인적으로 많이 사용하는 final 선언 패턴은 다음과 같습니다.

클래스 및 생성자 의존성에 대한 final

다음 예제의 EventStreamId는 Event Stream에 대한 고유 ID 객체로 언제나 동일한 인스턴스를 바라보도록 디자인이 필요하였습니다. 그래서 final class 으로 상속이 불가능하도록 선언하고 생성 시 전달받은 인자를 final member variables로 유지하도록 구현하였습니다.

간단하게 EventStreamId 객체 내에서 streamName, version 변경되지 않기를 의도한 코드입니다.

public final class EventStreamId {
    private final String streamName;
    private final long version;

    public EventStreamId(String streamName, long version) {
        this.streamName = streamName;
        this.version = version;
    }

    public EventStreamId(String streamName) {
        this(streamName, 1L);
    }

    public EventStreamId withVersion(long version) {
        return new EventStreamId(this.getStreamName(), version);
    }
  • github.com/lubang/drived 의 EventStreamId.java 중 일부
  • 본 클래스는 도메인 주도 개발[참고5]를 기반으로 구현

이 디자인은 Stream name, version은 전달받은 인자를 기반으로 Immutable object를 생성합니다. 새로운 version의 EventStreamId가 필요한 경우에는 withVersion 메소드를 이용하여 새로운 인스턴스로 반환합니다. 이는 SRP(Single Responsibility Principle)를 준수하여 EventStreamId의 생성자로만 멤버 변수의 값을 설정하고 EventStreamId의 생성자 호출이라는 일관성 있는 메소드를 제공합니다.

함수에 대한 final

함수에 대한 final은 단순합니다. 상속되면 절대 안되는 경우 final을 정의합니다.

사실 ISP(Interface Segregation Principle)에 따라 인터페이스를 선언하여 구현한다면 final을 사용하는 경우가 매우 한정적입니다.

  • Interface에서 정의한 변수는 final 선언과 같이 재정의가 불가능하다.
  • Interface에서 Method를 final로 선언할 수 없다. (당연하다고 생각된다)

결국 Interface로 정의 후 상속받은 구현체의 메소드에 final을 선언하여 추가적인 재정의를 막는 경우에 final 사용이 가능합니다. 개인적으로는 함수에 대해서는 final을 선언한 기억이 없습니다. (프로젝트 구성원님들이 잘 해주신 덕분에 버그가 없었는지도…)

코드 가독성

개발 의도가 가장 중요하지만 그 다음으로는 가독성을 고려합니다. 읽기 좋은 코드가 버그가 적다!

// final 이 있는 코드
@Test
public void apply_events_when_music_artist_releases_an_album() {
    final MusicArtistId id = MusicArtistId.createUniqueId();
    final MusicArtist musicArtist = new MusicArtist(id,
            "Red Velvet",
            ZonedDateTime.parse("2014-08-01T00:00:00+09:00"));
    musicArtist.releaseAlbum(
        "The Red",
        ZonedDateTime.parse("2015-09-09T00:00:00+09:00"));

    // Assert 생략
}

// final 이 없는 코드
@Test
public void apply_events_when_music_artist_releases_an_album() {
    MusicArtistId id = MusicArtistId.createUniqueId();
    MusicArtist musicArtist = new MusicArtist(id,
            "Red Velvet",
            ZonedDateTime.parse("2014-08-01T00:00:00+09:00"));
    musicArtist.releaseAlbum(
        "The Red",
        ZonedDateTime.parse("2015-09-09T00:00:00+09:00"));

    // Assert 생략
}

github.com/lubang/drived 의 EventSourcedRootTest.java 중 일부

위와 같이 지역 변수(예. MusicArtistId id)를 생성해서 처리하는 경우는 실수할 일이 매우 낮기 때문에 가독성을 확보하는 것이 더 좋다고 생각합니다. 예제는 짧아서 final의 영향이 미비하지만 조금 복잡한 로직, 특히 수식을 구현한 알고리즘으로 들어가면 final 단 5글자이지만 수식을 이해하는데 불편함을 줄 수 있습니다.

결국 final은!

결국 Java에서의 final은 Immutable/Read-only 속성을 선언하는 지시어입니다.

클래스, 함수, 변수가 변하지 못하도록 의도하고 싶다면 final로 선언하자.

🐳사설

왜 Kotlin, Scala 처럼 var, val 이 없는 것인가? Javascript도 let, const 로 이제 구분하는데… Java도 스펙에도 이런 지시어가 빨리 생기면 좋겠다는 생각입니다. 그럼 가독성도 좋고 항상 final 선언이 가능할텐데 아쉽네요.


  1. 참고[1] 위키백과 final (Java) | https://en.wikipedia.org/wiki/Final_(Java) [return]
  2. 참고[2] 스택오버플로우 Java String이 final인 이유 | https://stackoverflow.com/questions/2068804/why-is-string-class-declared-final-in-java [return]
  3. 참고[3] 클린 코드, 로버트 C. 마틴, 인사이트 | YES24 [return]
  4. 참고[4] Is that your final answer?, Brian Goetz, IBM developerWorks | https://www.ibm.com/developerworks/java/library/j-jtp1029/index.html [return]
  5. 참고[5] 도메인 주도 설계 구현, 반 버논, 에이콘 | YES24 [return]