[Spring MVC] JDBC 기반 데이터 엑세스 계층
JDBC란?
◼ Java Database Connectivity "자바 데이터베이스 연결"
◻ Java 언어를 이용하여 데이터베이스에 접근할 수 있도록 도와주는 자바 표 준 API

- 자바 언어를 통해 DB에 접근 가능
- SQL 쿼리 실행 가능
자바 표준 인터페이스인 JDBC가 필요한 이유?
1. 새로운 데이터베이스의 사용법을 배워야 한다.

- 각 데이터베이스마다 사용법이 다름
- 학습의 몫은 개발자에게 있음
2. 프로덕션 코드의 변경 범위가 커질 수 있다.

- DIP(의존 역전 원칙) 위배
- 기대하는 기능은 같아도, 코드 변경 범위가 클 수 있음
Java 진영에서는 이러한 문제를 해결하기 위해 표준 인터페이스인 JDBC를 만들게 된다.
JDBC 동작 흐름

"Java Appilcation에서 JDBC API를 이용해 적절한 JDBC Driver를 로딩한 후, DB와 인터랙션 한다."
👩🏻💼 JDBC Driver 란?
- DB와의 통신 담당 인터페이스
- 다양한 벤더(Oracle, MySQL...)에 각각 맞는 JDBC 드라이버 구현해서 제공 -> 해당 구현체를 통해 특정 벤더의 DB에 엑세스 가능
DB 접근을 위한 4가지 기능
1. Driver: 데이터 베이스와 연결

(1) Driver 객체를 통해 connect() 메서드 실행시켜 DB와 연결한다.
(2) 클라이언트가 직접적으로 Dirver API를 사용하는 것이 아닌 -> DriverManager API를 통해 간접적으로 Driver API 호출
👩🏻💼 DriverManager 란?
- 다양한 JDBC Dirver 들을 한 곳에 묶어 관리하는 유틸성 정적 클래스

👩🏻💼 어떻게 JDBC Driver 들이 DriverManager 안에 들어갈 수 있나?
- 구체 드라이버 클래스 로딩 시점에, 스태틱 블록에 있는 메서드를 통해 드라이버 매니저에 등록된다.
- DriverManager은 내부 ArrayList로 다양한 구체 클래스들을 관리할 수 있다.
2. Connection: 데이터베이스와의 연결 정보를 갖는다.

(1) Connection 요청
- 클라이언트가 DriverManager API를 통해 커넥션 요청
(2) Driver 찾기
- DriverManager 내부적으로 컨넥션 요청을 처리할 수 있는 Driver을 찾음
(3) 실제 DB와 연결
- 구체 Dirver 찾으면 connect() 메서드를 통해 실제 DB와 연결됨
(4) Connection 생성
- DB 연결에 대한 정보를 가지고 connection 객체를 생성한 다음
(5) Connecton 반환
- 클라이언트에게 해당 connection 을 반환
3. Statement: SQL 쿼리 관리, 실행은 나에게!

(1) SQL 쿼리 전달 (확정 X)
- 클라이언트가 확정되지 않은 SQL쿼리문을 준비
- 확정되지 않았다? : 오른쪽 끝에 '?' 들어가 있는 SQL 쿼리문
(2) Statement 생성
- 이 SQL 쿼리문을 connetction.prepareStatment() 메서드를 통해 전달하면
- 내부적으로 쿼리를 관리하는 Statement 객체를 생성하게 됨
(3) Statement 반환
- 이 Statement 객체를 클라이언트에게 반환
👩🏻💼 SQL 쿼리를 확정하는 방법은? SQL 바인딩
(4) SQL 바인딩 (Binding)
- '?' 에 원하는 값을 지정해주는 방법
- statement 객체의 setter 메서드를 통해 바인딩 가능
--> '?' 인자에 문자열 "jeon" 을 넣어주면 아래와 같이 바인딩 된다.

4. ResultSet: 실행한 SQL 쿼리 결과를 가져오고 싶다면?

(1) SQL 쿼리 실행 요청
- 클라이언트가 바인딩 된 SQL 쿼리문을 실행 요청
(2) SQL 쿼리 실행
- statement의 executerQuery() 메서드를 통해 내부적으로 데이터베이스에서 쿼리를 실행
(3) SQL 쿼리 결과 반환
- 데이터베이스에서 이에 대한 쿼리 결과를 반환
(4) ResultSet 생성
- 반환 결과를 가지고 ResultSet 객체로 포장시킨 다음
(5) ResultSet 반환
- 포장된 객체를 클라이언트에게 반환
JDBC API 사용 흐름

(1) DriverManager API를 통해 Driver API를 간접적으로 호출
(2) Drive의 connect() 메서드를 통해 DB와 연결한 후, 그에 대한 정보를 connection 객체를 생성해서 클라이언트에게 반환
(3) Connection 객체에 원하는 원하는 API 쿼리문을 넘겨주어 그에 대한 Statement 인스턴스를 생성
(4) Statement 객체를 통해 SQL 쿼리를 실행하게 되면, 그에 대한 실행 결과인 ResultSet으로 반환
JDBC를 통해 해결된 문제들
1. 새로운 데이터베이스를 배우지 않아도 된다.
- 추상화된 JDBC API의 사용법을 숙지하면 어떤 데이터베이스가 와도 동일한 기능을 할 것으로 기대할 수 있다.
2. 프로덕션 코드의 변경 범위를 최소화할 수 있다.

- 프로덕션 코드들은 더이상 각각의 구체 클래스에 의존하지 않음 => 추상화된 JDBC에만 의존!
- 구체 클래스가 바뀌더라도 프로덕션 코드는 바뀌지 않는다! (DIP 원칙 만족)
Spring 데이터 액세스 기술 유형
- 대표적인 데이터 엑세스 기술: mybatis, Spring JDBC, Spring Data JDBC, JPA, Spring Data JPA ...
SQL 중심 기술
- 애플리케이션에서 데이터베이스에 접근하기 위해 SQL 쿼리문을 애플리케이션 내부에 직접적으로 작성하는 것이 중심이 되는 기술
// mybatis의 SQL Mapper 코드 예시
<select id="findMember" resultType="Member">
SELECT * FROM MEMBER WHERE member_id = #{memberId}
</select>
mybatis의 경우: SQL Mapper 설정 파일에서 SQL 쿼리문을 직접적으로 작성
=> 작성된 SQL 쿼리문을 기반으로 DB의 특정 테이블에서 조회 후 => Java 객체로 변환
결론: SQL 쿼리문이 직접적으로 포함되어 있다.
// Spring JDBC의 Jdbc Template 사용 예시
Member member = this.jdbcTemplate.queryForObject(
"select * from member where member_id=?", 1, Member.class);
결론: SQL 쿼리문이 직접적으로 포함되어 있다.
SQL 쿼리문이 직접적으로 포함된 방식은 과거-현재 모두 사용되고 있지만,
Java 진영에서는 SQL 중심의 기술 => 객체(Object) 중심의 기술로 이전하고 있는 추세
객체(Object) 중심 기술
- 모든 데이터를 객체(Object) 관점으로 바라보는 기술
- DB에 접근하기 위해 SQL 쿼리문을 직접 작성하는 것 X
- DB 테이블에 데이터를 저장/조회할 경우 Java 객체(Object)를 이용해 SQL 쿼리문으로 자동 변환 => DB 테이블에 접근
ORM(Object-Relational Mapping)
객체 중심의 데이터 액세스 기술
JPA(Java Persistence API): Java의 대표적인 ORM 기술
Spring Data JDBC란?
- ORM(Object-Relational Mapping) 도구인 Hibernate나 JPA(Java Persistence API)를 사용 X
- 순수한 JDBC(Java Database Connectivity)를 기반으로 한 접근 방식
==> 이를 통해 개발자는 직접 SQL 쿼리를 작성하여 데이터베이스와 상호작용할 수 있음
Spring Data JDBC 사용해보기

인메모리 DB인 H2 사용
👩🏻💼 인메모리 (In-memory) DB 란?
- 메모리 안에 데이터를 저장하는 데이터베이스
- 메모리: 휘발성 => 애플리케이션 실행 도중에만 데이터를 저장
👩🏻💼 인메모리 DB를 사용하는 이유?
- 개발 진행 시엔 꾸준한 테스트를 진행할 것임 => 테스트에 필요한 데이터만 테이블에 존재하는 것이 테스트에 용이
- 따라서 로컬 개발 환경에서는 데이터베이스의 테이블에 남겨진 데이터가 깨끗이 비워지는 것이 중요

H2 DB 정상 동작 유무 확인

로그에 출력된 H2 console available at '/h2-console'. Database available at 'JDBC URL' 에서
본인의 JDBC URL을 확인 후 H2 데이터베이스에 접속 => H2 DB 디폴트 설정
🙁 H2 DB 디폴트 설정의 문제점
애플리케이션 로그에 출력되는 JDBC URL은 랜덤하게 바뀌기 때문에 매번 새롭게 입력하는 것은 불편
=> application.yml 파일에 H2에 대한 설정 추가
😎 H2 추가 설정

샘플 코드 구현
- messageDto(Dto class)
- MessageController
- MessageMapper
- MessageService
- Message
- MessageRepository
messageDto(Dto class)
// 클라이언트가 Request Body로 전달하는 “Hello, World” 문자열을 바인딩하는 DTO 클래스
@Getter
public class MessagePostDto {
@NotBlank
private String message;
}
// Response에 사용할 DTO 클래스
@Getter
@Setter
public class MessageResponseDto {
private long messageId;
private String message;
}
MessageController
// 클라이언트의 “Hello, World” 문자열 데이터를 전달받는 Controller 클래스
@RequestMapping("/v1/messages")
@RestController
public class MessageController {
private final MessageService messageService;
private final MessageMapper mapper;
public MessageController(MessageService messageService, MessageMapper mapper) {
this.messageService = messageService;
this.mapper = mapper;
}
@PostMapping
public ResponseEntity postMessage(@Valid @RequestBody MessagePostDto messagePostDto){
Message message = messageService.createMessage(mapper.messageDtoToMessage(messagePostDto));
return ResponseEntity.ok(mapper.messageToMessageResponseDto(message));
}
}
MessageMapper
// DTO 클래스와 엔티티(Entity) 클래스를 매핑해 주는 Mapper 인터페이스
@Mapper(componentModel = "spring")
public interface MessageMapper {
Message messageDtoToMessage(MessagePostDto messagePostDto);
MessageResponseDto messageToMessageResponseDto(Message message);
}
MessageService
@Service
public class MessageService {
private final MessageRepository messageRepository; // MessageService 클래스에서 DI를 통해 주입받음
public MessageService(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
public Message createMessage(Message message){
return messageRepository.save(message); // Message 엔티티 클래스에 포함된 데이터를 데이터베이스에 저장하는 데 사용
}
}
MessageRepository
// 데이터 액세스 계층에서 데이터베이스와의 연동을 담당하는 Repository인 MessageRepository 인터페이스
public interface MessageRepository extends CrudRepository<Message, Long> {
}

에러 발생 이유: MESSAGE 테이블을 발견할 수 없다 => 데이터를 저장할 테이블을 H2 DB에 생성하지 않음
H2 DB에 MESSAGE 테이블 생성
1. 테이블 생성을 위한 SQL문 추가

- sql-init-schema-locations: [.sql 파일의 경로] 지정
- 위 설정 코드는 애플리케이션 실행 시 schema.sql 파일에 있는 스크립트를 읽어 테이블을 자동 생성해줌
👩🏻💼 Remember!
- 인메모리 DB 사용 시, 애플리케이션 실행 시마다 schema.sql 파일의 스크립트가 매번 실행됨
2. 테이블 생성 스크립트 작성
- ‘schema.sql’ 파일 위치: ‘src/main/resources/db/h2’

👩🏻💼 Remember!
- MESSAGE 테이블의 식별자(Primary key)인 ‘message_id’ 열에 AUTO_INCREMENT 설정
=> ‘message_id’ 열에 값을 입력하지 않아도 데이터를 저장할 때마다 자동으로 포함
- MESSAGE 테이블 코드는 Message 클래스와 각 멤버변수들에 매핑된다.

- MESAGE 테이블 명 = Message 클래스명
- MESSAGE 테이블의 message_id = Message 클래스의 messageId
- MESSAGE 테이블의 message = Message 클래스의 message


DDD (Domain Driven Design)
- 도메인 주도 설계
- 비즈니스 도메인 별로 나누어 설계하는 방식
- 핵심 목표: "Loose Coupling", "High cohesion" - 애플리케이션 또는 그 안의 모듈 간의 의존성은 최소화, 응집성은 최대화
👩🏻💼 Domain 이란?
- 사전적인 의미 '영역','집합'
- DDD에서 말하는 Domain: 비즈니스적인 업무의 영역
- 비즈니스 Domain: 유사한 업무의 집합
=> 애플리케이션은 비즈니스 Domain 별로 나누어 설계 및 개발될 수 있음
👩🏻💼 Aggregate 란?
- 고유의 비즈니스 목적 수행을 위한 데이터 객체들의 집합

배달 주문 앱에 필요한 비즈니스 도메인은 대략 회원, 주문, 음식, 배달, 결제 5가지로 분류하였다.

위의 도메인을 조금 더 세분화 하면 다음과 같이 분류할 수 있다.
상위 도메인 | 회원, 주문, 음식, 결제 |
서브 도메인 | 회원 정보, 회원 포인트 주문 정보, 배달 주문자 정보, 배달 음식 정보, 배달 추적 정보, 배달 주소 정보 음식 정보 결제 정보 |
여기서의 애그리거트(Aggregate)는 상위 도메인에 포함된 <회원, 주문, 음식, 결제> 총 4개이다.
=> 결국 에그리거트가 무엇이냐? 고유 비즈니스 목적을 수행하기 위한 데이터 객체들의 집합
👩🏻💼 Aggregate Root 란?
- 하나의 애그리거트(Aggregate)를 대표하는 도메인
도메인 규칙을 지키기 위해서는 애그리거트에 속한 모든 객체가 정상적인 상태를 가져야 함
=> 전체를 일관적인 상태로 관리하는 책임을 지는 엔티티로 애그리거트 루트를 선정
=> 가장 중요한 역할은 애그리거트의 일관성을 깨지 않는 것

애그리거트 루트 선정 방법: 각 애그리거트의 도메인들 중 다른 모든 도메인과 직간접적으로 연관되어 있는 도메인
◼ 회원 정보(Member) - 주문 정보(Order) 1:N
- 한 명의 회원 1 => 여러번 주문 가능 N
◼ 주문 정보(Orders) - 커피 정보 (Coffee) N:N
- 한 번의 주문 1 => 여러 종류의 커피 선택 가능 N
- 한 개의 커피 1 => 여러 건의 주문 N
📢 N:N 관계는 일반적으로 1:N - N:1 관계로 재설계 됨
주문 정보(Orders) - 주문 커피 정보(Order_Coffee) 1:N
주문 커피 정보(Order_Coffee) - 커피 정보(Coffee) N:1
위에서 분류한 애그리거트의 데이터 객체들 중, 애그리거트 루트로 선정한 데이터 객체를 파란색으로 표시하면 다음과 같다.
회원 애그리거트 | 회원 정보, 회원 포인트 |
주문 애그리거트 | 주문 정보, 배달 주문자 정보, 배달 음식 정보, 배달 추적 정보, 배달 주소 정보 |
음식 애그리거트 | 음식 정보 |
결제 애그리거트 | 결제 정보 |

👩🏻💼 애그리거트 루트가 강제하는 도메인 규칙을 적용하려면?
애그리거트 외부에서 애그리거트에 속한 데이터 객체들을 직접 변경하면 안됨 ❌❌
=> 애그리거트 루트 엔티티의 식별자를 통해서만 접근해야 함
엔티티 클래스 간의 관계

데이터베이스 테이블 설계

- 도메인 엔티티 클래스 간의 관계: 객체의 참조
- 데이터베이스 테이블 간의 관계: 외래키(Foreign Key) 참조
Spring Data JDBC 통한 데이터 액세스 계층 구현
1️⃣ 도메인 엔티티 클래스 정의
◼ Spring Data JDBC 에서의 애그리거트(Aggregate) 객체 매핑
1. 모든 엔티티 객체의 상태는 애그리거트 루트를 통해서만 변경할 수 있다.
2. 하나의 동일한 애그리거트 내에서의 엔티티 객체 참조
3. 애그리거트 루트 대 애그리거트 루트 간의 엔티티 객체 참조
1. 모든 엔티티 객체의 상태는 애그리거트 루트를 통해서만 변경할 수 있다.

✅ 회원 애그리거트의 애그리거트 루트: 회원 정보
회원이 비용을 지불 => 회원 포인트 적립됨 (업데이트 필요)
이 때 회원 포인트에 직접적으로 적립 ❌ => '회원정보' 애그리거트 루트를 통해 '회원 포인트'에 접근 ❗
👩🏻💼 애그리거트 루트를 통해서만 엔티티 상태를 변경해야 하는 이유?
가정) 우리가 음식을 주문한 이후, 주소를 잘못 입력하여 배달 주소를 다시 변경하려고 한다.
문제) '배달 주소 정보' 엔티티에 직접 접근 => 주소 정보를 바꾸면 => 주소 정보 변경할 수 없는 상태에서 주소 정보 변경할 수 있게 됨
해설) 음식을 배달 도중 => 배달 주소를 변경하더라도 배달 업체는 변경된 주소로 배달하지 못함 => 음식점에 직접 전화해서 변경 요청
==> 도메인 규칙에 대한 일관성이 깨진다.
해결책) 항상 '주문 정보' 라는 애그리거트 루트를 거쳐 '음식이 조리중인지 완성됐는지 확인' 등의 규칙을 검증 후
=> 검증에 통과할 경우 '배달 주소 정보' 엔티티의 상태를 업데이트 하도록 해야 함
==> 도메인 규칙의 일관성을 유지하도록 한다.
2. 하나의 동일한 애그리거트 내에서의 엔티티 객체 참조
- 동일한 하나의 애그리거트 내에서는 엔티티 간의 객체로 참조
3. 애그리거트 루트 대 애그리거트 루트 간의 엔티티 객체 참조
- 애그리거트 루트 간의 참조는 개체 참조 대신 ID로 참조
- 1:1 혹은 1:N 관계일 경우, 테이블 간의 외래키 방식과 동일
- N:N 관계일 경우 외래키 방식인 ID 참조와 객체 참조 방식이 같이 사용
◼ 엔티티 구현
✅ Member 클래스 - Order 클래스간 애그리거트 루트 매핑
@Getter
@Setter
public class Member {
@Id // memberId를 식별자로 지정
private long memberId;
private String email;
private String name;
private String phone;
}
@Getter
@Setter
@Table("ORDERS")
public class Order {
@Id
private long orderId;
// 테이블 외래키처럼 memberId 추가해서 참조하도록 함
private long memberId;
}

✅ Order 클래스 - Coffee 클래스간 애그리거트 루트 매핑
문제) Order 클래스 - Coffee 클래스는 N:N 관계
@Getter
@Setter
public class Coffee {
@Id
private long coffeeId;
private String korName;
private String engName;
private Integer price;
private String coffeeCode; // Coffee의 중복 등록을 체크하기 위한 멤버변수
}
@Getter
@Setter
@Table("ORDERS")
public class Order {
@Id
private long orderId;
// 테이블 외래키처럼 memberId를 추가한다.
private long memberId;
// (1)
@MappedCollection(idColumn = "ORDER_ID")
private Set<OrderCoffee> orderCoffees = new LinkedHashSet<>();
...
...
}

✅@MappedCollection 애너테이션의 역할
@MappedCollection(idColumn = "ORDER_ID", keyColumn = "ORDER_COFFEE_ID")
private Set<OrderCoffee> orderCoffees = new LinkedHashSet<>();
◼ idColumn
- 자식 테이블에 추가되는 외래키에 해당되는 열명을 지정
- ORDERS 테이블의 자식 테이블: ORDER_COFFEE
- ORDER_COFFEE 테이블은 ORDERS 테이블의 기본키인 ORDER_ID 열의 값을 외래키로 가짐
◼ keyColumn
- 외래키를 포함하고 있는 기본키의 열명
- ORDERS 테이블의 자식 테이블인 ORDER_COFFEE 테이블의 기본 키는 ORDER_COFFEE_ID
이제 OrderCoffee 클래스와 Coffee 클래스 간에 N:1 관계를 만들면 된다.
@Getter
@Builder
@Table("ORDER_COFFEE")
public class OrderCoffee {
@Id
private long orderCoffeeId;
private long coffeeId;
private int quantity;
}
- OrderCoffee 클래스는 주문 애그리거트 내에 있는 엔티티 클래스
- Coffee 클래스는 커피 애그리거트 내에 있는 엔티티 클래스이자 애그리거트 루트
=> 애그리거트 간의 매핑 규칙을 따름 => Member 클래스와 Order 클래스에서 했던 것처럼 : coffeeId를 외래키처럼 추가
👩🏻💼 Remember!
1. N대 N의 관계를 —> 1대 N, N대 1의 관계로 변경
2. 1대 N, N대 1의 관계를 OrderCoffee 같이 중간에서 ID를 참조하게 해주는 클래스를 통해 다시 1대 N대1의 관계로 변경
✅ Order 클래스 멤버변수 추가
@Getter
@Setter
@Table("ORDERS")
public class Order {
@Id
private long orderId;
// 테이블 외래키처럼 memberId 추가해서 참조하도록 함
private long memberId;
@MappedCollection(idColumn = "ORDER_ID")
private Set<OrderCoffee> orderCoffees = new LinkedHashSet<>();
private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST; // 주문 상태 정보
private LocalDateTime createdAt = LocalDateTime.now(); // 주문 등록 시간
public enum OrderStatus { // 총 4개의 주문 상태를 가짐 (주문을 위한 전용 상태 값으로 사용 중이므로 Order 클래스의 멤버로 존재
ORDER_REQUEST(1, "주문 요정"),
ORDER_CONFIRM(2, "주문 확정"),
ORDER_COMPILE(3, "주문 완료"),
ORDER_CANCEL(4,"주문 취소");
@Getter
private int stepNumber;
@Getter
private String stepDescription;
OrderStatus(int stepNumber, String stepDescription){
this.stepNumber = stepNumber;
this.stepDescription = stepDescription;
}
}
}
◼ OrderStatus enum
- 주문의 상태를 나타내는 enum
- OrderStatus는 현재 Order 클래스에서만 사용 => 따라서 Order 클래스의 멤버로 존재하는 것이 적절
✅ 테이블 생성 스크립트 추가
CREATE TABLE IF NOT EXISTS MEMBER (
MEMBER_ID bigint NOT NULL AUTO_INCREMENT,
EMAIL varchar(100) NOT NULL UNIQUE,
NAME varchar(100) NOT NULL,
PHONE varchar(100) NOT NULL,
PRIMARY KEY (MEMBER_ID)
);
CREATE TABLE IF NOT EXISTS COFFEE (
COFFEE_ID bigint NOT NULL AUTO_INCREMENT,
KOR_NAME varchar(100) NOT NULL,
ENG_NAME varchar(100) NOT NULL,
PRICE int NOT NULL,
COFFEE_CODE char(3) NOT NULL,
PRIMARY KEY (COFFEE_ID)
);
CREATE TABLE IF NOT EXISTS ORDERS (
ORDER_ID bigint NOT NULL AUTO_INCREMENT,
MEMBER_ID bigint NOT NULL,
ORDER_STATUS varchar(20) NOT NULL,
CREATED_AT datetime NOT NULL,
PRIMARY KEY (ORDER_ID),
FOREIGN KEY (MEMBER_ID) REFERENCES MEMBER(MEMBER_ID)
);
CREATE TABLE IF NOT EXISTS ORDER_COFFEE (
ORDER_COFFEE_ID bigint NOT NULL AUTO_INCREMENT,
ORDER_ID bigint NOT NULL,
COFFEE_ID bigint NOT NULL,
QUANTITY int NOT NULL,
PRIMARY KEY (ORDER_COFFEE_ID),
FOREIGN KEY (ORDER_ID) REFERENCES ORDERS(ORDER_ID),
FOREIGN KEY (COFFEE_ID) REFERENCES COFFEE(COFFEE_ID)
);
- 마지막으로 src/main/resources/db/h2/schema.sql ’ 파일에 테이블 생성 스크립트를 추가
=> 애플리케이션 실행 시, 테이블이 생성되도록 함
엔티티 클래스에 대한 정의가 끝났다.
이제 서비스 클래스와 데이터 액세스 계층의 리포지토리 클래스를 구현해보자.
2️⃣ 서비스 ,리포지토리 구현
서비스 클래스가 리포지토리 클래스를 사용하는 입장이므로 리포지토리(Repository) 인터페이스를 먼저 알아보자.
리포지토리(Repository)
- Spring data JDBC, Spring Data JPA의 데이터 액세스 계층에서 데이터베이스와 상호작용하는 역할을 하는 인터페이스
- DDD(Domain Driven Design, 도메인 주도 설계)에서 사용하는 언어
리포지토리(Repository) 구현
MemberRepository 인터페이스 정의
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface MemberRepository extends CrudRepository<Member, Long> {
// ‘find + By + SQL 쿼리문에서 WHERE 절의 열명 + (WHERE 절 열의 조건이 되는 데이터)’
Optional<Member> findByEmail(String email);
}
✅ CrudRepository<Member, Long>
- Member: Member 엔티티 클래스를
- Long: Member 엔티티 클래스에서 @Id 애너테이션이 붙은 멤버 변수의 타입
✅ Spring Data JDBC에서의 쿼리 메서드(Query Method) 정의
- ‘find + By + SQL 쿼리문에서 WHERE 절의 열명 + (WHERE 절 열의 조건이 되는 데이터)’ 형식
=> 조건에 맞는 데이터를 테이블에서 조회
✅ Optional<Member> findByEmail(String email);
- 위 코드는 email 열을 WHERE 절의 조건으로 지정해서 MEMBER 테이블에서 하나의 row를 조회하겠다고 정의함
- 위에서 정의한 쿼리 메서드는 내부적으로 아래의 SQL 쿼리문으로 변환 => 데이터베이스의 MEMBER 테이블에 질의를 보냄
SELECT "MEMBER"."NAME" AS "NAME", "MEMBER"."PHONE" AS "PHONE", "MEMBER"."EMAIL" AS "EMAIL", "MEMBER"."MEMBER_ID" AS "MEMBER_ID" FROM "MEMBER" **WHERE "MEMBER"."EMAIL" = ?**
- 위 쿼리 메서드는 이미 테이블에 등록된 이메일 주소가 있는지 확인하기 위한 용도로 사용
- Spring Data JDBC 에서 지원하는 Optional로 래핑할 수 있음
CoffeeRepository 인터페이스 정의
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface CoffeeRepository extends CrudRepository<Coffee, Long> {
Optional<Coffee> findByCoffeeCode(String CoffeeCode);
@Query("SELECT * FROM COFFEE WHERE COFFE_ID = :coffeeId")
Optional<Coffee> findByCoffee(Long coffeeId);
}
✅ Optional<Coffee> findByCoffeeCode(String CoffeeCode);
- WHERE 절에서 COFFEE_CODE를 조건으로 질의하게 해주는 쿼리 메서드
✅ @Query("SELECT * FROM COFFEE WHERE COFFE_ID = :coffeeId")
- @Query: 개발자가 직접 쿼리문을 작성해서 질의할 수 있게 함
- :coffeeId는 findByCoffeeId(Long coffeeId)의 coffeeId 변수 값이 채워지는 동적 쿼리 파라미터(named parameter)
OrderRepository 인터페이스 정의
import org.springframework.data.repository.CrudRepository;
public interface OrderRepository extends CrudRepository<Order, Long> {
}
👩🏻💼 인터페이스의 내용이 비어있는데 굳이 정의한 이유?
- OrderRepository 인터페이스는 CrudRepository 인터페이스를 상속
=> CrudRepository에 이미 정의되어 있는 기본 쿼리메서드를 서비스 클래스에서 사용 가능
서비스(Service) 클래스 구현
MemberService 클래스 구현
@Service
public class MemberService {
private MemberRepository memberRepository;
// MemberRepository DI
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member createMember(Member member) {
// 이미 등록된 이메일인지 검증
verifyExistsEmail(member.getEmail());
// 회원 정보 저장
return memberRepository.save(member);
}
public Member updateMember(Member member) {
// 존재하는 회원인지 검증
Member findMember = findVerifiedMember(member.getMemberId());
// 이름, 휴대폰 정보 업데이트
Optional.ofNullable(member.getName()).ifPresent(name -> findMember.setName(name));
Optional.ofNullable(member.getPhone()).ifPresent(phone -> findMember.setPhone(phone));
// 회원 정보 업데이트
return memberRepository.save(findMember);
}
public Member findMember(long memberId) {
// 특정 회원 정보 조회
return findVerifiedMember(memberId);
}
public List<Member> findMembers() {
// 모든 회원 정보 조회
return (List<Member>) memberRepository.findAll();
}
public void deleteMember(long memberId) {
Member findMember = findVerifiedMember(memberId);
// 특정 회원 정보 삭제
memberRepository.delete(findMember);
}
// 이미 등록된 이메일인지 검증하는 메서드
private void verifyExistsEmail(String email) {
Optional<Member> member = memberRepository.findByEmail(email);
if (member.isPresent())
throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
}
// 존재하는 회원인지 검증하는 메서드
private Member findVerifiedMember(long memberId) {
Optional<Member> optionalMember = memberRepository.findById(memberId);
Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return findMember;
}
}
MemberRepositoy 인터페이스는 정의했으나 인터페이스의 구현 클래스는 별도로 구현하지 않았다.
👩🏻💼 MemberRepository 인터페이스의 구현 클래스는 누가 구현했나?
Spring Data JDBC에서 내부적으로 Java 리플렉션 기술 + Proxy 기술을 이용
=> MemberRepository 인터페이스의 구현 클래스 객체를 생성
✅ Optional.ofNullable(..) 사용하는 이유
- 파라미터로 전달받은 member 객체는 클라이언트 쪽에서 사용자가 이름, 휴대폰 정보를 선택적으로 수정 가능
=> name 멤버 변수: null 일 수가 있고, phone 멤버 변수: null 일 수 있음
==> 결국 멤버 변수 값이 null일 경우에는 Optional.of()가 아닌, Optional.ofNullable()이용하여 null값을 허용할 수 있음
따라서 값이 null일지라도 NullPointerException 발생 ❌ => ifPresent() 메서드 내의 코드가 실행 가능
✅ verifyExistsEmail(String email)
- MemberRepository에 정의되어 있는 findByEmail() 쿼리 메서드로 이메일에 해당하는 회원이 있는지 조회
✅ if(member.isPresent())
- findByEamil() 쿼리 메서드의 리턴 값: Optional 이기 때문에 => isPresent() 통해 결과값이 존재하는지 확인 가능
✅ deleteMember() 메서드의 리턴 값 memberRepository.delete(findMember);
- 여기서는 회원 정보 자체를 테이블에서 삭제했다.
But 실무에서는 테이블의 데이터 자체를 삭제 ❌ => MEMBER_STATUS 같은 열을 두어 상태 값만 변경 ❗
CoffeeService 클래스 구현
@Service
public class CoffeeService {
private CoffeeRepository coffeeRepository;
// CoffeeRepository DI
public CoffeeService(CoffeeRepository coffeeRepository) {
this.coffeeRepository = coffeeRepository;
}
public Coffee createCoffee(Coffee coffee) {
// 커피 코드를 대문자로 변경
String CoffeeCode = coffee.getCoffeeCode().toUpperCase();
// 이미 등록된 커피 코드인지 확인
verifyExistCoffee(CoffeeCode);
coffee.setCoffeeCode(CoffeeCode);
// 커피 정보 저장
return coffeeRepository.save(coffee);
}
public Coffee updateCoffee(Coffee coffee) {
// 조회하려는 커피가 검증된 커피인지 확인
Coffee findCoffee = findVerifiedCoffee(coffee.getCoffeeId());
// 한글메뉴, 영어메뉴, 가격 정보 업데이트
Optional.ofNullable(coffee.getKorName()).ifPresent(korName -> findCoffee.setKorName(korName));
Optional.ofNullable(coffee.getEngName()).ifPresent(engName -> findCoffee.setEngName(engName));
Optional.ofNullable(coffee.getPrice()).ifPresent(price -> findCoffee.setPrice(price));
// 커피 정보 업데이트
return coffeeRepository.save(findCoffee);
}
public Coffee findCoffee(long coffeeId) {
// 커피 정보 조회
return findVerifiedCoffee(coffeeId);
}
public List<Coffee> findCoffees() {
// 모든 커피 정보 조회
return (List<Coffee>) coffeeRepository.findAll();
}
public void deleteCoffee(long coffeeId) {
Coffee findCoffee = findVerifiedCoffee(coffeeId);
// 특정 커피 정보 삭제
coffeeRepository.delete(findCoffee);
}
// 주문에 해당하는 커피 정보 조회
public List<Coffee> findOrderedCoffees(Order order) {
return order.getOrderCoffees()
.stream()
.map(orderCoffee -> findCoffee(orderCoffee.getCoffeeId()))
.collect(Collectors.toList());
}
// 이미 등록된 커피 코드인지 확인하는 메서드
private void verifyExistCoffee(String CoffeeCode) {
Optional<Coffee> coffee = coffeeRepository.findByCoffeeCode(CoffeeCode);
if (coffee.isPresent()) throw new BusinessLogicException(ExceptionCode.COFFEE_CODE_EXISTS);
}
// 존재하는 커피인지 검증하는 메서드
public Coffee findVerifiedCoffee(long CoffeeId) {
Optional<Coffee> optionalCoffee = coffeeRepository.findById(CoffeeId);
Coffee findCoffee = optionalCoffee.orElseThrow(() -> new BusinessLogicException(ExceptionCode.COFFEE_NOT_FOUND));
return findCoffee;
}
}
✅ String CoffeeCode = Coffee.getCoffeeCode.toUppserCase();
- 클라이언트 쪽에서 사용자가 대소문자를 가리지 않고 입력하더라도 일괄적으로 대문자로 변경
=> 사용자가 대소문자에 신경 쓰지 않고 입력할 수 있도록 사용자 편의성을 높여주는 기능
✅ public List<Coffee> findOrderedCoffees(Order order)
- Order 객체는 memberId와 orderStatus 값만 얻을 수 있음 => 실제 회원이 주문한 커피 정보는 얻을 수 없음
=> getOrderCoffees() 통해 주문한 구체적인 커피 정보를 얻어와야 함
==> findOrderedCoffees(Order order) 메서드의 리턴값: OrderResponseDto 클래스에 포함됨
OrderService 클래스 구현
@Service
public class OrderService {
final private OrderRepository orderRepository;
final private MemberService memberService;
final private CoffeeService coffeeService;
// OrderRepository, MemberService, CoffeeService DI
public OrderService(OrderRepository orderRepository, MemberService memberService, CoffeeService coffeeService) {
this.orderRepository = orderRepository;
this.memberService = memberService;
this.coffeeService = coffeeService;
}
public Order createOrder(Order order) {
// 회원이 존재하는지 확인
memberService.findVerifiedMember(order.getMemberId());
// 커피가 존재하는지 조회
order.getOrderCoffees()
.stream()
.forEach(orderCoffee -> coffeeService.findVerifiedCoffee(orderCoffee.getCoffeeId()));
// 주문 정보 저장
return orderRepository.save(order);
}
public Order findOrder(long orderId) {
// 주문 정보 조회
return findVerifiedOrder(orderId);
}
// 주문 수정 메서드는 사용하지 않습니다.
public List<Order> findOrders() {
// 모든 주문 정보 조회
return (List<Order>) orderRepository.findAll();
}
public void cancelOrder(long orderId) {
Order findOrder = findVerifiedOrder(orderId);
int step = findOrder.getOrderStatus().getStepNumber();
// OrderStatus의 setp이 2 미만일 경우, 주문 취소가 되도록 함
if (step >= 2) {
throw new BusinessLogicException(ExceptionCode.CANNOT_CHANGE_ORDER);
}
findOrder.setOrderStatus(Order.OrderStatus.ORDER_CANCEL);
orderRepository.save(findOrder);
}
// 주문 정보가 유효한지 검증하는 메서드
private Order findVerifiedOrder(long orderId) {
Optional<Order> optionalOrder = orderRepository.findById(orderId);
Order findOrder = optionalOrder.orElseThrow(() -> new BusinessLogicException(ExceptionCode.ORDER_NOT_FOUND));
return findOrder;
}
}
✅ createOrder() 메서드 내의 getOrderCoffees()
order.getOrderCoffees()
.stream()
.forEach(orderCoffee -> {
coffeeService.findVerifiedCoffee(orderCoffee.getCoffeeId());
});
- 주문하려는 커피가 존재하는지 여부를 확인
(1) order.getOrderCoffees()를 통해서 Set<OrderCoffee>를 가져온 후
(2) Java의 Stream으로 각각의 coffeeId를 얻은 후에
(3) findVerifiedCoffee(orderCoffee.getCoffeeId()) 메서드를 통해 coffeeId에 해당하는 커피 정보가 유효한지 검증
기타 수정된 코드
- CoffeePostDto 코드
- OrderPostDto 코드
- OrderCoffeeResponseDto 코드 추가
- OrderResponseDto 코드
- OrderMapper 코드
- ExcepionCode 클래스
CoffeePostDto 코드
@Getter
public class CoffeePostDto {
@NotBlank
private String korName;
@NotBlank
@Pattern(regexp = "^([A-Za-z])(\\s?[A-Za-z])*$",
message = "커피명(영문)은 영문이어야 합니다(단어 사이 공백 한 칸 포함). 예) Cafe Latte")
private String engName;
@Range(min= 100, max= 50000)
private int price;
@NotBlank
@Pattern(regexp = "^([A-Za-z]){3}$",
message = "커피 코드는 3자리 영문이어야 합니다.")
private String CoffeeCode;
}
✅ private String CoffeeCode;
- 커피 상품의 고유 식별 코드
OrderController 코드
@RestController
@RequestMapping("/v10/orders")
@Validated
public class OrderController {
private final static String ORDER_DEFAULT_URL = "/v10/orders";
private final OrderService orderService;
private final OrderMapper mapper;
private final CoffeeService coffeeService;
public OrderController(OrderService orderService, OrderMapper mapper, CoffeeService coffeeService) {
this.orderService = orderService;
this.mapper = mapper;
this.coffeeService = coffeeService;
}
@PostMapping
public ResponseEntity postOrder(@Valid @RequestBody OrderPostDto orderPostDto) {
Order order = orderService.createOrder(mapper.orderPostDtoToOrder(orderPostDto));
// 등록된 주문에 해당하는 URI 객체
URI location =
UriComponentsBuilder
.newInstance()
.path(ORDER_DEFAULT_URL + "/{order-id}")
.buildAndExpand(order.getOrderId())
.toUri(); // "/v10/orders/{order-id}"
return ResponseEntity.created(location).build();
}
@GetMapping("/{order-id}")
public ResponseEntity getOrder(@PathVariable("order-id") @Positive long orderId) {
Order order = orderService.findOrder(orderId);
// 주문한 커피 정보 가져오도록 수정
return new ResponseEntity<>(mapper.orderToOrderResponseDto(coffeeService, order), HttpStatus.OK);
}
@GetMapping
public ResponseEntity getOrders() {
List<Order> orders = orderService.findOrders();
// 주문한 커피 정보 가져오도록 수정
List<OrderResponseDto> response =
orders.stream()
.map(order -> mapper.orderToOrderResponseDto(coffeeService, order))
.collect(Collectors.toList());
return new ResponseEntity<>(response, HttpStatus.OK);
}
@DeleteMapping("/{order-id}")
public ResponseEntity cancelOrder(@PathVariable("order-id") @Positive long orderId) {
orderService.cancelOrder(orderId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
✅ postOrder() 핸들러 메서드의 수정 내용
@PostMapping
public ResponseEntity postOrder(@Valid @RequestBody OrderPostDto orderPostDto) {
Order order = orderService.createOrder(mapper.orderPostDtoToOrder(orderPostDto));
// (2) 등록된 주문(Resource)에 해당하는 URI 객체
URI location =
UriComponentsBuilder
.newInstance()
.path(ORDER_DEFAULT_URL + "/{order-id}")
.buildAndExpand(order.getOrderId())
.toUri(); // "/v10/orders/{order-id}"
return ResponseEntity.created(location).build(); // (3) HTTP 201 Created status
}
- 이전까지는 디비 연동하지 않음
=> OrderController에서 OrderService의 createOrder(order) 호출해 등록할 주문 전달
=> 전달한 Order 객체를 createOrder(order) 리턴 값으로 리턴
- 등록한 주문 정보가 디비에 저장되며, ORDER 테이블에 하나의 ROW로 간주
<=> ORDER_ID 라는 고유 식별자를 가지는 주문 정보로써의 역할 수행
- 클라이언트 => 백엔드 측에 특정 리소스를 요청할 경우 => response header에 HTTP Status 추가
=> 클라이언트 측에 응답으로 전달
- 디비에 저장된 리소스 위치를 알려주는 위치 정보 URI => response header에 URI 추가
=> 클라이언트 측에 응답으로 전달
✅ UriComponentsBuilder: 등록된 리소스(주문 정보)의 위치 정보인 URI 객체를 생성
✅ ResponseEntity.*created*(location).build();
- 내부적으로 201 Created HTTP Status를 response header에 추가
- 별도의 response body는포함하지 않음
OrderPostDto 코드
- 여러 잔의 커피를 주문할 수 있도록 수정
package com.codestates.order.dto;
import lombok.Getter;
import javax.validation.Valid;
import javax.validation.constraints.Positive;
import java.util.List;
@Getter
public class OrderPostDto {
@Positive
private long memberId;
// 여러 잔의 커피를 주문할 수 있도록 수정
@Valid
private List<OrderCoffeeDto> orderCoffees;
// * jackson library가 빈 생성자가 없는 모델을 생성하는 방법을 모름
// -> 따로 빈 생성자를 추가해줘야 함!
public OrderPostDto(){
}
public OrderPostDto(long memberId, List<OrderCoffeeDto> orderCoffees) {
this.memberId = memberId;
this.orderCoffees = orderCoffees;
}
}
OrderCoffeeDto 클래스 추가
- 여러 잔의 커피 정보를 주문하기 위해 추가된 DTO 클래스
@Getter
@AllArgsConstructor
public class OrderCoffeeDto { // 여러 잔의 커피 정보를 주문하기 위해 추가된 DTO 클래스
@Positive
private long coffeeId;
@Positive
private int quantity;
}
OrderCoffeeResponseDto 코드 추가
- 주문한 여러 잔의 커피 정보를 응답으로 제공하기 위해 추가된 DTO 클래스
@Getter
@AllArgsConstructor
public class OrderCoffeeResponseDto { // 주문한 여러 잔의 커피 정보를 응답으로 제공하기 위해 추가된 DTO 클래스
private long coffeeId;
private String korName;
private String engName;
private int price;
private int quantity;
}
OrderReponseDto 코드
@Getter
@Setter
public class OrderResponseDto {
private long memberId;
private long coffeeId;
private long orderId;
private Order.OrderStatus orderStatus;
private List<OrderCoffeeResponseDto> orderCoffees;
private LocalDateTime createdAt;
}
추가된 기능
- 주문한 여러 건의 커피 정보를 응답으로 전송할 수 있도록 변경
- 주문 시간과 주문 상태를 응답으로 전송할 수 있도록 변경
OrderMapper 코드
@Mapper(componentModel = "spring")
public interface OrderMapper {
default Order orderPostDtoToOrder(OrderPostDto orderPostDto){
// orderPostDto에 포함된 memberId를 Order클래스의 memberId에 할당
Order order = new Order();
order.setMemberId(orderPostDto.getMemberId());
Set<OrderCoffee> orderCoffees = (Set<OrderCoffee>) orderPostDto.getOrderCoffees()
.stream()
.map(orderCoffeeDto ->
OrderCoffee.builder()
.coffeeId(orderCoffeeDto.getCoffeeId())
.quantity(orderCoffeeDto.getQuantity())
.build())
.collect(Collectors.toList());
order.setOrderCoffees(orderCoffees);
return order;
}
default OrderResponseDto orderToOrderResponseDto(CoffeeService coffeeService, Order order){
long memberId = order.getMemberId();
List<OrderCoffeeResponseDto> orderCoffees =
orderCoffeesToOrderCoffeeResponseDtos(coffeeService, order.getOrderCoffees());
OrderResponseDto orderResponseDto = new OrderResponseDto();
orderResponseDto.setOrderCoffees(orderCoffees);
orderResponseDto.setMemberId(memberId);
orderResponseDto.setCreatedAt(order.getCreatedAt());
orderResponseDto.setOrderId(order.getOrderId());
orderResponseDto.setOrderStatus(order.getOrderStatus());
return orderResponseDto;
}
default List<OrderCoffeeResponseDto> orderCoffeesToOrderCoffeeResponseDtos(CoffeeService coffeeService, Set<OrderCoffee> orderCoffees){
return orderCoffees.stream()
.map(orderCoffee -> {
Coffee coffee = coffeeService.findCoffee(orderCoffee.getCoffeeId());
return new OrderCoffeeResponseDto(coffee.getCoffeeId(),
coffee.getKorName(),
coffee.getEngName(),
coffee.getPrice(),
orderCoffee.getQuantity());
}).collect(Collectors.toList());
}
}
✅ orderPostDtoToOrder(OrderPostDto orderPostDto)
- 등록하고자 하는 커피 주문 정보(OrderPostDto)를 Order 엔티티 클래스의 객체로 변환하는 역할
default Order orderPostDtoToOrder(OrderPostDto orderPostDto) {
Order order = new Order();
// (1)
order.setMemberId(orderPostDto.getMemberId());
// (2)
Set<OrderCoffee> orderCoffees = orderPostDto.getOrderCoffees()
.stream()
.map(orderCoffeeDto ->
// (2-1)
OrderCoffee.builder()
.coffeeId(orderCoffeeDto.getCoffeeId())
.quantity(orderCoffeeDto.getQuantity())
.build())
.collect(Collectors.toSet());
order.setOrderCoffees(orderCoffees);
return order;
}
(1) orderPostDto에 포함된 memberId => Order 클래스의 memberId에 할당
(2) orderPostDto에 포함된 주문한 커피 정보인 List<OrderCoffeeDto> orderCoffees
=> Java의 Stream을 이용해 Order 클래스의 Set<OrderCoffee> orderCoffees으로 변환
(2-1) OrderCoffee 클래스에 @Builder 애너테이션이 적용됨 => lombok에서 지원하는 빌더 패턴 사용 가능
=> 빌더 패턴 이용해 List<OrderCoffeeDto> orderCoffees에 포함된 주문한 커피 정보 => OrderCoffee의 필드에 추가
빌더 패턴을 사용하는 것은 필수는 ❌ + new 키워드로 객체를 생성해도 ⭕
✅ orderToOrderResponseDto(CoffeeService coffeeService, Order order)
- 데이터베이스에서 조회한 Order 객체를 OrderResponseDto 객체로 변환해 주는 역할
default OrderResponseDto orderToOrderResponseDto(CoffeeService coffeeService,
Order order) {
// (1)
long memberId = order.getMemberId();
// (2)
List<OrderCoffeeResponseDto> orderCoffees =
orderCoffeesToOrderCoffeeResponseDtos(coffeeService, order.getOrderCoffees());
OrderResponseDto orderResponseDto = new OrderResponseDto();
orderResponseDto.setOrderCoffees(orderCoffees);
orderResponseDto.setMemberId(memberId);
orderResponseDto.setCreatedAt(order.getCreatedAt());
orderResponseDto.setOrderId(order.getOrderId());
orderResponseDto.setOrderStatus(order.getOrderStatus());
// TODO 주문에 대한 더 자세한 정보로의 변환은 요구 사항에 따라 다를 수 있습니다.
return orderResponseDto;
}
(1) Order의 memberId 필드 값을 얻음
(2) 주문한 커피 정보를 조회하기 위해 orderToOrderCoffeeResponseDto(coffeeService, order.getOrderCoffees()); 호출
- order.getOrderCoffees()의 리턴 값: Set<OrderCoffee> orderCoffees
=> orderCoffees에는 커피명이나 가격 같은 구체적인 커피 정보가 포함되지 ❌
=> 데이터베이스에서 구체적인 커피 정보를 조회하는 추가 작업을 수행
✅ orderCoffeesToOrderCoffeeResponseDtos(CoffeeService coffeeService, Set<OrderCoffee> orderCoffees)
- 데이터베이스에서 커피의 구체적인 정보를 조회한 후, OrderCoffeeResponseDto에 커피 정보를 채워 넣는 역할
default List<OrderCoffeeResponseDto> orderCoffeesToOrderCoffeeResponseDtos(
CoffeeService coffeeService,
Set<OrderCoffee> orderCoffees) {
// (1)
return orderCoffees.stream()
.map(orderCoffee -> {
// (1-1)
Coffee coffee = coffeeService.findCoffee(orderCoffee.getCoffeeId());
return new OrderCoffeeResponseDto(coffee.getCoffeeId(),
coffee.getKorName(),
coffee.getEngName(),
coffee.getPrice(),
orderCoffee.getQuantity());
}).collect(Collectors.toList());
}
(1) 파라미터로 전달받은 orderCoffees를 Java의 Stream을 이용해 데이터베이스에서 구체적인 커피 정보를 조회 => OrderCoffeeResponseDto로 변환
(1-1) 파라미터로 전달받은 coffeeService 객체를 이용해 coffeeId에 해당하는 Coffee를 조회
ExceptionCode 클래스
- Spring Data JDBC 적용으로 인해 수정된 ExceptionCode(ExceptionCode.java)
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member not found"),
MEMBER_EXISTS(409, "Member exists"),
COFFEE_NOT_FOUND(404, "Coffee not found"),
COFFEE_CODE_EXISTS(409, "Coffee Code exists"),
ORDER_NOT_FOUND(404, "Order not found"),
CANNOT_CHANGE_ORDER(403, "Order can not change"),
NOT_IMPLEMENTATION(501, "Not Implementation");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int code, String message) {
this.status = code;
this.message = message;
}
}
구현한 커피 주문 샘플 애플리케이션 실행
1. 회원 정보 등록


2. 커피 정보 등록


