JPA를 사용하지 않으면 어떻게 변경 감지를 해야 하지??
변경감지는 어떻게 만들어져 있을까 생각해 보면서 작성한 코드이기에, 부족한 점이 많을 수 있습니다
나왔던 코드는 대부분 간소화된 코드입니다!
미리 잘 부탁드립니다
간단한 테이블 소개
노선과 구간이 1:N의 관계로 엮여있습니다
역과 구간이 1:N의 관계로 2번 엮여 있습니다
단 3개의 테이블만으로 이루어진 간단한 상황이라는 것을 기억해 주시면 좋을 것 같습니다
간단한 문제 상황 소개
처음에 코드를 짰을 때
도메인 엔티티에서 어떤 부분이 변경되어서 직접 저장해야 하는지를 파악할 방법을 쉽게 떠올리지 못해서, 가장 편한 방법을 거쳤는데요
도메인 엔티티를 변경하게 된 경우에, db에 저장하는 과정에서, 모두 삭제하고, 모두 새롭게 저장하는 과정을 거쳤습니다
어떤 문제가 있었을까요?
언제나 무언가를 시작해야 하는 이유로는 극단적인 상황을 생각해 보면 도움이 많이 되는데요
변경이 필요한 극단적인 사례
1개의 노선은 1만 개의 구간을 가지고 있습니다.
이 구간에서 하나의 역이 제거되었습니다.
저장을 위해서 DB 에는 총 1만 개의 쿼리가 날아갑니다
1. delete from INTER_STATION where LINE_ID =?
9999. insert into INTER_STATION (DISTANCE, UP_STATION_ID, DOWN_STATION_ID, LINE_ID) values (?,?,?,?)
단 하나의 변경마다 1만 개의 쿼리가 날아가니, N+1과 비슷한 결과가 나옵니다 물론 조회는 아니기에, 정확히 N+1 문제는 아니긴 합니다
어떻게 변경할 수 있을까?
당연하지만, 변경된 부분만을 db를 통해서 업데이트한다면, 우리가 원했던 결과를 얻어낼 수 있다고 생각하는데요
그렇다면 어떻게 변경된 부분만을 변경할 수 있을까요?
JPA에서는 이를 Proxy 객체를 통해서 해결한다고 합니다
물론 정확한 네이밍은 데코레이터 패턴이 사용되어서 데코레이터 객체가 더 정확하지만, 용어를 통일하기 위해 Proxy 객체라고 칭하겠습니다
먼저 노선 클래스입니다
Line 클래스
@Getter
@ToString
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Line {
private final Long id;
private final InterStations interStations;
private LineName name;
private LineColor color;
public void deleteStation(final long existStationId) {
interStations.remove(existStationId);
}
public void updateName(final String name) {
this.name = new LineName(name);
}
}
편의를 위해서 다른 메서드들은 모두 제거하고, 변경을 하는 코드만을 가져왔는데요
우리는 이 클래스에서 delete, update 메서드를 호출했을 때, 그 변화를 기록해 둘 객체가 필요합니다
LineProxy
@Getter
public class LineProxy extends Line {
private final List<InterStation> removedInterStations = new ArrayList<>();
private final List<InterStation> addedInterStations = new ArrayList<>();
private final List<InterStation> beforeInterStations;
private boolean infoNeedToUpdated = false;
public LineProxy(final Long id,
final String name,
final String color,
final List<InterStation> interStations) {
super(id, name, color, interStations);
beforeInterStations = new ArrayList<>(super.getInterStations().getInterStations());
}
@Override
public void deleteStation(final long existStationId) {
super.deleteStation(existStationId);
calculateDifference(beforeInterStations);
}
private void calculateDifference(final List<InterStation> beforeInterStations) {
final List<InterStation> afterInterStations = new ArrayList<>(super.getInterStations().getInterStations());
final List<InterStation> removed = new ArrayList<>(beforeInterStations);
removed.removeAll(afterInterStations);
removedInterStations.addAll(removed);
final List<InterStation> added = new ArrayList<>(afterInterStations);
added.removeAll(beforeInterStations);
addedInterStations.addAll(added);
}
@Override
public void updateName(final String name) {
if (!super.getName().getValue().equals(name)) {
infoNeedToUpdated = true;
}
super.updateName(name);
}
}
변경이 발생하지 않는 다른 메서드는 line의 메서드를 그대로 사용할 수 있습니다
그럼 어떤 방식으로 변경을 감지하고, 필요한 부분만 업데이트할 수 있는지에 대해서 밑에서 설명드리도록 하겠습니다
이름을 변경해 보자
LineProxy 객체를 생성한다
일단 당연하지만, 기본 Line 객체를 통해서는 변경 감지를 할 수 없습니다. 그렇기에, Repository에서 반환할 때, Line 타입을 반환한다고 하지만, LineProxy 객체를 반환하는 방식을 통해서 업캐스팅을 진행했습니다
LineRepository
이때 save 만을 통해서 하면 더 깔끔하겠지만, 편의를 위해서 update를 추가했습니다
public interface LineRepository {
Line save(Line line);
Line update(Line line);
}
이렇게 인터페이스로 구성되어 있지만, Proxy 객체를 통해서 처리해야 하기 때문에
필요한 타입을 정확하게 표현하면 다음과 같습니다
public interface LineRepository {
LineProxy save(Line line);
LineProxy update(LineProxy lineProxy);
}
LineProxy에서 이름이 변경된다
@Getter
public class LineProxy extends Line {
private boolean infoNeedToUpdated = false;
@Override
public void updateName(final String name) {
if (!super.getName().getValue().equals(name)) {
infoNeedToUpdated = true;
}
super.updateName(name);
}
}
원래 이름은 super.getName()으로 가져오게 되는데요 line.getName()과 같습니다
이때, 원래 이름과 다르다면, infoNeedToUpdated 가 true로 변경됩니다
이 말은 line의 정보가 변경되었다는 것을 기록할 수 있습니다
따라서 DB에 update 쿼리가 발생해야 한다는 것을 알 수 있죠
Repository에서 변경을 한다
@Repository
public class LineRepositoryImpl implements LineRepository {
private final LineDao lineDao;
@Override
public Line save(final Line line) {
final LineEntity lineEntity = lineDao.insert(LineEntity.from(line));
return toLineProxy(lineEntity);
}
@Override
public Line update(final Line line) {
if (!(line instanceof LineProxy)) {
throw new LineProxyNotInitializedException();
}
final LineProxy lineProxy = (LineProxy) line;
updateInformation(lineProxy);
return findById(line.getId())
.orElseThrow(LineNotFoundException::new);
}
private void updateInformation(final LineProxy lineProxy) {
if (lineProxy.isInfoNeedToUpdated()) {
lineDao.updateInformation(LineEntity.from(line));
}
}
}
여기서 update 메서드를 보시면, LineProxy 인지를 확인합니다
update를 하는데, LineProxy 객체에서 정보가 변경되었는지 체크를 통해서, 실제 필요한 경우만을 업데이트할 수 있습니다
노선에 삭제가 발생하면 어떻게 될까요?
LineProxy 객체가 생성된다
Repository에서 LineProxy 객체를 초기화합니다
@Getter
public class LineProxy extends Line {
private final List<InterStation> removedInterStations = new ArrayList<>();
private final List<InterStation> addedInterStations = new ArrayList<>();
private final List<InterStation> beforeInterStations;
public LineProxy(final Long id,
final String name,
final String color,
final List<InterStation> interStations) {
super(id, name, color, interStations);
beforeInterStations = new ArrayList<>(super.getInterStations().getInterStations());
}
@Override
public void deleteStation(final long existStationId) {
super.deleteStation(existStationId);
calculateDifference(beforeInterStations);
}
private void calculateDifference(final List<InterStation> beforeInterStations) {
final List<InterStation> afterInterStations = new ArrayList<>(super.getInterStations().getInterStations());
final List<InterStation> removed = new ArrayList<>(beforeInterStations);
removed.removeAll(afterInterStations);
removedInterStations.addAll(removed);
final List<InterStation> added = new ArrayList<>(afterInterStations);
added.removeAll(beforeInterStations);
addedInterStations.addAll(added);
}
}
생성자에서, 변경이 일어나기 전에 interStations를 가지고 있습니다
변경 전 데이터를 가지고 있다는 부분이 핵심입니다
LineProxy에서 deleteStation메서드가 호출된다
line에 deleteStation 메서드를 호출한 뒤에, 변경된 부분을 기록합니다새롭게 추가된 부분은 added에 저장하고제거된 부분은 removed에 저장합니다
이때 변경된 부분을 편하게 계산하기 위해서 차집합을 구하는 방식을 통해서 사용했습니다
- 변경 후 부분 - 변경 전 부분 = 새롭게 추가된 부분
- 변경 전 부분 - 현재 부분 = 제거된 부분
이런 방식을 통해서 구할 수 있는데요
구하고 나면 저장해야겠죠?
Repository에서 변경된 부분을 저장한다
@RequiredArgsConstructor
@Repository
public class LineRepositoryImpl implements LineRepository {
private final LineDao lineDao;
public LineRepositoryImpl(final JdbcTemplate jdbcTemplate) {
lineDao = new LineDao(jdbcTemplate);
}
@Override
public Line update(final Line line) {
if (!(line instanceof LineProxy)) {
throw new LineProxyNotInitializedException();
}
final LineProxy lineProxy = (LineProxy) line;
addInterStations(lineProxy);
removeInterStations(lineProxy);
return findById(line.getId())
.orElseThrow(LineNotFoundException::new);
}
private void addInterStations(final LineProxy lineProxy) {
if (!lineProxy.getAddedInterStations().isEmpty()) {
lineDao.insertInterStations(toInterStationEntities(lineProxy.getAddedInterStations(), lineProxy.getId()));
}
}
private void removeInterStations(final LineProxy lineProxy) {
if (!lineProxy.getRemovedInterStations().isEmpty()) {
lineDao.deleteInterStations(lineProxy.getId(),
toInterStationEntities(lineProxy.getRemovedInterStations(), lineProxy.getId()));
}
}
}
위와 같은 로직을 진행하게 됩니다
update를 호출했을 경우에, 새롭게 추가된 배열을 그대로 가져와서 lineDao에 insertAll을 호출해 한꺼번에 처리하고
removed에서 제거된 배열을 통해서 lineDao에 직접 제거를 하게 됩니다
개선된 결과
원래라면 1만 개의 노선에서 역을 하나만 제거해야 할 경우에, 1만 개의 쿼리가 날아갔지만
현재는 단 한 개의 쿼리만이 날아가게 됩니다
3줄 요약
JPA의 Proxy 객체 같은 객체를 만들어서
처음 시작값을 관리하게 되면
실제 변경된 부분만 감지해 업데이트할 수 있다
'우아한테크코스' 카테고리의 다른 글
"pr 본문에 이슈 번호를 달아주는 기능을 만들었습니다" (0) | 2023.07.04 |
---|---|
[레벨3] 프로젝트 1주차 회고 (0) | 2023.07.01 |
레벨 인터뷰 스터디 (0) | 2023.05.07 |
레벨 인터뷰 스터디 준비 (0) | 2023.04.23 |
우아한테크코스 레벨1 레벨로그 준비 (0) | 2023.03.28 |