[JAVA] 마음이 편해지는 불변 객체

Kevin·2024년 3월 18일
1

JAVA

목록 보기
7/16
post-thumbnail

서론

이전부터 불변 객체에 대해서 생각을 했던 적이있다.

일급 컬렉션과 마찬가지로 어느정도 자바를 공부했다 싶으면 반드시 거치는 관문과도 같은 느낌으로만 생각했다.

그 때 당시에는 Setter를 왜 사용하면 안되는지에 대해서도 내 스스로 확립이 안되었기에, 불변 객체란 고수(?)의 기술 그 이상, 이하로도 생각하지 않았었다.

그러나 내가 여러 사람들과 함께 개발을 하면 할 수록 이런 생각들이 생겨났다.

Client로부터 받은 값을 담은 DTO가 Service단에 사용할 때 까지 아무도 이 값을 변경하지 않았으면 좋겠다.

나는 이 값을 가지고 영속 계층에 넘겨야 하는데, 값이 달라지면 어떡하지??

    @PostMapping("/")
    public ResponseEntity<PostResponseDTO> savePost(@RequestBody PostRequestDTO postRequestDTO){

        PostResponseDTO responseDTO = postService.savePost(postRequestDTO);

        return ResponseEntity.ok(responseDTO);
    }

Client가 보낸 데이터를 가지고 있는, PostRequestDTO 객체가 Service 단에서 내부 값이 변경되면 어떡하지??

맞다. 나는 이 객체의 불안정함에 대해서 불안감을 느꼈다.

그제서야 불변 객체에 대해서 찾아보게 되어, 이 글을 작성하는 계기가 되었다.

매번 느끼지만, 직접 필요성을 느껴야 공부가 잘되는 성격인 것 같다.

그럼 내가 겪은 문제의 예시 코드를 이펙티브 자바 책의 설명과 함께 보며, 같이 이야기 해보자.


불변 클래스?

불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스를 칭한다.

불변 인스턴스에 저장된 정보는 고정되어서 객체가 파괴되는 순간까지 절대, 저어어얼대 달라지지 않음이 보장된다.

이와 반대되는 가변 클래스는 우리가 늘 사용해오는 값이 변경될 수 있는 일반 클래스를 의미하는데, 불변 클래스는 이러한 가변 클래스보다 설계하고, 구현하고, 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

그러면 어떻게 클래스를 불변 클래스로 만들 수 있는지 하나 하나 살펴보자.

아래는 예시 클래스인 DTO 클래스이다.

@Getter
@Builder
public class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
    
    public void updateTitle(String title) {
			   this.title = title;
    }
}

위 클래스를 불변 객체로 만들어보자.

1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.

@Getter
@Builder
public class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
    
    public void updateTitle(String title) {
			   this.title = title;
    }
}

위 코드에서는 객체의 상태를 변경하는 updateTitle() 메서드를 가지고 있다.

여기서 핵심은 인스턴스 자신의 상태를 변경하는 메서드라는 것이다.

말 그대로 이러한 메서드를 제공하지 않는 방법도 있지만 아래와 같은 방법도 존재한다.

@Getter
@Builder
public class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
    
    public void getNewTitle(String title) {
			   return PostResDTO.builder()
							  .title(title)
								.user(this.user)
								.build();
    }
}

위 코드에서 기존 자신의 상태를 변경하는 메서드가, 자신의 상태를 수정하지 않고 새로운 PostResDTO 객체를 만들어 반환하는 메서드로 변경되었다.

조금 극단적으로 느껴질 수 있겠지만, 내부 상태가 변경될 수 있는 불안정함을 차단하는 코드이다.

또한 메서드 이름도 기존 update에서 getNew로 변경한 이유도 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하는 의도이다.

물론 이러한 방식은 DTO에 그렇게 맞는 방법은 아니다고 생각했다.

책에서는 사칙연산 클래스를 예시로 들었기에, DTO에도 적용을 시켜봤지만 서론에서도 말했듯이 내가 원한 진정한 불변 클래스의 DTO는 새로운 객체를 리턴해주는 것 또한 취지에서 벗어난다.

나는 DTO의 흐름을 예측하고, 통제하고 싶다.

조금이라도 변수를 두고 싶지 않다.

즉, 클라이언트에게 받은 값을 변경 시키고 싶지 않다.

뭐 사실 그냥 내 취향이며, 이 글의 시작부터 끝은 내 취향에 맞춰서 작성될거니 글을 읽는데 유의해주시면 감사드리겠다.

2. 클래스를 확장할 수 없다.

클래스를 확장할 수 없게 하는 대표적인 방법은 클래스를 아래와 같이 final로 선언하는 것이다.

@Getter
@Builder
public final class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
}

이는 상속을 통해 Overridding(재 작성)을 통해 객체의 상태를 변경시키는 코드를 작성하는 걸 방지하기 위함이다.

사실 이 방식으로도 충분하지만, 조금 더 유연한 방법이 존재한다.

바로 모든 생성자를 private로 만들고, public 정적 팩터리를 제공하는 방법이다.

위 코드에서는 public 정적 팩터리를 이미 제공하고 있기에, 기본 생성자를 private이나 package-private으로 만들어주자.

@Getter
@Builder
public class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
    
    private PostResDTO(){
    
    }
}

package-private 접근 제어가 같은 경우에는 같은 패키지 내에서는 상속이 가능하고, 패키지 밖에서 바라볼 때는 사실상 final이기에 조금 더 유연하다 이야기 했던 것이 이 때문이다.

사실 클래스를 final로 선언하는 것은 단순히 시스템 차원에서 상속을 방지하는 것을 넘어서, 이 클래스를 상속할 수 있는 잠재적 클라이언트들에게 보내는 일종의 경고이다. 야레야레

3. 모든 필드를 final로 선언한다.

이 또한 위 클래스를 final로 선언하는 것과 마찬가지로 시스템 차원에서 강제하는 의미와 설계자의 의도를 명확히 드러내는 방법이다.

@Getter
@Builder
public class PostResDTO {

    private final String title;

    private final user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
    
    private PostResDTO(){
    
    }
}

이는 새로 생성된 인스턴스를 동기화 없이, 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데도 필요하다.

4. 모든 필드를 private로 선언한다.

이미 아래 코드를 보면, 변수를 private로 선언 했기에 별도로 적용해야 될 것은 없다.

@Getter
@Builder
public class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }

}

변수를 private로 선언하는 것은 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.

위의 말을 위 코드의 예시에 빗대어 설명해보자.

만약 User 객체를 참조중인 필드인 User 필드가 public이라고 해보자.

이러면 외부에서 해당 User 객체에 대해서 접근이 가능할 것이다.

만약 User 클래스에 User의 상태를 변경시키는 메소드나 코드들이 있다면??

그렇다면 이미 우리의 원대한 꿈은 사라져버린다.

그렇다. 우리는 우리 자신외에는 내부 참조중인 컴포넌트도 접근할 수 없게 해야한다.

이러한 여러 조건을 가진 불변 객체는 그러면 값을 변경할 수 없다는 특징이 어떠한 장점들을 몰고 올까??

불변 객체의 장점

1. 마음이 놓인다.

이는 장난 처럼 적어두었지만, 적어도 나에게는 진심이다.

불변 객체는 단순하다.

불변 객체는 생성된 시점의 상태를 객체가 파괴될 때까지 간직하기에, 믿고 사용할 수 있게된다.

그렇기에 마음이 놓인다는 장점이 있다.

2. 스레드에 안전하다.

불변 객체는 근본적으로 스레드 안전하여, 따로 동기화 할 필요가 없다.

클래스를 스레드 안전하게 만드는 가장 쉬운 방법은 불변 객체로 만드는 방법인데, 그 이유는 당연하게도 내부 상태가 생성 시점 그대로이기에 변경될 가능성도 여러 스레드가 같은 자원을 동시에 수정할 일도 없기 때문이다.

3. 그 자체로 실패 원자성을 제공한다.

여기서 말하는 실패 원자성이란 메서드에서 예외가 발생한 후에도 그 객체의 상태는 메서드 호출전과 같은 상태이어야 한다는 것이다.

객체의 상태가 절대 변하지 않으니, 블일치에 상태에 빠질 가능성이 절대 없다.

그렇다면 불변 객체는 장점만 가지고 있을까?

아니다. 근데 사실 여기까지 함께 왔으면 단점은 모두 알 것 같다고 생각된다.

단점. 값이 다르면 반드시 독립된 객체로 만들어야 한다.

@Getter
@Builder
public class PostResDTO {

    private String title;

    private User user;

    public static PostResDTO from(Post post) {
        return PostResDTO.builder()
                .title(post.getTitle())
                .user(post.getUser())
                .build();
    }
    
    public void getNewTitle(String title) {
			   return PostResDTO.builder()
							  .title(title)
								.user(this.user)
								.build();
    }
}

위에서 다뤘던 위 코드를 기억하는가?

불변 객체에서는 상태가 달라지면, 이를 반드시 새로운 객체로 만들어야 한다.

당연하게도 내부 상태가 변경되면, 안되기 때문이다.

그렇기에 불변 객체로 만들어야 할 클래스와 가변 객체로 만들 때 더 효율적인 클래스를 나눠야 할 것 같다.

profile
Hello, World! \n

0개의 댓글

관련 채용 정보