Develog
코드스테이츠 47일차 본문
Spring Data JDBC 기반의 도메인 엔티티 및 테이블 설계
Spring Data JDBC 기반의 데이터 액세스 계층을 연동하기 위해 우리가 제일 먼저 해야될 일은 바로 데이터베이스의 테이블과 도메인 엔티티 클래스의 설계이다.
DDD(Domain Driven Design)란?
DDD(Domain Driven Design)는 용어의 의미 그대로 도메인 위주의 설계 기법을 의미한다.
도메인(Domain)이란?
“도메인이란 용어 자체는 한 마디로 우리가 실제로 현실 세계에서 접하는 업무의 한 영역이다”
애플리케이션 개발에서 흔하게 사용하는 도메인이란 용어는 주로 비즈니스적인 어떤 업무 영역과 관련이 있다.
예를 들어, 우리가 새로운 배달 주문 앱을 만들어야 한다면 고객과 음식점, 배달원, 그리고 카드사 또는 은행 등 배달 주문 앱을 구현하기 위해 필요한 업무들을 자세히 알면 알수록 퀄리티가 높은 애플리케이션을 만들 가능성이 높을 것이다.
즉, 고객이 음식을 주문하는 과정, 주문받은 음식을 처리하는 과정, 조리된 음식을 배달하는 과정 등의 도메인 지식(Domain Knowledge)들을 서비스 계층에서 비즈니스 로직으로 구현해야 하는 것이다.
애그리거트(Aggregate)란?
이 도메인을 조금 더 세분화한다면?
회원, 주문, 음식, 결제는 상위 수준의 도메인이고, 이 상위 수준의 도메인 하위에 있는 각각의 정보들을 하위 수준의 도메인이라고 부를 수 있다.
애그리거트(Aggregate)란 위 그림과 같이 비슷한 업무 도메인들의 묶음을 말한다.
회원 애그리거트, 주문 애그리거트, 음식 애그리거트, 결제 애그리거트라고 부를 수 있다.
애그리거트(Aggregate)는 한마디로 비슷한 범주의 연관된 업무들을 하나로 그룹화 해놓은 그룹이라고 생각하자
애그리거트 루트(Aggregate Root)란?
아까 그림을 다시 보면 정의한 네 개의 애그리거트 안에는 1개 이상의 도메인들이 있는데, 각각의 애그리거트에는 해당 애그리거트를 대표하는 도메인이 존재한다.
이처럼 하나의 애그리거트를 대표하는 도메인을 DDD에서는 애그리거트 루트(Aggregate Root)라고한다.
아파트 한 동을 대표하는 동대표가 애그리거트 루트라고 생각하면 편할 거 같다.(= 특정 집단, 그룹의 대표느낌)
✔ 애그리거트 루트(Aggregate Root) 선정 기준은?
각 애그리거트 내의 도메인들 중에서 다른 모든 도메인들과 직간접적으로 연관이 되어 있는 도메인들을 발견할 수 있는데
- 회원 애그리거트의 경우, ‘회원 포인트’가 얼마인지 알려면 해당 포인트를 가지는 ‘회원 정보’를 알아야 한다. 즉, ‘회원 정보’ 도메인이 애그리거트 루트가 된다.
- 주문 애그리거트의 경우, ‘주문 정보’가 다른 모든 도메인과 직접적으로 관련이 있습니다. 즉 ‘주문 정보’ 도메인이 애그리거트 루트가 된다.
데이터베이스의 테이블 간 관계로 보자면, 애그리거트 루트는 부모 테이블이 되고, 애그리거트 루트가 아닌 다른 도메인들은 자식 테이블이 되는 셈이다.
즉, 애그리거트 루트(Aggregate Root)의 기본키 정보를 다른 도메인들이 외래키 형태로 가지고 있다고 볼 수 있다.
관계형 데이터베이스에서 A 테이블의 기본키를 B 테이블이 가지고 있다면 A는 부모 테이블이 되고, B는 자식 테이블이 된다. B인 자식 테이블이 가지고 있는 A 테이블의 기본키를 외래키라고 한다.
총정리
- DDD(Domain Driven Design, 도메인 주도 설계)는 도메인 위주의 설계 기법이다.
- 도메인(Domain)이란?
- 애그리거트(Aggregate)와 애그리거트 루트(Aggregate Root)는 DDD에서 사용되는 용어이다.
- 데이터베이스 테이블 간의 관계는 외래키를 통해 맺어지지만 클래스끼리 관계는 객체의 참조를 통해 관계가 맺어진다.
Spring Data JDBC를 통한 데이터 액세스 계층 구현(1) - 도메인 엔티티 클래스 정의
테이블의 외래키(Foreign key) vs 클래스의 객체 참조 리스트(List)
테이블 간의 관계는 외래키라는 연결 요소가 있어서 직관적이지만 클래스들 간에는 외래키라는 연결 요소가 없다.
대신에 클래스들은 객체 간에 참조가 가능하기때문에 이 객체 참조를 사용해서 외래키의 기능을 대신한다.
List에 대한 추가적인 정리를 하자면 예를 들어 우리가 카페에 가면 커피를 여러번 주문할 수 있듯이 Member 클래스 역시 Order 클래스의 객체를 여러개 가질 수 있다.
그렇기에 List, Set 같은 컬렉션을 사용해서 표현할 수 있는 것이다.
Spring Data JDBC에서의 애그리거트(Aggregate) 객체 매핑
도메인 엔티티 클래스의 관계를 DDD(Domain Driven Design, 도메인 주도 설계)의 애그리거트(Aggregate) 매핑 규칙에 맞게 변경하는 법은?
우선 애그리거트 객체 매핑 규칙을 살펴보자
(1) 모든 엔티티 객체의 상태는 애그리거트 루트를 통해서만 변경할 수 있다.
(2) 하나의 동일한 애그리거트 내에서의 엔티티 객체 참조
- 동일한 하나의 애그리거트 내에서는 엔티티 간에 객체로 참조한다.
(3) 애그리거트 루트 대 애그리거트 루트 간의 엔티티 객체 참조
- 애그리거트 루트 간의 참조는 객체 참조 대신에 ID로 참조한다.
- 1대1과 1대N 관계일 때는 테이블 간의 외래키 방식과 동일하다.
- N대N 관계일 때는 외래키 방식과 객체 참조 방식이 함께 사용된다.
아까 살펴봤던 배달 주문 앱의 애그리거트 루트 그림을 보면
회원 애그리거트의 경우, 애그리거트 루트는 회원 정보라는 엔티티 클래스가 될 것이다.
또한 비용을 지불하고 주문을 하면 주문한 만큼 회원 포인트를 얻게된다.
이 때 프로그래밍적으로는 회원의 포인트를 업데이트 해주어야 하는데 회원 포인트라는 엔티티를 직접적으로 접근하지 말고 반드시 ‘회원 정보’라는 애그리거트 루트를 통해서 ‘회원 포인트’ 엔티티에 접근한 뒤, 포인트의 상태를 변경한다는 것이 바로 (1)번 규칙의 핵심이다.
결과적으로 애그리거트 루트를 통해서 나머지 엔티티에 접근한다는 것은 어떤식으로든 애그리거트 루트가 나머지 모든 엔티티에 대한 객체를 직간접적으로 참조 할 수 있다는 의미이다.
왜 애그리거트 루트를 통해서만 나머지 엔티티의 상태를 변경해야 하는건데?
예를 들어 음식을 주문한 이후에 주소를 잘못 입력해서 배달 주소 정보를 다시 변경하는 경우를 생각해보자
이 때, ‘배달 주소 정보’라는 엔티티에 직접적으로 접근해서 주소 정보를 바꾸어 버리면 주소 정보를 변경할 수 없는 상태에도 주소 정보를 변경할 수 있게 되어버린다.
이를테면 음식이 이미 다 만들어져서 처음에 잘못 입력한 주소 정보로 배달을 하는 중인데, 배달 주소를 바꾼다고 배달 업체가 변경된 주소로 배달을 하지는 않을거고, 음식점에 전화를 해 수동으로 변경을 해야할 것이다.
즉, ‘배달 주소 정보’라는 엔티티에 직접 접근해서 주소 정보를 변경하게 되면 ‘음식이 다 만들어지기 전 까지만 주소 정보를 변경할 수 있다’라는 규칙을 무시하고 상태를 변경하는 것이 되기 때문에 이런 도메인 규칙에 대한 일관성이 깨진다.
따라서 항상 ‘주문 정보’라는 애그리거트 루트를 먼저 거쳐서 ‘음식이 이미 다 만들어졌는지 아직 조리 중인지’ 등의 규칙을 잘 검증한 후에 검증에 통과하면 ‘배달 주소 정보’ 엔티티의 상태를 업데이트 하도록 해서 도메인 규칙의 일관성을 유지하도록 하는 것이다.
(2)번 규칙은 동일한 애그리거트 내의 엔티티 끼리 참조할 경우에만 적용되는 규칙이다.
(3)번 규칙의 핵심은 애그리거트 루트 간에 객체로 참조하지 않고, 테이블에 외래키를 추가하듯이 참조하고자 하는 애그리거트 루트의 ID를 참조 값으로 멤버 변수에 추가하는 것이다.
1대N 관계의 애그리거트 루트 간 ID 참조는 AggregateReference 클래스로 한번 감싸주어 해결할 수 있다.
만약 N대N관계라면 N대N 관계이기 때문에 N대N 관계를 1대N, N대1 관계로 풀어줄 엔티티가 중간에 하나 필요하다.
테이블 간에 관계를 만들어주는 주체는 외래키인데, @MappedCollection(idColumn = "") 에 외래키에 해당하는 컬럼명을 적어주어 관계를 만들어 줄 수 있다.
✅ 포인트
1. N대N의 관계를 —> 1대N, N대 1의 관계로 변경
2. 1대N, N대1의 관계를 다룰 때에는 중간에서 ID를 참조하게 해주는 클래스를 통해 다시 1대N대1의 관계로 변경
1대N대1의 관계는 마치 두 개의 N이 하나로 합쳐지는 듯한 효과가 발생한다.
총정리
- 테이블 간의 관계는 외래키를 통해서 이루어지며, 클래스들 간의 관계는 해당 클래스의 객체 참조를 통해서 이루어진다.
- AggregateReference 클래스는 다른 애그리거트 루트 간의 참조 역할을 한다.
- AggregateReference 클래스는 테이블의 외래키처럼 다른 객체의 ID 값을 참조할 수 있도록 해준다.
- 애그리거트 객체 매핑 규칙
ㄴ모든 엔티티 객체의 상태는 애그리거트 루트를 통해서만 변경할 수 있다.
ㄴ하나의 동일한 애그리거트 내에서의 엔티티 객체 참조
ㄴ동일한 하나의 애그리거트 내에서는 엔티티 간에 객체로 참조한다.
ㄴ애그리거트 루트 대 애그리거트 루트 간의 엔티티 객체 참조
ㄴ애그리거트 루트 간의 참조는 객체 참조 대신에 ID로 참조한다.
ㄴ1대N 관계일 때는 AggregateReference 를 사용할 수 있다.
ㄴ즉, 테이블 간의 외래키 방식과 동일하다.
ㄴN대N 관계일 때는 참조할 테이블에 해당되는 클래스의 @Id 필드를 멤버 변수로 가지는 별도의 참조 클래스를 사용한다.
Spring Data JDBC를 통한 데이터 액세스 계층 구현(2) - 서비스, 리포지토리 구현
Spring Data JDBC, Spring Data JPA에서는 데이터 액세스 계층에서 데이터베이스와 상호작용하는 역할을 하는 인터페이스를 리포지토리(Repository)라고한다.
리포지토리(Repository)라는 용어는 DDD(Domain Driven Design, 도메인 주도 설계)에서 사용하는 용어임을 기억하자
리포지토리(Repository) 인터페이스 정의
쿼리 메서드(Query Method)
Spring Data JDBC에서는 쿼리 메서드를 이용해서 SQL 쿼리문을 사용하지 않고 데이터베이스에 질의를 할 수 있다.
기본적인 사용법은 ‘find + By + SQL 쿼리문에서 WHERE 절의 컬럼명 + (WHERE 절 컬럼의 조건이 되는 데이터) ’ 형식이며, WHERE 절의 조건 컬럼을 여러 개 지정하고 싶다면 ‘And’를 사용하면 된다.
예를 들어 EMAIL 컬럼과 NAME 컬럼을 조건으로 지정하고 싶다면, findByEmailAndName(String email, String name) 과 같이 쿼리 메서드를 정의하면 된다.
주의해야되는 부분은 findByxxxx에서 사용하는 컬럼명은 내부적으로는 테이블의 컬럼명으로 변경되지만 Spring JDBC 입장에서는 엔티티 클래스를 바라보고 작업을 하기 때문에 반드시 엔티티 클래스의 멤버 변수명을 적어주어야 한다는 것이다.
Member 엔티티 클래스에 FirstName이라는 멤버 변수가 있고, 테이블에 있는 FIRST_NAME이라는 컬럼명과 매핑이 된다고 가정할 경우, 쿼리메서드는 FindByFirstName 이 되어야지 findByFIRST_NAME 이 되어서는 안된다는 의미이다.
이런 규칙이 헷갈린다면 엔티티 클래스의 멤버 변수명과 테이블 컬럼명을 일치 시키는 것이 가장 좋지만 두 단어 이상 조합이 될 경우 일반적으로 Java에서는 Carmel Case 표기법을 사용하고 테이블 컬럼명은 언더스코어( _ ) 표기법을 사용하는 경우가 많음을 참고하자
@Query 애너테이션을 이용하면 SQL 쿼리문을 직접 작성할 수 있기 때문에 복잡한 쿼리문의 경우 @Query 애너테이션을 이용해서 직접 쿼리문을 작성할 수 있다.
하지만 단순한 쿼리의 경우 Spring Data JDBC에서 지원하는 Query Method를 정의해서 사용하는 것이 간결한 코드 유지와 생산성 면에서 바람직하다고 볼 수 있다.
findById(ID id)는 테이블에서 기본키를 WHERE절의 조건으로 지정해 데이터를 조회할 수 있는 편리한 쿼리메서드이다.
✅
여기서 우리가 깨달아야 하는 것은 MemberRepository 인터페이스는 정의했지만 인터페이스의 구현 클래스는 별도로 구현을 한 적이 없는데 MemberRepository 인터페이스의 구현 클래스는 누가 구현을 하고 있는걸까?
바로 Spring Data JDBC에서 내부적으로 Java의 리플렉션 기술과 Proxy 기술을 이용해서 MemberRepository 인터페이스의 구현 클래스 객체를 생성해준다.
✅
멤버 변수 값이 null일 경우에는 Optional.of()가 아닌 Optional.ofNullable()을 이용해서 null 값을 허용할 수 있다.
따라서 값이 null이더라도 NullPointerException이 발생하지 않고, 다음 메서드인 ifPresent() 메서드를 호출할 수 있다.
수정할 값이 있다면(name 또는 phone 멤버 변수의 값이 null이 아니라면) ifPresent() 메서드내의 코드가 실행이 되고, 수정할 값이 없다면 (name 또는 phone 멤버 변수의 값이 null이라면) 아무 동작도 하지 않는다.
✅
Spring Data JDBC에서는 @Id 애너테이션이 추가된 엔티티 클래스의 멤버 변수 값이 0 또는 null이면 신규 데이터라고 판단하여 테이블에 insert 쿼리를 전송한다.
반면에 @Id 애너테이션이 추가된 엔티티 클래스의 멤버 변수 값이 0 또는 null이 아니라면 이미 테이블에 존재하는 데이터라고 판단하여 테이블에 update 쿼리를 전송한다.
따라서 이미 테이블에 존재하는 회원 정보를 테이블에서 조회한 findMember 객체에서 name 또는 phone 멤버 변수만 setter 메서드로 값을 변경하는 방식을 이용해서 테이블에 update 쿼리를 보내게된다.
✅
회원의 회원 가입 상태를 ‘가입’, ‘휴면’, ‘탈퇴’ 등의 상태 정보로 나누어서 관리하는 것이 바람직하다.
총정리
- Spring Data JDBC의 CrudRepository 인터페이스를 상속하면 CrudRepository에서 제공하는 CRUD 메서드를 사용할 수 있다.
- Spring Data JDBC에서는 SQL 쿼리를 대신하는 다양한 쿼리 메서드(Query Method) 작성을 지원한다.
- Spring Data JDBC에서 지원하는 쿼리 메서드의 정의가 어렵다면 @Query 애너테이션을 이용해서 SQL 쿼리를 직접 작성할 수 있다.
- 회원 정보, 커피 정보 등의 리소스를 데이터베이스에 Insert할 경우 이미 Insert된 리소스인지 여부를 검증하는 로직이 필요하다.
- Optional을 이용하면 데이터 검증에 대한 로직을 간결하게 작성할 수 있다.
- 복잡한 DTO 클래스와 엔티티 클래스의 매핑은 Mapper 인터페이스에 default 메서드를 직접 구현해서 개발자가 직접 매핑 로직을 작성해줄 수 있다.
페이지네이션(Pagination)이란?
예를 들어, 데이터 베이스에 회원 정보가 100건이 저장되어 있는데 클라이언트 쪽에서 100건의 데이터를 모두 요청하는 것이 아니라 한 페이지에 일정 개수 만큼만 나누어서 달라고 요청하는 것을 페이지네이션(Pagination)이라고 한다.
즉 page 번호가 1이고, 페이지에 포함되는 데이터의 개수가 10건일 경우,
데이터베이스의 테이블에서는 1 row부터 10 row까지만 조회되어야 한다.
만약 page 번호가 2이고, 페이지에 포함되는 데이터의 개수가 10건일 경우,
데이터베이스의 테이블에서는 11 row부터 20 row까지만 조회되어야 한다.
'코드스테이츠' 카테고리의 다른 글
코드스테이츠 50일차 (0) | 2022.07.06 |
---|---|
코드스테이츠 48-49일차 (0) | 2022.07.04 |
코드스테이츠 46일차 (0) | 2022.06.30 |
코드스테이츠 45일차 (0) | 2022.06.29 |
코드스테이츠 44일차 (0) | 2022.06.28 |