
final 키워드는 특정 변수, 메서드, 또는 클래스가 변경되지 않도록 제한하는 역할을 합니다.
변수에 사용될 때:
final로 선언한 변수는 초기화 후에 다시 값을 변경할 수 없습니다. 지역 변수에 붙는다면 초기화 후 값을 변경하지 못하도록 하고, 멤버 변수에 붙는다면 생성자에서 초기화한 후 다시는 값을 변경하지 못하게 됩니다.
메서드에 사용될 때:
final 메서드는 서브클래스에서 오버라이드(상속받은 메소드의 내용을 자식 클래스에서 변경하는 것)할 수 없습니다.
클래스에 사용될 때:
final 클래스는 다른 클래스가 상속할 수 없습니다.
@RequiredArgsConstructor는 Lombok 라이브러리에서 제공하는 어노테이션으로, 클래스 내의 final로 선언된 모든 필드와 @NonNull로 지정된 필드들을 초기화하는 생성자를 자동으로 생성해 줍니다. 그렇다면 왜 final로 선언된 모든 필드와 @NonNull로 지정된 필드들을 초기화하는 것일까요?
그 이유는 위에서 final에 대해 살펴 보았듯, 생성자 내부에서 초기화 후 다시는 변경할 수 없는 특징을 가졌기 때문에 생성자에 초기화 코드가 자동으로 들어가게 되기 때문입니다.
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class Service {
private final String name;
private final int level;
// @RequiredArgsConstructor을 DeLombok하면 나타날 코드
// public Service(String name, int level) {
// this.name = name;
// this.level = level;
// }
}
@RestController
@RequiredArgsConstructor
public class MyController {
private final MyService myService;
@GetMapping("/create-one")
public List<String> createOne() {
...
myService.createOne();
...
}
}
final 키워드와 @RequiredArgsConstructor 어노테이션이 Spring Framework 내에서 쓰이는 예제를 한 가지 살펴보겠습니다. 위 Controller 코드에서 Service Bean을 final로 선언 후, @RequiredArgsConstructor을 붙여 자동으로 의존성을 주입받고 있습니다.
Controller도 Bean이기 때문에 생성 단계가 Spring Application Context에 이루어질 텐데, 생성 단계에서 myService가 주입되는 것 마저도 Spring Application Context에 의해 이루어 지겠네요. 정말 어마어마하게 편리한 기능인 것 같습니다.
편리함 이외에 이런 코드 작성 방식이 어떤 장점을 갖는지 자세히 살펴보겠습니다.
final 키워드를 사용해서 객체의 불변성이 보장되기 때문에 멀티스레드 환경에서 동시성 문제를 줄일 수 있습니다.
@RequiredArgsConstructor를 사용하면, 필요한 서비스 객체를 주입받지 않은 채로 컨트롤러를 생성할 수 없게 됩니다. 이로 인해 의존성 주입 누락을 방지할 수 있으며, 컴파일 타임에 문제를 잡을 수 있습니다.
모든 의존성이 객체가 생성될 때 명확하게 초기화되므로, 주입 순서로 인한 문제를 방지할 수 있습니다. 또한 생성자 주입은 순환 의존성(circular dependency) 문제를 쉽게 발견할 수 있게 도와줍니다. 만약 A가 B를, B가 다시 A를 주입받으려고 할 경우, 생성자 주입에서는 순환 의존성을 컴파일 타임에 발견할 수 있습니다.