우리는 대게 Entity에 setter 사용을 지양해야 한다는 것을 알고 있습니다. 아직 spring에 대해 깊게 파악하지 못하고 OOP의 본질을 정확히 알지 못하는 저로서는 이 제한이 기능 구현에 꽤 답답함을 느끼게 했습니다. 그래서 이번 포스트에서는 setter 사용을 지양해야하는 이유를 알아보되, 구글링했을시에 나오는 뻔한 답변들과 좀 더 깊은 이유에 대해서 알아보고 제가 느꼈던 불편함들과 이에 대한 해결방안을 알아보도록 하겠습니다.
# Entity에 setter를 사용하지 않는 (뻔한) 이유
1. 의도를 파악하기 어렵다.
@Service
public class TempService {
public void update(Long id, String name){
Temp temp=tempRepository.findById(id).orElse(null);
temp.setName(name);
tempRepository.save(temp);
}
public void create(String name){
Temp temp=new Temp();
temp.setName(name);
tempRepository.save(temp);
}
}
해당 코드에서 method 이름을 가리고 봤을때 생성하는 기능인지 변경하는 기능인지 정확한 의도를 파악하기 힘듭니다.
2. 일관성을 파괴한다.
public으로 작성된 setter는 어디든 접근 가능하므로 해당 객체의 내부변수 값이 어디서 수정이 발생했는지 파악하기 힘듭니다.
여기까지가 구글링을 했을때 일반적으로 나오는 전형적인 답변입니다. 이런 답도 충분히 일리가 있지만 저는 좀 더 논리적인 근거가 필요했습니다. 이것만 보았을때는 크게 문제되지 않아보였기 때문입니다... 그러면 추가적인 근거를 더 알아봅시다.
# Entity에 setter를 사용하지 않는 (깊은) 이유
1. SOLID중 OCP를 위반하게 된다.
OCP(Open-Closed Principle)는 소프트웨어 entity는 확장에는 열려 있어야하고, 수정에는 닫혀 있어야 한다는 객체 지향 설계 원칙을 위반하게 됩니다.
setter를 사용한다면 엔티티의 변경이 필요할 때 외부에서 직접 상태를 변경하게 되며, entity의 내부 구현이 외부 요인에 의해 쉽게 변경될 수 있게 되어 OCP에 반합니다.
따라서 setter가 아닌 "행위"를 통해 상태를 변경하는 method를 제공하여 캡슐화해야합니다.
2. 객체의 불변성 보장
객체가 생성된 후 그 상태를 변경할 수 없게 만들면, 여러 부분에서 동시에 같은 객체를 사용할 때 발생할 수 있는 예기치 않은 부작용을 방지할 수 있습니다. 불변 객체는 멀티스레드 환경에서도 안전하게 사용될 수 있으며, 버그 발생 가능성을 줄여줍니다.
3. Layer간의 결합도 감소
setter를 무분별하게 사용하면, 데이터를 전달하는 데 있어서 각 레이어간의 결합도가 증가할 수 있습니다. 엔티티의 상태 변경을 제한된 메소드를 통해서만 가능하게 함으로써, 레이어간의 결합도를 낮출 수 있으며, 각 레이어의 책임을 명확히 할 수 있습니다.
# 불편했던 점
사실 기능 구현할때는 그리 불변함을 느낄 수 없었습니다. 오히려 setter를 사용하지 않고 entity 내부 method로 "행동"에 대한 기능을 추가하니 코드 가독성도 높아지고 entity 값이 무분별하게 변경되는 것을 막을 수 있어서 길게 보았을때 더 편리했습니다. 문제는 test code에서 발생했습니다.
@Entity
@Table
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@Builder
public Member(
String name,
String email) {
this.name = name;
this.email = email;
}
}
예를 다음과 같은 Member table이 있다고 합시다. 대부분 Mysql의 경우 @GeneratedValue(strategy = GererationType.IDENTITY)로 인조키 id를 사용합니다. 그렇기 때문에 생성자에 id를 인자로 받지 않습니다. 즉, 개발자는 일반적인 방법으로는 Member 객체의 id를 지정해줄 수 없다는 겁니다. 이 특성이 Mockito를 사용한 테스트 코드에서 문제를 일으킵니다. 이것만 보고는 문제가 없어보이지만 Member의 id가 다른 entity의 외래키로 사용될 경우 문제가 됩니다.
코드로 예시를 들기가 쉽지 않아 시나리오 설명하자면, Board라는 entity가 Member id를 외래키로 사용하며 sevice layer Mockito test code를 작성한다고 합시다. 이에 대한 테스트 코드를 짜려면 임시 Member 객체를 하나 만들고 Board 객체를 만들어서 해당 service method가 잘 동작하는지 작성해야합니다. 하지만 Member의 id값이 없기 때문에 Board 객체를 생성할 수 없어 에러가 발생합니다. 직접 repository에 저장하지 않기 때문에 @GeneratedValue(strategy = GererationType.IDENTITY)로 설정된 id는 계속 null로 남게 됩니다.
이렇게 외래키가 하나만 연결되어 있는경우도 있지만 계속해서 꼬리를 무는 식으로 DB가 설계되어 있다면 id가 null임을 항상 생각해야 했습니다.
# 해결방안
위에서 서술한 불편한 점들을 해결하는 가장 쉬운 방법은 ReflectionUtils 클래스를 사용하여 필드 값을 설정해주는 방법입니다.
class BookTest {
@Test
void testSetIdWithReflection() throws Exception {
// Book 엔티티 생성
Book book = new Book("Reflection in Action");
// 리플렉션을 사용하여 private ID 필드에 접근
Field idField = Book.class.getDeclaredField("id");
// private 필드 접근을 가능하게 함
ReflectionUtils.makeAccessible(idField);
// ID 필드에 값을 설정
idField.set(book, 1L);
// 검증: ID 필드 값 검증
assertEquals(1L, book.getId());
}
}
리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성할 수 있도록 지원하고, 인스턴스의 필드와 메소드를 접근 제어자와 상관 없이 사용할 수 있도록 지원하는 API입니다. 하지만 직접적인 코드 접근에 비해 속도가 느리고 타입 미검사, 보안 취약성 문제로 사용이 지양되긴 합니다. 하지만 테스트 코드에는 크게 문제되지 않아보입니다.
'Back-end > Spring 기초개념' 카테고리의 다른 글
JPA 성능 최적화 (0) | 2024.04.28 |
---|---|
JPA와 hibernate / Spring data JPA (0) | 2023.05.05 |
Instagram-clone 분석 (3) Controller (0) | 2023.05.05 |
Instagram-clone 분석 (3) DTO (0) | 2023.04.23 |
Instagram-clone 분석 (2) entity (3) | 2023.04.22 |