2021.04.13 ~ 2021.04.26
API 테스트용 Java 라이브러리. Request를 보내고, Response를 받아 비교하는식으로 테스트
참고
[TEST] REST Assured를 사용한 REST API 테스트
url을 컨트롤러에 매핑 해준다. 클래스에 @RequestMapping(url)
를 붙여주면 메서드 별 공통 경로를 분리할 수 있다.(각 메서드 별로 붙여줄 수도 있다.)
@RestController
@RequestMapping("/http-method")
public class HttpMethodController {
@RequestMapping(path = "/users", method = RequestMethod.POST)
public ResponseEntity createUser(@RequestBody User user) {
Long id = 1L;
return ResponseEntity.created(URI.create("/users/" + id)).build();
}
}
@GetMapping : @RequestMapping(path = "", method = RequestMethod.GET)
을 줄인것
@PostMapping : @RequestMapping(path = "", method = RequestMethod.POST)
을 줄인것
path : 요청 경로를 나타낸다.
consumes : 소비 가능한 미디어 타입의 목록 지정. 요청 헤더가 지정해준 타입인 경우만 처리한다.
produces : 생산 가능한 미디어 타입의 목록 지정. 응답 헤더로 지정해준 타입을 반환한다.
params: 파라미터로 넘어오는 값을 받는다.
###### HTTP Request ######
GET /message?name=hello HTTP/1.1
@GetMapping(path = "/message", params = "name=hello")
public ResponseEntity<String> messageForParam() {
return ResponseEntity.ok().body("hello");
}
headers: 헤더로 넘어오는 값을 받는다.
###### HTTP Request ######
GET /message HTTP/1.1
header: hi
@GetMapping(path = "/message", headers = "HEADER=hi")
public ResponseEntity<String> messageForHeader() {
return ResponseEntity.ok().body("hi");
}
경로를 통해 변수를 받아온다.
###### HTTP Request ######
GET /users/1 HTTP/1.1
GET /users/2 HTTP/1.1
@GetMapping("/users/{id}")
public ResponseEntity<User> pathVariable(@PathVariable Long id) {
User user = new User(id, "이름", "email");
return ResponseEntity.ok().body(user);
}
-----------------------------------------------------------------
###### HTTP Request ######
GET /patterns/a HTTP/1.1
GET /patterns/b HTTP/1.1
@GetMapping("/patterns/{pattern:[a-g]}")
public ResponseEntity<String> pattern(@PathVariable String pattern) {
return ResponseEntity.ok().body("pattern");
}
@GetMapping("/patterns/?")
public ResponseEntity<String> pattern(@PathVariable String pattern) {
return ResponseEntity.ok().body("pattern");
}
-----------------------------------------------------------------
###### HTTP Request ######
GET /patterns/all HTTP/1.1
GET /patterns/names HTTP/1.1
GET /patterns/all/names HTTP/1.1
@GetMapping("/patterns/**")
public ResponseEntity<String> patternStars() {
return ResponseEntity.ok().body("pattern-multi");
}
요청 파라미터를 1:1로 받아옴. @RequestParam을 붙여주면 필수적으로 값을 넘겨줘야한다.(required 값을 false로 바꿔주면 넘겨주지않아도 됨)
###### HTTP Request ######
GET /method-argument/users?name=hello HTTP/1.1
@GetMapping("/users")
public ResponseEntity<List<User>> requestParam(@RequestParam("name") String userName) {
List<User> users = Arrays.asList(
new User(userName, "email"),
new User(userName, "email")
);
return ResponseEntity.ok().body(users);
}
----------------------------------------------------------------
// 요청 파라미터랑 메서드 파라미터 이름이 같은 경우 @RequestParam 생략 가능
###### HTTP Request ######
GET /method-argument/users?name=hello HTTP/1.1
@GetMapping("/users")
public ResponseEntity<List<User>> requestParam(String name) {
List<User> users = Arrays.asList(
new User(name, "email"),
new User(name, "email")
);
return ResponseEntity.ok().body(users);
}
http 요청 body를 객체로 변환시켜준다. body가 존재하지 않는 get방식에는 사용불가하며 post방식과 함께 사용된다. json이나 xml 형태의 데이터를 message converter를 통해 객체로 바인딩한다.
###### HTTP Request ######
POST /method-argument/users/body HTTP/1.1
Body:
{
"id": null,
"name": "이름",
"email": "email@email.com"
}
@PostMapping("/users/body")
public ResponseEntity requestBody(@RequestBody User user) {
User newUser = new User(1L, user.getName(), user.getEmail());
return ResponseEntity.created(URI.create("/users/" + newUser.getId())).body(newUser);
}
여러 파라미터들을 1:1로 객체에 바인딩해준다. 파라미터의 타입이 객체의 타입과 일치하는지를 검증하는 작업이 진행된다. 여러 개의 파라미터를 바로 자바빈 객체로 매핑시킨다. form 태그를 통해 전달받은 파라미터들을 객체로 바인딩 할 때 사용한다. 이때 객체는 모든 필드를 가진 생성자나, setter를 가지고 있어야한다.
참고
[Spring] @RequestBody, @ModelAttribute, @RequestParam의 차이
메소드에서 리턴되는 값이 view를 반환하지않고, 자바 객체를 message converter를 통해 http response body 에 쓰여진다.
클라이언트의 http request에 대한 응답 데이터를 포함하는 클래스이다.(HttpStatus, HttpHeaders, HttpBody)
참고
[Spring Boot] ResponseEntity란 무엇인가?
@Controller - view를 반환. 데이터를 반환해주려면 @ResponseBody를 추가적으로 붙여줘야한다.
@RestController - (@Controller + @ResponseBody) json 형태로 객체 데이터를 반환해준다.
Exception이 발생할 때 괄호안에 처리 할 exception.class를 적어주면 예외처리를 할 수 있다.
@ExceptionHandler(처리 할 exception.class)
@RestController
@RequestMapping("/exceptions")
public class ExceptionsController {
@GetMapping("/hello")
public ResponseEntity exceptionHandler() {
throw new CustomException();
}
@GetMapping("/hi")
public ResponseEntity exceptionHandler2() {
throw new HelloException();
}
@ExceptionHandler({CustomException.class, HelloException.class})
public ResponseEntity<String> handle(RuntimeException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
하나의 클래스가 아닌 모든 Controller에서 발생할 수 있는 예외를 잡아 처리.
@ControllerAdvice
public class HelloAdvice {
@ExceptionHandler(HelloException.class)
public ResponseEntity<String> handle() {
return ResponseEntity.badRequest().body("HelloException");
}
}
insert 구문을 편리하게 사용하도록 해주는 것.(insert 쿼리 없이 insert를 할 수 있다)
@Repository
public class SimpleInsertDao {
private SimpleJdbcInsert insertActor;
public SimpleInsertDao(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource) // DataSource(DB connection)
.withTableName("players") // 연결할 테이블 이름
.usingGeneratedKeyColumns("id"); // auto generated keys 칼럼 이름
}
executeAndReturnKey: 쿼리를 실행하고 key를 반환. Map이나, SqlParameterSource를 통해 칼럼과 값을 설정해줄 수 있다.
public Player insertWithMap(Player player) {
Map<String, Object> parameters = new HashMap<>();
parameters.put("firstName", player.getFirstName());
parameters.put("lastName", player.getLastName());
long id = insertActor.executeAndReturnKey(parameters).longValue();
return new Player(id, player.getFirstName(), player.getLastName());
}
SqlParameterSource는 인터페이스이므로 구현체인 BeanPropertySqlParameterSource를 사용한다. BeanPropertySqlParameterSource는 생성시 인자로 자바빈 클래스를 넣어주면 자동으로 매핑해준다.(클래스 변수와 칼럼명이 같아야하며 getter가 있어야한다)
public Player insertWithBeanPropertySqlParameterSource(Player player) {
SqlParameterSource parameterSource = new BeanPropertySqlParameterSource(player);
long id = insertActor.executeAndReturnKey(parameterSource).longValue();
return new Player(id, player.getFirstName(), player.getLastName());
}
SqlParameterSource의 다른 구현체로 MapSqlParameterSource가 있다. .addValue()
로 map처럼 사용할 수 있다.
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", firstName);
쿼리문에서 ? 대신 :칼럼명을 사용하게 해주는 것. 여러개의 파라미터가 있는경우 사용하길 권장
public int useMapSqlParameterSource(String firstName) {
String sql = "select count(*) from players where first_name = :first_name";
Map<String, Object> parameters = new HashMap<>();
parameters.put("first_name", firstName);
return namedParameterJdbcTemplate.queryForObject(sql, parameters, Integer.class);
}
ResultSet에서 값을 추출해 원하는 객체로 변환하는것.
public Player findPlayerById(Long id) {
String sql = "select id, first_name, last_name from players where id = ?";
RowMapper<Player> rowMapper = (rs, rowNum) ->
new Player(
rs.getLong("id"),
rs.getString("first_name"),
rs.getString("last_name")
);
return jdbcTemplate.queryForObject(sql, rowMapper, id);
}
update: db를 갱신시켜줄때(insert, delete, update)
query: 많은 row를 처리(sql문, RowMapper)
queryForObject: 하나의 결과만 반환해서 객체에 넣어준다.(sql문, 파라미터 값, 반환값), 결과 값이 없다면 EmptyResultDataAccessException 발생
update()는 변경된 행의 수만 리턴한다. 따라서 자동 생성 키 값을 알 수 없다. 이럴때 KeyHolder를 사용한다. KeyHolder는 자동 생성되는 칼럼 값을 가져온다.
public Long insertWithKeyHolder(Player player) {
final String sql = "insert into players (first_name, last_name) values (?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(con -> {
PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"});
ps.setString(1, player.getFirstName());
ps.setString(2, player.getLastName());
return ps;
}, keyHolder);
return keyHolder.getKey().longValue();
}
다른 페이지로 이동시켜준다(다른 요청을 보낸다)
forward : 이동한 url로 요청정보를 그대로 전달한다. 최초 요청 url만 보여지고 이동한 url 정보는 볼 수 없다.
redirect : 요청정보를 가지고 있지 않는다. url자체가 바뀐다.
참고
Redirect VS, Forward (Redirect와 forward의 차이)
jdbc 의존을 걸어준 경우에 한해 @Repository
어노테이션이 붙어있는 클래스가 final 클래스라면 에러가 발생한다.(@Service
나 @Controller
는 상관없다.)
브라운의 답변!
gradle로 org.springframework.boot:spring-boot-starter-jdbc 의존을 걸어줄 경우 @Repository 클래스는 CGLib을 통해 프록시 객체를 생성하는데 final을 붙여서 에러가 발생하네요.
우선 기본적으로 스프링 빈을 활용하더라도 프록시 객체를 생성하진 않고 필요할 때만 CGLib을 통해 프록시 객체를 생성합니다.
그래서 spring-boot-starter-jdbc 추가하지 않으면 @Service, @Component 뿐만아니라 @Repository를 붙여도 객체가 잘 생성됩니다.
하지만 spring-boot-starter-jdbc 의존을 추가하면 @Repository 가 붙은 객체는 @Service, @Component 와는 달리 별도의 처리(자세한 거는 더 찾아봐야함ㅠㅠ)를 하게 되는데 이 때 CGLib를 통해서 프록시 객체를 생성하는데 final이 붙어서 생성이 불가능해지고 에러가 발생하는 거 같아요
(@Transactional을 붙이면 @Service를 붙여도 CGLib을 통해 프록시 객체를 생성합니다.)
미립은 유틸리티 객체나 꼭 상속을 막아야하는 경우에만 final Class 를 사용한다고 함.
에러를 처리하는 방법은 2가지로 나눠볼 수 있다.
1) 에러 메세지만 보여주는 방식
2) 에러 페이지로 이동시키는 방식
언제 어떤 방식을 사용할지는 사용성 측면에서 생각해봐야한다.
에러 메세지만 보여주는 방식은 클라이언트의 잘못을 수정하면 올바른 동작으로 유도할 수 있는 경우
에 사용한다.(ex. 체스 말을 이동할 수 없는 곳으로 옮기는 경우)
에러 페이지로 이동시키는 방식은 클라이언트가 요청을 수정하여도 해당 요청이 승인되지 못하거나 해당 어플리케이션의 취약점을 노출 시킬 수 있는 경우
에 사용한다.
요새는 어떤 에러가 발생했는지에 관해 상세한 정보는 주지 않는다고 한다. 과거에는 아이디가 잘못되었습니다
, 비밀번호가 잘못되었습니다
등 상세한 정보를 보여줬지만 요새는 아이디 또는 비밀번호가 잘못되었습니다
와 같은 방식으로 어디가 잘못되었는지 유추하지 못하도록 메세지를 만든다고 한다.
현재 생각으로는 4xx에러는 에러 메세지를, 5xx에러는 에러 페이지 이동 방법을 사용하면 좋을 것 같다.
클라이언트 에러는 프론트쪽에서 처리하는 것 아닌가?
라는 의문이 들 수 있지만 클라이언트에서 넘어온 데이터을 무조건 신뢰 할 수는 없기 때문에 서버쪽에서도 유효성검증이나 에러 처리를 해줘야한다.
Model: 인터페이스
ModelMap: 클래스
ModelAndView: Model + view
자바 빈이란 자바 빈 규약 또는 자바빈 관례에 따라 만들어진 클래스를 뜻한다. 프론트엔드와 백엔드를 분리하기 위해 사용한다.(공통 규약을 지키면 일관된 방식으로 자바 클래스를 사용할 수 있게된다.)
자바 빈 규약을 알아보자
1) 디폴트 패키지 이외의 패키지에 속해야한다.
2) 기본 생성자가 존재해야한다.
3) 멤버 변수의 접근제한자는 private이어야한다.
4) getter / setter가 존재해야한다. (네이밍은 getXXX, setXXX)
5) getter / setter의 접근제한자는 public이어야한다.
6) 직렬화 되어있어야한다.(선택사항)
참고
[JAVAEE] 자바빈(JavaBean) 이란? 자바빈 규약에 대해
@RequestBody
는 MappingJackson2HttpMessageConverter
를 통해 body를 읽는데 결국 ObjectMapper(Jackson 라이브러리)
가 기본 생성자를 이용해서 DTO를 생성하기 때문에 기본 생성자가 필요한 것이다.
Jackson 라이브러리는 JSON 필드의 이름을 자바 객체의 필드 자체가 아닌 getter나 setter 메서드를 사용하여 매칭한다.(둘 중 하나만 존재하면 매칭할 수 있다.)
값을 주입할 때는 setter가 아닌 reflection을 통해서 해주기 때문에 setter가 필수로 존재해야하는 것은 아니다.
즉, 특별한 설정이 없다면 @RequestBody는 기본 생성자 + getter나 setter 둘 중 하나만 있으면 값을 바인딩 할 수 있다.(getter를 만들어 주는 것이 나을 것 같다.)
ObjectMapper : JSON을 읽고 쓰는 기능과 변환 수행을 위한 기능을 제공(자바 객체와 JSON을 변환시켜주는 것) 자바 객체 -> JSON(직렬화) / JSON -> 자바 객체(역직렬화)
참고
@ModelAttribute는 파라미터로 받은 데이터를 객체로 매핑해준다. 이때 내부적으로 주생성자를 찾아서 객체를 생성해준다.
1) setter 사용
2) 필드를 주입해주는 생성자 사용
위의 2가지 경우 중 하나를 만족하면 바인딩을 할 수 있는데, 1번의 경우 생성자를 두지 않으면 기본 생성자로 DTO가 생성되고, 후에 request 파라미터를 통해 넘어온 데이터를 setter를 통해 바인딩해준다.
2번의 경우는 주생성자로 DTO를 만들 때 바로 필드값을 주입받으므로 setter가 필요없다.
spring boot는 기본 에러를 처리해주는 BasicErrorController
가 빈으로 등록되어 있다. server.error.path
로 따로 경로를 설정해주지 않으면 /error
로 요청이 갈 경우 BasicErrorController
가 처리해준다. 이때 error
라는 뷰를 찾아 ModelAndView
를 리턴해준다. 따로 /error
컨트롤러를 만들어도 BasicErrorController
가 우선순위를 가진다.
데이터만 리턴해주어도 정상작동하지만, 상태코드, 헤더 등의 부가 정보도 함께 보내주기 위해 사용한다.
+) 사내의 표준 응답이 있다면 한번 더 감싸서 응답하기도 한다고 한다. 이렇게해주면 표준 응답을 통해 비지니스가 가지는 응답 코드라던지 요소에 카운트등을 유연하게 추가할 수 있는 장점이 있다. 실제 데이터 밖에서 메타데이터의 추가 삭제가 용이하게 된다.
// 예시
ResponseEntity<ExampleResponse>
{
name : 'milib'
}
ResponseEntity<WoowacourseResponse<ExampleResponse>>
{
code: "H1000" // 회사에서 정의된 응답코드
data : {
name : 'milib'
}
}
실제로 @Controller
클래스의 메서드들은 접근제한자가 뭐든지 상관없이 잘 동작한다. @Controller
어노테이션이 붙어있으면 실제로 런타임에 리플렉션을 통해 조작하기 때문에 접근제한자가 의미가 없지만, 공개되어있다는 걸 표현해주기 위해 public 으로 두는 것이 컨벤션이다. 팀 내부 규칙에 따르면 될듯.
http 메서드로 행위를 나타내므로, url은 정보의 자원을 표현하는 명사를 사용한다.
post로 생성하고, 201 CRREATED로 응답한다. 이때 헤더에 Location 정보를 넣어주는 것이 좋다.
// 예시
201 CREATED
-H Location : /{id}
/
는 계층관계를 나타냄/
를 사용하지 않는다.-
을 사용하여 가독성을 높일 수 있다.(_
은 사용하지 않는다.)컬렉션 ( Collection )
도큐먼트의 디렉터리 리소스
도큐먼트의 리스트
ex) http://api.your-service-books.com/books
스토어 ( Store )
클라이언트가 특별히 관리하는 형태를 갖는 것
favorites, mark, done
ex) http://api.your-service-books.com/users/1/favorites
참고
REST API 제대로 알고 사용하기
RESTful API란 ?
객체를 통해 클래스의 정보를 분석하고 조작하는 기법. 동적으로 클래스를 사용해야 할 때 사용한다. 스프링, 하이버네이트, Jackson 라이브러리 등에서 사용된다. 스프링의 경우 BeanFactory가 객체의 인스턴스를 생성하는데, 이 때 리플렉션을 사용한다.
자바 클래스 파일은 JVM의 힙 영역에 저장되므로, 클래스 이름을 통해 정보를 가져올 수 있다. 클래스 이름만 알고 있다면 언제든 이 영역에 들어가서 클래스의 정보를 가져올수 있다.
(다음 레벨 때 좀 더 공부해보기..)
db의 상태를 변화시키기 위해 수행하는 작업의 단위. 하나의 쿼리문을 뜻하는 것이 절대 아니다. 여러 과정을 하나의 행위로 묶는 것으로 게시판에 글을 작성하는 경우를 예시로 들면, 글을 쓰고 포스트할 때의 insert문. 이후 게시글의 전체 목록을 가져오는 select문. 이 2개의 질의문을 하나의 작업단위로 볼 수 있다.
과정 실행 중 하나라도 실패하면 다시 원 상태로 돌아가야한다. 즉 모든 과정이 반영되거나, 하나의 과정도 반영되지 않아야한다.
참고
트랜잭션이란?
스프링에서 지원하는 트랜잭션 처리 방법 중 어노테이션을 사용하는 방법. 선언적 트랜잭션으로 불려진다.
클래스나 메서드에 @Transactional
어노테이션을 붙여주면 된다. 그러면 해당 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성된다. 이 객체는 @Transactional
이 붙은 메서드가 호출될 때 PlatformTransactionManager
을 사용하여 트랜잭션을 시작하고 commit 또는 roll back 한다.
(구체적인 설정과 내용들은 다음 미션 때 추가하기)
참고
[Spring] Transactional 정리 및 예제
도메인이 엔티티와 VO를 포함하는 큰 개념. 엔티티와 VO가 아닌 객체(ex. Person 객체. 이름이 같다고 같은 객체가 아니다(동명이인인 경우))도 도메인에 포함된다.
엔티티 : DB 테이블과 1대1로 매칭되는 객체. DB 테이블의 칼럼만을 필드로 가짐. 상속받거나 구현체여서는 안됨. 도메인 로직을 가질 수 있음
VO : 값 객체. equals & hashcode를 오버라이딩. 필드의 모든 값이 서로 같으면 같은 객체로 취급.
참고
Entity, DTO, VO 바로 알기
DDD#01. Domain Object
Dave의 설명
개념적인 내용
DAO는 Spring 이전에 주로 사용하던 EJB에서 도메인 레이어와 퍼시스턴스 레이어를 구분하기 위한 개념으로 DB의 CRUD쿼리와 1:1 매칭
Repository는 DDD에서 나온 개념으로 어그리게이트 루트를 저장 및 조회하는 역할을 함.
어그리게이트가 여러 엔티티의 조합이기 때문에 굳이 비교하자면 Repository는 구현 안에 여러 Dao의 쿼리 호출 로직이 포함될 수 있음.
-> Dao는 테이블 위주로 바라본 관점, Repository는 도메인 위주로 바라본 관점이라고 보면 될 것 같아요.
현업에서 사용
현업에서는 Spring 이전엔 Dao 객체를 만들어서 DB접근을 했지만, Spring을 사용하면서 Dao대신 Repository를 사용하고 있음.
프로젝트가 도메인주도설계(DDD)를 사용하지 않는다면 사실상 Repository와 Dao는 비슷한 개념으로 보여지는 부분이 있음(Repository에 여러 Dao를 사용할 수 있지만, 사실상 하나의 Dao를 사용하기 때문)
CU설명
repository는 도메인 객체의 생명주기를 관리하며 Public Operation을 제공합니다. 이에 Domain Layer에 해당합니다. 도메인 모델과 생애주기가 같기 때문이죠.
실제 구현체인 SimpleJpaRepository는 Infrastructure Layer에 해당합니다.
이렇게 추상에 의존하고 구체에 의존하지 않도록 구성함으로써(DIP) 유연성있는 시스템을 구성할 수 있습니다.
반면, DAO는 CRUD와 1:1로 매칭되어 Persistence Operation을 적극적으로 드러냅니다.
즉, repository는 도메인 모델의 일부로 보며 추상인 반면, dao는 추상이 필수는 아니라고 생각합니다.
참고
DAO와 REPOSITORY 논쟁
DAO와 Repository의 차이점에 대해 알고 싶습니다.
What is the difference between DAO and Repository patterns?
상태를 가지지 않는 녀석들을 Bean으로 등록해서 사용. Bean으로 등록되면 공유되기 때문에 상태를 가지면 쓰레드 세이프하지 못하다.
빈으로 등록할 객체를 찾는 역할. @Component
가 붙은 녀석들을 찾는다. @Component
가 붙은 녀석들을 찾는 역할이지, 실제로 등록하는 것은 다른 녀석이다.
빈의 확장 버전으로 Spring이 Bean을 다루기 쉽도록 기능들이 추가된 공간. 빈을 다루기 위한 Ioc 컨테이너(Bean Factory라고 불리며 ApplicationContext의 구현체) 내부에 존재하는 공간.
빈 등록은 이 Context에서 일어난다.
즉 컨텍스트는 빈을 다루기 위해 설정할 수 있는 공간이다.
스프링 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진으로 빈의 생성, 관계설정 등의 제어를 총괄한다.
참고
[Spring] Ioc, DI, ApplicationContext, Bean, BeanFactory 개념
어떤 특정 기술이 가미되지 않은 순수한 자바 객체
스프링이 설정하기전에 스프링에서 빈으로 관리하고 싶은 객체
비즈니스 도메인 별로 나눠서 설계하는 방식. 기존 설계 방식이 비즈니스 도메인에 대한 이해가 부족한 상태에서 설계 및 개발되었다는 점에서 시작된 방식.
모듈간 의존성을 최소화하고 응집성을 최대화하는 것이 목적.
(개념만 알아두고 필요하다 싶을때 자세히 볼 것!)
request로 들어오는 값을 검증할 때 사용. 어떤 내용을 검증할 것 인지는 받는 객체에 명시해줘야함
파라미터에 @Valid
를 붙여줘서 사용.
@PostMapping("/{roomId}/piece/move")
public ResponseEntity<StatusDTO> movePiece(@PathVariable final String roomId, @RequestBody @Valid final MoveDTO moveDTO) {
// ...
}
이런식으로 내부에 어노테이션을 이용해서 검증한다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public final class MoveDTO {
@NotBlank
@Pattern(regexp = "^[a-hA-H]{1}[1-8]{1}$")
private String startPoint;
@NotBlank
@Pattern(regexp = "^[a-hA-H]{1}[1-8]{1}$")
private String endPoint;
}
아래의 의존성을 넣어줘야 사용 가능
implementation 'org.springframework.boot:spring-boot-starter-validation'
컨트롤러를 사용하지 않으면서 특정 뷰에 대한 컨트롤러 추가하는 방법
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/") // 요청 url
.setViewName("hello"); // 리턴할 뷰 이름
}
url로 컨트롤러에 접근할 때 무언가를 제어할 때 사용. 요청을 중간에 가로채서 작업을 해준다.
HandlerInterceptorAdapter
를 상속받아 구현한다.
4개의 메서드로 동작을 정의할 수 있다.
preHandle
: url을 호출했을때, Controller에 접근하기 전에 실행되는 메서드
postHandle
: Controller를 거쳐 view로 결과를 전달하기 직전에 실행되는 메서드
afterCompletion
: view가 렌더링 된 후에 실행되는 메서드. view 생성시 예외가 발생하더라도 실행된다.
afterConcurrentHandlingStarted
: 비동기 요청 시 postHandle
, afterCompletion
수행되지 않고 afterConcurrentHandlingStarted
메서드가 수행된다.
// 예제
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String accessToken = request.getHeader("Authorization");
if (accessToken == null) {
throw new AuthorizationException();
}
return super.preHandle(request, response, handler);
}
}
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()) // LoginInterceptor를 거친다.
.addPathPatterns("/admin/members"); // /admin/members로 요청이 들어온 경우
}
사용자가 Controller Mapping 메서드의 인자값으로 임의의 값을 전달하려고 할때 사용하는 것이다.(ex. 세션에 저장된 값 중, 특정 값을 메서드 인자로 전달할 때 사용)
@RequestBody나 @PathVariable도 HandlerMethodArgumentResolver를 사용해서 바인딩해준다.
HandlerMethodArgumentResolver
인터페이스를 구현한다.
2개의 메서드를 구현해야한다.
supportsParameter
: 원하는 타입의 인자가 있는지 확인하는 메서드. 있다면 true를 리턴한다.
resolveArgument
: 실제 바인딩 할 값을 리턴한다.
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthenticationPrincipal.class); // AuthenticationPrincipal 어노테이션이 붙어있는지 확인
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return new LoginMember(1L, "email", 120); // 실제 바인딩 할 값
}
}
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthenticationPrincipalArgumentResolver());
}
}
Cookie : 클라이언트 로컬에 저장되는 키와 값이 들어있는 파일.
클라이언트 request -> 서버에서 쿠키 생성 -> http 헤더에 쿠키 넣어서 response -> 클라이언트 request시 쿠키 함께 보냄 -> 서버에서 업데이트 필요시 쿠키 업데이트해서 response
Session : 쿠키 기반. but 정보를 브라우저가 아닌 서버에 저장. 서버는 클라이언트를 구분하기 위해 세션 ID를 부여. 쿠키보다 보안이 좋지만, 서버 메모리를 많이 차지.
클라이언트 request -> 세션 ID 발급 -> 클라이언트는 세션 ID에 대한 쿠키를 가지고 있음 -> 클라이언트 request시 세션 ID를 같이 전달 -> 세션 ID를 통해 클라이언트 정보 가져옴 -> request 처리 후 response
(다음 미션에서 자세하게 알아보기!)
참고
쉽게 알아보는 서버 인증 1편(세션/쿠키 , JWT)
로그인은 어떻게 동작하나?
쿠키와 세션 개념
post 요청에 대한 응답이 다른 url로의 get 요청을 위한 redirect인 패턴. post 요청의 응답이 바로 view를 리턴하는 것이 위험하기 때문에 나온 패턴.
PRG 패턴을 사용하지 않으면 2가지 문제점이 발생할 수 있다.
1) 새로고침으로 인한 동일한 post 요청이 중복 발생시 의도치 않은 결과 초래
: ex) 온라인 물품 구매 후 새로고침 시 중복 구매
2) post 요청의 결과에 해당하는 페이지를 북마크하거나 다른 사람과 공유하기 어려워짐.
: 북마크는 url로 공유되는데, url로는 get 요청 밖에 할 수 없기 때문에, 의도한 페이지와 다른 페이지로 이동할 수 있다.
[Web] PRG (Post-Redirect-Get) 패턴
크롬의 secret mode는 같은 세션을 사용해서, 별개의 브라우저에서 접속하는 환경을 크롬을 사용해서 테스트하기 위해선, 하나는 일반모드, 나머지는 secret mode로 띄워서 테스트해야합니다 ㅎㅎ..
api : 프로그램들이 상호작용하는 것의 매개체.
endpoint : api가 서버에서 리소스에 접근할 수 있도록 하는 url
참고
[Web] API 그리고 EndPoint
API와 ENDPOINT란 무엇인가?
도메인에서 발생하는 이벤트의 순차적인 기록을 저장해두는 것. 도메인의 상태는 처음부터 현재 시점 까지 발생한 기록의 결과물이다. DB에는 이벤트의 insert만 수행되며, update나 delete는 수행되지 않는다.
이벤트 소싱은 크게 결합도를 낮춰줄 수 있지만, 운영중에 신경써야 할 부분이 많이지는 부분도 있다.
참고
이벤트 소싱 소개
서비스에따라 다르지만 JSP 를 비롯한 템플릿 엔진을 사용하는 회사는 여전히 다수 존재합니다. 그래도 방향성만 놓고 봤을때는 서버에서는 데이터만 응답하는것이 좋은 것 같아요.
템플릿 엔진을 사용하지 않을 경우 view 랜더시점에 데이터를 전달하기 어려울 것 같아요.
공통적인 인증/인가 같은경우에는 인터셉터에서 공용으로 처리 해주고, 값에 대한 검증은 컨트롤러에서 진행하고 비지니스 로직에 대한 검증은 서비스에서 하는것이 좋은 것 같습니다. 예를들어 move 라는 명령을 전달했을때, 위치가 제대로 전달되었는지는 서비스 레이어 이전에서 검증하고 해당 기물이 원하는대로 동작 할 수 있는지는 서비스레이어에서 검증 하는 것이 좋다고 생각합니다. 공통 서비스는 한번도 고민해본적이 없네요.
여러 이유가 있지만, 도메인을 직접 노출하면 노출되면 안되는 데이터가 클라이언트에 노출 될 위험이 있을 수 있겠네요
예를들어 Member Domain 에는 pwd 란 필드가 있을 수 있는데, 이게 클라이언트에 그대로 노출 될 수 있을 것 같아요.
추가로, API 는 클라이언트와 약속인데 이게 도메인이 변화할때마다 변경된다면, 클라이언트가 API 를 신뢰하기 어려울 것 같아요.
만약 어떤 resource를 식별하고 싶으면 Path Variable을 사용하고,
정렬이나 필터링을 한다면 Query Parameter를 사용하는 것이 Best Practice이다
참고
[번역] Path Variable과 Query Parameter는 언제 사용해야 할까?
ex) mysql의 user..