[Spring Data JDBC]
객체 중심 기술(ORM)
데이터를 SQL 쿼리문 위주로 생각하는 것이 아니라, 모든 데이터를 객체 관점으로 바라보는 기술.
DB에 접근하기 위해서 SQL 쿼리문을 직접 작성하기 보다는,
애플리케이션 내부에서 Java 객체를 SQL 쿼리문으로 자동 변환 후 DB에 접근 하는 방식.
[Spring Data JDBC 적용 설정 4가지]
1. 의존 라이브러리 추가 - build.gradle 파일에 코드 추가.
2. resources - application.yml에 DB 설정.
3. schema.sql 파일에 필요한 테이블 쿼리문 작성.
4. application.yml에 schema.sql 파일을 읽어서 테이블을 생성할 수 있도록 초기화 설정.
[Spring Data JDBC 적용에 필요한 클래스/인터페이스 7가지]
1. Entity 클래스
2. Service 클래스
3. Controller 클래스
4. ResponseDto 클래스
5. Dto 클래스
6. Repository 인터페이스
7. Mapper 인터페이스
[Spring Data JDBC 적용 설정 4가지]
1. 의존 라이브러리 및 Mapper 설정 추가
패키지 경로에 있는 build.gradle 파일에 코드 추가.
[인메모리 DB H2 추가] 인메모리 : 애플리케이션 동작시에만 사용이 가능한 DB, 애플리케이션이 종료시 내용이 사라진다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'com.h2database:h2'
}
[Mapper 설정 추가]
dependencies {
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}
2. application.yml 파일에 사용할 DB 설정
src/main/resources/application.properties에 application.yml 파일이 있다.
이 파일에 Spring에서 사용하는 다양한 설정 정보를 입력할 수 있다.
spring:
h2: // [인메모리 h2 추가]
console:
enabled: true
이제 웹 브라우저 상에서 H2 DB에 접속 후 DB 관리가 가능하다.
Spring을 실행시킨 뒤, 나오는 log에서 하기 내용을 찾는다.
H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:26d0d5d3-dcef-47f8-8e6b-67898bdcfbd0'
위의 내용에서 /h2-console 파트는 웹 브라우저에 localhost:8080/h2-console 입력으로 H2 DB 접속 가능.
접속 후 JDBC URL 파트에는 'jdbc:h2:mem:26d0d5d3-dcef-47f8-8e6b-67898bdcfbd0' 을 입력한다.
[H2 DB URL Default 설정]
기존의 URL은 입력하기 번거로운 구조로 되어 있기에, 하기 코드로 변경한다.
spring:
h2:
console:
enabled: true
path: /h2 # (1) Context path 변경 // 웹 브라우저 H2 DB URL 주소
datasource:
url: jdbc:h2:mem:test # (2) JDBC URL 변경 // H2 DB의 JDBC URL 주소
3. Schema.sql 파일에 테이블 쿼리문 작성 // src/main/resources/db/h2 - schema.sql
CREATE TABLE IF NOT EXISTS MESSAGE (
message_id bigint NOT NULL AUTO_INCREMENT,
message varchar(100) NOT NULL,
PRIMARY KEY (message_id)
);
[테이블과 클래스의 매핑 관계]
MESSAGE 테이블 = Message 엔티티 클래스
message_id (컬럼) = Message 엔티티 클래스 EX) int messageId; (멤버 변수)
message (컬럼) = Message 엔티티 클래스 EX) String message; (멤버 변수)
4. application.yml에서 schema.sql 읽을 수 있도록 파일 위치 설정 추가.
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
sql:
init:
schema-locations: classpath*:db/h2/schema.sql // (1) 테이블 생성 파일 경로
[클라이언트 요청부터 Spring Data JDBC의 작동 순서 ]
1. Controller 클래스에서 클라이언트로부터 요청 데이터 전달받음.
2. 전달받은 요청 데이터의 HttpMethod에 일치하는 핸들러 메서드를 실행.
3. 핸들러 메서드는 요청 데이터에 필요한 데이터들을 DTO 객체로 전달받고 비즈니스 로직 처리를 위해
Mapper 인터페이스 객체를 통해 DTO -> Entity 객체로 변경하여 Service 객체를 통해 메서드 실행
4. Service 클래스의 메서드는 Repository 인터페이스 객체+Crud 인터페이스 메서드를 통해
비즈니스 로직을 처리함. 비즈니스 로직을 거친 데이터는 Entity 객체 타입의 데이터 이므로
API계층의 핸들러 메서드로 응답 데이터 타입인 DTO로 변환하기 위해,
Mapper 인터페이스 객체를 통해 Entity -> Dto 타입으로 변경하여
HttpRequest 처리 결과 메시지와 함께 ResponseEntity 타입으로 응답 데이터 리턴.
[Spring Data JDBC 적용에 필요한 클래스/인터페이스 7가지 및 역할]
1. Controller 클래스 // @RestController, @RequestMapping("/URI") 포함.
Mapper 인터페이스, Service 객체 필드로 가짐.
클라이언트로부터 요청 데이터를 받고, 응답 데이터를 전송하는 역할.
2. 핸들러 메서드 // @PostMapping / GetMapping / DeleteMapping / PatchMapping
Controller 클래스에 작성된 @HttpMethod 애너테이션이 붙은 메서드.
ResponseEntity 타입이며, 파라미터로 DTO 객체를 주입받는다.
주입받은 DTO 객체를 활용하여 Mapper를 통해 DTO -> Entity 전환 후
Service 객체를 통해 Service 클래스의 메서드를 실행시킨다.
Service 메서드는 Repository 인터페이스가 상속하고 있는 CrudRepository 메서드로 동작하여 값이 나온다.
처리된 데이터는 아직 Entity 타입이므로 다시 Mapper 객체를 통해 Dto 타입으로 전환하여 리턴한다.
혹은 ResponseEntity 타입으로 처리 데이터와 HttpRequest 메세지를 포함하여 생성한다.
3. Service 클래스 // @Service 포함
Repository 인터페이스를 필드로 가짐.
Controller 클래스의 핸들러 메서드와 연결되는 메서드를 가지는 객체.
4. DTO 클래스 // @Getter, @Setter 포함
클라이언트로부터 전달받는 요청 데이터, 응답 데이터의 타입. 핸들러 메서드 타입별로 DTO 클래스가 필요
요청 데이터에 포함된 데이터들을 private 속성의 필드로 가지며 각 필드들은 유효성 검사 애너테이션을 가짐.
5. ResponseEntity
클라이언트에게 전달할 응답 데이터의 형태. ResponseDto + HttpStatus의 형태.
6. Entity 클래스 // @Getter, @Setter, @NoArgsConstructor, @AllArgsConstructor
Controller 패키지당 1개의 Entity 클래스를 가지고 있으며,
DTO에 포함되는 데이터들을 private 속성 필드로 가지고 있으며, 변수들을 구분 짓는 id 변수를 가진다.
7. Mapper 인터페이스 // @Mapper(componentModel="spring") 포함.
API 계층 : DTO, Service 계층 : Entity 와 같은 계층간에 요구되는 데이터 타입으로 변환해주는 역할.
@Mapper(componentModel = "spring")
public interface MessageMapper {
// Entity타입 DtoToEntity (핸들러메서드별Dto dto);
Message messageDtoToMessage(MessagePostDto messagePostDto);
// ResponseDto타입(응답) EntityToResponseDto (Entity entity);
MessageResponseDto messageToMessageResponseDto(Message message);
}
8. Repository 인터페이스 // extends CrudRepository<Entity클래스명, id 데이터 타입>
비즈니스 로직을 위한 Service 클래스의 메서드에 return 데이터 타입을 처리하는데 사용하는 객체
@Service
public class MessageService {
// (1) Service클래스는 Repository를 필드로 가진다.
private final MessageRepository messageRepository;
// (2) 생성자로 필드 초기화.
public MessageService(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
// (3) Controller 핸들러 메서드와 연결되어 처리하는 비즈니스 로직의 메서드.
public Message createMessage(Message message) {
// (4) repository인터페이스 구현체를 사용하여 .save(Entity객체) 처리하여 리턴.
return messageRepository.save(message);
}
}
[Spring Data JDBC 적용에 필요한 클래스/인터페이스 7가지 작성하기]
1. DB 테이블과 매핑할 Entity 클래스 작성. // 필드로는 테이블에 컬럼들을 선언함. PK는 @Id 필요.
핸들러 메서드 들이 필요로 하는 변수들을 private 타입으로 가진다. DTO 클래스와는 다르게 @Id 식별자 변수가 있다.
@Getter
@Setter
public class Message { // (1)
@Id // (2)
private long messageId;
private String message;
}
2. 응답으로 전달할 ResponseDto 클래스 생성 // 필요한 필드 : 식별자, 메세지 내용.
@Getter
@Setter
public class MessageResponseDto {
private long messageId;
private String message;
}
3. 핸들러 메서드별로 DTO 생성 // Post : 등록에 필요한 데이터 String message;
각 필드들은 유효성 애너테이션을 가진다.
@Getter
public class MessagePostDto {
@NotBlank
private String message;
}
4. Controller 클래스 생성 // Service 클래스, Mapper 인터페이스 필드로 필요. 핸들러 메서드.
@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 = // mapper로 Dto => Entity 타입 변환
messageService.createMessage(mapper.messageDtoToMessage(messagePostDto));
// mapper로 Entity => ResponseDto 타입 변환
return ResponseEntity.ok(mapper.messageToMessageResponseDto(message));
}
}
5. Mapper 인터페이스 생성 // API 계층 데이터타입, Service 계층 데이터타입 변환을 위함.
2가지 타입의 메서드를 가진다.
A : 엔티티 타입 엔티티toDTO (메서드별Dto타입 Dto이름);
B : ResponseDto타입 entityToResponse (Entity entity);
@Mapper(componentModel = "spring") // (1)
public interface MemberMapper {
Member memberPostDtoToMember(MemberPostDto memberPostDto);
Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
MemberResponseDto memberToMemberResponseDto(Member member);
}
6. Entity 클래스를 기반으로 실질적으로 DB에 작업을 처리할 Repository 인터페이스 작성
import org.springframework.data.repository.CrudRepository;
public interface MessageRepository extends CrudRepository<Message, Long> {
}
** Repository 인터페이스는 Service 클래스에 DI 주입 후 메서드를 통한 DB 작업에 사용된다.
7. Service 클래스에서 Repository 인터페이스 사용할 수 있도록 필드 선언 및 메서드 활용.
@Service // 해당 클래스가 Service 클래스임을 명시.
public class MessageService {
// (1) : Repository 멤버 변수로 선언.
private final MessageRepository messageRepository;
public MessageService(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
public Message createMessage(Message message) {
// (2) : repository 구현체를 통해 Crud 인터페이스의 .save() 메서드 사용하여 DB 테이블에 저장.
return messageRepository.save(message);
}
}
[Spring Data JDBC 기반의 도메인 엔티티 및 테이블 설계]
더 나은 애플리케이션의 설계를 위한 DDD (도메인 주도 설계) 방식.
*도메인 엔티티 : 비즈니스 적인 업무의 영역, 상위 수준 도메인과 하위 수준 도메인으로 구분.
큰 도메인 엔티티 5가지를 아래와 같이 하위 도메인 엔티티로 나눠서 세분화 가능.
회원, 주문, 음식, 결제 : 상위 수준의 도메인 엔티티 (=Controller를 갖는 단위)
에그리거트 (Aggregate) // = 상위 수준의 Entity Domain 클래스
비슷한 업무 도메인들의 묶음, 비슷한 범주의 연관된 업무를 하나로 그룹화 한 그룹.
ex) 회원 애그리거트, 주문 애그리거트, 음식 애그리거트, 결제 애그리거트
에그리거트 루트 (Aggregate Root)
애그리거트에 속한 하위 수준의 도메인들 중
나머지 다른 도메인들과 직간접적으로 연관이 있는 소위 대표 하위 도메인.
DB 테이블 관계로 보자면,애그리거트 루트의 기본키 정보를 하위 수준의 도메인들이 외래키로 가지고 있다.
애그리거트 루트(대표 하위 도메인) : 부모 테이블, 하위 도메인 : 자식 테이블.
[애그리거트 간의 관계 as DB 테이블]
[회원 애그리거트] : [주문 애그리거트] = 1:N
1명의 회원은 여러 주문을 가질 수 있고, 1개의 주문은 여러 회원을 가질 수 없다.
[주문 애그리거트] : [커피 애그리거트] = N:N
1개의 주문은 여러 개의 커피를 가질 수 있고, 1개의 커피는 여러개의 주문에 포함될 수 있다.
N:N 관계에서는 중간 객체를 생성하여 1:N, N:1 관계로 순환한다.
[애그리거트 객체 매핑 규칙]
1. 모든 도메인 엔티티 객체(하위 수준 도메인)의 상태는 애그리거트 루트를 통해서만 바꿀 수 있다.
2. 동일한 애그리거트 내의 도메인 엔티티 간에는 객체로 참조한다.
3. 애그리거트 루트 간의 참조는 ID로 참조한다.
1:1, 1:N 관계에서는 N클래스에서 1클래스의 @Id를 필드로 가진다.
N:N 관계에서는 중간 객체를 통해 1:N, N:1 로 바꾸고, 1:N에서는 객체 참조, N:1에서는 ID 참조 방식 혼용
Spring Data JDBC - 데이터 액세스 계층 구현.
데이터 액세스 계층은 Service 클래스에서 비즈니스 로직을 처리하는 과정에서
DB를 다루는 기능이 필요할 경우 Entity 타입의 데이터를 Repository 인터페이스를 통해 DB와 소통해야 한다.
즉 데이터 엑세스 계층을 구현하려면 Entity 클래스, Service 클래스, Repository 인터페이스를 구현해야 한다.
1. [도메인 엔티티 클래스 간의 관계]
A. [Member 클래스 : Order 클래스] = 1:N 관계
1클래스에서는 N클래스의 객체를 List/Set<N클래스> 형태로 가진다.
B. [Order 클래스 : OrderCoffee 클래스] = 1:N 관계
1클래스에서는 N클래스의 객체를 List/Set<N클래스> 형태로 가진다.
C. [OrderCoffee 클래스 : Coffee 클래스] = N:1 관계
1클래스에서는 N클래스의 객체를 List/Set<N클래스> 형태로 가진다.
1-1. [도메인 엔티티 클래스 간의 관계를 토대로한 테이블 관계]
각 엔티티 클래스들의 필드는 테이블의 컬럼과 매핑된다.
클래스의 관계는 객체의 참조를 통해 맺어지고, 테이블 간의 관계는 Primary Key, Foreign Key로 맺어진다.
1-2. [엔티티 도메인 클래스들 간의 관계 구현 코드]
1:N 관계의 엔티티 도메인 클래스 // Member 클래스 : Order 클래스 = N클래스에서 1클래스 객체 가짐.
@Getter
@Setter
public class Member {
@Id // 테이블 코드에서 PRIMARY KEY와 매핑됨.
private Long memberId;
private String email;
private String name;
private String phone;
}
@Getter
@Setter
@Table("ORDERS")
public class Order {
@Id
private long orderId;
// AggregateReference<1클래스명, 1클래스@Id변수타입> 1클래스PK변수명
private AggregateReference<Member, Long> memberId;
중간 객체를 거치지 않은 Original 의 1:N 관계의 엔티티 도메인 클래스들 끼리는
부모 테이블의 PK를 자식 테이블에서 FK로 가져오듯,
1클래스의 @Id 필드를 N클래스에서 AggregateReference<1클래스명, @Id필드타입> @Id필드명으로 가짐.
N:N 관계의 엔티티 도메인 클래스 // Order 클래스 : Coffee 클래스
A. 중간 객체를 통해 N:N => 1:N, N:1 으로 변환.
Order : Coffee ==> Order : OrderCoffee : Coffee
@Getter
@Setter
@Table("ORDERS")
public class Order {
@Id // ORDERS 테이블의 PK
private long orderId;
// Member 클래스와의 1:N 관계이므로 매핑 규칙 3번에 의해
// member 객체가 아닌 @Id인 memberId를 필드로 가짐.
private AggregateReference<Member, Long> memberId;
}
@Table("ORDER_COFFEE") // 중간 객체의 테이블명
public class OrderCoffee { // 중간 객체
private int quantity;
}
@Getter
@Setter
public class Coffee {
@Id // COFFEE 테이블의 PK
private long coffeeId;
private String korName;
private String engName;
private int price;
private String coffeeCode;
}
B. 1:N, N:1 중에 1:N 먼저 매핑 처리
1클래스에서 중간객체(N)를 List/Set <중간객체> 형태로 필드로 가진다.
@Getter
@Setter
@Table("ORDERS")
public class Order {
@Id // 1클래스에서 PK 생성
private long orderId;
// Member 클래스와의 1:N 관계이므로 1클래스 변수를 가지는데
// 애그리거트 매핑 규칙 3번에 의해 객체가 아닌 memberId를 가짐.
private AggregateReference<Member, Long> memberId;
// N:N => 1:N, N:1 로 바꾼만큼, 1:N 관계인 Order : OrderCoffee 클래스.
// N 입장의 OrderCoffee 클래스를 List/Set<N클래스명> 을 필드로 가져옴.
@MappedCollection(idColumn = "ORDER_ID", keyColumn = "ORDER_COFFEE_ID")
private Set<CoffeeRef> orderCoffees = new LinkedHashSet<>();
}
중간 객체를 통해 형성된 1:N 관계의 엔티티 클래스는 Original 1:N 클래스에서 처리했던 방식인
N클래스에서 AggregateReference <1클래스명, 1클래스Id필드 타입> 1클래스Id 변수명 이 아닌
1클래스에서 N클래스를 List/Set<N클래스명> 형태로 필드로 가진다. = 객체 참조
@MappedCollection(idColun = "A", keyColumn = "B") : 1클래스에서 N클래스를 필드로 가질때 사용.
A : 1클래스와 매핑된 테이블의 PK 컬럼
B : N클래스와 매핑된 테이블의 PK 컬럼
C. 1:N 처리 이후, N:1 클래스 관계 매핑. (PK 참조)
@Getter
@AllArgsConstructor
@Table("ORDER_COFFEE")
public class CoffeeRef {
private long coffeeId; // (1)
private int quantity;
}
중간 객체를 통해 형성된 N:1 관계의 엔티티 클래스의 관계에서는
중간 객체<N클래스>에서 1클래스의 @Id 변수를 필드로 가지면 된다.
@Getter
@Setter
public class Coffee {
@Id // COFFEE 테이블의 PK
private long coffeeId;
private String korName;
private String engName;
private int price;
private String coffeeCode;
}
< Order 클래스의 추가 정보, 주문 상태 >
// (1)
private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;
// (2)
private LocalDateTime createdAt = LocalDateTime.now();
// (3)
public enum OrderStatus {
ORDER_REQUEST(1, "주문 요청"),
ORDER_CONFIRM(2, "주문 확정"),
ORDER_COMPLETE(3, "주문 완료"),
ORDER_CANCEL(4, "주문 취소");
// (4)
@Getter
private int stepNumber;
@Getter
private String stepDescription;
OrderStatus(int stepNumber, String stepDescription) {
this.stepNumber = stepNumber;
this.stepDescription = stepDescription;
}
}
}
(1) 주문 상태 정보를 나타내는 변수로, enum 타입.
(2) 주문이 등록되는 시간 정보를 나타내는 변수, LocalDateTime 타입
(3) 주문 상태를 나타내는 enum 타입.
(4) 주문 상태에 포함될 상태번호와 상태를 생성자로 생성.
<핵심 포인트>
A. Original 1:N 관계의 엔티티 클래스
N클래스에서 AggregateReference<1클래스명, 1클래스@Id변수타입> 1클래스@Id변수명 을 필드로 가짐.
= 테이블 관계의 FK와 같음.
B. Original N:N 관계의 엔티티 클래스 => <중간 객체>를 통해 1:N, N:1로 변환.
B-1. <1:N>
1클래스에서 N클래스 객체를 List/Set <N클래스명> 필드로 가짐 = 객체 참조
@MappedCollection(idColumn="1클래스와 매핑 테이블PK", keyColumn="N클래스와 매핑 테이블PK")
List<N클래스명> 변수명 = new LinkedList();
Set<N클래스명> 변수명 = new LinkedHashSet<>();
B-2. <N:1>
N클래스는 1클래스의 @Id 필드를 본인의 필드에 가져옴. = PK 참조
2. [Repository 클래스 정의]
Spring Data JDBC에서 DB와 상호 작용하는 역할을 하는 Repository 인터페이스.
각 Entity Class마다 Repository가 필요하다.
즉, 예시의 Member, Order, Coffee의 정보를 모두 저장 및 조회 할 수 있도록 정의해야한다.
A. [Member Repository] :
import com.codestates.member.entity.Member;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
// (1)
public interface MemberRepository extends CrudRepository<Member, Long> {
// (2)
Optional<Member> findByEmail(String email);
}
(1) : CrudRepository<Member, Long> 에서
Member : 해당 Repository 인터페이스가 다룰 Entity 클래스명,
Long : 해당 Repository 인터페이스에서 다룰 Member 엔티티 클래스의 @Id의 변수 타입.
(2) : 쿼리 메서드 정의를 이용한 데이터 조회 메서드 형식.
find + by + SQL 쿼리문에서 WHERE의 컬럼명 + (WHERE 컬럼의 조건이 되는 데이터)
여러가지의 조건을 달고 싶다면, findbyEmailAndName (String email, String name);
*주의사항*
find + by + XXXX 를 입력할 경우, 테이블의 컬럼명이 아닌, 엔티티 클래스의 변수명을 적어야 한다.
B. [Coffee Repository]
import com.codestates.coffee.entity.Coffee;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface CoffeeRepository extends CrudRepository<Coffee, Long> {
// (1)
Optional<Coffee> findByCoffeeCode(String coffeeCode);
// (2) 복잡한 쿼리문을 개발자가 작성할 수 있음을 보여주는 예시, 실제는 사용 X.
// findById (Id id) 로 사용 가능.
@Query("SELECT * FROM COFFEE WHERE COFFEE_ID = :coffeeId")
Optional<Coffee> findByCoffee(Long coffeeId);
}
C. [Order Repository]
import com.codestates.order.entity.Order;
import org.springframework.data.repository.CrudRepository;
public interface OrderRepository extends CrudRepository<Order, Long> {
}
3. [Service 클래스 정의]
Service 클래스는 Repository 인터페이스를 사용하여 데이터를 저장, 조회하도록 비즈니스 로직을 처리.
A. [MemberService]
@Service
public class MemberService {
private MemberRepository memberRepository;
// (1) MemberRepository DI 받아 생성자 초기화
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member createMember(Member member) {
// (2) (member.getEmail()) : member객체에 있는 email호출하여 이미 등록된 email인지 확인.
verifyExistsEmail(member.getEmail());
// (3) 이미 등록된게 아닌 email이라면 회원 정보 저장
return memberRepository.save(member);
}
public Member updateMember(Member member) {
// (4) member객체에있는 memberId 호출하여 존재하는 회원인지 검증
Member findMember = findVerifiedMember(member.getMemberId());
// (5) 이미 존재하는 회원인지 확인 후 setName, setPhone으로 이름, 휴대폰 번호 정보 업데이트
Optional.ofNullable(member.getName())
.ifPresent(name -> findMember.setName(name));
Optional.ofNullable(member.getPhone())
.ifPresent(phone -> findMember.setPhone(phone));
// (6) set메서드로 수정한 이름, 번호를 회원 정보에 저장.
return memberRepository.save(findMember);
}
// (7) findById(memberId) 메서드로 특정 회원 정보 조회
public Member findMember(long memberId) {
return findVerifiedMember(memberId);
}
public List<Member> findMembers() {
// (8) List<Member> 타입으로 repository메서드로 저정된 모든 회원 정보 조회
return (List<Member>) memberRepository.findAll();
}
public void deleteMember(long memberId) {
Member findMember = findVerifiedMember(memberId);
// (9) memberRepository에 저장된 member 정보 중 findMember 회원 정보 삭제
memberRepository.delete(findMember);
}
// (10) 이미 존재하는 회원인지를 검증
public Member findVerifiedMember(long memberId) {
Optional<Member> optionalMember =
memberRepository.findById(memberId);
Member findMember =
optionalMember.orElseThrow(() ->
new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
return findMember;
}
// (11) 이미 등록된 이메일 주소인지 검증
private void verifyExistsEmail(String email) {
Optional<Member> member = memberRepository.findByEmail(email);
if (member.isPresent())
throw new BusinessLogicException(ExceptionCode.MEMBER_EXISTS);
}
}
B. [CoffeeService]
@Service
public class CoffeeService {
private CoffeeRepository coffeeRepository;
public CoffeeService(CoffeeRepository coffeeRepository) {
this.coffeeRepository = coffeeRepository;
}
public Coffee createCoffee(Coffee coffee) {
// (1) 커피 코드를 대문자로 변경
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 findVerifiedCoffeeByQuery(coffeeId);
}
// (2) 주문에 해당하는 커피 정보 조회
public List<Coffee> findOrderedCoffees(Order order) {
return order.getOrderCoffees()
.stream()
.map(coffeeRef -> findCoffee(coffeeRef.getCoffeeId()))
.collect(Collectors.toList());
}
public List<Coffee> findCoffees() {
return (List<Coffee>) coffeeRepository.findAll();
}
public void deleteCoffee(long coffeeId) {
Coffee coffee = findVerifiedCoffee(coffeeId);
coffeeRepository.delete(coffee);
}
public Coffee findVerifiedCoffee(long coffeeId) {
Optional<Coffee> optionalCoffee = coffeeRepository.findById(coffeeId);
Coffee findCoffee =
optionalCoffee.orElseThrow(() ->
new BusinessLogicException(ExceptionCode.COFFEE_NOT_FOUND));
return findCoffee;
}
private void verifyExistCoffee(String coffeeCode) {
Optional<Coffee> coffee = coffeeRepository.findByCoffeeCode(coffeeCode);
if(coffee.isPresent())
throw new BusinessLogicException(ExceptionCode.COFFEE_CODE_EXISTS);
}
private Coffee findVerifiedCoffeeByQuery(long coffeeId) {
Optional<Coffee> optionalCoffee = coffeeRepository.findByCoffee(coffeeId);
Coffee findCoffee =
optionalCoffee.orElseThrow(() ->
new BusinessLogicException(ExceptionCode.COFFEE_NOT_FOUND));
return findCoffee;
}
}
C. [OrderService] - OrderRepository 필드 + 생성자 초기화.
package com.codestates.order.service;
import com.codestates.coffee.service.CoffeeService;
import com.codestates.exception.BusinessLogicException;
import com.codestates.exception.ExceptionCode;
import com.codestates.member.service.MemberService;
import com.codestates.order.entity.Order;
import com.codestates.order.repository.OrderRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class OrderService {
final private OrderRepository orderRepository;
final private MemberService memberService;
final private CoffeeService coffeeService;
public OrderService(OrderRepository orderRepository,
MemberService memberService,
CoffeeService coffeeService) {
this.orderRepository = orderRepository;
this.memberService = memberService;
this.coffeeService = coffeeService;
}
[createOrder 메서드]
public Order createOrder(Order order) {
// (1) 회원이 존재하는지 확인
memberService.findVerifiedMember(order.getMemberId().getId());
// (2) 커피가 존재하는지 조회해야 됨
order.getOrderCoffees()
.stream()
.forEach(coffeeRef -> {
coffeeService.findVerifiedCoffee(coffeeRef.getCoffeeId());
});
return orderRepository.save(order);
}
1. memberService.findVerifiedMember (order.getMemberId().getId());
2. order.getOrderCoffees()
Order 클래스에서 가지고있는 List/Set<OrderCoffee> orderCoffees 필드변수를 가져와서 getOrderCoffees()를 통해
존재하는 커피 주문인지 확인하고, 스트림을 통해 중간객체.커피 id를 가져와서 이미 존재하는 주문인지 확인함.
[findOrder 메서드]
public Order findOrder(long orderId) {
return findVerifiedOrder(orderId);
}
public List<Order> findOrders() {
return (List<Order>) orderRepository.findAll();
}
// (3)
public void cancelOrder(long orderId) {
Order findOrder = findVerifiedOrder(orderId);
int step = findOrder.getOrderStatus().getStepNumber();
// (4) OrderStatus의 step이 2미만일 경우(ORDER_CONFIRM)에만 주문취소가 되도록한다.
if (step >= 2) {
throw new BusinessLogicException(ExceptionCode.CANNOT_CHANGE_ORDER);
}
// (5)
findOrder.setOrderStatus(Order.OrderStatus.ORDER_CANCEL);
orderRepository.save(findOrder);
}
[cancelOrder 메서드]
public void cancelOrder(long orderId) {
Order findOrder = findVerifiedOrder(orderId);
int step = findOrder.getOrderStatus().getStepNumber();
// (4) OrderStatus의 step이 2미만일 경우(ORDER_CONFIRM)에만 주문취소가 되도록한다.
if (step >= 2) {
throw new BusinessLogicException(ExceptionCode.CANNOT_CHANGE_ORDER);
}
// (5)
findOrder.setOrderStatus(Order.OrderStatus.ORDER_CANCEL);
orderRepository.save(findOrder);
}
[findVerifiedOrder : 이미 등록되있는 주문인지 확인]
private Order findVerifiedOrder(long orderId) {
Optional<Order> optionalOrder = orderRepository.findById(orderId);
Order findOrder =
optionalOrder.orElseThrow(() ->
new BusinessLogicException(ExceptionCode.ORDER_NOT_FOUND));
return findOrder;
}
}
'백엔드 학습 과정 > Section 3 [Spring MVC, JDBC, JPA, RestDo' 카테고리의 다른 글
Spring JPA 기능 코드 (0) | 2023.01.01 |
---|---|
Spring Data JDBC - Service 클래스 기능별 코드 (0) | 2022.12.31 |
#3. Spring 예외 처리 (0) | 2022.12.23 |
#2. Spring MVC [Service 계층] - 역할, 생성, 적용, 필요 애너테이션 (0) | 2022.12.20 |
** 참고 ** REST-API (0) | 2022.12.17 |