[JPA] 9.JPA 활용-2

재우·2025년 11월 3일

JPA

목록 보기
9/11

참고 - <회원 등록 API>

  • @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 문서

  • 즉 API가 어떤 기능을 제공하고, 어떻게 호출해야 하는지를 정의한 문서.
  • 어떤 URL로 어떤 HTTP 메서드(GET, POST 등) 를 사용해서 어떤 요청 데이터(request) 를 보내면 어떤 응답(response) 을 받을 수 있는지를 명시한 문서.

예를 들어 회원가입 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 구조를 말한다.

참고 - <회원 등록 API>

@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 각각의 내부에 기본생성자가 자동으로 생기게 된다.

  • 하지만, CreateMemberResponse에 별도로 생성자를 작성해준 이유는, new CreateMemberResponse(id) 형태로 응답 객체를 만들기 때문에 필요하다.
  • CreateMemberRequest에서도 생성자가 별도로 있어야 할 것 같지만, 스프링이 CreateMemberRequest 내부에 기본생성자와 setter만 있으면 자동으로 JSON데이터를 CreateMemberRequest객체로 만들어주기 때문에 기본 생성자만 있으면 된다.

참고 - <회원 수정 API 中>

@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);
    } 
  • update() 메서드 실행이 끝나고 트랜잭션이 끝나면서 커밋되는 시점에 변경감지가 일어나고, update쿼리가 날라감.

참고 - <회원조회 API>

  1. 클래스의 제네릭 타입 정의 시 클래스명 옆에 <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;
        }
    }
  2. 객체 생성 시(생성자 호출 시)에 T의 타입이 결정된다.
    2-1. collect의 타입을 확인한다. -> List
    2-2. Result의 생성자가 Result(T data) 인것을 확인한다.
    2-3. 컴파일러가 "아, 지금 T는 List 타입이구나" 하고 자동으로 결정한다.
    즉, 생성자 호출 시점에 T가 구체적인 타입으로 치환된다.

  3. Result result = new Result(collect);
    Result<List<MemberDto>> result = new Result<List<MemberDto>>(collect);
    Result<List<MemberDto>> result = new Result<>(collect);
    위 3가지와 같이 사용한다.


참고 - <회원조회 API>

참고로 리스트 컬렉션을 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"
        	}
	    }
   ]
}

와 같은 형태로 된다.
리스트를 반환하면 키가 없고, 객체를 반환하면 필드명이 키가 된다.


참고 - <간단한 주문 조회 V1>

@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<>();
}
  • 참고로, 엔티티를 직접 JSON으로 응답할때 무한루프에 빠질수 있다.
    예를들어 Order에 대해서 json으로 응답을 하는데 만약 Order안에는 Member가 있고 이 Member안에는 List orders가 있어서 또 Order를 json으로 응답해야하고, 이 Order안에는 또 Member가 있어서 Meber를 json으로 응답해야하고...
    즉, 양방향 연관관계로 설정되어있으면 무한루프에 빠질수 있다.
  • 그래서 양방향 연관관계로 설정되어있는 모든곳에서는, 둘 중 한곳에서(Order나 Member에서) @JsonIgnore를 해줘야한다.
    @JsonIgnore는 객체를 JSON으로 만들때 특정 필드를 제외시키고 JSON으로 만들어준다.
  • 그래서 Order안에는 Member가 있고 이 Member안에는 List orders가 있어서 또 Order를 json으로 응답해야하기 때문에 이를 방지하기 위해 Member에서 Order를 json으로 응답하는 List order부분에 @JsonIgnore를 해준다.

참고 - <주문 조회 V3.1>

@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 {
  • Order클래스의 orderItems처럼 컬레션이면 컬렉션 필드에, OrderItem클래스의 item처럼 엔티티이면 해당 엔티티 클래스에 @BatchSize를 작성한다.

참고 - <주문조회 v4>

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();
    }
  • OrderItem에서 Order의 PK인 id를 가져오려면, oi.order.id로 적어주면된다.
    jpql에서는 경로탐색방식으로 oi.order.id처럼 작성해야하지만, JPA가 실제 SQL로 변환할 때에는 oi.order_id로 변환된다.
    OrderItem의 Order order필드는 @JoinColumn(name = "order_id")으로 되어있는 Order의 FK이기 때문에, 해당 컬럼의 값인 FK값을 가져온다.

참고 - <주문조회 v5>

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();
	.
	.
	.	
}
  • where oi.order.id in :orderIds ==> jpql에서 List컬렉션을 in 절의 파라미터로 넣으면, JPQ가 SQL로 변환할때 내부적으로 자동으로 각각의 값들을 in절에 넣어준다.
    예를들어 orderIds가 [1,2,3] 처럼 되어있다면, where oi.order.id in (1,2,3)으로 해준다.

참고 - <주문조회 v5>

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으로 하는게 편리하고 성능도 좋다.

0개의 댓글