Object 클래스의 equals()는 매개변수로 받은 참조변수의 값(객체의 주소)을 비교하여 같은 객체인지 아닌지를 boolean 값으로 반환하는 메서드이다.
비교하고 싶은 인스턴스 변수(iv)끼리 비교하도록 equals() 메서드를 오버라이딩해서 사용하면 된다.
class Value {
int value;
Value () {};
Value (int value) {
this.value = value;
}
public boolean equals(Object obj) { // 오버라이딩 안하면 객체의 주소를 비교
// 참조변수의 형변환 전에는 반드시 instanceof로 확인해야한다.
if(!(obj instanceof Value)) return false;
Value v = (Value) obj;
return this.value == v.value;
}
}
instanceof 연산자는 형변환이 가능한지 확인하는 연산자이다. 참조변수의 형변환은 상속관계에서만 가능하다. 상속관계가 아닌 다른 참조변수가 매개변수로 들어온다면 비교할 필요 없이 false를 반환하면 된다.
메서드 오버라이딩은 선언부가 같아야되므로 아래와 같이 정의하면 Object 클래스의 equals()를 오버라이딩한 게 아니라 그냥 별개의 메서드를 정의한 게 되므로 주의하자.
public boolean equals(Value v) {
return this.value == v.value;
}
2. hashCode()
Object 클래스의 hashCode() 메서드는 객체의 주소를 int로 변환한 해시코드를 만들어 반환하는 함수이다.
equals() 메서드를 오버라이딩하면 hashCode() 메서드 또한 오버라이딩해야 한다.
class Card {
String kind;
int number;
static int width = 100;
static int height = 250;
Card () {
this("SPADE", 1);
}
public Card(String kind, int number) {
this.kind = kind;
this.number = number;
}
@Override
public boolean equals(Object obj) {
if(!(obj instanceof Card)) return false;
Card c = (Card) obj;
return this.kind.equals(c.kind) && this.number == c.number;
}
@Override
public int hashCode() {
return Objects.hash(kind, number);
}
}
* String 클래스에는 이미 equals()가 문자열의 내용끼리 비교하도록 오버라이딩되어있기 때문에 String 타입의 변수 kind는 equals()로 비교한다.
* Objects.hash() : 여러 객체의 해시 코드를 결합하여 새로운 해시 코드를 생성하는 메서드이다. 가변 인자를 받으므로 여러 개의 필드를 넣어도 된다. Object가 아닌 Objects임에 유의하자
BidProductResponseDto가 생성될 때 경매 상품의 제시자가 아무도 없어 topBid가 null인 경우 NPE가 발생했다.
public BidProductResponseDto(BidProduct bidProduct) {
id = bidProduct.getId();
name = bidProduct.getName();
description = bidProduct.getDescription();
startPrice = bidProduct.getStartPrice();
expirationPeriod = bidProduct.getExpirationPeriod();
feetSize = bidProduct.getFeetsize();
footSize = bidProduct.getFootsize();
footPicture = bidProduct.getFootpicture();
brand = new BrandResponseDto(bidProduct.getBrand());
topBid = new BidResponseDto(bidProduct.getTopBid());
status = bidProduct.getStatus();
createdAt = bidProduct.getCreatedAt();
updatedAt = bidProduct.getUpdatedAt();
for (int i = 0; i < bidProduct.getBids().size(); i++) {
bidResponseDtoList.add(new BidResponseDto(bidProduct.getBids().get(i)));
}
}
해결방안
입찰이 하나도 없을 때 빈 객체를 생성한 뒤, topBid의 제시자는 경매상품 출품자로, topBid의 가격은 경매 시작가로 설정한다.
public BidProductResponseDto(BidProduct bidProduct) {
this.id = bidProduct.getId();
this.name = bidProduct.getName();
this.author = bidProduct.getUser().getName();
this.description = bidProduct.getDescription();
this.startPrice = bidProduct.getStartPrice();
this.expirationPeriod = bidProduct.getExpirationPeriod();
this.feetSize = bidProduct.getFeetsize();
this.footSize = bidProduct.getFootsize();
this.footPicture = bidProduct.getFootpicture();
this.brand = new BrandResponseDto(bidProduct.getBrand());
// topBid 설정 (경매 제시가가 아직 없을 경우 처리)
if (bidProduct.getTopBid() != null) {
this.topBid = bidProduct.getTopBid();
} else {
// topBid가 없을 때 topBid 제시자는 경매상품 등록자로, 가격은 startPrice로 설정
this.topBid = new Bid();
this.topBid.setUser(bidProduct.getUser());
this.topBid.setBidPrice(bidProduct.getStartPrice());
}
this.status = bidProduct.getStatus();
this.createdAt = bidProduct.getCreatedAt();
this.updatedAt = bidProduct.getUpdatedAt();
this.bidResponseDtoList = bidProduct.getBids().stream()
.map(BidResponseDto::new)
.sorted(Comparator.comparing(BidResponseDto::getBidPrice))
.toList();
}
고찰할 점
@Table(name = "bidproduct")
public class BidProduct extends Timestamped{
...
/**
* 연관관계 - Foreign Key 값을 따로 컬럼으로 정의하지 않고 연관 관계로 정의합니다.
*/
@OneToMany(mappedBy = "bidProduct", cascade = CascadeType.REMOVE)
private List<Bid> bids = new ArrayList<>();
@OneToOne
@JoinColumn(name = "topBid")
private Bid topBid;
...
}
BidProduct와 topBid는 일대일 단방향 연관관계인데 topBid가 null인 경우를 (topBid의 price가 null인 경우, topBid의 user가 null인 경우 등등..) 메서드마다 매번 핸들링해줘야 했다. 아예 topBid의 초기값을 설정하는 게 더 나은 방법이었을까?
var newChannel = Channel.builder().name("new-channel").build();
var newThread = Thread.builder().message("new-message").build();
var newThread1 = Thread.builder().message("장마 끝!!").build();
newThread.setChannel(newChannel);
newThread1.setChannel(newChannel);
var savedChannel = channelRepository.insertChannel(newChannel);
var savedThread = threadRepository.insertThread(newThread);
var savedThread1 = threadRepository.insertThread(newThread1);
// when
var foundChannel = channelRepository.selectChannel(savedChannel.getId());
foundChannel.getThreads().remove(savedThread);
1. CascadeType.ALL만 적용한 경우
@OneToMany(mappedBy = "channel", cascade = CascadeType.ALL)
private Set<Thread> threads = new LinkedHashSet<>(); // 중복 x 순서 o
Hibernate:
insert
into
channel
(name, type)
values
(?, ?)
Hibernate:
insert
into
thread
(channel_id, message)
values
(?, ?)
Hibernate:
insert
into
thread
(channel_id, message)
values
(?, ?)
delete 쿼리가 날아가지 않는다.
2. CascadeType.ALL과 orphanRemoval = true 둘 다 적용한 경우
@OneToMany(mappedBy = "channel", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Thread> threads = new LinkedHashSet<>(); // 중복 x 순서 o
Hibernate:
insert
into
channel
(name, type)
values
(?, ?)
Hibernate:
insert
into
thread
(channel_id, message)
values
(?, ?)
Hibernate:
insert
into
thread
(channel_id, message)
values
(?, ?)
Hibernate:
delete
from
thread
where
id=?
delete 쿼리가 날아가고 스레드 테이블에서 스레드가 삭제된다.
3. orphanRemoval = true 만 적용한 경우
Hibernate:
insert
into
channel
(name, type)
values
(?, ?)
Hibernate:
insert
into
thread
(channel_id, message)
values
(?, ?)
Hibernate:
insert
into
thread
(channel_id, message)
values
(?, ?)
delete 쿼리가 날라가지 않는다.
Cascade.REMOVE 와 orphanRemoval 차이점
Cascade.REMOVE : 부모 엔티티를 em.remove로 직접 삭제하면 자식 엔티티도 삭제된다.
orphanRemoval : 부모 엔티티를 em.remove로 직접 삭제하면 자식 엔티티도 삭제된다. + 부모 엔티티의 리스트에서 어떤 요소를 삭제하면 그에 해당하는 자식 엔티티도 삭제된다. 즉, 리스트 요소의 영속성 전이도 해준다.
위 테스트 코드로 예를 들면 부모 객체인 savedChannel의 threads 안에 있는 savedThread를 제거하면 스레드 테이블에서도 해당 스레드가 삭제된다.
프로젝트를 진행하면서 게시글을 수정, 삭제 api에서 작성자 확인이 제대로 되지 않는 문제가 발생했다.
@Transactional
public PostResponseDto updatePost(Long id, PostRequestDto requestDto, User user) {
Post post = findPost(id);
//게시글 작성자와 요청자가 같은지 또는 관리자인지 체크 -> 아닐시 예외 발생
// 해당 post의 작성자가 맞는지 확인
if (user.equals(post.getUser()) || user.getRole().equals(UserRoleEnum.ADMIN)) {
// requestDto로부터 받은 게시글의 제목과 내용으로 해당 post 내용 수정하기
post.setTitle(requestDto.getTitle());
post.setContent(requestDto.getContent());
Category category = categoryRepository.findById(requestDto.getCategoryId()).orElseThrow(() ->
new IllegalArgumentException("선택한 게시글이 존재하지 않습니다"));
post.setCategory(category);
} else {
// 해당 post의 작성자가 아니라면 null 반환하기
throw new RejectedExecutionException();
}
return new PostResponseDto(post);
}
user.equals(post.getUser())가 계속 false로 나와 이런 문제가 발생했는데 원인은 User 클래스에 equals 메서드를 오버라이딩 해주지 않아서 발생한 거였다.