기본적인 엔티티 생성하기

내가 맡은 친구 도메인은 친구 요청 정보를 관리하는 FriendRequest 엔티티가 필요하다.

FriendRequest 엔티티는 고유 id, 친구 요청을 받는 사용자, 친구 요청을 보내는 사용자, 친구 요청의 상태를 가지는 status를 필드로 가진다.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "requester_id")
    @Column(nullable = false)
    private User requester;

    @ManyToOne
    @JoinColumn(name = "receiver_id")
    @Column(nullable = false)
    private User receiver;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Status status = PENDING;

    public FriendRequest(){}

    public FriendRequest(User requester, User receiver, Status status){
        this.requester = requester;
        this.receiver = receiver;
        this.status = status;
    }

 

알게 된 부분

@Enumerated는 Enum 타입 필드를 JPA가 데이터베이스에 삽입할 때 어떤 타입으로 변환하여 삽입할 건지를 설정할 수 있다.

이 어노테이션을 설정하지 않아도 JPA는 자동으로 ordinal 타입으로 Enum을 삽입한다.

ordinal 타입은 각 Enum을 0, 1, 2와 같이 숫자로 변환해서 관리하는 옵션인데, 이렇게 되면 개발 중에는 큰 문제가 없지만,

혹시라도 추후에 Enum을 변경하게 되면 매핑할 때 오류가 발생하게 된다.

그러므로 @Enumerated(EnumType.STRING)으로 설정해서 문자열로 데이터베이스에 저장하는 것은 사실상 필수이다.

 


 

복합키 매핑 방법

@EmbeddedId 방식

복합키 자체를 하나의 객체로 다루는 방식이다.

@Entity
public class CommentLike {

    @EmbeddedId
    private CommentLikeId id;  // 복합키 전체를 하나로 묶음

    private LocalDateTime likedAt;
}
@Embeddable
public class CommentLikeId implements Serializable {

    @Column(name = "comment_id")
    private Long commentId;

    @Column(name = "user_id")
    private Long userId;

    // 기본 생성자
    public CommentLikeId() {}

    public CommentLikeId(Long commentId, Long userId) {
        this.commentId = commentId;
        this.userId = userId;
    }

    // equals()와 hashCode() 오버라이딩 필수
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CommentLikeId)) return false;
        CommentLikeId that = (CommentLikeId) o;
        return Objects.equals(commentId, that.commentId) &&
               Objects.equals(userId, that.userId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(commentId, userId);
    }
}

 

equals()를 오버라이딩 해야하는 이유

데이터를 조회할 때 id를 사용해서 조회하는데, 복합키를 사용하면 복합키 객체를 가지고 동일한 데이터인지 비교해야 한다.

 

만약, dto를 통해 전달받은 데이터를 엔티티의 생성자를 사용해서 인스턴스를 만들었다고 가정하자.

그리고 해당 인스턴스를 레포지토리 레이어에 전달하여, DB에 저장되어 있는 데이터 중 동일한 값이 있는지 비교 후 값을 반환받는다.

단순히 비스니스적인 관점에서는 아래와 같이 복합키의 각 id 값을 꺼내서 비교하면 된다.

if (like.getId().getUserId().equals(requestUserId) 
	&& like.getId().getCommentId().equals(commentId)) {
    // 같음
}

 

하지만 JPA는 이렇게 각 데이터를 꺼내서 비교하는 방식으로 동작하지 않는다.

JPA는 equals()와 hashCode()를 사용해서 데이터를 조회하는 시스템이기 때문에, 복합키를 사용하는 상황에서는 반드시 두 메서드가 필요하다.

 

그리고 Object.equals()는 내부적으로 ==으로 값을 비교하는 로직이기 때문에, 두 데이터의 동등성을 비교하기 위해서는 오버라이딩을 해주어야 한다.

그동안 String의 동등성을 비교할 때 equals를 사용했던 것은 String.equals()를 내부적으로 이미 오버라이딩하고 있었기 때문이다.

 

 

hashCode()를 오버라이딩 해야하는 이유

해시 기반 자료구조에서 equals가 true인 객체는 hashcode도 같아야 정상 작동한다.

JPA가 직접적으로 hashMap을 사용하는 건 아니지만 내부적으로 객체 식별을 위해 HashMap처럼 동작하는 구조를 사용한다.

1차 캐시가 거의 Map<Id, Entity> 구조여서, 같은 트랜잭션 안에서 같은 ID값을 가진 엔티티는 동일한 객체로 유지된다.

 


@IdClass 방식

엔티티에 @Id를 여러 개 지정하고, 별도의 ID 클래스를 연결하는 방식이다.

즉, 첫 번째 방법과 다르게 엔티티에 각 id가 필드로 존재한다.

@Entity
@IdClass(CommentLikeId.class) // 복합키 클래스 연결
public class CommentLike {

    @Id
    private Long commentId;

    @Id
    private Long userId;

    private LocalDateTime likedAt;
}
public class CommentLikeId implements Serializable {
    private Long commentId;
    private Long userId;

    // 기본 생성자, equals(), hashCode() 필수
}

 

@IdClass 방식의 단점은 재사용과 관리가 어렵다.

@EmbeddedId는 ID 클래스를 객체로 다루니까 재사용, null 체크, 로직 캡슐화가 쉽다.

반면에 @IdClass는 단순 값 필드의 모음이기 때문에 로직이나 유효성 검사 등을 넣기 힘들다.