우아한테크코스 레벨2 기간에 학습한 지하철 경로 조회 미션 전체 피드백 강의 내용을 정리한다.
Q. Service 객체가 다른 Service를 의존하는 것과 Repository를 의존하는 것은 어떻게 다를까요?
Service끼리 의존하면 순환 참조가 발생할 수도 있을 것 같아 Service가 가지는 비즈니스 로직이 필요한 게 아닌 단순 CRUD가 필요한 것이면 Repository를 의존하도록 했는데요..
A. (내 생각) 질문에서 나온 것처럼 어떠한 비즈니스 로직이 필요한 게 아닌 단순 CRUD 인 경우에는 Repository를 의존해도 좋다고 생각한다. 하지만 비즈니스 로직이 필요하고, 복잡하다면 중복된 로직을 여러번 작성하는 것 보다 Service를 의존하는게 낫다고 생각한다. 또한 Service를 빈으로 등록한 경우 순환 참조 발생시 서버 부팅 자체가 안되므로 이 경우에는 Redesign, 즉 설계를 다시 해보는 것이 좋은 방법이라고 생각한다.
정답이 정해진 문제는 아니라고 생각한다!
1단계 경로 조회
@RequestMapping("/paths")
@RestController
public class PathController {
@GetMapping
public ResponseEntity<PathResponse> searchPath(@RequestParam Long source, @RequestParam Long target) {
...
}
}
2단계 경로 조회
@RequestMapping("/paths")
@RestController
public class PathController {
@GetMapping
public ResponseEntity<PathResponse> searchPath(@RequestParam Long source, @RequestParam Long target, @RequestParam int age) {
...
}
}
이렇게 필드가 추가될 때 마다 추가한 필드를 사용하는 메소드에 인자를 추가하는 것은 불필요한 변경점이 많아지는 문제가 있다. 이럴 때에는 객체(DTO)로 매핑해서 @ModelAttribute
를 사용하여 변경점을 DTO 클래스로 좁힐 수 있다.
@RequestMapping("/paths")
@RestController
public class PathController {
@GetMapping
public ResponseEntity<PathResponse> searchPath(@ModelAttribute PathRequest request) {
...
}
}
이런 @ModelAttribute
는 생략가능하다.
실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야한다.
이를 자동화 해주는 기능이 @ModelAttribute
이다.
username
이면 setUsername()
호출BindException
이 발생한다.하지만 나는 @ModelAttribute
를 붙여준 DTO 에 대해서 setter를 열어준 적이 없던 것으로 기억한다. 그럼 어떻게 스프링은 이를 역직렬화 해줄 수 있었을까?
원칙적으로는 빈생성자 + setter메소드가 필요하다. 빈생성자를 호출한뒤 setter 메소드들을 호출해서 데이터를 바인딩해줘야 한다. 하지만 자바 빈즈 패턴의 많은 단점 (final 불가, setter 등)으로 모든 필드를 할당하는 생성자가 있다면 setter 없이도 역직렬화를 할 수 있다. (단, 빈 생성자가 존재하면 안된다.)
정리하면 @ModelAttribute
는 빈(기본)생성자 + setter를 만들거나 모든 필드를 받는 생성자 하나만을 만들면 스프링이 직접 역직렬화 해준다.
(@RequestBody
는 내장된 Jackson
라이브러리를 통해서 리플렉션을 사용해 역직렬화를 하며 빈(기본)생성자 + getter 나 setter 등 property(필드명)을 알 수 있는 메소드를 통해서 만든다.)
참고 : 우테코 4기 크루 오찌의 글
DTO에서 Java Bean Validation
을 이용해 검증하는 것이 옳다고 생각한다.
그럼 동일한 검증 로직은 도메인에서 제거해도 될까?
나의 생각은 다음과 같다.
DTO에서의 검증과 도메인에서의 검증은 그 목적
이 다르다고 생각한다. (비록 그 검증이 동일한 검증이여도 말이다..)
Request DTO에서 하는 검증은 사용자 입력에 대한 검증이다. 예를 들어 사용자가 입력한 값을 토대로 도메인 객체를 만든다면 DTO에서 이루어진 검증을 통해서 우리는 신뢰할 수 있는 도메인 객체를 생성할 수 있다. 하지만 만약 사용자 요청과는 별개로 애플리케이션 내에서(Service계층에서) 도메인을 생성한다면 어떻게 될까? 도메인에서는 검증 절차가 없기 때문에 신뢰할 수 없는 도메인 객체가 생성된다.
따라서 비록 검증 과정이 동일하더라도 DTO와 도메인 모두에서 검증을 하는 것이 옳다고 생각한다.
문제가 생길 가능성이 있는 경계 조건
을 생각해보고 그 부분을 집중적으로 테스트하자.
예를 들면 10~50km : 5km 마다 100 원 추가, 50 초과 : 8km마다 100원 추가 인 경우 경계 값들만을 테스트하는 것이다. (이 때, @ParameterizedTest
방법을 고려할 수 있다.)
@Sql
를 활용해서 DB에 테스트할 데이터를 넣고 시작해줄 수 있지만, 테스트 코드 자체만으로는 어떤 데이터가 있는지 확인하기 어렵다.public static final
로 선언된 상수들 혹은 메소드들을 활용할 수 있을 것이다. 이로써 중복되는 코드를 제거할 수 있다.서비스에서 라이브러리에 직접 의존하는 예시 상황
@Service
public class PathService {
public PathResponse getPath(Long from, Long to) {
List<Section> allSections = sectionRepository.findAll();
PathFinder pathFinder = JGraphPathFinder.of(allSections);
...
}
}
예상되는 문제점은?
외부 라이브러리에 의존하지 않기 위해서는?
Q. 프로덕션 코드에 새로운 기능을 추가하는 것에는 크게 어려움이 없었지만, 새로운 필드를 추가함으로써 기존에 객체를 생성하던 코드가 깨지는 문제가 있었습니다. 모든 new 키워드를 사용하던 곳에 변경이 필요했습니다. 좋은 방법이 있을까요?
A. 요구사항 변경시 발생하는 코드의 수정은 어쩔 수 없지만, 변경의 부분을 최소화하는 방법은 찾아볼 수 있을 것이다. 예를 들어 이런 경우, change signature
, "빌더 패턴"