기존 프로젝트를 할 때, 스프링과 비슷하게 "객체지향" 에 대해서도 깊은 고민 없이 자바를 사용했었다.
돌아보면 도메인 객체를 사용하지 않고 그저 영속성 entity를 가지고 개발을 했던 것 같다.
이번 9단계 요구사항을 반영하기 위해 코드를 볼아보면서, 기껏 열심히 레벨 1동안 배운 내용들을 써먹지 못하고 옛 프로젝트를 할 때처럼 사용한 것 같아서 아쉬웠다.
따라서 이 프로젝트에서의 엔티티 객체들을 어떻게 바라보아야 할까 고민하다가 혼란이 와버렸다.
public class Reservation {
private final Long id;
private final String name;
private final ReservationDate reservationDate;
private final ReservationTime reservationTime;
public Reservation(final Long id, final String name, final LocalDate date, final Long timeId, final LocalTime time) {
this.id = id;
this.name = name;
this.date = new ReservationDate(date);
this.reservationTime = new ReservationTime(timeId, time);
}
}
영속성 entity 와 도메인 entity
두 entity의 형태가 일치할 수도 있지만, 아닐 수도 있다.
내가 생각하는 영속성 entity 와 도메인 entity는 다음과 같다.
영속성 entity
- DB 테이블와 매핑되는 컬럼 값들을 가진 개체
- 테이블과 1대1로 매핑되어야 한다.
- DB단의 규칙인 기본키를 가지고 있다.
도메인 entity
- 비즈니스 로직을 가지고 있는 비즈니스 객체
- 테이블과 1대1 매핑되지 않아도 된다.
내가 생각하는 두 entity의 핵심 차이점
- 영속성 엔티티는 id와 같은 식별자를 가지고 매핑 용도로 사용되지만 도메인 엔티티는 단순 매핑 용도라기 보다는 "역할"을 가져야 한다.
🤔 혼란스러웠던 부분
현재 "DAO인 JdbcRepository 관점" 에서는 저장하는 객체인Reservation가 Domain 객체가 아닌 영속성 Entity여야 한다.
그런데, Reservation은 비즈니스 로직을 지닐 수 있을 뿐만 아니라 Domain 모델인 ReservationDate, ReservationTime(VO)를 가지고 있는데, 이를 영속성 Entity로 볼 수 있는가? 하는 의문이 들었다.
또한 "비즈니스 플로우 책임을 가진 서비스 관점"에서는 DAO로부터 받은 영속성 Entity를 Domain 객체로 변환하여 로직을 처리하게 하고 응답해야 한다.
Reservation이 Entity가 아니라면 Domain 객체로 봐야하는가?
Reservation은 식별자 Id 필드를 가지고 있는데 이는 DB 단의 규칙이지, Domain 객체가 가져야 하는 Domain 규칙이라고는 생각이 들지 않는다.
결국 "현재의 Reservation 객체가 Domain 모델인지, Entity 모델인지, 둘 다 맞는지, 둘 다 아니라면 어떤 것인지"하는 의문이 들었다.
🔥 분리 시작
이런 혼란을 느끼면서 그냥 냅다 분리를 해보는 것이 낫을 것 같다는 생각이 들었다.(분리해보고 괜춘하면 쓰고 아님 말고 ㅋ)
각 entity 객체 생성
분리를 위해 각각에 대한 엔티티를 생성하였다.
public class ReservationEntity {
private final Long id;
private final String name;
private final LocalDate date;
private final Long timeId;
// ...
}
public class ReservationDomain {
private final ReservationName name;
private final ReservationDate date;
private final ReservationTimeDomain reservationTime;
// ...
}
주요 차이점은 ReservationEntity에는 데이터 영속을 위한 id라는 임의의 식별자가 있다는 것과,
ReservationDomain에는 예약이라는 비즈니스 객체가 가져야할 비즈니스 규칙이 있다는 것이었다.
주요 로직 변경
각 객체를 생성한 후, 로직을 변경해주었다.
1. Persistence Layer 로직 변경
조회 기능을 위주로 변경 예시를 확인해보자. 먼저 Persistence Layer의 조회 변경 사항은 다음과 같았다.
ReservationEntity 정보와, 그 ReservationEntity가 가지고 있는 ReservationTimeEntity까지 조회해야 했기 때문에 Map을 통해 mapper를 다음과 각각 만들어 같이 변경해주었다.
[분리 전 코드]
private final RowMapper<Reservation> reservationRowMapper =
(resultSet, rowNum) -> new Reservation(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getDate("date").toLocalDate(),
resultSet.getLong("time_id"),
resultSet.getTime("start_at").toLocalTime()
);
@Override
public List<Reservation> findAll() {
String sql = """
select r.id, r.name, r.date, t.id as time_id, t.start_at
from reservation as r
inner join reservation_time as t
on r.time_id = t.id
""";
return jdbcTemplate.query(sql, reservationRowMapper);
}
[분리한 코드]
// ReservationRepository.java
@Override
public Map<ReservationEntity, ReservationTimeEntity> findAll() {
String sql = """
select r.id, r.name, r.date, t.id as time_id, t.start_at
from reservation as r
inner join reservation_time as t
on r.time_id = t.id
""";
Map<ReservationEntity, ReservationTimeEntity> result = new HashMap<>();
jdbcTemplate.query(sql, (resultSet, rowNum) -> {
ReservationEntity reservation = new ReservationEntity(
resultSet.getLong("id"),
resultSet.getString("name"),
resultSet.getDate("date").toLocalDate(),
resultSet.getLong("time_id")
);
ReservationTimeEntity time = new ReservationTimeEntity(
resultSet.getLong("time_id"),
resultSet.getTime("start_at").toLocalTime()
);
result.put(reservation, time);
return result.entrySet().stream().iterator().next();
});
return result;
}
각 Entity 별 RowMapper 를 선언해주고 join 데이터를 가지고 오기 위한 추가 코드가 필요하다.
(Map을 사용하지 않고 따로 ReservationEntity와 ReservationEntity를 가지고 있는 DTO를 만들 수도 있다. 단, 해당 DTO를 관리해줘야 하는 비용 발생.)
2. Application Layer 로직 변경
// ReservationService.java
public Long createReservation(final CreateReservationRequest createReservationRequest) {
ReservationTimeEntity reservationTimeEntity = reservationTimeRepository.fetchById(
createReservationRequest.timeId());
ReservationEntity reservationEntity = ReservationEntity.of(
createReservationRequest.toDomain(reservationTimeEntity.toDomain()), createReservationRequest.timeId());
return reservationRepository.save(reservationEntity);
}
Service에서는 전달받은 Entity를 Domain으로 변환하여 반환하도록 하였다.
비슷하게 현재는 도메인 객체가 가지는 책임이 크게 없었기 때문에, 분리의 장점은 크게 느끼지 못했다.
결론
직접 구분을 하면서 느낀 점은, 도메인 엔티티의 역할이 크지 않는 현재 프로젝트에서 두 엔티티를 구분을 하여 얻을 수 있는 장점이 크지 않다는 것이다.
분리를 하면서 Dao 단에서 사용이 많이 어색해지는 부분들(join한 객체를 가지고 올 경우 등)이 많아졌다.
결국 분리를 하지 않고, Reservation 객체를 "Domain 객체이지만, entity의 특징도 가지고 있는 객체로 보는 것이 어떤가" 하는 생각이 들었다.
결국 백엔드 개발자의 관점에서 보았을 때, 자바 애플리케이션에서 하고 싶은 것은 비즈니스 로직을 처리하고 싶은 것이라고 생각했다.
따라서 당장은 다뤄야 하는 객체가 Domain 객체이지만 DB에게 데이터를 불러와야 했기 때문에 어쩔 수 없이 그 속성이 조금 추가된 것이라고 여기자 라는 결론을 내렸다.
~~~~아무튼 지금은 내 결론으로 넘어가고 레벨 더 진행하면서 DDD도 추가로 공부해봐야겠다~~~~~
'우아한테크코스 6기 > 2단계' 카테고리의 다른 글
JPQL new 연산은 지양해야 할까? JPQL은 어떻게 동작할까? (0) | 2024.06.04 |
---|---|
[방탈출 사용자 예약] @Bean, @Component, 그리고 POJO (0) | 2024.05.14 |
[방탈출 예약 관리] 템플릿 엔진과 @RestController (0) | 2024.05.12 |
[방탈출 예약 관리] @DiritesContext, 알고 쓰자 (0) | 2024.04.27 |
[레벨2] 1주차 회고 (0) | 2024.04.27 |