우아한테크코스 레벨2 기간에 학습한 체스 미션 전체 피드백 강의 내용을 정리한다.
pathvariable의 이름과 파라미터의 이름이 같으면 생략해도 된다.
@RestController
public class ChessController {
@GetMapping("/game/{id}")
public String room(@PathVariable(value = "id") Long id) {
...
}
}
Spring 공식 사이트의 설명에 따르면, URI 변수가 자동으로 적절한 유형으로 변환되거나 TypeMismatchException
이 발생할 수 있는데, 기본 타입(int, long, Date 등)은 기본적으로 자동으로 변환되는 것이 지원이 되며 다른 모든 데이터 유형에 대해서 등록할 수 있다고 한다. 즉, 기본 타입에 대해서는 pathVariable과 같은 URI 변수가 자동으로 적절한 타입으로 변환이 되고 다른 유형에 대해서는 등록할 수 있다는 의미이다.
또한 명시적으로 URI 변수 이름을 지정할 수도 있지만 위의 예시 코드와 같이 이름이 같고 코드가 디버깅 정보를 준수하거나 Java8에서 -parameters
컴파일러 플래그를 사용하는 경우에는 생략가능하다고 이야기하고 있다.
you can leave that detail out if the names are the same
ResponseBody를 포함하는 RestController를 사용함으로써 ResponseBody 어노테이션의 중복 사용을 제거할 수 있다.
ResponseBody 어노테이션은 자바객체를 HTTP 응답 바디로 변환해주는 역할을 한다.
즉, 뷰 페이지가 아닌 객체자체를 HTTP 응답 바디에 담아 반환하겠다는 의미이다.
@Controller
@RequestMapping("/game")
public class SpringWebChessController {
@ResponseBody
@PostMapping("/start")
public ChessGameDto start() {
...
}
@ResponseBody
@PostMapping("/emd")
public ChessGameDto end() {
...
}
@ResponseBody
@GetMapping("/status")
public ChessGameDto status() {
...
}
}
스프링 MVC는 어노테이션 기반의 프로그래밍 모델
@Controller
와@RestController
을 제공하며 해당 어노테이션을 통해서 요청 매핑, 요청 입력, 예외 처리 등을 표현할 수 있다.
어노테이션 기반의 컨트롤러는 유연한 메소드 시그니처를 가지며 기본 클래스를 확장하거나 특정 인터페이스를 구현할 필요가 없다. 예시는 어노테이션 기반으로 정의된 컨트롤러이다.
@RestController
는 자체@Controller
와@ResponseBody
로 메타 주석화된 어노테이션으로 구성되어 있으며, 모든 메소드가 type-level의 @ResponseBody 어노테이션을 상속받아 직접 response body를 쓰겠다는 것이다. (HTML 템플릿으로 뷰를 생성하고 렌더링을 하는 것이 아니라..)
결론적으로 RestController는 @RestController = @Controller + @ResponseBody 와 같다고 할 수 있다.
ResponseEntity는 제네릭으로 정의되어 있다. 타입 안정성등의 이유로 ResponseEntity에 대해 로타입을 사용하지 말자!
@RestController
public class WebController {
@PostMapping("/game")
public ResponseEntity insertGame() {
final Long gameId = chessService.insertGame();
return ResponseEntity.ok().body(new GameDto(gameId));
}
}
만약 ResponseEntity에 아무것도 담지 않고, 아무것도 반환하지 않고 상태코드만 반환하고 싶다면 Void
를 사용하자!
행위는 HTTP 메소드로 지정하자.
행위는 URL이 아닌 HTTP Method로 표현하자.
(자원을 path로, 행위를 mehtod로)
@RestController
@RequestMapping("/game")
public class ChessGameController {
@DeleteMapping("/{id}")
public ResponseEntity<StatusDto> deleteGame(@PathVariable Long id) {
final StatusDto status = chessGameService.deleteGame(id):
return ResponseEntity.ok(status);
}
}
@Controller
public class ChessController {
@GetMapping("/new-board/{id}")
public String initBoard(@PathVariable Long id) {
chessService.initGame(id);
return "redirect:../board/" + id;
}
}
위와 같은 코드의 경우 새로운 "체스 게임"을 생성하는 경우이다. 이 경우 리소스 조회
에 목적이 있는 HTTP Method인 GET
으로 매핑하는 것은 바람직 하지 않다.
새로운 것을 생성하는 경우에는 POST 메소드가 더 적절하다. (POST 메소드는 주로 신규 리소스를 등록할 때 사용한다.)
@RestController
public class GameController {
@PostMapping
public ResponseEntity<GameDto> createGame(@RequestBody CreateGameDto createGameDto) {
...
return ResponseEntity.ok().body(gameDto):
}
}
위와 같이 새로운 리소스를 생성한 경우 200 OK
상태 코드 보다는 요청이 성공해서 새로운 리소스가 생성되었음
을 의미하는 201 Created
가 더 적절하다.
구제척인, 정해진 설계 방법은 없다고 이야기하는 것이 옳다.
그럼 많이 쓰이는 설계는 무엇일가? -> Resource
에 집중하는 설계이다.
URL은 자원의 위치를 나타내 주는 것이고, URI는 자원의 식별자를 나타낸다.
즉 하나의 URI는 하나의 리소스만 식별한다.
보다 자세한 내용은 요청과 응답의 약속, HTTP를 참고할 수 있다.
예를 들어 "stations/1" 은 "1번 station"을 의미한다.
체스 미션에서는 체스 게임(ChessGame)
등이 리소스에 해당할 수 있다.
우리는 리소스를 잘 표현하기 위해서 Restful 한 API를 설계할 수 있다.
(하지만 개인적으로 너무 Restful한 설계를 유지하려고 노력하지 않아도 된다고 생각한다. 또한 현실적으로 Restful 규칙을 모두 지키지란 어렵다고도 본다. 물론 아래 그림과 같은 정도의 HTTP Method 및 API 는 어느정도 지킬 수 있다고 본다.)
참고 : RESTful API란?
예를 들어 GET Method로 "/todos"를 요청
한다면 그에 대한 기능은 List all todos
이고, POST 로 동일한 URI로 요청시에는 Create a new todo
이다 와 같은 것들이다.
위의 그림에서 설명하고 있는 것과 같이, 이상적으로 서버에 리소스가 생성되는 경우 응답은 HTTP 응답 코드 201이어야 하며 새 리소스를 참조하는 엔티티와 Location 헤더를 포함해야 한다.
하지만 POST메소드에서 수행되는 작업이 URI로 식별할 수 있는 리소스를 생성하지 못할 수 있고, 이 경우에는 200 이나 204 상태 코드를 응답하기도 한다.
즉 POST 라고 해서 무조건 201 Created
상태 코드를 반환해줘야지와 같은 생각은 너무 안일한 생각이라는 것이다.
앞서 언급한 것과 같이 Restful하다
를 만족하면서 설계를 하기에는 현실적으로 어려움이 있을 수 있다 하지만 다음과 같은 사항은 잘 들어나고 잘 지켜져야 한다고 본다.
@Service
public class ChessService {
public MoveResultDto getMoveResult(...) {
...
return new MoveResultDto(200, "", chessGame.isFInished());
}
}
본인이 생각하는 Controller 및 Service의 책임은 다음과 같다.
장바구니 미션 피드백 중 코멘트
간단하게, Controller는 사용자로부터 요청을 받고, 이에 대한 처리를 한 후(직접 하는 것이 아니라 Service 계층으로 위임), 응답한다.
즉, Controller는 "요청을 받는다." 와 "응답을 보낸다."에 초점이 맞춰져 있다고 생각한다.
그리고 Service는 트랜잭션 단위의 비즈니스 흐름에 대한 책임을 가진다고 생각한다.
이러한 관점에서 보면 Service에서 응답
과 관련된 상태 코드
에 대해서 알 필요는 전혀 없다. 이는 Controller
의 역할이자 책임이다.
@Repository
public class ChessBoardDao {
public ChessGameDto load() {
final String gameInfoSql = "SELECT * FROM gameInfos";
final String piecesSql = "SELECT * FROM pieces ORDER BY x ASC, y ASC";
List<PieceDto> pieces = jdbcTemplate.query(picesSql, pieceRowMapper);
GameInfoDto gameInfo = jdbcTemplate.queryForObject(gameInfoSql, gameInfoRowMapper);
return new ChessGameDto(pieces, gameInfo);
}
}
이렇게 하나의 메소드에서 여러 쿼리를 호출하는 경우 책임이 여러개가 아닌지 고민해볼 필요가 있다. 그리고 이 경우 해당 메소드가 실패할 때 어느 쿼리에서 실패했는지를 찾기도 쉽지 않을 것이다.
그렇다면 DB에서 조회한 엔티티를 어디에 매핑해야할까? DTO일까? 도메인 객체일까?
우선 도메인에서 DTO의 존재를 알고 DTO를 생성하는 것 보다는 DTO에서 도메인을 알고 생성하는 것이 낫다고 생각한다. 도메인에서는 DTO의 존재 여부에 대해서 알 필요가 없고, 비즈니스 로직과 관련된 역할과 책임만 가져도 충분하기 때문이다. 또 DTO에 변경사항이 발생했다고 도메인이 변경되는 것은 말이 되지 않는다. 적절하지도 않다.
또한 흔히 DTO에는 비즈니스 로직이 포함되면 안된다라고 한다. 왜냐하면 DTO는 말 그대로 데이터를 전달하기 위한 객체이기 때문이다. 그렇다면 DTO에 도메인 객체를 가져와서 매핑하는 로직은 있어도 될까? 라는 고민이 있을 수 있는데, 이러한 로직은 비즈니스 로직이 아니라고 생각한다. (어쩌면 Getter, 생성자와 동일한 수준의 메소드(기능)이라고 보는 것이다.)
다시 돌아와서 DB에서 조회한 엔티티는 도메인 객체에 매핑하는 것이 적절하다고 생각한다. DAO에서 Service 계층으로 해당 도메인 객체를 전달하게 될 것이고 Service 계층에서는 앞서 언급한 것과 같이 트랜잭션 단위의 비즈니스 로직을 처리할 것이고 이 때 각 도메인 객체의 비즈니스 로직도 수행될 것이기 때문이다.
레이어 끼리도 객체처럼 결합도를 낮추고 단일 책임을 갖게 함으로써 응집도를 높일 수 있다.
즉, 역할에 따라 레이어를 나눠 관심사를 분리할 수 있다. Controller는 앞서 언급한 것과 같이 요청 및 응답에 대한 책임을 가지고 서비스는 트랜잭션 단위의 비즈니스 흐름의 처리, Repository는 DB에서 데이터를 조회하고 저장하는 책임을 가질 수 있다.
그리고 이렇게 되면 레이어들의 의존 관계는 Controller -> Service -> Repository 의 흐름으로 흘러갈 수 있다.