@Transactional
public void updateName(int personId, String address){
Person person = new Person();
person.setId(personId); // id를 통해서 기존의 데이터를 업데이트 한다고 생각할수 있다.
person.setAddress(address);
personRepository.save(person);
}
로직을 구현한 이는 Entity 객체에 ID를 부여했으니 기존 데이터에 수정된 필드들만 업데이트될거라 생각할 것이다.
이름을 수정하는 테스트 코드를 구현해보자
// 테스트 코드
@Test
void updateName() {
// given
int id = 1;
String address = "광주시";
// when
personService.updateName(id, address);
// then
}
// 출력 로그 중 일부 발췌
2022-02-27 15:40:46.346 INFO 66362 --- [ Test worker] p6spy : #1645944046346 | took 1ms | statement | connection 3| url jdbc:h2:tcp://localhost/~/jpashop
update person set address=?, name=? where id=?
update person set address='광주시', name=NULL where id=1;
2022-02-27 15:40:46.349 INFO 66362 --- [ Test worker] p6spy
의도한 address 값은 정상적으로 update 되었지만 name 필드에 null로 업데이트된 것을 확인할 수 있다.
// SimpleJpaRepository.save()
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
예제 코드에서는 id가 존재하기 때문에 persist가 아닌 merge 메소드를 호출할 것이다.
merge를 호출 후, dirty check를 하여 update 쿼리가 수행된다고 생각할 수 있다.
save메소드에서 dirty check를 하는 기준은 영속 상태에 객체에 한해서 동작한다.
그렇기 때문에 예제의 사용된 객체(new Person())는 비영속 상태이기 때문에 dirty check를 하지 않는다.
dirty check를 하지 않기 때문에 저장하려는 객체를 그대로 데이터베이스에 반영 되므로 수정하려는 객체의 name 필드의 값이 그대로 저장하게 된다.
update 로직은 어디에서 하는게 좋은가?
서비스를 구현하다보면 당연히 수정하는 로직들이 많이 들어가게 된다.
그럼 다음과 같은 상황일 경우 어떻게 구현할 것인가?
// Person Entity
public class Person {
@Id
private int id;
private String name;
private String address;
private int age;
}
// Person Service
public class PersonService {
private final PersonRepository personRepository;
@Transactional
public void updatePerson(int person_id, String name, String address, int age) {
Person person = personRepository.findbyId(person_id); // Optional로 예외처리해야 하지만 현재 로직에선 생략하겠습니다.
// 수정 로직
... // 해당 로직을 구현해보시오
}
}
서비스에 구현하기
일반적으로는 서비스의 구현 로직이니 서비스 클래스에 구현하려 할 것이다.
// Person Service
public class PersonService {
private final PersonRepository personRepository;
@Transactional
public void updatePerson(int person_id, String name, String address, int age) {
Person person = personRepository.findbyId(person_id); // Optional로 예외처리해야 하지만 현재 로직에선 생략하겠습니다.
person.setName(name);
person.setAddress(address);
person.setAge(age);
}
}
서비스 로직을 한곳에서 볼수 있어서 가독성이 좋고 명확하다고 생각할 수 있다.
Entity에 구현하기
데이터를 가지고 있는 해당 클래스에 직접 구현할 수 있다.
// Person Service
public class PersonService {
private final PersonRepository personRepository;
@Transactional
public void updatePerson(int person_id, String name, String address, int age) {
Person person = personRepository.findbyId(person_id); // Optional로 예외처리해야 하지만 현재 로직에선 생략하겠습니다.
person.updateInfo(name, address, age);
}
}
// Person Entity
public class Person {
@Id
private int id;
private String name;
private String address;
private int age;
public updateInfo(String name, String address, int age) {
this.name = name;
this.address = address;
this.age = age;
}
}
수정 로직을 Entity 클래스 내부에 두어 좀더 응집력 있도록 발전되었다.
어느 코드가 더 적절하다고 생각하는가?
예제만 보았을땐 두 코드가 차이가 없다고 볼수 있다.
하지만 시간이 지나면서 PersonService의 복잡성은 증가하고 연관된 서비스들도 늘어나게 되었다.
다음과 같은 요구사항을 추가해보자
미성년자는 나이를 변경할 수 없다.
성이 ‘정'일 경우, 주소를 변경할 수 없다.
사용자 정보를 수정하게 되면 이메일을 발송한다.
사용자 정보를 수정하게 되면 주문 서비스의 주문 정보도 수정해야 한다.
사용자 정보를 수정하게 되면 알림 서비스의 수신자 정보도 수정해야 한다.
주소 정보는 주소 서비스에서 조회하여 조회된 값으로 수정한다.
서비스 레이어에 모든 구현 로직을 구현하는데에는 한계가 있다.
추가 요구사항을 서비스 레이어에 다 구현할 경우 해당 메소드내의 코드 라인은 100줄 이상 될 수 있다.
지금 당장은 어떻게든 구현할 수 있지만 코드의 가독성은 떨어질 것이다.
가독성이 어떻게 안좋아질 수 있을까?
// Person Service
public class PersonService {
private final PersonRepository personRepository;
@Transactional
public void updatePerson(int person_id, String name, String address, int age) {
Person person = personRepository.findbyId(person_id); // Optional로 예외처리해야 하지만 현재 로직에선 생략하겠습니다.
person.setName(name);
person.setAge(age);
~~~~~
// 추가 요구사항 구현 (구현 로직 50라인 이상)
// - 미성년자는 나이를 변경할 수 없다.
// - 성이 ‘정'일 경우, 주소를 변경할 수 없다.
// - 사용자 정보를 수정하게 되면 이메일을 발송한다.
// - 사용자 정보를 수정하게 되면 주문 서비스의 주문 정보도 수정해야 한다.
// - 사용자 정보를 수정하게 되면 알림 서비스의 수신자 정보도 수정해야 한다.
~~~~~
searchParam param = createSearchParam(address);
String findAddress = addressService.findAddress(param);
person.setAddress(findAddress);
}
}
이와 같이 추가 요구사항이 들어가면서 주소값을 마지막에 바인딩 한다고 하면 person이라는 객체의 변경되는 필드들을 확인하기 위해서는 50라인 이상의 로직을 다 확인해바야 할것이다.
해당 메소드를 작성한이는 한번에 파악이 될 수 있지만 모든 소스 코드에는 정해진 주인은 없다.
협업하는 다른 팀원이 해당 메소드를 수정할 수 도 있고, 제거할 수도 있다.
또는 해당 메소드를 직접적으로 수정하지는 않지만 로직에서 사용하는 공통 클래스를 수정하여 사이드 이펙트가 발생할 수 있다.
그렇기 때문에 수정된 내역을 한눈에 볼수 있도록 응집력있는 코드를 작성하는 노력은 무엇보다 중요하다.
수정할 경우, Repository의 save 메소드는 주의해야 한다.
기존 데이터를 업데이트 할 경우에 간혹 새로운 객체를 생성하여 save하는 경우가 있다.
로직을 구현한 이는 Entity 객체에 ID를 부여했으니 기존 데이터에 수정된 필드들만 업데이트될거라 생각할 것이다.
이름을 수정하는 테스트 코드를 구현해보자
엔티티 생명주기를 이해해야 한다.
https://ultrakain.gitbooks.io/jpa/content/chapter3/chapter3.3.html
엔티티의 생명주기를 보면 새로운 객체는 비영속 상태를(new) 뜻한다.
그러면 Repository의 save 구현 로직을 살펴보자
예제 코드에서는 id가 존재하기 때문에 persist가 아닌 merge 메소드를 호출할 것이다.
merge를 호출 후, dirty check를 하여 update 쿼리가 수행된다고 생각할 수 있다.
save메소드에서 dirty check를 하는 기준은 영속 상태에 객체에 한해서 동작한다.
그렇기 때문에 예제의 사용된 객체(new Person())는 비영속 상태이기 때문에 dirty check를 하지 않는다.
dirty check를 하지 않기 때문에 저장하려는 객체를 그대로 데이터베이스에 반영 되므로 수정하려는 객체의 name 필드의 값이 그대로 저장하게 된다.
update 로직은 어디에서 하는게 좋은가?
그럼 다음과 같은 상황일 경우 어떻게 구현할 것인가?
서비스에 구현하기
일반적으로는 서비스의 구현 로직이니 서비스 클래스에 구현하려 할 것이다.
서비스 로직을 한곳에서 볼수 있어서 가독성이 좋고 명확하다고 생각할 수 있다.
Entity에 구현하기
데이터를 가지고 있는 해당 클래스에 직접 구현할 수 있다.
수정 로직을 Entity 클래스 내부에 두어 좀더 응집력 있도록 발전되었다.
어느 코드가 더 적절하다고 생각하는가?
다음과 같은 요구사항을 추가해보자
서비스 레이어에 모든 구현 로직을 구현하는데에는 한계가 있다.
가독성이 어떻게 안좋아질 수 있을까?