Backend boot camp/Session3
[Spring MVC] JDBC DB Access Layer
by orioncsy
2022. 10. 29.
JDBC
JDBC 개념
- Java DataBase Connectivity
- 자바 기반 애플리케이션 코드에서 데이터를 db에 저장하거나 가져오는 Java에서 제공하는 표준 사양(specification)
- JDBS API를 사용하여 oracle, MS SQL, MySQL 등의 db와 연동 가능
- Spring Data JDBC, Spring Data JPA 모두 내부적으로 JDBC를 사용한다.
JDBC 동작 흐름
Java 애플리케이션 → JDBC API → JDBC 드라이버 → 데이터베이스
- Java 애플리케이션 내 JDBC API를 사용하여 db에 접근하는 구조
- JDBC API는 드라이버를 로딩 후에 db에 연결
JDBC 드라이버(JDBC Driver)
- JDBC 드라이버는 다양한 벤더에 맞는 JDBC 드라이버를 구현해서 제공하고 구현체를 이용해 벤터의 db에 접근 가능
JDBC API 사용 흐름
- JDBC 드라이버 로딩
- Connection 객체 생성
- DriverManager를 통해 db와 연결되는 Connection 객체 생성
- Statement 객체 생성
- SQL 쿼리문을 실행하기 위한 객체, SQL 쿼리 문자열을 입력으로 가진다
- Query 실행
- ResultSet 객체로부터 데이터 조회
- ResultSet 객체 close → Statement 객체 close → Connection 객체 close
Connection Pool
- Connection 객체를 애플리케이션 로딩 시점에 미리 생성해두고 미리 만들어 둔 객체를 사용하여 성능 향상
- 미리 Connection을 만들어서 보관하고 제공해주는 Connection 관리자를 Connection Pool
Spring Data JDBC
데이터 액세스 기술 유형
- mybatis, Spring JDBC, Spring Data JDBC, JPA, Spring Data JPA
SQL 중심 기술
- 대표적 SQL 중심 기술 : mybatis, Spring JDBC
- SQL문을 애플리케이션 내부에 직접 작성하는 방식
객체(Object) 중심 기술
- 자바 객체를 이용해 SQL 쿼리문으로 자동 변환 후 데이터베이스에 접근
- ORM(Object-Relational Mapping)
- 대표적 object 중심 기술 : JPA, Spring data JDBC
Spring Data JDBC
- Spring Data JDBC - 최근에 릴리즈 되었지만 규모가 작고 복잡하지 않은 경우 사용
- JPA - 실무에서 가장 많이 사용하는 기술
- Spring Data JPA - Spring에서 JPA를 사용하기 위한 기술
JDBC 사용
의존 라이브러리 추가
- implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
- Spring Data JDBC 사용 라이브러리
- runtimeOnly 'com.h2database:h2'
- 인메모리 DB인 H2 사용 라이브러리
- 인메모리 사용은 로컬 개발 환경에서 테스트를 진행하고 저장된 내용이 초기화되는 것에 유리하여 자주 사용
H2 Browser 활성화
- src/main/resources 디렉터리에 application/properties 파일
- application.yml(혹은 application.yaml)로 수정하여 저장(shift + F6)
spring:
h2:
console:
enabled: true
간단한 메세지 전송 코드 구현
- Dto, entity, controller, mapper, service, repository를 구현
- Repository 구현
- public interface MessageRepository extends CrudRepository<Message, Long>{}
- CrudRepository를 선언하고 제너릭 타입을 해당 객체, long으로 선언
- 해당 엔티티 클래스 객체에 담긴 데이터를 db 테이블에 가공한 데이터를 엔티티 클래스로 변환 가능
- long은 entity 클래스에서 식별자를 의미하는 @Id라는 애너테이션이 붙은 멤버 변수 타입
- Service에 repository를 final로 선언
- repository를 이용하여 save 메서드를 사용하여 객체를 저장 및 다양한 메서드 활용하여 crud 작업 가능
- Entity
- entity 명은 db에서 테이블 명에 해당
- @Id를 추가한 멤버 변수는 고유 식별자로 PK로 지정한 칼럼
- application.yml에서 테이블 생성 경로 추가
- src/main/resources/db/h2에 schema.sql 파일을 읽어서 테이블 생성
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
sql:
init:
schema-locations: classpath*:db/h2/schema.sql
Spring Data JDBC 기반 도메인 엔티티 및 테이블 설계
DDD(Domain Driven Design)
Domain
- 비즈니스적인 업무 영역
- 도메인 지식을 서비스 계층에서 비즈니스 로직으로 구현해야 한다
- 상위 수준의 도메인은 큰 범주의 기능들(회원, 주문, 음식, 결제)
- 하위 수준의 도메인은 상위 수준 도메인 아래에 세부 기능
Aggregate
Aggreate Root
- 각 aggregate 안에 대표적인 도메인
- 선정 기준
- aggregate 내에 다른 도메인과 직간접으로 연관되어 있는 도메인으로 선정
- DB에서는 aggregate root가 부모 테이블이 되고 다른 도메인이 자식 테이블이 된다
- 자식 테이블은 부모 테이블의 기본키를 가지고 있고 외래 키라 부른다.
애플리케이션 도메인 엔티티 및 테이블 설계
- 도메인에서 aggregate root를 찾기
- 회원 정보 - (주문 정보 - 주문 커피 정보) - 커피정보
- 각 aggregate 간의 관계는 1대 N으로 표시
- 회원 정보와 주문 정보는 1대 N
- 주문 정보와 커피 정보는 N대 N
- 주문 커피 정보를 매개로 1대 N, N대 1로 재구성
엔티티 클래스 간 관계
- 객체 참조를 통해서 관계 형성
- Member entity class
- Member 클래스와 Order 클래스는 1대 N
- Member 클래스에 List <Order> 멤버 변수 추가
- Order entity class
- Order class와 Coffee class는 N대 N 관계를 가지기 때문에 1대 N을 가지도록 List<OrderCoffee>를 맴버 변수로 추가
- Coffee entity class
- Coffee class 와 Order class는 N대 N 관계를 가지기 때문에 1대 N을 가지도록 List <OrderCoffee>를 멤버 변수로 추가
- OrderCoffee table
- 주문하는 커피가 한잔 이상일 수 있어 quantity 추가
데이터베이스 테이블 설계
- 테이블 간의 관계를 외래 키 참조로 형성
- 주문 테이블은 ORDERS로 하여 ORDER BY와 혼돈 방지
- 1대 N의 관계를 가질 때 1에 해당하는 테이블 pk를 N에 해당하는 테이블에서 외래 키로 참조
Spring Data JDBC 데이터 액세스 계층 구현
엔티티 설계 확인
- 회원, 주문, 커피를 엔티티 클래스로 각각 객체를 참조하여 관계를 설정한 설계
테이블 설계 확인
- 회원, 주문, 커피를 엔티티 클래스로 각각 외래 키를 참조하여 설계
Aggregate 객체 매핑
- 데이터베이스 테이블은 스키마를 사용해서 테이블 생성
- 엔티티 클래스는 DDD의 aggregate 매핑 규칙에 맞게 변경
Aggregate 객체 매핑 규칙
- 모든 엔티티 객체 상태는 aggregate root를 통해서만 변경 가능
- 동일한 하나의 aggregate 내에서는 엔티티 간 객체로 참조
- aggregate root 간 엔티티 객체 참조
- 객체 참조 대신 ID로 참조
- 1대 1과 1대 N 관계일 때 테이블 간 외래 키 방식과 동일
- N대 N일 때 외래 키 방식인 ID참조와 객체 참조 방식이 함께 사용
Entity 구현
- Member 클래스와 Order 클래스의 aggregate root 매핑
- Member 클래스에서 memberId를 @Id로 지정
- Order 클래스에서는 @Table(”ORDERS”)로 이름 변경
- Order 클래스의 orderId를 @Id로 지정
- Order 클래스에 AggregateReference <Member, Long> memberId로 멤버 변수 생성하여 외래키 처럼 참조
- Order 클래스와 Coffee 클래스의 aggregate root 매핑(M:N 관계)
- Coffee 클래스에서 coffeeId를 @Id로 지정
- Coffee 클래스에서 String coffeeCode를 맴버 변수로 생성(중복 등록 체크를 위한 변수)
- M:N 관계를 풀어줄 CoffeeRef 클래스를 생성
- Order 클래스
- @MappedCollection(idColumn = “ORDER_ID”)
- private Set <CoffeeRef> orderCoffees=new LinkedHashSet <>();
- @MappedCollection 사용법
- CoffeeRef entity는 order와 동일한 aggregate에 속하고 이에 따라 객체 참조 방식 사용
- MappedCollection 애너테이션은 ORDERS와 ORDER_COFFEE 테이블을 관계를 맺어주고 ORDER_ID(ORDER의 기본키)가 ORDER_COFFEE의 외래 키가 되어 idColumn에 배정해준다.(자식 테이블에 추가되는 외래키 칼럼명)
- idColumn이 아닌 keyColumn은 외래 키를 포함하는 테이블의 기본키 칼럼명
- 필드가 List인 경우 keyColumn 값 입력이 필수, Set인 경우 필수가 아니다.
- CoffeeRef 클래스
- @Table(”ORDER_COFFEE”)로 테이블 이름 변경(이름을 변경하지 않으면 클래스 이름 그대로 설정)
- coffeeId, quantity를 필드 멤버로 설정
- coffeeId를 AggregateReference로 감쌀 필요가 없는 이뉴는 CoffeeRef는 Order Aggregate 내에 있는 클래스이고 Coffee 클래스는 Aggregate root에 해당하기 때문이다.
- Order 클래스 맴버 변수 추가
- OrderStatus라는 enum을 생성하여 number와 description을 필드로 가지는 주문 단계를 만들어 Order 클래스 필드 맴버로 추가
- LocalDateTime 클래스를 활용하여 createdAt을 선언하고 현재 시간으로 초기화
- 해당 클래스 관계 설정에 맞게 schema.sql에 정의
Spring Data JDBC 서비스, 레포지토리 구현
레포지토리 인터페이스 정의
- CrudRepository <Object, Long>를 extends 하여 각 정보의 레포지토리 인터페이스 생성
- 제너릭에서 Object는 해당 정보의 객체를 의미하고 Long은 엔티티 클래스에서 @Id가 붙은 멤버 변수 타입을 의미
- 바디에는 쿼리 메서드 작성
- findBy [엔티티 명](조건 데이터) 형태로 반환형은 Optional로 감싼 객체를 반환
- 조건이 두 개일 경우 메서드명을 And로 붙이고 파라미터를 두 개로 받는다.
- @Query 애너테이션을 사용한 쿼리 매서드
- 괄호 안에 쿼리문 직접 작성 가능 :[칼럼명]은 매개변수로 받은 변수 값이 채워지는 동적 쿼리 파라미터
- findById(ID id)를 사용하면 기본키를 조회하는 쿼리 메서드 사용 가능
서비스 클래스 구현
- 레포지터리를 필드 멤버로 선언하고 DI를 통해 생성자 작성
- verifyExists 메서드 생성 엔티티의 특정 멤버로 이미 존재하는지 확인하는 메서드를 생성
- Optional로 선언하여 repository에서 findBy 메서드로 할당
- isPresent()를 사용하여 존재하면 MEMBER_EXISTS로 예외 발생
- findVerifiedMember 메서드 선언
- memberId를 받아서 Optional로 선언하여 findById를 통해 할당
- orElseThrow(()→…))를 통해 null이 아니라면 객체를 반환하고 null이면 예외를 던진다.
- create 서비스나 update 서비스
- create는 repository.save를 통해 저장
- update도 repository.save를 통해 저장하면 @Id가 0이거나 null이 아니면 update 형식으로 저장 가능
- update 할 때 Optional.ofNullable로 감싸고 ifPresent()를 사용하여 값이 null이 아니면 람다로 값을 넣어주는 방식 사용
- findMember 서비스, findMembers 서비스
- findMember는 findVerifiedMember메서드 사용
- findMembers는 findAll() 메서드 사용하고 반환형이 Iterable <T>이기 때문에 해당 타입으로 캐스팅 필요
- deleteMember 서비스