이번 미션을 하면서 한 실수 중 가장 큰 것은 도메인 로직에 집중하지 않았던 것이다. 1, 2 단계에서 도메인 로직이 크게 존재하지 않아서 스프링쪽 코드에 집중을 했다. 그러다가 3단계를 시작하다보니 이번에도 API 문서를 가장 먼저 켜고 컨트롤러 부분을 먼저 만들기 시작했다. 그렇게 시작하니 필요한 로직들이 거의 서비스 계층에 다 들어가는 꼴이 되어 그제서야 급하게 도메인에 로직들을 넣기 시작했다.
결국 도메인에 대한 고민 없이 시작을 하니 나중에 이리저리 상황에 따라 코드를 변경해야하고 로직이 복잡해졌다. 또 후반부에 급하게 도메인 개발을 하다보니 레벨1에서 잘 적용했던 일급컬렉션 같은 것들도 놓치고 구현만 하기에 급급했다.
여러 개발 방법이 있겠지만 다음에는 도메인 로직을 먼저 고민해보고 코드를 작성해보자.
Line
객체끼리 비교할일이 있어 Equals 와 HashCode를 재정의 해줘야할 일이 생겼다.
이때 어떤 것들 기준으로 재정의할지 처음에는 큰 고민이 없었다. db에 pk가 id이니 id로 재정의하면 될 것이라 생각했다.
하지만 어썸오랑 얘기를 나누다보니 다른 생각도 해보게 되었다. 처음에 request로 dto로서 들어올 때는 db에 안들어간 상태이니 id가 null이다. 그래서 id가 아닌 중복이 안되는 name이나 color 혹은 둘다를 기준으로 eq/hc를 재정의해줘야 하는게 아닌가 하는 생각도 들었다.
하지만 최종적으로 생각하게 된 것은 크게 두 가지다.
db에서 pk 선정 기준과 비슷한 것 같다. 변경 가능한 비즈니스 로직으로부터 자유로워야하고, 말 그대로 식별자(identifier)의 역할을 할 수 있어야 한다. 그래서 최종적으로 id만 이용해서 eq/hc를 재정의 했다.
이펙티브 자바 스터디를 하며 아이템 72. 표준 예외를 사용하라 아이템 발표를 맡았었다. 당시에 책 내용에도 공감이 갔었고 실제로 레벨1 미션을 하면서 커스텀 예외를 사용할 이유가 크게 없다고 느꼈다.
하지만 이번 미션을 하면서 커스텀 예외를 사용할 필요성을 느꼈다. 예를 들어 Line
을 DB에서 조회하는데 존재하지 않거나 Station
을 조회하는데 존재하지 않으면 두 경우 모두 DB에서 EmptyResultException이 발생한다. 하지만 이 예외만 보고서 구체적으로 무엇을 조회하다가 예외가 터졌는지, 어떤 id 조회를 했는데 예외가 발생했는지 알기 어렵다.
LineNotFoundException
public class LineNotFoundException extends RuntimeException {
public LineNotFoundException(Long id) {
super(String.format("ID: %d 라인을 찾는데 실패하였습니다.", id));
}
}
이렇게 커스텀 예외를 던지고 서비스 예외에서 받으니 어디서 예외를 터졌는지 원인을 파악하기 쉬웠다.
LienDao
public Optional<LineEntity> findById(Long id) {
String sql = "select id, name, color, down_station_id, up_station_id, distance from line where id = (?)";
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, lineRowMapper, id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
LineService
public LineResponse findById(Long id) {
LineEntity lineEntity = lineDao.findById(id).orElseThrow(() -> new LineNotFoundException(id));
List<StationResponse> stations = extractUniqueStationsFromSections(lineEntity);
return new LineResponse(lineEntity.getId(), lineEntity.getName(), lineEntity.getColor(), stations);
}
id로 db에서 조회했을 때 결과는 있을 수도 없을 수도 있다. 그래서 dao의 findById
를 이용하는 측에 옵셔널을 이용해서 결과를 반환하게 하는 것이 낫다고 생각했다. 이렇게 될 경우 장점은 서비스 계층에서 적절하게 비즈니스 옵션을 넣을 수 있다. 옵셔널의 결과에 따라 커스텀 예외를 반환할 수가 있는 것이다. 코드에서도 .orElseThrow()
를 이용하고 있다. 혹은 예외를 발생시키지 않을 수도 있을 것이다.
다만 다른 크루랑 이야기를 하다가 생각이 든 부분은 과연 dao에서 try-catch
를 하는게 적절한가이다. EmptyResultDataAccessException
은 사실 DB 자체의 에러라기 보다는 비즈니스 로직에 가까울 수 있기 때문이다. 비즈니스 로직을 dao에서 처리하는 것은 아닌가 하는 생각도 남아있다.
얼핏 주워들은 것으로는 나중에 JPA를 사용하면 객체를 객체 그대로 저장할 수 있다고 들었지만 지금은 객체의 필드 값들을 따로 DB에 primitive type들로 저장하고 있다.
그래서 도메인과 엔티티의 간극이 느껴졌다. 예를 들어 도메인에서 Section의 경우 upStation과 downStation을 Station 객체로 가지는데 DB에는 upStationId와 downStationId로 저장을 하기 때문이다.
네오에게 이 질문을 했고 지금은 두 경우 다 생각해보고, 직접 해보며 장단점을 느끼는게 좋다라는 조언을 받았다. 나는 Section이 downStation과 upStation을 가지는 것이 좀더 객체지향적으로 코드를 짤 수 있을 것이라 생각하고 도메인과 엔티티를 분리했다.
결과적으로 dto, domain, entity 세 개를 분리함으로서 서로간에 전환해주는데 코드가 장황하고 길어졌다. 또한 Section 도메인을 하나 만들기 위해 upStation도 조회해서 도메인으로 만들어서 넣어주고 downStation도 조회해서 도메인으로 만들어서 넣어주는 작업을 해야해서 귀찮은 부분이 있었다.
지금은 station에 큰 비즈니스 로직이 없지만 나중에 비즈니스 로직이 복잡해진다면 그때 장점이 있을 것 같다. 하지만 또 반대로 만약에 객체 안에 객체 안에 객체 이런 구조가 된다면 지금의 방식으로는 코드를 작성하는 비용이 기하급수적으로 커질 것 같다는 생각도 했다.
AcceptanceTest
LineControllerTest
이렇게 AcceptanceTest에서 어노테이션들과 port, 의존성 등을 정의해놓고 테스트하고자 하는 테스트에서 AcceptanceTest를 상속하고 있다. 이러면 AcceptanceTest에 정의된 속성들을 그대로 쓸 수 있다.
필드가 많아지며 실제로 생성자를 경우의 수에 따라 다양하게 만들어야했다. 또한 Line에서 name과 color 필드를 반대로 넣어서 한참동안 예외를 찾는데 시간을 쓰기도 했다.
그래서 이펙티브 자바 스터디를 할 때 봤던 빌더 패턴이 생각났다. 어노테이션을 붙이면 쉽게 빌더 패턴을 사용할 수 있다는 것을 알았지만 한번쯤은 직접 작성해서 써보자하고 미뤄두다가 결국 다른 것들을 하느라 적용하지 못했다.