2 minute read

엔티티를 설계할때

  • 기본적으로 엔티티는 서비스의 데이터에 가장 직접적인 영향을 끼치는 객체라고 할 수 있지 않나 싶습니다.
  • 사용자의 데이터는 아무렇지 않게 만들어지고 바뀌어서는 안되며, 이를 허용케 할 시 서비스의 여러 부분에서 예상치 못한 치명적인 일들이 발생할 수도 있습니다.
  • 때문에 엔티티를 설계할때 가장 기본적인 사항이라 한다면, 특정한 데이터의 생성/수정/삭제가 진행되어야 하는 기능에 대해서만 특정 데이터의 생성/수정/삭제를 진행하게 해주고 그 외에 대해서는 데이터의 핸들링을 최대한 억제하는것을 고려하는것이 중요하다고 생각됩니다.

캡슐화

  • 기본적으로 엔티티의 인스턴스 필드로의 접근에 대해서 첫번째는 데이터를 변경하는것과 두번째로는 데이터를 조회하는것으로 구분할 수 있습니다.
  • 접근 제어자가 허용되어 직접적으로 필드에 접근하는것은 이 두가지 내용을 모두 허용하기 때문에, 보통 캡슐화하여 필드를 숨긴 후, 필요 여하에 따라 데이터의 값을 변경하는 개방된 메소드를 제공하거나(e.g. setter), 데이터의 값을 획득하는 개방된 메소드를 제공합니다(e.g. getter)

setter

  • 하지만 모든 필드에 대해 setter를 제공한다는것은 모든 필드의 데이터를 언제든지 바꿀수 있다는 것을 의미합니다.
  • 회원정보를 변경해야 할때, 변경할 특정 필드의 setter를 일일히 호출하여 값을 변경하는것보단 회원정보 변경용 함수를 선언하여, 회원정보 변경에 대해서만 해당 함수를 호출하여 원하는 필드의 값을 바꾸어 주는것이 좀 더 명시적이고, 불규칙하고 무분별한 데이터 핸들링을 막을 수 있습니다
  • 아래는 Java로 구현할때의 예시입니다
@Entity
@NoArgsConstructor
@Setter
@Getter
public class Member{
    private id Long;
    private name String;
    private age Integer;

    @Builder
    public Member(Long id, String name, Integer age){
        //...
    }
}

public Response changeMemberInfo(Long id, Request param){
    //조회
    Member member = memberRepository.findById(id)
            .orElseThrow(()-> new NoSuchElementException());
    
    //회원정보 변경
    member.setName(param.getName());
    member.setAge(param.getAge());
    
    //...
}
  • before
@Entity
@NoArgsConstructor
@Getter
public class Member{
    private id Long;
    private name String;
    private age Integer;
    
    @Builder
    public Member(Long id, String name, Integer age){
        //...
    }
    
    public void changeMemberInfo(String name, Integer age){
        this.name = name;
        this.age = age;
    }
}

public Response changeMemberInfo(Long id, Request param){
    //조회
    Member member = memberRepository.findById(id)
            .orElseThrow(()-> new NoSuchElementException());
    
    //회원정보 변경
    member.changeMemberInfo(param.getName(), param.getAge());
    
    //...
}
  • after

Kotlin

  • 결국 Java에서는 접근과 수정을 모두 허용하는 접근제어자는 private 등으로 제한하여 캡슐화한 뒤, 데이터의 조회는 getter로 전체 제공하며, 데이터의 수정은 특별한 이유가 있을시 그에 맞는 함수를 선언하여 해당 이유에만 수정 할 수 있도록 특정한 경우에만 허용한다고 볼 수 있습니다.
  • 그러면 Kotlin에서는 어떨까요? Kotlin은 기본적으로 Kotlin 컴파일러가 getter와 setter를 생성하며, 프로퍼티에 접근할때 컴파일러에 의해 getter와 setter를 하용하는 코드로 변경하여 컴파일 합니다.
  • 하지만 저희는 setter의 존재를 없애고 getter만 남겨 조회하고 싶게만 만들고 싶은 상황입니다.
@Entity
class Member (
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id:Long? = null,
    var email:String? = null,
    var password:String? = null,
    var name:String? = null
) : DatetimeAt() {

}
  • 변경 전
  • 모든 프로퍼티가 public하므로 접근이 가능합니다
  • 또한 모든 프로퍼티가 var로 지정되어, 모든곳에서 자유롭게 프로퍼티의 값을 변경할 수 있습니다.
  • 이는 매우 맘에 들지 않습니다. 저는 특정 사항에만 특정 메소드를 호출하여 특정 값만 변경했으면 좋겠고, 그 외에는 기본적으로 프로퍼티의 값을 직접 변경하지는 못하되, 조회만 자유로웠으면 좋겠습니다.
@Entity
class Member (
    @Id 
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    email:String? = null,
    password:String? = null,
    name:String? = null
) : DatetimeAt() {
    var email:String? = email
        protected set

    var password:String? = password
        protected set

    var name:String? = name
        protected set
}
  • 변경 후
  • ID 값은 어떠한 사항에서도 변경되어선 안되므로 기본적으로 불변으로 선언합니다
  • 그 외의 프로퍼티들은, 기본적으로 public하게 값을 조회하는것은 가능하되, setter에 한에서만 protected로 선언하여(protected set) public한 값의 변경을 막습니다.
  • setter를 private이 아닌 protected로 선언한 가장 큰 이유는 Kotlin의 All-open을 적용하기 위해선 private을 지정 할 수 없기 때문에 All-open이 적용되는 최소한의 접근제어자인 protected로 선언하였습니다

참고

  • https://spoqa.github.io/2022/08/16/kotlin-jpa-entity.html