지하철 경로 조회 미션 전체 피드백 강의 정리

디우·2022년 6월 25일
0

우아한테크코스 레벨2 기간에 학습한 지하철 경로 조회 미션 전체 피드백 강의 내용을 정리한다.


질문(Q & A)

Q. Service 객체가 다른 Service를 의존하는 것과 Repository를 의존하는 것은 어떻게 다를까요?
Service끼리 의존하면 순환 참조가 발생할 수도 있을 것 같아 Service가 가지는 비즈니스 로직이 필요한 게 아닌 단순 CRUD가 필요한 것이면 Repository를 의존하도록 했는데요..

A. (내 생각) 질문에서 나온 것처럼 어떠한 비즈니스 로직이 필요한 게 아닌 단순 CRUD 인 경우에는 Repository를 의존해도 좋다고 생각한다. 하지만 비즈니스 로직이 필요하고, 복잡하다면 중복된 로직을 여러번 작성하는 것 보다 Service를 의존하는게 낫다고 생각한다. 또한 Service를 빈으로 등록한 경우 순환 참조 발생시 서버 부팅 자체가 안되므로 이 경우에는 Redesign, 즉 설계를 다시 해보는 것이 좋은 방법이라고 생각한다.
정답이 정해진 문제는 아니라고 생각한다!


미션 피드백

Request 처리

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 는 생략가능하다.

HTTP 요청 파라미터 - @ModelAttribute

실제 개발을 하면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야한다.
이를 자동화 해주는 기능이 @ModelAttribute이다.

  • 해당 객체를 생성한다.
  • 요청 파라미터의 이름으로 해당 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)한다.
    ex) username 이면 setUsername() 호출
    [프로퍼티] : 객체에 setter와 getter를 가지면 해당 필드는 프로퍼티를 가지고 있다. 변경은 Setter 호출이고, 조회는 Getter 호출
  • 바인딩 오류 : 예를 들어 숫자가 들어가야 할 곳에 문자를 넣으면 BindException이 발생한다.
  • ModelAttribute는 생략가능하다. : 그런데 @RequestMapping도 생략할 수 있으니 혼란이 발생할 수 있는데, 단순 타입은 @RequestParam, 나머지는 @ModelAttribute라고 생각하면 된다.

하지만 나는 @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에 테스트할 데이터를 넣고 시작해줄 수 있지만, 테스트 코드 자체만으로는 어떤 데이터가 있는지 확인하기 어렵다.
    -> Test Fixture를 이용해 public static final로 선언된 상수들 혹은 메소드들을 활용할 수 있을 것이다. 이로써 중복되는 코드를 제거할 수 있다.

변경에 유연한 코드

서비스에서 라이브러리에 직접 의존하는 예시 상황

@Service
public class PathService {
	
    public PathResponse getPath(Long from, Long to) {
    	List<Section> allSections = sectionRepository.findAll();
        PathFinder pathFinder = JGraphPathFinder.of(allSections);
        
        ...
    }
}

예상되는 문제점은?

  • 최단 경로 조회 구현체가 바뀌면 서비스 계층에도 변경의 영향이 있다.
  • 서비스는 어떤 알고리즘을 사용하든지 무관하다.

외부 라이브러리에 의존하지 않기 위해서는?

  • 분리해준다.
  • PathFactory 인터페이스를 두고 이에 대한 구현체를 둔다. 그리고 구현체를 주입하는 방법을 사용한다.

Q. 프로덕션 코드에 새로운 기능을 추가하는 것에는 크게 어려움이 없었지만, 새로운 필드를 추가함으로써 기존에 객체를 생성하던 코드가 깨지는 문제가 있었습니다. 모든 new 키워드를 사용하던 곳에 변경이 필요했습니다. 좋은 방법이 있을까요?

A. 요구사항 변경시 발생하는 코드의 수정은 어쩔 수 없지만, 변경의 부분을 최소화하는 방법은 찾아볼 수 있을 것이다. 예를 들어 이런 경우, change signature, "빌더 패턴"

profile
꾸준함에서 의미를 찾자!

0개의 댓글