@ModelAttribute : Http요청 파라미터(URL쿼리스트링, POST Form)을 다룰 떄 사용.
@RequestBody : Http Body의 데이터(json)을 객체로 변환할 떄 사용
참고로, @RequestBody는 @Valid 앞에 와야하고 @ModelAttribute는 @Valid뒤에 와야한다.
@RequestBody : json데이터를 객체로 바꿔서 컨트롤러 메소드의 파라미터로 넘겨줌.
@ResponseBody : 컨트롤러 메소드의 리턴값인 객체를 json 데이터로 바꿔서 응답.
ex)
@PostMapping("/api/vi/members")
public CreateMemberResponse saveMemberV1(@RequestBody Member member) {
}
API 스펙 = API 설명서 = API 문서
예를 들어 회원가입 API은,
기능 : 회원가입
HTTP Method : POST
Request URL : /api/members
Request Body(JSON) :
{
"name": "홍길동",
"email": "hong@example.com",
"password": "1234"
}
Response :
{
"id": 1,
"name": "홍길동",
"email": "hong@example.com"
}
하지만 이렇게 엔티티를 바로 파라미터로 받으면 안된다. 엔티티(엔티티의 필드)가 변경되면, API 스펙이 변한다.
==> 엔티티가 변경되면, API 스펙의 Request 부분(JSON 구조)과 Response 부분(JSON 구조)이 변한다.
그래서 API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받아야한다.
API 요청 스펙에 맞는 DTO를 파라미터로 받고, 그 DTO를 애플리케이션 내부 로직에서 엔티티로 변환해서 처리한다.
API 요청 스펙은 API 스펙중에서 "요청"에 해당하는 JSON 구조를 말한다.
@PostMapping("/api/v2/members")
public CreateMemberResponse saveMemberV2(@RequestBody @Valid CreateMemberRequest request) {
Member member = new Member();
member.setName(request.getName());
Long id = memberService.join(member);
return new CreateMemberResponse(id);
}
@Data
static class CreateMemberRequest {
private String name;
}
@Data
static class CreateMemberResponse {
private Long id;
public CreateMemberResponse(Long id) {
this.id = id;
}
}
위 코드에서,
@Data에 의해 CreateMemberRequest와 CreateMemberResponse 각각의 내부에 기본생성자가 자동으로 생기게 된다.
@PutMapping("/api/v2/members/{id}")
public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id, @RequestBody @Valid UpdateMemberRequest request) {
memberService.update(id, request.getName());
Member findMember = memberService.findOne(id);
return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}
@Transactional
public void update(Long id, String name) {
Member member = memberRepository.findOne(id);
member.setName(name);
}
클래스의 제네릭 타입 정의 시 클래스명 옆에 <T>를 적어준다.
@GetMapping("/api/v2/members")
public Result memberV2() {
List<Member> findMembers = memberService.findMembers();
List<MemberDto> collect = findMembers.stream()
.map(m -> new MemberDto(m.getName()))
.collect(Collectors.toList());
return new Result(collect);
}
static class Result<T> {
private T data;
public Result(T data) {
this.data = data;
}
}
객체 생성 시(생성자 호출 시)에 T의 타입이 결정된다.
2-1. collect의 타입을 확인한다. -> List
2-2. Result의 생성자가 Result(T data) 인것을 확인한다.
2-3. 컴파일러가 "아, 지금 T는 List 타입이구나" 하고 자동으로 결정한다.
즉, 생성자 호출 시점에 T가 구체적인 타입으로 치환된다.
Result result = new Result(collect);
Result<List<MemberDto>> result = new Result<List<MemberDto>>(collect);
Result<List<MemberDto>> result = new Result<>(collect);
위 3가지와 같이 사용한다.
참고로 리스트 컬렉션을 json으로 바로 반환하면
[
{
"id": 1,
"name": "member1",
"address": {
"city": "seoul",
"street": "test",
"zipcode": "111111"
}
},
{
"id": 2,
"name": "member2",
"address": {
"city": "busan",
"street": "test2",
"zipcode": "2222"
}
}
]
이런식으로 되지만, 이 리스트 컬렉션을 Result와 같은 객체 안에 넣어 Result객체를 반환하게 되면,
{
"data" : [
{
"id": 1,
"name": "member1",
"address": {
"city": "seoul",
"street": "test",
"zipcode": "111111"
}
},
{
"id": 2,
"name": "member2",
"address": {
"city": "busan",
"street": "test2",
"zipcode": "2222"
}
}
]
}
와 같은 형태로 된다.
리스트를 반환하면 키가 없고, 객체를 반환하면 필드명이 키가 된다.
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
.
.
.
}
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@JsonIgnore
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@BatchSize(size = 1000)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
}
@Entity
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
}
@BatchSize(size = 1000)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
private List<OrderItemQueryDto> findOrderItems(Long orderId) {
return em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id = :orderId", OrderItemQueryDto.class)
.setParameter("orderId", orderId)
.getResultList();
}
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders();
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList();
.
.
.
}
public List<OrderQueryDto> findAllByDto_optimization() {
List<OrderQueryDto> result = findOrders(); // 쿼리 1번 실행
List<Long> orderIds = result.stream()
.map(o -> o.getOrderId())
.collect(Collectors.toList());
List<OrderItemQueryDto> orderItems = em.createQuery(
"select new jpabook.jpashop.repository.order.query.OrderItemQueryDto(oi.order.id, i.name, oi.orderPrice, oi.count)" +
" from OrderItem oi" +
" join oi.item i" +
" where oi.order.id in :orderIds", OrderItemQueryDto.class)
.setParameter("orderIds", orderIds)
.getResultList(); // 쿼리 1번 실행
Map<Long, List<OrderItemQueryDto>> orderItemMap = orderItems.stream()
.collect(Collectors.groupingBy(OrderItemQueryDto::getOrderId));
result.forEach(o -> o.setOrderItems(orderItemMap.get(o.getOrderId())));
return result;
}
1. OrderItemQueryDto::getOrderId
==> 메소드참조라고 불린다. 스트림에서 OrderItemQueryDto::getOrderId와 같이 사용하면, 각 스트림 요소인 OrderItemQueryDto객체의 getOrderId()메서드를 호출하는 것과 동일하게 동작한다. 즉 oi -> oi.getOrderId()를 하는것과 동일하다.
2. Collectors.groupingBy(OrderItemQueryDto::getOrderId)
==> Map으로 그룹화 할때 사용한다. 스트림의 요소들을 OrderItemQueryDto::getOrderId결과를 기준으로 그룹화한다.
OrderItemQueryDto::getOrderId가 실행된 결과를 Map의 key로 사용한다.
같은 key를 가진 요소(OrderItemQueryDto)들을 하나의 리스트로 묶고 이 리스트를 Map의 value로 사용한다.
결과적으로 Map<Long, List> 형태가 된다.
그래서 해당 Map 결과를 보면 아래와 같이 되어있다.
{
1L = [OrderItemQueryDto(orderId=1,...), OrderItemQueryDto(orderId=1,...)],
2L = [OrderItemQueryDto(oderId=2,...)]
}
즉 key는 orderId, value는 같은 orderId를 가진 리스트이다.
3. 객체 자체를 System.out.println으로 출력하면 내부적으로 객체의 toString()이 호출되는데,
일반적인 객체 자체를 System.out.println으로 출력하면 해당 객체의 toString()이 따로 오버라이딩 되어있지 않기 때문에 주소값이 출력된다.
하지만 List와 Map 자체를 System.out.println으로 출력하면, List와 Map은 toString()이 이미 자바 내부적으로 오버라이딩이 되어있기 때문에 주소값이 출력되는게 아니라,
List는 key없이 [value1,value2,value3]처럼 출력되고 Map은 {key1=value1, key2=value2}처럼 출력된다.
List를 JSON으로 변환하면, 동일하게 key없이 [1, 2, 3] 와 같이 된다.
Map을 JSON으로 변환하면, 중괄호로 감싸고, "key": value 형태로 된다.
이때, JSON 라이브러리(Jackson)가 Map의 key가 문자열이든, 정수이든 상관없이 JSON으로 변환될때 무조건 key를 문자열로 처리해서 "key1"처럼 만들어준다. JSON에서 key는 항상 문자열이어야하기 때문이다.
그래서 아래와 같이 된다.
{
"key1": value1,
"key2": value2
}
4. Map으로 하지않고,
for (OrderQueryDto order : result) {
List<OrderItemQueryDto> items = new ArrayList<>();
for (OrderItemQueryDto orderItem : orderItems) {
if (order.getOrderId().equals(orderItem.getOrderId())) {
items.add(orderItem);
}
}
order.setOrderItems(items);
}
이렇게 orderItems에 대해서 loop를 돌려서 OrderItemQueryDto의 orderId와 OrderQueryDto의 orderId가 같은지 확인하고 setOrderItems를 해줘도 되지만, Map으로 하는게 편리하고 성능도 좋다.