├── .gitmodules ├── DDD Start! ├── 10장 이벤트 │ └── CHAPTER 10.md ├── 11장 CQRS │ └── README.md ├── 1장 도메인 모델 시작 │ └── README.md ├── 2장 아키텍처 개요 │ └── CHAPTER 2.md ├── 3장 애그리거트 │ └── CHAPTER 3.md ├── 4장 리포지터리와 모델 구현 (JPA 중심) │ └── README.md ├── 5장 리포지터리의 조회 기능(JPA 중심) │ └── CHAPTER 5.md ├── 6장 응용 서비스와 표현 영역 │ └── README.md ├── 7장 도메인 서비스 │ └── CHAPTER 7.md └── 8장 애그리거트 트랜잭션 관리 │ ├── CHAPTER 8.md │ └── image │ ├── img.png │ ├── img_1.png │ └── img_2.png ├── README.md ├── 자바 ORM 표준 JPA 프로그래밍 ├── 12장 스프링 데이터 JPA │ └── 12.1 ~ 12.11.md ├── 13장 웹 애플리케이션과 영속성 관리 │ └── 13.1 ~ 13.5.md ├── 14장 컬렉션과 부가기능 │ └── 14.1 ~ 14.5.md ├── 15장 고급 주제와 성능 최적화 │ └── 15.1 ~ 15.5.md ├── 16장 트랜잭션과 락, 2차캐시 │ └── 16.1 ~ 16.3.md ├── 1장 JPA 소개 │ └── 1.1 ~ 1.4.md ├── 2장 JPA 시작 │ └── 2.1 ~ 2.7.md ├── 3장 영속성 관리 │ └── 3.1 ~ 3.7.md ├── 4장 엔티티 매핑 │ └── 4.1 ~ 4.8.md ├── 5장 연관관계 매핑 기초 │ └── 5.1 ~ 5.7.md ├── 6장 다양한 연관관계 매핑 │ └── 6.1~6.5.md ├── 7장 고급 매핑 │ └── 7.1~7.6.md ├── 8장 프록시와 연관관계 관리 │ └── 8.1 ~ 8.7.md └── 9장 값 타입 │ └── 9.1 ~ 9.5.md └── 토비의 스프링 3.1 ├── .DS_Store ├── 1장 오브젝트와 의존관계 ├── 1.1 ~ 1.5.md └── 1.6 ~ 1.9.md ├── 2장 테스트 └── 2.1 ~ 2.6.md ├── 3장 템플릿 └── 3.1 ~ 3.7.md ├── 4장 예외 └── 4.1 ~ 4.3.md ├── 5장 서비스 추상화 ├── 5.1 ~ 5.2.md └── 5.3 ~ 5.5.md ├── 6장 AOP ├── 6.1 ~ 6.2.md ├── 6.3 ~ 6.5.md └── 6.6 ~ 6.9.md ├── 8장 스프링이란 무엇인가 └── 8.1 ~ 8.4.md └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "domain-analysis-inflearn"] 2 | path = domain-analysis-inflearn 3 | url = https://github.com/Today-I-Learn/domain-analysis-inflearn.git 4 | -------------------------------------------------------------------------------- /DDD Start!/10장 이벤트/CHAPTER 10.md: -------------------------------------------------------------------------------- 1 | ## 시스템 간 강결합의 문제 2 | 3 | 쇼핑몰에서 구매를 취소하려면 환불을 처리해야 한다. 이때 환불 기능을 실행하려면 환불 기능을 제공하는 도메인 서비스를 파라미터로 전달받고 취소 도메인 기능에서 도메인 서비스를 실행해야 한다. 또는 응용 서비스에서 환불 기능을 실행해야 한다. 보통 결제 시스템은 외부에 존재하므로 외부의 환불 시스템 서비스를 호출하는데, 이때 두 가지 문제가 발생한다. 4 | 5 | - 외부 서비스가 정상이 아닐 경우 트랜잭션 처리 6 | - 성능 7 | 8 | 추가로 설계상 문제가 나타날 수 있다. 9 | 10 | 위와 같은 문제들이 발생하는 이유는 BOUNEDED CONTEXT간의 강결합 때문이다. 이런 강한 결합을 없앨 수 있는 방법이 바로 **이벤트**를 사용하는 것이다. 특히 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다. 11 | 12 | ### 이벤트 개요 13 | 14 | 이벤트 15 | 16 | - '과거에 벌어진 어떤 것'을 의미한다. 17 | - 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다. 18 | - 이벤트가 발생하면 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다. 19 | 20 | 보통 '~할 때', '~가 발생하면', '만약 ~하면' 과 같은 요구사항은 도메인의 상태 변경과 관련된 경우가 많고 이런 요구사항을 이벤트를 이용해서 구현할 수 있다. 21 | 22 | ### 이벤트 관련 구성 요소 23 | 24 | ![ddd_start_10_01](https://user-images.githubusercontent.com/40006670/137708572-982b5792-4412-4fef-a18e-d1c1602b7343.png) 25 | 26 | - 도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다. 이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생한다. 27 | - 이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다. 이벤트 핸들러는 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다. 28 | - 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처이다. 이벤트 생성 주체는 이벤트를 생성해서 디스패처에 이벤트를 전달한다. 이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다. 29 | 30 | ### 이벤트의 구성 31 | 32 | - 이벤트 종류 : 클래스 이름으로 이벤트 종류를 표현 33 | - 이벤트는 현재 기준으로 과거에 벌어진 것을 표현하기 때문에 이벤트 이름에는 **과거 시제**를 사용한다. 34 | - 이벤트 발생 시간 35 | - 추가 데이터 : 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보 36 | - 이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 최소한의 데이터를 담아야 한다. 이 것이 부족할 경우 핸들러는 필요한 데이터를 읽기 위해 관련 API를 호출하거나 DB에서 데이터를 직접 읽어와야 한다. 37 | 38 | ### 이벤트 용도 39 | 40 | - 트리거 41 | - 도메인의 상태가 바뀔 때 다른 후처리를 해야 할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다. 42 | - 데이터 동기화 43 | - 도메인은 이벤트를 발생시키고 이벤트 핸들러는 정보를 동기화시킨다. 44 | 45 | ### 이벤트 장점 46 | 47 | - 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다. 48 | - 기능 확장이 용이하다. 기능을 확장해도 도메인 로직은 수정할 필요가 없다. 49 | 50 | ### 이벤트, 핸들러, 디스패처 구현 51 | 52 | - 이벤트 클래스 53 | - 이벤트 핸들러 54 | - 이벤트 55 | 56 | ### 이벤트 클래스 57 | 58 | - 이벤트 자체를 위한 상위 타입은 존재하지 않는다. 원하는 클래스를 이벤트로 사용한다. 59 | 주의해야 할 점은 과거에 벌어진 상태 변화나 사건을 의미하므로 이름을 결정할 때는 과거 시제를 사용해야 한다는 점이다. 60 | - 이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 한다. 61 | - 모든 이벤트가 공통으로 갖는 프로퍼티가 존재한다면 관련 상위 클래스를 만들 수도 있다. 62 | 63 | ### EventHandler 인터페이스 64 | 65 | - 이벤트 핸들러를 위한 상위 인터페이스이다. 66 | - EventHandler 인터페이스를 상속받는 클래슨느 `handle()` 메서드를 이용해서 필요한 기능을 구현하면 된다. 67 | - `canHandle()` 메서드는 핸들러가 이벤트를 처리할 수 있는지 여부를 검사한다. 68 | 69 | ### 이벤트 디스패처인 Events 구현 70 | 71 | - 도메인을 사용하는 응용 서비스는 이벤트를 받아 처리할 핸들러를 `Events.handle()`로 등록하고, 도메인 기능을 실행한다. 72 | - 이벤트를 발생시킬 때는 `Events.raise()`메서드를 사용한다. 73 | - Events는 핸들러 목록을 유지하기 위해 [ThreadLocal](https://javacan.tistory.com/entry/ThreadLocalUsage) 변수를 사용한다. 톰캣과 같은 웹 애플리케이션 서버는 스레드를 재사용하므로 THreadLocal에 보관한 값을 제거하지 않으면 기대했던 것과 다르게 동작할 수 있다. 74 | 75 | ### 이벤트 처리 흐름 76 | 77 | ![ddd_start_10_02](https://user-images.githubusercontent.com/40006670/137708597-e4a4b937-675b-4cef-b81d-154f7dbd3a12.png) 78 | 79 | 80 | 1. 이벤트 처리에 필요한 이벤트 핸들러 생성 81 | 2. 이벤트 발생 전에 이벤트 핸들러를 `Events.handle()` 메서드를 이용해서 등록 82 | 3. 이벤트를 발생하는 도메인 기능 실행 83 | 4. 도메인은 `Events.raise()`를 이용해서 이벤트 발생 84 | 5. `Events.raise()` 는 등록된 핸들러의 `canHandle()`을 이용해서 이벤트를 처리할 수 있는지 확인 85 | 6. 핸들러가 이벤트를 처리할 수 있다면 `handle()`메서드를 이용해서 이벤트 처리 86 | 7. `Events.raise()` 실행을 끝내고 리턴 87 | 8. 도메인 기능 실행을 끝내고 리턴 88 | 9. `Events.reset()`을 이용해 ThreadLocal을 초기화 89 | 90 | 코드 흐름을 보면 응용 서비스와 동일한 트랜잭션 범위에서 핸들러의 `handle()`이 실행되는 것을 알 수 있다. 즉, 도메인의 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행된다. 91 | 92 | ### AOP를 이용한 Events.reset() 실행 93 | 94 | 응용 서비스가 끝나면 ThreadLocal에 등록된 핸들러 목록을 초기화하기 위해 `Events.reset()` 메서드를 실행하는데 이는 중복이 발생한다. 따라서 AOP를 이용해서 `Events.reset()`을 실행할 수 있다. 95 | 96 | ### 동기 이벤트 처리 문제 97 | 98 | 외부 시스템이 느려진다면 이벤트를 발생시키는 메서드도 함께 느려진다. 이 것은 외부 시스템의 성능 저하가 바로 내 시스템의 성능 저하로 연결된다는 것을 의미한다. 성능 저하뿐만 아니라 트랜잭션도 문제가 된다. 99 | 100 | 위와 같은 문제를 해결하는 방법 중 하나가 이벤트를 비동기로 처리하는 것이다. 101 | 102 | ### 비동기 이벤트 처리 103 | 104 | - 로컬 핸들러를 비동기로 실행 105 | - 메시지 큐 사용 106 | - 이벤트 저장소와 이벤트 포워더 사용 107 | - 이벤트 저장소와 이벤트 제공 API 사용 108 | 109 | ### 로컬 핸들러의 비동기 실행 110 | 111 | - 이벤트 핸들러를 별도 스레드로 실행하는 것이다. 112 | - `ExecutorService`를 이용하여 비동기를 실행한다. executor는 내부적으로 사용하는 스레드 풀을 이용해서 인자로 전달받은 람다식을 실행하므로 결과적으로 `raise()` 메서드를 실행하는 스레드가 아닌 다른 스레드를 이용해서 이벤트 핸들러를 비동기로 실행하게 된다. 113 | - `executor.submit(() -> handler.handle(event));` 114 | - 별도 스레드로 이벤트 핸들러를 사용한다는 것은 `raise()`메서드와 관련된 트랜잭션 범위에 이벤트 핸들러 실행이 묶이지 않는다는 것을 의미한다. 115 | - 별도 스레드를 이용해서 이벤트 핸들러를 실행하면 이벤트 발생 코드와 같은 트랜잭션 범위에 묶을 수 없기 때문에 한 트랜잭션으로 실행해야 하는 이벤트 핸들러는 비동기로 처리해서는 안된다. 116 | 117 | > 스프링의 트랜잭션 관리자는 일반적으로 스레드를 이용해서 트랜잭션을 전파한다. 물론, 스레드가 아닌 다른 방식을 이용해서 트랜잭션을 전파할 수 있지만 일반적으로 사용하는 트랜잭션 관리자는 스레드를 이용해서 트랜잭션을 전파한다. 이런 이유로 다른 스레드에서 실행되는 두 메서드는 서로 다른 트랜잭션을 사용하게 된다. 118 | > 119 | 120 | ### 메시징 시스템을 이용한 비동기 구현 121 | 122 | ![99918450-400f8e80-2d5a-11eb-9b31-b7d4608989aa](https://user-images.githubusercontent.com/40006670/137708634-cbea739c-2ee9-4a9f-8ae4-a9afeabe1b04.png) 123 | 124 | 125 | 1. 이벤트가 발생하면 이벤트 디스패처는 이벤트를 메시지 큐에 보낸다. 126 | 2. 메시지 큐는 이벤트를 메시지 리스너에 전달하고. 메시지 리스너는 알맞은 이벤트 핸들러를 이용해서 이벤트를 처리한다. 127 | 이때 이벤트를 메시지 큐에 저장하는 과정과 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리된다. 128 | - 필요하다면 이벤트를 발생하는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 하는데 이를 위해서는 글로벌 트랜잭션이 필요하다 129 | - 글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있는 장점이 있지만 전체 성능이 떨어지는 단점도 있다. 130 | - 메시지 큐를 사용하면 보통 이벤트를 발생하는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다. 이는 Java의 경우 이벤트 발생 JVM과 이벤트 처리 JVM이 다르다는 것을 의미한다. 동일 JVM에서 비동기 처리를 위해 메시지 큐를 사용하는 것은 시스템을 복잡하게 만들뿐이다. 131 | - RabbitMQ처럼 많이 사용되는 메시징 시스템은 글로벌 트랜잭션 지원과 함께 클러스터와 고가용성을 지원하기 때문에 안정적으로 메시지를 전달할 수 있는 장점이 있다. 또한 다양한 개발 언어와 통신 프로토콜을 지원하고 있다. 132 | 133 | ### 이벤트 저장소를 이용한 비동기 처리 134 | 135 | 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달하는 것이다. 136 | 137 | ![99919198-58ce7300-2d5f-11eb-897a-d5f5de43a851](https://user-images.githubusercontent.com/40006670/137708652-52869cf7-1003-4db9-80e3-b2a2b1faf013.png) 138 | 139 | 140 | 1. 이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다. 141 | 2. 포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다. 142 | 3. 포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리된다. 143 | 144 | 이 방식은 도메인의 상태와 이벤트 저장소로 동일한 DB를 사용한다. 즉, 도메인의 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리된다. 145 | 146 | API 방식과 포워더 방식의 차이점은 이벤트를 전달하는 방식에 있다. 포워더 방식에서는 포워더를 이용해서 이벤트를 외부에 전달하는 방식이라면, API 방식에서는 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져오는 방식이다. 147 | 148 | - 포워더 방식 149 | - 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다. 150 | - API 방식 151 | - 이벤트 목록을 요구하는 외부 핸들러가 자신이 어디까지 이벤트를 처리했는지 기억해야 한다. 152 | 153 | ### 이벤트 적용 시 추가 고려사항 154 | 155 | 1. 이벤트 소스를 EventEntry에 추가할 지 여부 156 | 2. 포워더에서 전송 실패를 얼마나 허용할 것인지 157 | - 포워드를 구현할 때는 실패한 이벤트의 재전송 횟수에 제한을 두어야 한다. 158 | 3. 이벤트 손실 159 | - 이벤트 저장소를 사용하는 방식은 이벤트 발생과 이벤트 저장을 한 트랜잭션으로 처리하기 때문에 트랜잭션에 성공하면 이벤트가 저장된다는 것을 보장할 수 있다. 반면에 로컬 핸들러를 이용해서 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다. 160 | 4. 이벤트 순서 161 | - 이벤트를 발생 순서대로 외부 시스템에 전달해야 할 경우 이벤트 저장소를 사용하는 것이 좋다. 이벤트 저장소는 일단 저장소에 이벤트를 발생 순서대로 저장하고, 그 순서대로 이벤트 목록을 제공하기 때문이다. 반면에 메시징 시스템은 사용 기술에 따라 이벤트 발생 순서와 메시지 전달 순서가 다를 수도 있다. 162 | 5. 이벤트 재처리 163 | - 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할 지 결정해야 한다. 164 | -------------------------------------------------------------------------------- /DDD Start!/11장 CQRS/README.md: -------------------------------------------------------------------------------- 1 | # 11장. CQRS 2 | 3 |
4 | 5 | 6 | ## 단일 모델의 단점 7 | 8 | - 주문 내역을 조회하기 위해서는 여러 애그리거트에서 데이터를 가져와야 한다. 9 | - 주문 도메인에서 주문 정보를 가져와야 하고 10 | - 상품 도메인에서 상품 이름을 가져와야하고 11 | - 회원 도메인에서 회원의 이름과 아이디를 가져와야 한다 12 | 13 |
14 | 15 | ### ID 참조 방식의 한계 16 | 17 | - 조회 화면의 특성상 조회 속도가 빠를수록 좋은데 여러 애그리거트에서 데이터를 가져와야 할 경우 구현 방법을 고민해보아야 한다. 18 | - 3장의 ID를 이용한 애그리거트 참조 방식을 사용하면 즉시 로딩 방식과 같은 JPA의 쿼리 관련 최적화 기능을 사용할 수 없다. 19 | - 즉, 한번의 SELECT 쿼리로 조회 화면에 필요한 데이터를 읽어올 수 없기 때문에 조회 속도에 문제가 생길 수 있다. 20 | 21 |
22 | 23 | ### 직접 참조 방식의 한계 24 | 25 | - 애그리거트 간의 연관관계를 ID가 아닌 직접 참조하는 방식으로 설정하더라도 고민이 발생한다. 26 | - 조회 화면의 특성에 따라 직접 참조하는 애그리거트들도 즉시 로딩 또는 지연 로딩 방식으로 처리해야하는 경우가 생기기 때문이다. 27 | - RDBMS가 제공하는 전용 기능을 이용해서 조회 쿼리를 작성해야하는 경우에는 네이티브 쿼리를 사용해야 할 수도 있다. 28 | 29 | 30 |
31 | 32 | ### 구현의 복잡도를 낮추는 방법 33 | 34 | - 결과적으로 이러한 고민이 발생하는 이유는 시스템의 상태를 변경할 떄와 조회할 때 단일 도메인 모델을 사용하기 때문이다. 35 | - 객체지향 도메인 모델 구현을 위한 ORM기법은 도메인 상태 변경 구현에는 적합하지만 36 | - 여러 애그리거트에서 데이터를 가져와 출력하는 기능을 구현하는데는 고민할 것들이 많아 구현을 복잡하게 만드는 원인이 된다. 37 | - 이러한 구현의 복잡도를 낮추기 위한 간단한 방법은 상태 변경을 위한 모델과 조회를 위한 모델을 분리하는 것이다. 38 | 39 |
40 | 41 | ## CQRS 42 | 43 | - 시스템이 제공하는 기능을 크게 두 가지로 나누어 생각할 수 있다. 44 | - 상태를 변경하는 기능 (ex. 새로운 주문 생성, 배송지 정보 변경, 회원 정보 변경) 45 | - 상태 정보를 조회하는 기능 (ex. 주문 상세 내역 보기, 게시글 목록 보기, 판매 통계 데이터 보기) 46 | - 도메인 모델 관점에서 상태 변경 기능은 주로 한 애그리거트의 상태를 변경한다. 47 | - ex. 주문 취소 기능과 배송지 정보 변경 등의 기능은 한 개의 주문 애그리거트를 변경한다. 48 | - 반면 조회기능은 한 애그리거트의 데이터를 조회할 수 있지만 둘 이상의 애그리거트에서 데이터를 조회할 수도 있다. 49 | - 상태를 변경하는 범위와 조회하는 범위가 정확하게 일치하지 않기 때문에 단일 모델로 두 종류의 기능을 구현하면 모델이 불필요하게 복잡해진다. 50 | - 상태 변경 명령을 위한 모델과 상태를 제공하는 조회를 위한 모델을 분리하는 CQRS (Command Query Responsibility Segregation) 패턴은 이러한 문제를 쉽게 해결해준다. 51 | 52 |
53 | 54 | ### 복잡한 도메인에서의 조회 기능 구현 55 | 56 | - 도메인이 복잡할수록 명령 기능과 조회 기능이 다루는 데이터의 범위에 많은 차이가 발생하게 된다. 57 | - 두 기능을 단일 모델로 처리할 경우 조회 기능의 로딩 속도를 위해 모델 구현이 필요 이상으로 복잡해지는 문제가 발생한다. 58 | - 온라인 쇼핑에서 다양한 차원에서 주문 및 판매 통계를 조회하는 경우를 가정하자. 59 | - JPA 기반의 단일 도메인 모델을 사용하면 통계 값을 빠르게 조회하기 위해서 JPA와 관련된 다양한 성능 관련 기능을 모델에 적용해야한다. 60 | 61 |
62 | 63 | ### CQRS를 이용한 조회 기능 성능 최적화 64 | 65 | - CQRS를 사용하면 각 모델에 맞는 구현 기술을 선택할 수 있다. 66 | - ex. 명령 모델은 객체지향에 기반해서 JPA를 사용해서 구현하고 조회 모델은 MyBatis를 사용해서 구현할 수 있다. 67 | - 조회 모델에는 단순히 데이터를 읽어와 조회하는 기능이기 때문에 컨트롤러에서 DAO를 직접 실행하여 응용 서비스가 존재하지 않을 수 있다. 68 | - 뿐만 아니라 명령 모델과 조회 모델이 서로 다른 데이터 저장소를 사용할 수도 있다. 69 | - ex. 명령 모델은 RDBMS를 사용하고 조회 모델은 Redis와 같은 인메모리 데이터베이스를 사용할 수 있다. 70 | - 두 저장소 사이의 데이터 동기화는 이벤트를 활용해서 처리할 수 있는데 명령 모델에서 상태를 변경하면 이벤트가 발생하고 해당 이벤트를 조회 모델에 전달해서 변경을 반영하면 된다. 71 | - 서로 다른 저장소 사이에 동기화를 제공하는 방법은 다양하며(글로벌 트랜잭션 및 어느정도 딜레이를 허용하는 경우 비동기 적으로 처리) 상황에 따라 이를 적절하게 선택할 수 있다. 72 | 73 |
74 | 75 | ### 웹 서비스와 CQRS 76 | 77 | - 일반적인 웹 서비스는 상태를 변경하는 요청보다 조회하는 요청이 많다. (파레토의 법칙에 따라서 약 2:8 정도) 78 | - ex. 온라인 쇼핑몰을 예로들면 주문 요청보다 카탈로그를 조회하고 상품의 상세 정보를 조회하는 요청이 비교할 수 없을 정도로 많다. 79 | - 조회 성능을 높이기 위해서 쿼리 최적화, 읽기 전용 데이터베이스, 캐시 등의 방법을 이용할 수도 있는데 이를 위해 다양한 기법을 사용하는 것은 결과적으로 CQRS를 적용한 것과 동일한 효과를 만든다. 80 | - 이처럼 대규모 트래픽이 발생하는 웹 서비스는 단지 명시적으로 명령과 조회 모델을 구분하지 않을 뿐 알게모르게 CQRS를 적용하게 된다. 81 | - 조회 속도를 높이기 위해 별도 처리를 하고 있다면 명시적으로 명령 모델과 조회 모델을 구분하여 모델이 복잡해지는 것을 방지할 수 있고 조회에 특화된 구현 기법을 쉽게 적용할 수 있다. 82 | 83 |
84 | 85 | ### CQRS의 장점 86 | 87 | - CQRS를 적용할 때 얻을 수 있는 장점 중 하나는 명령 모델을 구현할 때 도메인 자체에 집중할 수 있다는 것이다. 88 | - 복잡한 도메인은 상태 변경 로직이 복잡한데 이를 분리하면 조회 성능을 위한 코드가 명령 모델에 없기 때문에 도메인 로직을 구현하는데 집중할 수 있다. 89 | - 조회 단위로 캐시를 적용하거나 조회에 특화된 쿼리를 마음대로 적용하는 등 조회 성능을 높이기 위한 코드가 명령 모델에 영향을 주지 않기 떄문에 조회 성능 향상시키는데 유리하다. 90 | 91 |
92 | 93 | ### CQRS의 단점 94 | 95 | - 조회 전용 모델을 만들기 위한 구현 코드 및 비용이 증가하게 된다. 96 | - 트래픽이 많지 않은 서비스라면 굳이 조회 전용 모델을 만들 이유가 없다. 97 | - 경우에 따라서 다양하고 많은 구현 기술들을 도입해야 할 수 있다. 98 | -------------------------------------------------------------------------------- /DDD Start!/1장 도메인 모델 시작/README.md: -------------------------------------------------------------------------------- 1 | # 1장. 도메인 모델 시작 2 | 3 |
4 | 5 | ## 1. 도메인 6 | 7 | - 도메인은 소프트웨어로 해결하고자하는 문제의 영역이라고 할 수 있다. 8 | - ex. 온라인 서점 9 | - 도메인은 다시 하위 도메인으로 나눌 수 있으며 하나의 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다. 10 | - ex. 온라인 서점에서 고객이 물건을 구매하면 하위 도메인인 주문, 결제, 배송 등의 기능이 엮여서 고객에게 주문한 상품을 전달하는 일련의 과정을 처리한다. 11 | - 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야할 모든 기능을 구현하는 것은 아니며 기능을 제공하기 위해 외부 시스템을 이용할 수 있다. 12 | - ex. 온라인 서점 도메인에서 결제와 배송과 같은 하위 도메인은 외부 PG 회사나 물류 회사의 시스템을 이용할 수 있다. 13 | - 도메인마다 고정된 하위 도메인이 존재하는 것은 아니기 때문에 하위 도메인을 어떻게 구성할지 여부는 상황에 따라서 달라지게 된다. 14 | 15 |
16 | 17 | ## 2. 도메인 모델 18 | 19 | - 도메인 모델의 기본적인 정의는 특정 도메인을 개념적으로 표현한 것을 의미한다 즉, 도메인 자체를 이해하기 위한 개념 모델이다. 20 | - 도메인 모델은 객체 기반의 클래스 다이어그램이나, 상태 전이 모델링을 이용한 상태 다이어그램과 같은 방법들을 이용해서 표현할 수 있다. 21 | - 즉, 도메인 모델은 도메인을 이해하는데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않는다. 22 | - 이러한 도메인 모델들은 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는데 도움이 된다. 23 | - 개념적인 모델을 이용해서 바로 코드를 작성하는 것은 어렵기 때문에 구현 기술에 맞는 구현 모델이 따로 필요하다. 24 | - 개념 모델과 구현 모델은 서로 다른 것이지만 구현 모델이 개념 모델을 최대한 따르게 할 수 있다. 25 | - ex. 객체 기반 모델을 이용해서 도메인을 표현한 경우 객체지향 언어를 이용해서 개념 모델에 가깝게 구현할 수 있다. 26 | 27 | 28 | 29 |
30 | 31 | ### 하위 도메인과 모델 32 | 33 | - 도메인은 다수의 하위 도메인으로 구성되며 하위 도메인이 다루는 영역이 서로 다르기 때문에 같은 용어라도 도메인 마다 의미가 달라질 수 있다. 34 | - ex. 카탈로그 도메인의 상품이 상품 가격, 상세 내용을 담고있는 정보를 의미한다면 배송 도메인의 상품은 고객에게 실제 배송되는 물리적인 상품을 의미한다. 35 | - 즉, 도메인에 따라서 용어의 의미가 결정되기 때문에 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안된다. 36 | - 각각의 모델의 구성요소는 특정 도메인을 한정할 때 의미가 완전해지기 때문에 하위 도메인마다 별도의 모델을 만들어야 한다. 37 | 38 |
39 | 40 | ## 3. 도메인 모델 패턴 41 | 42 | - 일반적인 애플리케이션 아키텍처는 표현 계층, 응용 계층, 도메인 계층, 인프라 계층으로 구성된다. 43 | - 표현 계층 : 클라이언트 요청을 처리하고 정보를 보여주는 계층으로 클라이언트는 유저 뿐만 아니라 외부 시스템도 클라이언트가 될 수 있다. 44 | - 응용 계층 : 사용자가 요청안 기능을 실행하는 계층으로 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다. 45 | - 도메인 계층 : 시스템이 제공할 도메인 규칙을 구현한다. 46 | - 인프라 계층 : 데이터베이스나 메세징 시스템과 같은 외부 시스템과의 연동을 처리한다. 47 | 48 |
49 | 50 | ### 주문 도메인 모델 51 | 52 | - 주문 도메인의 경우 다음과 같은 규칙을 구현한 코드가 도메인 계층에 위치하게 된다. 53 | - 출고 전에 배송지를 변경할 수 있다. 54 | - 주문 취소는 배송 전에만 할 수 있다. 55 | - 도메인 모델 패턴은 이러한 도메인 규칙을 객체지향 기법으로 구현하는 것을 의미한다. 56 | - 배송 변경지 가능 여부를 판단하는 핵심 규칙을 구현하는 코드를 주문이나 주문 상태 도메인 모델 안에 구현하게 되면 규칙이 바뀌거나 확장해야 될 때 다른 코드에 영향을 덜 주고 변경 내역을 쉽게 모델에 반영할 수 있다. 57 | 58 |
59 | 60 | ### 개념 모델과 구현 모델 61 | 62 | - 개념 모델은 순수하게 문제를 분석한 결과물로 데이터베이스, 트랜잭션, 성능, 구현 기술 등과 같은 것들을 고려하지 않고 있기 때문에 실제 코드를 작성할 때 개념 모델을 있는 그대로 사용할 수 없다. 63 | - 때문에 개념 모델을 구현 가능한 형태의 모델로 전환하는 과정을 거치게 된다. 64 | - 개념 모델을 처음부터 완벽하게 도메인을 표현하는 모델로 만드는 것은 불가능에 가까우며 도메인 지식이 시간이 지나 새로운 통찰을 얻으면서 완전히 새로운 의미로 해석되는 경우도 존재한다. 65 | - 떄문에 프로젝트 초기에 완벽한 도메인 모델을 만들더라도 결국 도메인에 대한 새로운 지식이 쌓이면서 모델을 보완하거나 수정하는 일이 발생하게 된다. 66 | - 때문에 처음부터 완벽한 개념 모델을 만들기 보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야하고 이를 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다. 67 | 68 |
69 | 70 | ## 4. 도메인 모델 도출 71 | 72 | - 도메인에 대한 이해 없이 개발을 하는 것은 불가능하기 때문에 기획서, 사용 사례, 사용자 스토리와 같은 요구사항과 관련자와의 대화를 통해 도메인을 이해하고 이를 바탕으로 도메인 모델 초안을 만들어야 코드를 작성하는 것이 가능하다. 73 | - 즉, 어떠한 방법이나 도구를 선택하든지 간에 구현을 시작하기 위해서는 도메인에 대한 초기 모델이 필요하다. 74 | 75 |
76 | 77 | ### 도메인 모델링 78 | 79 | - 도메인을 모델링 할 때 가장 기본이 되는 작업은 모델을 구성하는 핵심 구성 요소와 규칙을 찾는 것이다. 80 | - 이러한 과정은 요구사항에서 출발하며 주문 도메인과 관련된 요구사항을 분석하면 다음과 같다. 81 | 82 |
83 | 84 | #### 주문 도메인 요구사항 85 | 86 | - 최소 한 종류 이상의 상품을 주문해야한다. 87 | - 한 상품을 한 개 이상 주문할 수 있다. 88 | - 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다. 89 | - 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다. 90 | - 주문할 때 배송지 정보를 반드시 지정해야 한다. 91 | - 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다. 92 | - 출고를 하면 배송지 정보를 변경할 수 없다. 93 | - 출고 전에 주문을 취소할 수 없다. 94 | - 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다. 95 | 96 |
97 | 98 | #### 요구사항을 통해 기능을 도출하기 99 | 100 | - 요구사항에서 알 수 있는 것은 다음과 같다. 101 | - 주문 상태를 출고 상태로 변경하기 102 | - 배송지 정보 변경하기 103 | - 주문 취소하기 104 | - 결제 완료로 변경하기 105 | 106 |
107 | 108 | ### 문서화 109 | 110 | - 문서화를 하는 주된 이유는 지식을 공유하기 위함이다. 111 | - 실제 구현은 코드를 보면 알 수 있지만 코드는 상세한 모든 내용을 다루고있기 때문에 코드를 이용해서 전체 소프트웨어를 분석하려면 많은 시간을 투자해야한다. 112 | - 전반적인 기능 목록이나 모듈 구조, 빌드 과정은 코드를 보고 직접 이해하는 것보다 상위 수준에서 정리한 문서를 참조하는 것이 소프트웨어 전반을 빠르게 이해하는데 도움된다. 113 | - 코드를 보면서 도메인을 깊게 이해하게 되기 때문에 코드 자체도 문서화의 대상이 된다. 114 | - 도메인 지식이 잘 묻어나도록 코드를 작성하지 않으면 코드의 동작 과정은 해석할 수 있어도 도메인 ㅗ간점에서 왜 코드를 그렇게 작성했는지 이해하는데는 도움이 되지 않는다. 115 | - 단순히 코드를 보기 좋게 작성하는 것뿐만 아니라 도메인 관점에서 코드가 도메인을 잘 표현해야 비로소 코드의 가독성이 높아지며 문서로서 코드가 의미를 갖게 된다. 116 | 117 |
118 | 119 | ## 5. 엔티티와 벨류 120 | 121 | - 도출한 모델은 크게 `Entity` 와 `Value` 로 구분할 수 있는데 이 둘을 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 둘의 차이를 명확하게 이해하는 것은 도메인을 구현하는데 있어 중요하다. 122 | 123 |
124 | 125 | ### 엔티티 126 | 127 | - 엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다. 128 | - 주문에서 배송지의 주소가 바뀌거나 상태가 변하더라도 주문 번호가 바뀌지 않는 것처럼 엔티티의 식별자는 바뀌지 않는다. 129 | - 엔티티를 생성하고 삭제할 때 까지 식별자는 유지된다. 130 | - 엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티의 식별자가 같으면 두 엔티티가 같다고 판단할 수 있다. 131 | - 때문에 엔티티를 구현한 클래스는 식별자를 이용해서 `equals()` 와 `hashCode()` 를 구현할 수 있다. 132 | 133 |
134 | 135 | #### 엔티티의 식별자 생성 136 | 137 | - 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라지지만 대부분 다음의 방식 중 한가지로 생성하게 된다. 138 | - 특정 규칙에 따라서 생성 → 주문번호, 운송장번호, 카드 번호 139 | - UUID 140 | - 값을 직접 입력 → 회원의 아이디나 이메일 141 | - 데이터베이스 시퀀스나 자동 증가 컬럼과 같은 일련번호를 사용 142 | 143 |
144 | 145 | ### 벨류 타입 146 | 147 | - `Value` 타입은 개념적으로 완전한 하나를 표현할 때 사용한다. 예를들어 받는 사람을 위한 `Value` 타입인 `Receiver` 를 다음과 같이 작성할 수 있다. 148 | 149 | ```java 150 | public class Receiver { 151 | 152 | private String name; 153 | private String phoneNumber; 154 | 155 | public Receiver(String name, String phoneNumber) { 156 | this.name = name; 157 | this.phoneNumber = phoneNumber; 158 | } 159 | } 160 | ``` 161 | 162 |
163 | 164 | - `ShoppingInfo` 의 각각의 필드인 `receiverName` , `receiverPhonenumber` 필드가 필드 이름을 통해서 받는 사람을 위한 데이터 라는 것을 유추할 수 있다면 `Receiver` 는 그 자체로 받는 사람을 의미한다. 165 | - 즉, `Value` 타입을 통해서 개념적으로 완전한 하나를 잘 표현할 수 있다. 166 | - `Value` 타입은 두 개 이상의 데이터를 하나로 묶는 경우 뿐만 아니라 의미를 명확하게 표현하기 위해 사용하는 경우도 존재한다. 167 | - `OrderLine` 클래스의 `price` , `amounts` 는 돈을 의미하는데 돈을 의미하는 `Money` 타입을 만들어서 사용하면 코드를 이해하는데 도움이 된다. 168 | 169 | ```java 170 | public class Money { 171 | 172 | private int value; 173 | 174 | public Money(int value) { 175 | this.value = value; 176 | } 177 | } 178 | ``` 179 | 180 |
181 | 182 | - `Value` 타입을 사용할 때의 또 다른 장점은 `Value` 타입을 위한 기능을 추가로 작성할 수 있다는 것인데 `Money` 타입의 경우 돈 계산을 위한 기능을 추가할 수 있다. 183 | 184 |
185 | 186 | #### 벨류 타입과 불변 객체 187 | 188 | - `Value` 객체의 데이터를 변경할 떄는 기존의 데이터를 변경하는 것보다 변경된 데이터를 갖는 새로운 `Value` 객체를 생성하는 방식을 선호한다. 189 | - 이러한 타입의 객체를 불변 타입이라고 하는데 `Value` 타입을 불변으로 구현하는 가장 중요한 이유는 안전한 코드를 작성할 수 있기 때문이다. 190 | - 즉, 동시에 여러 쓰레드가 `Value` 타입에 접근해서 데이터를 변경하는 경우나 의도하지 않은 방법으로 값을 갱신하는 것과 같은 상황에서 불변 객체로 설계할 경우 이로 인한 문제를 사전에 예방할 수 있다. 191 | 192 |
193 | 194 | ### 엔티티 식별자와 벨류 타입 195 | 196 | - `Entity` 타입을 비교할 때 식별자를 사용한다면 `Value` 타입을 비교할 때는 모든 속성이 같은지를 비교해야한다. 197 | - 엔티티의 식별자의 실제 데이터는 카드 번호나 이메일 주소와 같이 문자열로 구성된 경우가 많다. 198 | - `Money` 가 단순한 숫자가 아닌 도메인의 돈을 의미하는 것 처럼 식별자는 단순 문자열이 아닌 도메인에서 특별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 `Value` 타입을 사용해서 의미가 잘 들어나도록 할 수 있다. 199 | 200 |
201 | 202 | ### DTO의 접근자와 수정자 메서드 203 | 204 | - DTO는 Data Transfer Object의 약자로 프레젠테이션 계층과 도메인 계층이 데이터를 서로 주고 받을 때 사용하는 일종의 구조체이다. 205 | - 이전에는 요청 파라미터나 데이터베이스 컬럼의 값을 설정할 때 `setter` 메서드를 필요로 했기 떄문에 구현 기술을 적용하려면 `getter` , `setter` 메서드를 DTO에 정의해야했다. 206 | - DTO가 도메인 로직을 담고있지는 않기 때문에 `getter` , `setter` 을 제공해도 도메인 객체의 데이터 일관성에 영향을 줄 가능성은 높지 않다. 207 | - 최근 프레임워크는 `setter` 메서드가 아닌 `private` 필드에 직접 값을 할당할 수 있는 기능을 제공하기 때문에 `setter` 를 반드시 제공하지 않아도된다. 208 | - 위의 기능을 활용하면 DTO도 불변 객체로 설계할 수 있고 불변 객체의 장점을 DTO까지 확장할 수 있게 된다. 209 | 210 |
211 | 212 | ## 6. 도메인 용어 213 | 214 | - 도메인 용어는 매우 중요하며 이를 코드에 반영하지 않으면 개발자에게 코드의 의미를 해석해야하는 부담감을 주게 된다. 215 | - 때문에 도메인 용어를 코드에 최대한 반영하면 코드를 도메인 용어로 해석하거나 도메인 용어를 코드로 해석하는 과정이 줄어들고 코드만 보고도 의미를 명확하게 이해할 수 있다. 216 | - 즉, 코드의 가독성을 높여서 코드를 분석하고 이해하는 시간을 줄여준다. 217 | -------------------------------------------------------------------------------- /DDD Start!/2장 아키텍처 개요/CHAPTER 2.md: -------------------------------------------------------------------------------- 1 | # 2장. 아키텍처 개요 2 | 3 | - 아키텍처 4 | - DIP 5 | - 도메인 영역의 주요 구성요소 6 | - 인프라스터럭처 7 | - 모듈 8 | 9 | --- 10 | 11 | ## 네 개의 영역 12 | 13 | 아키텍처를 설계할 때 전형적으로 표현, 응용, 도메인, 인프라스트럭처 네 개의 영역으로 나누게 된다. 14 | 15 | ### 표현 영역 16 | 17 | 사용자의 요청을 해석해서 응용 서비스에 전달하고 응용 서비스의 실행 결과를 사용자가 이해할 수 있는 형식으로 변환해서 응답한다. 18 | 19 | ### 응용 영역 20 | 21 | 도메인 모델을 이용해서 시스템이 사용자에게 제공해야 할 기능을 구현한다. 22 | 23 | 실제 로직 구현은 도메인 모델에 위임한다. 24 | 25 | ### 도메인 영역 26 | 27 | 도메인 모델을 구현하고, 도메인의 핵심 로직을 구현한다. 28 | 29 | ### 인프라스트럭처 영역 30 | 31 | RDBMS 연동, 메세징 전송 & 수신, HTTP 클라이언트를 이용한 REST API 호출 등 논리적인 개념을 표현하기보다는 실제 구현을 다룬다. 32 | 33 | 표현, 응용, 도메인 영역은 구현 기술을 사용한 코드를 직접 만들지 않고, 인프라스트럭처 영역에서 제공하는 기능을 사용해서 필요한 기능을 개발한다. 34 | 35 | --- 36 | 37 | ## 계층 구조 아키텍처 38 | 39 | 기본적으로 계층 구조는 표현 → 응용 → 도메인 → 인프라스트럭처 로 구성되고, 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다. 40 | 41 | 계층 구조를 엄격하게 적용하면 상위 계층은 바로 아래의 계층에만 의존을 가져야 하지만 보통 유현하게 적용한다. 42 | 43 | 예를 들어 응용 계층은 외부 시스템과의 연동을 위해 두단계 아래 계층인 인프라스트럭처 계층에 의존하기도 한다. 44 | 45 | 계층 구조는 직관적으로 이해하기는 쉽지만 표현, 응용, 도메인 계층이 상세한 구현 기술을 다루는 인프라스트럭처 계층에 종속된다는 문제가 있다. 46 | 47 | 첫번째 문제로는 테스트 하기 어려워진다는 문제점이 있고, 두번째 문제는 구현 방식을 변경하기 어렵다는 점이다. 48 | 49 | 결론적으로 이처럼 인프라스트럭처에 의존하게 되면 '테스트 어려움'과 '기능 확장의 어려움' 이라는 두가지 문제가 발생한다. 50 | 51 | 이를 해결하기 위해 DIP를 적용할 수 있다. 52 | 53 | --- 54 | 55 | ## DIP 56 | 57 | 고수준 모듈 - 의미 있는 단일 기능을 제공하는 모듈 58 | 59 | 저수준 모듈 - 고수준 모듈의 기능을 구현하기 위한 하위 기능을 실제로 구현한 모듈 60 | 61 | 고수준 모듈이 제대로 동작하려면 저수준 모듈을 사용해야하는데 이렇게 되면 계층 구조 아키텍처에서 언급했던 두 가지 문제('테스트 어려움'과 '기능 확장의 어려움')가 발행한다. 62 | 63 | DIP는 이 문제를 해결하기 위해 추상화한 인터페이스를 통해 저수준 모듈이 고수준 모듈에 의존하도록 바꾼다. 64 | 65 | 고수준 모듈의 개념을 추상화한 인터페이스는 고수준 모듈에 속하게 되고 실제 구현(저수준 모듈)은 이를 의존(상속은 의존의 다른 형태)한다. 66 | 67 | 이렇게 반대로 저수준 모듈이 고수준 모듈에 의존한다고 해서 이를 DIP(Dependency Inversion Principle, 의존 역전 원칙)라고 부른다. 68 | 69 | ### DIP 주의사항 70 | 71 | DIP 를 적용할 때 단순하게 인터페이스와 구현 클래스 분리가 아니라 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야한다. 즉, 저수준 모듈에서 인터페이스를 추출하면 안된다. 72 | 73 | ### DIP와 아키텍처 74 | 75 | 아키텍처 수준에서 DIP 를 적용하면 인프라스트럭처 영역이 응용 영역과 도메인 영역에 의존하는 구조가 된다. 76 | 77 | 인프라스트럭처에 위치한 클래스가 도메인이나 응용 영역에 정의한 인터페이스를 상속받아 구현하는 구조가 되면 도메인과 응용 영역에 영향을 최소화하면서 구현 기술을 변경하는 것이 가능하다. 78 | 79 | ## 도메인 영역의 주요 구성요소 80 | 81 | 도메인 영역의 모델은 도메인의 주요 개념을 표현하며 핵심이 되는 로직을 구현한다. 82 | 83 | |요소|설명| 84 | |:---|:---| 85 | |엔티티 (ENTITY)|고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 가짐
도메인의 고유한 개념을 표현하고 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.
ex) 주문(Order), 회원(Member), 상품(Product)| 86 | |밸류 (VALUE)|개념적으로 하나인 도메인 객체의 속성을 표현할 때 사용된다.
엔티티의 속성으로 사용될 뿐만 아니라 다른 밸류 타입의 속성으로도 사용될 수 있다.
ex) 주소(Address), 금액(Money)| 87 | |애그리거트 (AGGREGATE)|관련된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
ex) 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 '주문' 애그리거트로 묶을 수 있다.| 88 | |리포지터리 (REPOSITORY)|도메인 모델의 영속성을 처리한다.
ex) DBMS 테이블에서 엔티티 객체를 로딩 하거나 저장하는 기능을 제공| 89 | |도메인 서비스 (DOMAIN SERVICE)|특정 엔티티에 속하지 않은 도메인 로직을 제공
ex) '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현 - 도메인 로직이 여러 엔티티와 밸류를 필요로 할 경우 도메인 서비스에서 로직을 구현한다.| 90 | 91 | --- 92 | 93 | ### 엔티티와 밸류 94 | 95 | 도메인 모델의 엔티티와 DB 관계형 모델의 엔티티는 같은 것이 아니다! 96 | 97 | 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다. 예를 들어, 주문을 표현하는 엔티티는 주문과 관련된 데이터뿐만 아니라 배송지 주소 변경을 위한 기능을 함께 제공한다. 98 | 99 | 또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다. RDBMS와 같은 관계형 데이터베이스는 밸류 타입을 제대로 표현하기 힘들다. 덕분에 도메인 모델의 엔티티를 통해 도메인을 보다 잘 이해할 수 있게 된다. 100 | 101 | 밸류는 불변으로 구현하는 것을 권장하는데, 이는 엔티티의 밸류 타입 데이터를 변경할 때 객체 자체를 완전히 교체한다는 것을 의미한다. 102 | 103 | --- 104 | 105 | ### 애그리거트 106 | 107 | 도메인 모델의 구성요소는 규모가 커질수록 복잡해진다. 108 | 109 | 도메인 모델을 볼 때 개별 객체뿐만 아니라 상위 수준에서 모델을 볼 수 있어야 전체 모델의 관계와 개별 모델을 이해하는데 도움이 된다. 도메인 모델에서 전체 구조를 이해하는데 도움이 되는 것이 바로 애그리거트(AGGREGATE)이다. 110 | 111 | 한마디로, 애그리거트는 관련 객체를 하나로 묶은 군집이다. 애그리거트를 통해 큰 틀에서 도메인 모델을 관리할 수 있게 된다. 112 | 113 | 애그리거트는 군집에 속한 객체들을 관리하는 루트 엔티티를 갖고, 이는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능을 제공한다. 114 | 115 | 애그리거트를 사용하는 코드는 애그리거트 루트를 통해 애그리거트 내의 다른 엔티티나 밸류 객체에 접근하게 되고 이는 내부 구현을 숨겨서 애그리거트 단위로 구현을 캡슐화 하기 위함이다. 116 | 117 | 애그리거트를 어떻게 구성했느냐에 따라 구현이 복잡해지기도 하고 트랜잭션 범위가 달라지기도 한다. 118 | 119 | --- 120 | 121 | ### 리포지터리 122 | 123 | 도메인 객체를 지속적으로 사용하려면 물리적인 저장소에 도메인 객체를 보관해야 하는데, 이를 위한 도메인 모델이 리포지터리(repository)이다. 124 | 125 | 리포지터리는 애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다. 애그리거트 루트는 애그리거트에 속한 모든 객체를 포함하고 있으므로 결과적으로 애그리거트 단위로 저장하고 조회한다. 126 | 127 | 도메인 모델 관점에서 리포지터리 인터페이스는 도메인 객체를 영속화하는데 필요한 기능을 추상화한 것으로 고수준 모듈에 속한다. 기반 기술을 이용해서 해당 인터페이스를 구현한 리포지터리 구현체는 저수준 모듈로 인프라스트럭처 영역에 속한다. 128 | 129 | --- 130 | 131 | ## 요청 처리 흐름 132 | 133 | 사용자가 애플리케이션에 기능 실행을 요청하면 처음에 표현 영역에서 요청을 받게된다. 스프링 MVC 에서는 컨트롤러가 그 역할을 하게 된다. 134 | 135 | 표현 영역은 사용자가 전송한 데이터 형식이 올바른지 검사하고 문제가 없다면 데이터를 이용해서 응용 서비스에 기능 실행을 위임한다. 136 | 137 | 응용 서비스는 도메인 모델을 이용해서 기능을 구현한다. 기능 구현에 필요한 도메인 객체를 리포지터리에서 가져와 실행하거나 신규 도메인 객체를 생성해서 리포지터리에 저장한다. 138 | 139 | --- 140 | 141 | ## 인프라스트럭처 개요 142 | 143 | 인프라스트럭처(Infrastructure)는 표현 영역, 응용 영역, 도메인 영역을 지원한다. 이때 도메인 영역과 응용 영역에서 인프라스트럭처의 기능을 직접 사용하는 것보다 이 두 영역에 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는것이 시스템을 더 유연하고 테스트하기 쉽게 만들어준다. 144 | 145 | 구현의 편리함은 DIP가 주는 장점(변경의 유연함, 테스트가 쉬움)만큼 중요하기 때문에 DIP의 장점을 해치지 않는 범위에서 응용 영역과 도메인 영역에서 구현 기술에 대한 의존을 가져가는 것이 현명하다. 응용 영역과 도메인 영역이 인프라스트럭처에 대한 의존을 완전히 갖지 않도록 시도하게 되면 자칫 구현을 더 복잡하고 어렵게 만들 수 있기 때문이다. 146 | 147 | --- 148 | 149 | ## 모듈 구성 150 | 151 | 아키텍처의 각 영역은 별도 패키지에 위치한다. 152 | 153 | 모듈 구조를 얼마나 세분화해야 하는지에 대한 정해진 규칙은 없다. 단지, 한 패키지에 너무 많은 타입이 몰려서 코드를 찾을 때 불편한 정도만 아니면 된다. 패키지가 너무 복잡해지는 것 같다면 모듈을 분리하는 시도를 하면 될 것이다. 154 | -------------------------------------------------------------------------------- /DDD Start!/3장 애그리거트/CHAPTER 3.md: -------------------------------------------------------------------------------- 1 | # 3장 애그리거트 2 | 3 | - 애그리거트 4 | - 애그리거트 루트와 역할 5 | - 애그리거트와 리포지터리 6 | - ID를 이용한 애그리거트 참조 7 | 8 | ## 애그리거트 9 | 10 | 애그리거트를 통해 복잡한 도메인을 이용하고 관리하기 쉬운 단위로 만들수 있다. 11 | 12 | 수많은 객체를 애그리거트로 묶어서 바라보면 좀 더 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다. 13 | 14 | 애그리거트를 사용함으로써 모델 간의 관계를 개별 모델 수준뿐만 아니라 상위 수준에서도 이해할 수 있다. 15 | 16 | 애그리거트는 복잡한 도메인을 단순한 구조로 만들어주고 복잡도가 낮아지는 만큼 도메인 기능을 확장하고 변경하는 데 필요한 노력을 줄여준다. 17 | 18 | 애그리거트는 독립된 객체 군이며, 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다. 19 | 20 | 또한, 애그리거트의 경계를 설정할 때 기본이되는 것은 도메인 규칙과 요구사항이다. 21 | 22 | ## 애그리거트 루트 23 | 24 | 애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데 이 책임을 지는 것이 바로 애그리거트 루트 엔티티이다. 25 | 26 | 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속한다. 27 | 28 | ### 도메인 규칙과 일관성 29 | 30 | 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 유지하는 것이다. 31 | 32 | 이를 위해, 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현해야 한다. 33 | 34 | 또한, 애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 35 | 36 | 만일 직접 변경하게 되면 DB 테이블에서 직접 데이터를 수정하는 것과 같은 결과를 발생시켜 데이터의 일관성이 깨지기 때문이다. 37 | 38 | 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들기 위해 아래 2가지를 적용해야한다. 39 | 40 | 1. 단순히 필드를 변경하는 `set` 메서드를 `public` 범위로 만들지 않는다. 41 | 2. 벨류 타입은 불변으로 구현한다. 42 | 43 | 공개 `set` 메서드를 사용하지 않게 되면 의미가 드러나는 메서드를 사용해서 구현할 가능성이 높아지고, 밸류 객체가 불변이면 밸류 객체의 값을 변경하기 위해 새로운 밸류 객체를 할당해야 한다. 44 | 45 | ### 트랜잭션 범위 46 | 47 | 트랜잭션 범위는 작을수록 좋다. 48 | 49 | 다수의 테이블에 수정이 필요한 경우 잠금의 대상이 많아지고, 이것은 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 뜻하며 전체적인 성능을 떨어뜨릴수 있다. 50 | 51 | 또한, 한 트랜잭션에서는 한 애그리거트만 수정하는 것이 좋다. 52 | 53 | 애그리거트는 서로 최대한 독립적이어야 하는데 한 애그리거트가 다른 애그리거트의 기능에 의존하기 시작하면 애그리거트 간 결합도가 높아지게 된다. 54 | 55 | ## 리포지터리와 애그리거트 56 | 57 | 애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다. 58 | 59 | 리포지터리는 적어도 `save`, `findById` 메서드를 제공하여 애그리거트를 저장하고, 사용할 수 있어야한다. 60 | 61 | 또한 애그리거트르 영속화할 저장소로 무엇을 사용하든지 간에 애그리거트의 상태가 변경되면 모든 변경을 원자적으로 저장소에 반영해야 한다. 62 | 63 | ## ID를 이용한 애그리거트 참조 64 | 65 | 애그리거트에서 다른 애그리거트를 참조한다는 것은 애그리거트 루트를 참조한다는 것과 같다. 66 | 67 | 필드 또는 `get` 메서드를 이용한 애그리거트 참조를 사용하면 `편한 탐색 오용`, `성능에 대한 고민`, `확장 어려움` 이라는 문제를 야기할 수 있다. 68 | 69 | `편한 탐색 오용`은 애그리거트 내부에서 다른 애그리거트 객체에 접근이 쉽고, 이런 편리함 때문에 다른 애그리거트를 수정하고자 하는 유혹에 빠지게 된다. 70 | 71 | 한 애그리거트에서 다른 애그리거트의 상태를 변경하는 것은 애그리거트간의 의존 결합도를 높여서 결과적으로 애그리거트의 변경을 어렵게 만든다. 72 | 73 | `성능에 대한 고민`은 JPA 를 사용할 경우 지연로딩과 즉시로딩중 어떤 방식을 선택할지에 대한 다양한 경우의 수를 고려해서 전략을 결정해야 한다. 74 | 75 | `확장 어려움`은 추후 도메인별로 시스템을 분리하여 서로 다른 종류의 DB를 사용하게 된다면, 더 이상 다른 애그리거트 루트를 참조하기 위해 JPA 와 같은 단일 기술을 사용할 수 없음을 의미한다. 76 | 77 | 이러한 문제들을 해결하기 위한 방법이 ID를 이용해서 다른 애그리거트를 참조하는 것이다. 78 | 79 | ID를 이용한 참조 방식을 사용하면 복잡도를 낮추는 것과 함께 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 원천적으로 방지할 수 있다. 80 | 81 | ## 애그리거트 간 집합 연관 82 | 83 | 개념적으로는 애그리거트 간에 `1:N` 연관이 있더라도 성능상의 문제 때문에 애그리거트 간의 `1:N` 연관을 실제 구현에 반영하는 경우는 드물다. 84 | 85 | `M:N` 연관은 개념적으로 양쪽 애그리거트에 컬렉션으로 연관을 만든다. 86 | 87 | 예를들어 상품이 여러 카테고리에 속할 수 있다고 가정하면 카테고리와 상품은 `M:N`이고 실제 요구사항을 고려해서 구현에 포함시킬지 여부를 결정해야한다. 88 | 89 | 요구사항을 통해 카테고리에서 상품으로의 집한 연관은 필요하지 않다면, 개념적으로는 상품과 카테고리의 양방향 `M:N` 연관이 존재하지만 실제 구현에서는 상품에서 카테고리로의 단방향 `M:N` 연관만 적용하면 된다. 90 | 91 | 또한 `M:N` 연관을 `RDBMS`를 이용해 구현하는 경우에는 조인 테이블을 사용한다. 92 | 93 | ## 애그리거트를 팩토리로 사용하기 94 | 95 | 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해볼 필요가 있다. -------------------------------------------------------------------------------- /DDD Start!/4장 리포지터리와 모델 구현 (JPA 중심)/README.md: -------------------------------------------------------------------------------- 1 | # JPA를 이용한 리포지터리 구현 2 | 3 | - 애그리거트를 어떤 저장소에 저장하느냐에 따라서 리포지터리를 구현하는 방법이 다르다. 4 | - 도메인 모델과 리포지터리를 구현할 때 선호하는 기술 중 하나는 JPA를 들 수 있따. 5 | 6 | ## 모듈 위치 7 | 8 | - 리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고 리포지터리를 구현한 클래스는 인프라 영역에 속한다. 9 | - DIP 따라서 리포지터리 구현 클래스는 인프라 영역에 위치하게 된다. 10 | - 리포지터리 구현 클래스를 도메인의 하위 패키지에 위치시키는 경우도 있지만 좋은 설계 원칙은 아니며 가능하면 리포지터리 구현 클래스를 인프라 영역에 위치시켜 인프라에 대한 의존을 낮춰야한다. 11 | 12 | ## 리포지터리 기본 기능 구현 13 | 14 | - 리포지터리의 기본 기능은 아이디로 애그리거트를 조회하는 것과 애그리거트를 저장하는 것이다. 15 | - 인터페이스는 애그리거트 루트를 기준으로 작성한다. 16 | 17 | 삭제 요구사항이 있더라도 데이터를 실제로 삭제하는 경우는 많지 않다. 18 | 관리자 기능에서 삭제한 데이터까지 조회하거나 데이터를 복구하기 위해 일정 기간 보관해야하는 경우도 있기 때문에 사용자가 삭제 기능을 실행할 때 데이터를 바로 삭제하기 보다는 삭제 플래그를 이용해서 데이터를 화면에 보여줄지 여부를 결정하는 방식으로 구현한다. 19 | 20 | # 매핑 구현 21 | 22 | ## 앤티티와 밸류 기본 매핑 구현 23 | 24 | 하이버네이트는 데이터베이스에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 사용해서 객체를 생성하기 때문에 기본 생성자를 반드시 추가해야한다. 25 | 26 | ## 필드 접근 방식 사용 27 | 28 | - 앤티티에 프로퍼티를 위한 `get` , `set` 메서드를 추가하면 도메인의 의도가 사라지고 객체가 아닌 데이터 기반으로 엔티티를 구현할 가능성이 높아진다. 29 | - 특히 `set` 메서드는 내부 데이터를 외부에서 변경할 수 있는 수단이 되기 때문에 캡슐화를 깨는 원인이 될 수있다. 30 | - 엔티티가 객체로서 제 역할을 하려면 외부에 `set` 메서드 대신 의도가 잘 드러나는 기능을 제공해야한다. 31 | - ex. 상태 변경을 위한 `setState()` 메서드 보다는 주문 취소를 위한 `cancel()` 메서드가 도메인을 더 잘 표현한다. 32 | - 불변 객체로 설계할 떄 뿐만 아니라 JPA의 구현 방식 떄문에서라도 공개적인 `set` 메서드를 추가하는 것은 좋지않다. 33 | - 엔티티를 객체가 제공할 기능 중심으로 구현하도록 유도하려면 JPA 매핑 처리를 프로퍼티 방식이 아닌 필드 방식으로 선택해서 불필요한 `get` , `set` 메서드를 구현하지 말아야한다. 34 | 35 | ## AttributeConverter를 이용한 밸류 매핑 처리 36 | 37 | - 두 개 이상의 프로퍼티를 가진 밸류 타입을 한 개의 컬럼에 매핑해야하는 경우 `@Embeddable` 로는 처리할 수 없다. 38 | - JPA 2.0 이하 버전에서는 이를 처리하기 위해 컬럼과 매핑하기 위한 프로퍼티를 따로 추가하고 `get` , `set` 메서드에서 실제 밸류 타입과 변환 처리를 수행해야 했다. 39 | - JPA 2.1 버전부터는 데이터베이스 컬럼과 밸류 사이의 변환 코드를 모델에 구현하지 않아도 `AttributeConverter` 를 이용해서 처리할 수 있다. 40 | 41 | ## 밸류 컬렉션 : 별도 테이블 매핑 42 | 43 | - 주문 엔티티는 한 개 이상의 주문 리스트를 가질 수 있으며 주문 리스트에 순서가 있다면 `List` 타입을 이용해서 주문 리스트 타입의 컬렉션을 프로퍼티로 가지게 된다. 44 | 45 | ```java 46 | public class Order { 47 | 48 | private List orderLines; 49 | } 50 | ``` 51 | 52 | - 밸류 타입의 컬렉션은 별도 테이블에 보관하며 밸류 컬렉션을 저장하는 테이블은 외부키를 이용해서 엔티티에 해당하는 테이블을 참조한다. 53 | - 외부키는 컬렉션이 속할 엔티티를 의미하며 `List` 타입의 컬렉션은 인덱스 값도 필요하기 때문에 인덱스 값을 저장하기 위한 별도의 컬럼도 존재한다. 54 | - 밸류 컬렉션을 별도 테이블로 매핑할 떄는 `@ElementCollection` 과 `@CollectionTable` 을 함꼐 사용하며 인덱스 값을 저장하기 위해서 `@OrderColumn` 을 이용할 수 있다. 55 | 56 | ```java 57 | @Entity 58 | public class Order { 59 | 60 | @ElementCollection 61 | @CollectionTable(name = "order_line", 62 | joinColumns = @JoinColumn(name = "order_number")) 63 | @OrderColumn(name = "line_idx") 64 | private List orderLines; 65 | } 66 | ``` 67 | 68 | ## 밸류 컬렉션 : 한 개 컬럼 매핑 69 | 70 | # 애그리거트 로딩 전략 71 | 72 | - JPA매핑을 설정할 떄 항상 기억해야 할 점은 애그리거트에 속한 객체가 모두 모여야 완전한 하나가 된다는 것이다. 73 | - 즉, 애그리거트 루트를 로딩하면 루트에 속한 모든 객체가 완전한 상태여야 함을 의미한다. 74 | 75 | ## 애그리거트와 즉시로딩 76 | 77 | - 조회 시점에서 애그리거트를 완전한 로딩 상태가 되도록 하려면 애그리거트 루트에서 연관 매핑의 조회 방식을 즉시 로딩으로 설정하면된다. 78 | - 즉시 로딩 방식으로 설정하면 애그리거트 루트를 로딩하는 시점에 애그리거트에 속한 모든 객체를 함꼐 로딩할 수 있다는 장점이 있지만 오히려 단점으로 작용할 경우가 많다. 79 | - 특히 컬렉션 로딩 전략을 즉시 로딩으로 설정하는 경우가 문제가되는데 이 경우 모든 데이터를 불러오기 위해 테이블을 조인한 쿼리가 카타시안 조인을 사용해 결과에 중복을 발생시킬 수 있다. 80 | - 하이버네이트가 중복된 데이터를 적절하게 제거해서 변환해주지만 애그리거트가 커지면 문제가 될 수 있다. 81 | - 실제 필요한 데이터보다 훨씬 많은 데이터를 조회하면서 조회 성능이 오히려 나빠지는 상황이 발생한다. 82 | 83 | ## 애그리거트와 지연로딩 84 | 85 | - 애그리거트는 개념적으로는 하나여야 하지만 루트 엔티티를 로딩하는 시점에 애그리거트에 속한 모든 객체를 로딩해야하는 것은 아니다. 86 | - 애그리거트가 완전해야하는 이유는 다음과 같은 이유로 생각해볼 수 있다. 87 | - 상태를 변경하는 기능을 실행할 때 애그리거트 상태가 완전해야한다. 88 | - 표현 영역에서 애그리거트의 상태 정보를 보여줄 때 필요하다. 89 | - 두 번째는 별도의 조회 전용 기능을 구현하는 방식을 사용하는 것이 유리할 떄가 많기 때문에 애그리거트의 완전한 로딩과 관련된 문제는 상태 변경과 더 관련이있다. 90 | - 상태 변경 기능을 위해서 조회 시점에 즉시 로딩을 이용해서 애그리거트를 완전한 상태로 로딩할 필요는 없으며 JPA는 트랜잭션 범위 내에서 지연 로딩을 허용하기 떄문에 실제로 상태를 변경하는 시점에 필요한 구성 요소만 로딩해도 문제가 되지 않는다. 91 | 92 | # 애그리거트의 영속성 전파 93 | 94 | - 애그리거트가 완전한 상태여야 한다는 것은 애그리거트 루트를 조회할 때 뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야 함을 의미한다. 95 | - 저장 메서드는 애그리거트 루트만 저장하면 안되고 애그리거트에 속한 모든 객체를 저장해야한다. 96 | - 삭제 메서드는 애그리거트 루트 뿐만 아니라 애그리거트에 속한 모든 객체를 삭제해야 한다. 97 | 98 | # 식별자 생성 기능 99 | 100 | - 식별자 생성은 사용자 직접 생성, 도메인 로직으로 생성, 데이터베이스를 이용한 일련번호 사용 등의 방법으로 생성할 수 있다. 101 | - 이메일 주소처럼 사용자가 직접 식별자를 입력하는 경우 식별자 생성 주체가 사용자이기 때문에 도메인 영역에 식별자 생성 기능을 구현할 필요는 없다. 102 | - 식별자 생성 규칙이 있는 경우 엔티티를 생성할 때 이미 생성한 식별자를 전달하기 때문에 엔티티가 직접 이러한 기능을 제공하기 보다는 별도의 서비스로 식별자 생성 기능을 분리해야한다. 103 | - 식별자 생성 규칙은 도메인 규칙이기 떄문에 도메인 영역에 기능을 위치시켜야한다. 104 | - 식별자 생성으로 데이터베이스의 자동 증가 컬럼을 사용할 경우 데이터베이스의 `insert` 쿼리를 실행해야 식별자가 생성되기 때문에 도메인 객체를 리포지터리에 저장할 때 식별자가 생성된다. 105 | - 즉, 도메인 생성 시점에는 식별자를 알 수 없고 도메인을 저장할 뒤에 식별자를 구별할 수 있음을 의미한다. 106 | -------------------------------------------------------------------------------- /DDD Start!/5장 리포지터리의 조회 기능(JPA 중심)/CHAPTER 5.md: -------------------------------------------------------------------------------- 1 | # 5장 리포지터리의 조회 기능 (JPA 중심) 2 | 3 | ## 검색을 위한 스펙 4 | 5 | 리포지터리는 애그리거트의 저장소이다. 애그리거트를 저장하고 찾고 삭제하는 것이 기본 기능이다. 6 | 7 | 애그리거트를 찾을 때 식별자를 이용하는 것이 기본이지만 식별자 외에 여러 다양한 조건으로 애그리거트를 찾아야 할 때가 있다. 검색 조건의 조합이 다양할 경우 **스펙**을 이용해야 한다. 8 | 9 | ### 스펙 10 | 11 | 스펙 (Specification)은 애그리거트가 특정 조건을 충족하는지 검사한다. 12 | 13 | ```java 14 | public interface Specification { 15 | public boolean isSatisfiedBy(T agg); 16 | } 17 | ``` 18 | 19 | `isSatisfiedBy()` 메서드는 검사 대상 객체가 조건을 충족하면 true를 리턴하고, 그렇지 않으면 false를 리턴한다. 20 | 21 | ### 스펙 조합 22 | 23 | 두 스펙을 AND 연산자나 OR 연산자로 조합해서 새로운 스펙을 만들 수 있고, 조합한 스펙을 다시 조합해서 더 복잡한 스펙을 만들 수 있다. 24 | 25 | ```java 26 | public class AndSpec implements Specification { 27 | private List> specs; 28 | 29 | public AndSpecification(Specification ... specs) { 30 | this.spec = Arrays.asList(specs); 31 | } 32 | 33 | public boolean isSatisfiedBy(T agg) { 34 | for (Specification spec : specs) { 35 | if (!spec.isSatisfied(agg)) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | } 41 | } 42 | ``` 43 | 44 | specs라는 List를 만들어 `isSatisfiedBy()` 메서드를 통해 해당 스펙들을 검사하여 AND로 조합하는 스펙 AndSpec을 구현할 수 있다. 마찬가지로 OrSpec도 비슷하게 구현할 수 있다. 45 | 46 | ## JPA를 위한 스펙 구현 47 | 48 | 위의 예로 보여준 방식은 모든 애그리거트를 조회한 다음에 스펙을 이용해서 걸러내는 방식을 사용하는데 이 방식을 이용하면 실행 속도 문제가 발생한다. 49 | 50 | 위 문제를 해결하려면 쿼리의 where 절에 조건을 붙여서 필요한 데이터를 걸러야 한다. 51 | 52 | 이는 스펙 구현도 메모리에서 걸러내는 방식에서 쿼리의 where를 사용하는 방식으로 바꿔야 한다는 뜻이다. 53 | 54 | ### JPA 스펙 구현 55 | 56 | JPA는 다양한 검색 조건을 조합하기 위해 CriteriaBuilder와 Predicate를 사용한다. 57 | 58 | ```java 59 | public interface Specification { 60 | Predicate toPredicate(Root root, CriteriaBuilder cb); 61 | } 62 | ``` 63 | 64 | ```java 65 | public class OrdererSpec implements Specification { 66 | private String OrdererId; 67 | 68 | ... 69 | 70 | @Override 71 | public Predicate toPredicate(Root root, CriteriaBuilder cb) { 72 | return cb.equal(root.get(Order_.orderer) 73 | .get(Orderer_.memberId).get(MemberId_.id), ordererId); 74 | } 75 | } 76 | ``` 77 | 78 | OrdererSpec의 `toPredicate()` 메서드는 Order의 `orderer.memberId.id` 프로퍼티가 생성자로 전달받은 ordererId와 같은지 비교하는 Predicate를 생성해서 리턴한다. 79 | 80 | 응용 서비스는 원하는 스펙을 생성하고 리포지터리에 전달해서 필요한 애그리거트를 검색하면 된다. 81 | 82 | Specification 구현 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아도 된다. 83 | 84 | ### @StaticMetamodel 85 | 86 | 위 코드에 Order_ 클래스와 Orderer_ 클래스가 있는데 이 것은 JPA의 정적 메타 모델을 정의한 코드이다. 87 | 88 | @StaticMetamodel 애노테이션을 이용해서 관련 모델을 지정한다. 메타 모델 클래스는 모델 클래스의 이름 뒤에 '_'을 붙인 이름을 갖는다. 89 | 90 | 정적 모델 클래스는 대상 모델의 각 프로퍼티와 동일한 이름을 갖는 정적 필드를 정의한다. 이 정적 필드를 프로퍼티에 대한 메타 모델로서 프로퍼티 타입에 따라 SingualAttribute, ListAttribute 등의 타입을 사용해서 메타 모델을 정의한다. 91 | 92 | 정적 메타 모델을 사용하는 대신 문자열로 프로퍼티를 지정할 수도 있지만 코드 안정성이나 생산성 등의 이유로 정적 메타 모델 클래스를 사용한다. 93 | 94 | [참고 자료](https://docs.jboss.org/hibernate/orm/5.4/topical/html_single/metamodelgen/MetamodelGenerator.html) 95 | 96 | ### AND/OR 스펙 조합을 위한 구현 97 | 98 | 생성자로 전달받은 Specification 리스트를 Predicate 리스트로 바꾸고 CriteriaBuilder의 and() 와 or()를 사용해서 새로운 Predicate를 생성한다. 99 | 100 | ### 스펙을 사용하는 JPA 리포지터리 구현 101 | 102 | 1. 스펙의 루트를 생성 103 | 2. 파라미터로 전달받은 스펙을 이용해서 Predicate를 생성 104 | 3. Predicate 전달 105 | 106 | ## 정렬 구현 107 | 108 | 정렬 순서가 고정된 경우에는 CreteriaQuery#orderBy()나 JPQL의 order by 절을 이용하면 되지만, 정렬 순서를 응용 서비스에서 결정하는 경우에는 정렬 순서를 리포지터리에 전달할 수 있어야 한다. 109 | 110 | 정렬 순서를 지정하는 코드는 리포지터리를 사용하는 응용 서비스에 위치하게 되는데 응용 서비스는 CriteriaBuilder에 접근할 수 없다. 따라서 응용 서비스는 JPA Order가 아닌 다른 타입을 이용해서 리포지터리에 정렬 순서를 전달하고 JPA 리포지터리는 다시 JPA Order로 변환하는 작업을 해야 한다. 111 | 112 | ## 페이징과 개수 구하기 구현 113 | 114 | JPA 쿼리는 `setFirstResult()`와 `setMaxResults()` 메서드를 제공하고 있는데 이 두 메서드를 이용해서 페이징을 구현할 수 있다. 115 | 116 | - `setFirstResult()` 메서드는 읽어올 척 번째 행 번호를 지정한다. 117 | - `setMaxResults()` 메서드는 읽어올 행 개수를 지정한다. 118 | 119 | ## 조회 전용 기능 구현 120 | 121 | 리포지터리는 애그리거트의 저장소를 표현하는 것으로서 다음 용도로 리포지터리를 사용하는 것은 적합하지 않다. 122 | 123 | - 여러 애그리거트를 조합해서 한 화면에 보여주는 데이터 제공 124 | - JPA의 지연 로딩과 즉시 로딩 설정, 연관 매핑의 어려움 125 | - 애그리거트 간에 직접 연관을 맺으면 ID로 참조할 때의 장점을 활용할 수 없다. 126 | - 각종 통계 데이터 제공 127 | - 다양한 테이블을 조인하거나 DBMS 전용 기능을 사용해야 하는데 JPQL이나 Criteria로 처리하기 힘들다. 128 | 129 | 이런 기능은 조회 전용 쿼리로 처리해야 한다. 130 | 131 | ### 동적 인스턴스 생성 132 | 133 | JPQL에서 new 키워드 뒤에 생성할 인스턴스의 완전한 클래스 이름을 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정한다. 134 | 135 | 조회 전용 모델을 만드는 이유는 표현 영역을 통해 사용자에게 데이터를 보여주기 위함이다. 136 | 137 | 동적 인스턴스의 장점은 JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다는 점이다. 138 | 139 | ### 하이버네이트 @Subselect 140 | 141 | - @Subselect 142 | - 쿼리 결과를 @Entity로 매핑할 수 있는 기능이다. 143 | - 조회 쿼리를 값으로 갖는다. 하이버네이트는 select 쿼리의 결과를 매핑할 테이블처럼 사용한다. DBMS에서 여러 테이블을 조인해서 조회한 결과를 한 테이블처럼 보여주기 위한 용도로 뷰를 사용하는 것처럼 쿼리 실행 결과를 매핑할 테이블처럼 사용한다. 144 | - 뷰를 수정할 수 없듯이 @Subselect로 조회한 @Entity 역시 수정할 수 없다. 실수로 수정하면 매핑한 테이블이 없으므로 에러가 발생한다. 145 | - 일반 @Entity와 같기 때문에 EntityManger#find(), JPQL, Criteria를 사용해서 조회할 수 있다는 장점이 있다. 146 | - @Subselect의 값으로 지정한 쿼리를 from 절의 서브쿼리로 사용한다. 147 | - 서브쿼리를 사용하고 싶지 않다면 네이티브 SQL 쿼리를 사용하거나 MyBatis와 같은 별도 매퍼를 사용해서 조회 기능을 구현해야 한다. 148 | - @Immutable 149 | - 위와 같은 문제를 방지하기 위해 사용된다. 150 | - 해당 엔티티의 매핑 필드/프로퍼티가 변경되어도 DB에 반영하지 않고 무시한다. 151 | - @Synchronize 152 | - 하이버네이트는 변경사항을 트랜잭션을 커밋하는 시점에 DB에 반영하므로 반영하지 않은 상태에서 조회하면 이전의 값이 발생하는데 이 문제를 해결하기 위한 용도로 사용한다. 153 | - 지정한 테이블과 관련된 변경이 발생하면 플러시를 먼저 실행한다. 154 | -------------------------------------------------------------------------------- /DDD Start!/6장 응용 서비스와 표현 영역/README.md: -------------------------------------------------------------------------------- 1 | # 표현 영역과 응용 영역 2 | 3 | - 도메인이 제 기능을 하려면 클라이언트와 도메인을 연결해주는 매개체가 필요한데 표현 영역과 응용 영역이 이에 해당한다. 4 | 5 |
6 | 7 | ## 표현 영역과 응용 영역의 역할 8 | 9 | - 표현 영역은 클라이언트의 요청을 해석한다. 10 | - 요청을 받은 표현 영역은 URL, 요청 파라미터, 쿠키, 헤더 등을 이용해서 사용자가 어떠한 기능을 실행하고 싶어하는지를 판별하고 그 기능을 제공하는 응용 서비스를 실행한다. 11 | - 응용 영역은 실제 사용자가 원하는 기능을 제공한다. 12 | - 응용 서비스는 기능을 실행하는데 필요한 입력값을 메서드 파라미터로 전달받고 실행 결과를 반환한다. 13 | 14 |
15 | 16 | # 응용 서비스의 역할 17 | 18 | - 응용 서비스는 클라이언트가 요청한 기능을 실행하는데 사용자의 요청을 처리하기 위해 리포지터리로부터 도메인 객체를 구하고 이를 이용한다. 19 | - 표현 영역의 입장에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해주는 파사드의 역할을 수행한다. 20 | - 응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 단순한 형태를 가지며 만약 응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직을 일부를 구현하고있을 가능성이 높다. 21 | - 응용 서비스의 또 다른 중요한 역할은 트랜잭션 처리이다. 22 | - 응용 서비스는 도메인 상태 변경을 트랜잭션으로 처리해야 한다. 23 | - 만약 응용 서비스의 메서드가 트랜잭션 내에서 실행되지 않는다면 데이터의 일관성이 깨지게된다. 24 | 25 |
26 | 27 | ## 도메인 로직을 응용 서비스에서 구현하면 안되는 이유 28 | 29 | - 도메인 로직을 도메인 영역과 응용 서비스에서 분산해서 구현하면 코드의 품질에 문제가 발생하게 된다. 30 | - 코드의 응집성이 떨어진다. 31 | - 도메인 데이터와 이를 조작하는 도메인 로직이 서로 다른 영역에 위치한다는 것은 로직을 파악하기 위해 여러 영역을 분석해야 한다는 것을 의미한다. 32 | - 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다, 즉 중복이 발생할 가능성이 존재한다. 33 | - 코드의 중복을 막기 위해서 응용 서비스 영역에 별도의 보조 클래스를 만들 수 있지만 애초에 도메인 영역에 이를 구현했다면 응용 서비스는 그 기능을 사용하기만 하면 된다. 34 | - 결국 이러한 단점들은 코드의 변경을 어렵게 만들며 소프트웨어의 중요한 경쟁 요소인 변경 용이성을 낮추고 소프트웨어의 가치를 저하시킨다. 35 | 36 |
37 | 38 | # 응용 서비스의 구현 39 | 40 | - 응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역하을 하는데 이는 파사드와 같은 역할을 한다. 41 | 42 |
43 | 44 | ## 응용 서비스의 크기 45 | 46 | - 응용 서비스를 구현할 때는 다음과 같은 두 가지 선택지 사이에서 고민을 하게 된다. 47 | - 하나의 응용 서비스 클래스에 도메인의 모든 기능을 구현하기 48 | - 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기 49 | - 하나의 응용 서비스 클래스에서 모든 기능을 구현하는 경우에는 동일 로직에 대한 코드 중복을 제거할 수 있다는 장점이 있다. 50 | - 하지만 클래스의 크기가 커진다는 것이 단점으로 작용하는데 즉, 클래스의 크기가 커진다는 것은 연관성이 적은 코드가 한 클래스에 위치할 가능성이 높아짐을 의미한다. 51 | - 결과적으로 관련 없는 코드가 뒤섞여 코드를 이해하기 어려워질 수 있으며 결국 이러한 문제점은 코드의 품질을 낮추는 결과를 초래한다. 52 | - 구분되는 기능별로 서비스 클래스를 구현하는 방식은 하나의 응용 서비스 클래스에서 1~3개 사이의 기능을 구현한다. 53 | - 이 방식을 사용하면 클래스의 개수는 많아지지만 하나의 클래스에 관련 기능을 모두 구현하는 것과 비교해서 코드의 품질을 일정 수준으로 유지할 수 있다는 장점이 존재한다. 54 | - 또한 각 클래스별로 필요한 의존 객체만 포함하기 때문에 다른 기능을 구현한 코드에 영향을 받지 않는다. 55 | - 각 기능마다 동일한 로직을 구현한 경우 여러 클래스에 동일한 코드를 구현해서 중복이 발생할 가능성이 존재하는데 이는 공통 로직을 구현하는 별도의 클래스를 이용하면 이를 방지할 수 있다. 56 | 57 |
58 | 59 | ```java 60 | public final class MemberServiceHelper { 61 | public static Member findExistingMember(String memberId) { 62 | Member memeber = memberRepository.findById(memberId); 63 | if(member == null) { 64 | throw new NoMemberException(memberId); 65 | } 66 | return member; 67 | } 68 | } 69 | ``` 70 | 71 |
72 | 73 | ## 응용 서비스의 인터페이스와 클래스 74 | 75 | - 응용 서비스를 구현할 때 논쟁이 될만 한 것은 인터페이스가 필요한지 여부이다. 76 | - 인터페이스가 필요한 몇가지 상황이 있는데 그 중 하나는 구현 클래스가 여러개인 경우이다. 77 | - 구현 클래스가 다수 존재하거나 런타임에 구현 객체를 교체해야할 경우에 인터페이스를 유용하게 사용할 수 있다. 78 | - 하지만 응용 서비스에서 런타임에 이를 교체하는 경우는 거의 없을 뿐더러 하나의 응용 서비스의 구현 클래스가 두 개 이상인 경우도 매우 드물다. 79 | - 이러한 이유에서 인터페이스와 클래스를 따로 구현하게되면 소스 파일만 많아지게되고 구현 클래스에 대한 간접 참조가 증가해서 전체 구조만 복잡해지는 결과를 낳게된다. 80 | - 따라서 인터페이스가 명화갛게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것은 좋은 설계라고 볼 수 없다. 81 | 82 |
83 | 84 | ### 테스트 주도 개발과 인터페이스 85 | 86 | - TDD를 이용해서 표현 영역부터 개발을 시작한다면 미리 응용 서비스를 구현할 수 없기 때문에 응용 서비스의 인터페이스부터 작성하게 될 것이다. 87 | - 표현 영역이 아닌 도메인 영역이나 응용 영역의 개발을 먼저 시작하면 응용 클래스가 먼저 만들어진다. 88 | - 이렇게 되면 표현 영역의 단위 테스트를 위해 응용 서비스의 가짜 객체가 필요한데 이를 위해서 인터페이스를 추가할 수도 있다. 89 | - Mockito와 같은 테스트 도구는 클래스에 대해서도 테스트용 Mock 객체를 만들수 있도록 도와주기 때문에 서비스에 대한 인터페이스 없이도 표현 영역을 테스트할 수 있다. 90 | - 결과적으로 이러한 도구들은 응용 서비스에 대한 인터페이스 필요성을 약화시켜준다. 91 | 92 |
93 | 94 | ## 메서드 파라미터와 값 리턴 95 | 96 | - 응용 서비스가 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는데 필요한 값을 파라미터를 통해 전달받아야 한다. 97 | - 이 때 메서드는 각각의 값을 개별 파라미터로 받을수도 있고 별도의 데이터 클래스를 만들어서 전달받을 수도 있다. 98 | - 표현 영역에서는 필요에 따라 응용 서비스에서 처리한 결과 값이 필요한 경우가 있는데 이 때 응용 영역에서는 애그리거트 객체를 그대로 리턴할 수도 있다. 99 | - 애그리거트 자체를 리턴하면 코드를 작성하는 것은 편할 수 있지만 도메인의 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게되며 이러한 기능 실행 로직의 분산은 코드의 응집도를 낮추는 원인이 된다. 100 | - 때문에 응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다. 101 | 102 |
103 | 104 | ## 표현 영역에 의존하지 않기 105 | 106 | - 응용 서비스의 파라미터 타입을 결정할 때 중요한 것은 HttpServeletRequest, HttpServeletResponse 등의 표현 영역에 해당하는 객체를 파라미터로 전달하면 안된다는 것이다. 107 | - 이는 응용 서비스에서 표현 영역에 대한 의존이 발생하는 원인이 되며 응용 서비스만 단독으로 테스트하기 어려워지는 결과로 이어지게된다. 108 | - 게다가 표현 영역의 구현이 변경됨에 따라서 의존하고 있는 응용 서비스의 구현도 함께 변경해야하는 문제가 발생하게 된다. 109 | - 더욱 심각한 문제는 응용 서비스가 세션 인증과 관리 등의 표현 영역의 역할까지 대신하는 상황이 발생할 수 있다는 것이다. 110 | - 즉, 표현 영역의 응집도가 깨지게 되어 결과적으로 표현 영역 코드만으로는 표현 영역의 상태가 어떻게 변경되는지 이해하고 관리하기 어려워지고 코드를 유지보수하는 비용을 증가시키게 된다. 111 | - 이러한 문제가 발생하지 않기 위해서는 철저하게 응용 서비스가 표현 영역의 기술을 사용하지 않도록 해야하며, 이를 위한 가장 쉬운 방법은 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 기술을 사용하지 않는 것이다. 112 | 113 |
114 | 115 | ## 도메인 이벤트 처리하기 116 | 117 | - 응용 서비스의 역할 중 하나는 도메인 영역에서 발생시킨 이벤트를 처리하는 것이다. 118 | - 이벤트는 도메인에서 발생한 상태 변경을 의미하며 암호 변경, 주문 취소 등이 이벤트가 될 수 있다. 119 | - 이벤트를 사용하면 코드가 다소 복잡해지는 대신에 도메인 간ㅇ의 의존성이나 외부 시스템에 대한 의존성을 낮춰주는 장점을 얻을 수 있다. 120 | 121 |
122 | 123 | # 표현 영역 124 | 125 | - 표현 영역의 책임은 크게 다음과 같다. 126 | - 사용자가 시스템을 사용할 수 있는 화면상의 흐름을 제공하고 제어한다. 127 | - 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다. 128 | - 사용자의 세션을 관리한다. 129 | 130 |
131 | 132 | # 값 검증하기 133 | 134 | - 값에 대한 검증은 표현 영역과 응용 서비스 영역 두 곳에서 모두 수행할 수 있다. 135 | - 원칙적으로는 모든 값에 대한 검증은 응용 서비스에서 처리하는데 예를들어 회원 가입을 처리하는 응용 서비스는 파라미터로 전달받은 값이 올바른지 검사해야한다. 136 | - 응용 서비스에서 값을 검증하는 로직을 사용하는 경우 폼에 에러 메세지를 보여주기 위해서 표현 계층에서는 다음과 같은 번잡한 코드를 작성해야 한다. 137 | 138 |
139 | 140 | ```java 141 | @Controller 142 | public class MemberController { 143 | 144 | @RequestMapping 145 | public String join(JoinRequest joinRequest, Errors errors) { 146 | try { 147 | joinService.join(joinRequest); 148 | return successView; 149 | } catch(EmptyPropertyException e) { 150 | errors.rejectValue(e.getPropertyName(), "empty"); 151 | return formView; 152 | } catch(InvalidPropertyException e) { 153 | errors.rejectValue(e.getPropertyName(), "invalid"); 154 | return formView; 155 | } catch(DuplicatedIdException e) { 156 | errors.rejectValue(e.getProepertyName(), "duplicate"); 157 | return formView; 158 | } 159 | } 160 | } 161 | ``` 162 | 163 |
164 | 165 | - 응용 서비스에서 각 값이 존재하는지 형식이 올바른지 확인할 목적으로 익셉션을 사용할 때의 문제점은 사용자에게 좋지 않은 경험을 제공한다는 것이다 166 | - 사용자는 폼에 값을 입력하고 전송했는데 입력한 값이 잘못되어 다시 폼에 입력해야 할 때 한개의 항목이 ㅇ아닌 입력한 모든 항목에 대해 잘못된 값이 존재하는지 알고싶을 것이다. 167 | - 응용 서비스에서 값을 검사하는 시점에 첫 번째 값이 올바르지 않아 익셉션을 발생시키면 나머지 항목에 대해서는 값을 검사하지 않게된다. 168 | - 이렇게되면 나머지 항목에 대해서 값이 올바른지 여부를 알 수 없게되며 이는 사용자가 같은 폼에 값을 여러번 입력하게 만든다. 169 | - 이러한 불편을 해소하기 위해서는 응용 서비스에 값을 전달하기 전에 표현 영역에서 검사하면 된다. 170 | - 스프링과 같은 프레임워크는 값 검증을 위한 Validator 인터페이스를 별도로 제공하므로 인터페이스를 구현한 검증기를 따로 구현하면 코드를 간결하게 줄일 수 있다. 171 | 172 |
173 | 174 | ```java 175 | @Controller 176 | public class MemberController { 177 | 178 | @RequestMapping 179 | public String join(JoinRequest joinRequest, Errors errors) { 180 | new JoinRequestValidator().validate(joinRequest, errors); 181 | 182 | ... 183 | } 184 | } 185 | ``` 186 | 187 |
188 | 189 | - 이렇게 표현 영역에서 필수 값과 값의 형식만 검사하면 실질적으로 응용 서비스는 아이디 중복 여부와 같은 논리적인 오류만 검사하면 된다. 190 | - 즉, 같은 값 검사를 표현 영역과 응용 서비스에서 중복적으로 할 필요가 없는 것이다. 191 | - 때문에 응용 서비스를 사용하는 표현 영역 코드가 한 곳이면 구현의 편리함을 위해 다음과 같이 역할을 분리할 수 있다. 192 | - 표현 영역 : 필수 값, 값의 형식, 범위 등을 검증 193 | - 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류 검증 194 | 195 |
196 | 197 | # 권한 검사 198 | 199 | - 개발할 시스템마다 권한의 복잡도가 달라지며 단순한 시스템은 인증 여부만 검사하면 되는데 반해, 어떤 시스템은 관리자인지 여부에 따라 사용할 수 있는 기능이 달라지기도한다. 200 | - 또, 역할마다 실행할 수 있는 기능이 달라지는 경우도 있으며 이런 다양한 상황을 충족하기 위해 스프링 시큐리티나 아파치 Shiro 같은 프레임워크는 유연하고 확장 가능한 구조를 가지고 있다. 201 | - 보안 프레임워크에 대한 이해가 부족하면 프레임워크를 무턱대고 도입하는 것보다는 개발할 시스템에 맞는 권한 검사 기능을 구현하는 것이 유지보수에 유리할 수 있다. 202 | 203 |
204 | 205 | ## 영역에 따른 권한 검사 수행 206 | 207 | - 표현 영역에서 할 수 있는 가장 기본적인 검사는 인증된 사용자인지 아닌지 여부를 검사하는 것이다. 208 | - 대표적으로 회원 정보 변경이 이에 해당하며 회원 정보 변경과 관련된 URL은 인증된 사용자만 접근해야 한다. 209 | - 회원 정보를 변경을 처리하는 URL에 대해 다음과 같이 접근 제어를 할 수 있다. 210 | - URL을 처리하는 컨트롤러에 웹 요청을 전달하기 전에 인증 여부를 검사해서 인증된 사용자의 요청만 컨트롤러에 전달한다. 211 | - 인증된 사용자가 아닌 경우 로그인 화면으로 리다이렉트 시킨다. 212 | - 이러한 제어를 하기 좋은 위치가 서블릿 필터이며 이 곳에서 사용자의 인증 정보를 생성하고 검사하는 것이다. 213 | - 스프링 시큐리티는 이와 유사한 방식으로 필터를 이용해서 인증 정보를 생성하고 웹 접근을 제어하게 된다. 214 | - URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 검사를 수행해야하며 이는 곧 응용 서비스 코드에서 직접 권한 검사를 해야함을 의미하는 것은 아니다. 215 | - 스프링 시큐리티는 AOP를 활용하여 애너테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다. 216 | - 도메인 단위로 권한 검사를 해야하는 경우는 구현이 다소 복잡해지는데 게시글의 삭제는 본인 또는 관리자만 가능한 경우를 고려해보자. 217 | - 이 경우 게시글 작성자가 본인인지 관리자인지 확인하려면 게시글 애그리거트를 먼저 로딩해야한다. 218 | - 즉, 응용 서비스 메서드 수준에서 권한 검사를 할 수 없기 때문에 직접 권한 검사 로직을 구현해야한다. 219 | 220 |
221 | 222 | # 조회 전용 기능과 응용 서비스 223 | 224 | - 서비스에서 조회 전용 기능을 사용하게 되면 서비스 코드가 다음과 같이 단순히 조회 전용 기능을 호출하는 것으로 끝나는 경우가 많다. 225 | 226 | ```java 227 | public List getOrderList(String ordererId) { 228 | return orderViewDao.selectBoyOrderer(ordererId); 229 | } 230 | ``` 231 | 232 |
233 | 234 | - 서비스에서 수행하는 추가적인 로직이 없을 뿐더러 조회 전용 기능이기 떄문에 트랜잭션이 필요하지도 않다. 235 | - 이러한 경우에는 굳이 서비스를 만들 필요없이 표현 영역에서 조회 전용 기능을 사용해도 무방하다. 236 | - 응용 서비스가 존재해야한다는 강박관념을 가지면 표현 영역에서 응용 서비스 없이 조회 전용 기능이나 도메인 리포지터리에 접근하는 것이 이상하게 느껴질 수 있다. 237 | - 하지만 응용 서비스가 사용자 요청 기능을 실행하는데 별다른 기여를하지 못한다면 굳이 서비스를 만들지 않아도 된다고 생각한다. 238 | -------------------------------------------------------------------------------- /DDD Start!/7장 도메인 서비스/CHAPTER 7.md: -------------------------------------------------------------------------------- 1 | # 7장. 도메인 서비스 2 | 3 | ## **여러 애그리거트가 필요한 기능** 4 | 5 | 한 애그리거트에 넣기 애매한 도메인 기능을 특정 애그리거트에서 억지로 구현하면 안된다. 6 | 자신의 책임 범위를 넘어서는 기능을 구현하게되면 코드가 길어지고 외부 의존이 높아진다. 7 | 때문에 코드가 복잡해지고 유지보수하기 어려워진다. 8 | 게다가 애그리거트의 범위를 넘어서는 도메인 개념이 애그리거트에 숨어들어서 명시적으로 드러나지 않게 된다. 9 | 10 | 이런 문제를 해소하는 가장 쉬운 방법이 바로 도메인 서비스를 별도로 구현하는 것이다. 11 | 12 | ## **도메인 서비스** 13 | 14 | 한 애그리거트에 넣기 애매한 도메인 개념을 구현하기위해 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러낼 수 있다. 15 | 응용 영역의 서비스가 응용 로직을 다루듯, 도메인 서비스는 도메인 로직을 다룬다. 16 | 17 | 도메인 영역의 애그리거트나 밸류와 다른 점은 상태 없이 로직만 구현한다는 점이다. 18 | 도메인 서비스를 구현하는데 필요한 상태는 애그리거트나 다른 방법으로 전달받는다. 19 | 20 | --- 21 | 22 | ```java 23 | // 할인 금액 계산 로직을 위한 도메인 서비스 24 | public class DiscountCalculationService { 25 | public Money calculateDiscountAmounts( 26 | List orderLines, 27 | List coupons, 28 | MemberGrade grade) { 29 | Money couponDiscount = coupons.stream() 30 | .map(coupon -> calculateDiscount(coupon)) 31 | .reduce(Money(0), (v1, v2) -> v1.add(v2)); 32 | 33 | Money membershipDiscount = calculateDiscount(orderer.getMember().getGrade()); 34 | 35 | return couponDiscount.add(membershipDiscount); 36 | } 37 | 38 | ... 39 | } 40 | ``` 41 | 42 | 이러한 도메인 서비스를 사용하는 주체는 애그리거트가 될 수도 있고 응용 서비스가 될 수도 있다. 43 | 44 | 위 도메인 서비스(DiscountCalculationService)를 애그리거트의 결제 금액 계산 기능에 전달하면 사용 주체는 애그리거트가 된다. 45 | 46 | ```java 47 | // 주문 애그리거트 48 | public class Order { 49 | public void calculateAmounts(DiscountCalculationService disCalSvc, MemberGrade grade) { 50 | Money totalAmounts = getTotalAmounts(); 51 | Money discountAmounts = disCalSvc.calculateDiscountAmounts(this.orderLInes, this.coupons, greade); 52 | this.paymentAmounts = totalAmounts.minus(discountAmounts); 53 | } 54 | ... 55 | ``` 56 | 57 | 애그리거트 객체에 도메인 서비스를 전달하는 것은 응용 서비스 책임이다. 58 | 59 | ```java 60 | // 주문 응용 서비스 61 | public class OrderService { 62 | private DiscountCalculationService discountCalculationService; 63 | 64 | @Transactional 65 | public OrderNo placeOrder(OrderRequest orderRequest) { 66 | OrderNo orderno = orderRepository.nextId(); 67 | Order order = createOrder(orderNo, orderRequest); 68 | orderRepository.save(order); 69 | // 응용 서비스 실행 후 표현 영역에서 필요한 값 리턴 70 | 71 | return orderNo; 72 | } 73 | 74 | private Order createOrder(OrderNo orderNo, OrderRequest orderReq) { 75 | Member member =findMember(orderReq.getOrdererId()); 76 | Order order = new Order(orderNo, orderReq.gerOrderLines(), 77 | orderReq.getCoupons(), createOrderer(member), 78 | orderReq.getShippingInfo()); 79 | order.calculateAmounts(this.discountCalculationService, member.getGrade()); 80 | return order; 81 | } 82 | ... 83 | } 84 | ``` 85 | 86 | --- 87 | 88 | > **도메인 서비스 객체를 애그리거트에 주입하지 않기** 89 | 90 | 위 예시처럼 애그리거트의 메서드를 실행할 때 도메인 서비스 객체를 파라미터로 전달한다는 것은 애그리거트가 도메인 서비스에 의존한다는 것을 뜻한다. 이를 의존 주입으로 처리하고 싶어질 수 있다. 91 | 92 | 하지만, 필자는 이는 좋은 방법이 아니라고 생각한다. 93 | 94 | 도메인 객체는 필드(프로퍼티)로 구성된 데이터와 메서드를 이용한 기능을 이용해서 개념적으로 하나인 모델을 표현한다. 모델의 데이터를 담는 필드는 모델에서 중요한 구성요소이다. 그런데, 도메인 서비스 필드는 데이터 자체와는 관련이 없다. 해당 도메인 객체를 DB에 저장할 때 다른 필드와는 달리 저장 대상도 아니다. 또, 주입한 도메인 서비스를 해당 도메인에서 제공하는 모든 기능에서 필요로 하는 것도 아니다. 95 | 96 | 그러므로 굳이 도메인 서비스 객체를 애그리거트에 의존 주입할 이유는 없다. 97 | 98 | --- 99 | 100 | 애그리거트 메서드를 실행할 때 도메인 서비스를 인자로 전달하지 않고 반대로 도메인 서비스의 기능을 실행할 때 애그리거트를 전달하기도 한다. 101 | 102 | ```java 103 | // 계좌 이체 도메인 서비스 104 | public class TransferService { 105 | public void transfer(Account fromAcc, Account toAcc, Money amounts) { 106 | fromAcc.withdraw(amounts); 107 | toAcc.credit(amounts); 108 | } 109 | } 110 | ``` 111 | 112 | 응용 서비스는 두 Account 애그리거트를 구한 뒤에 해당 도메인 영역의 TransferService 를 이용해서 계좌 이체 도메인 기능을 실행할 것이다. 113 | 114 | 도메인 서비스는 도메인 로직을 수행하지 응용 로직을 수행하지는 않는다. 115 | 116 | 트랜잭션 처리와 같은 로직은 응용 로직이므로 도메인 서비스가 아닌 응용 서비스에서 처리해야 한다. 117 | 118 | > 특정 기능이 응용 서비스인지 도메인 서비스인지 감을 잡기 어려울 때는 해당 로직이 애그리거트의 상태를 변경하거나 애그리거트의 상태 값을 계산하는지 검사해 보면 된다. 예를 들어, 계좌 이체 로직은 계좌 애그리거트의 상태를 변경한다. 결제 금액 로직은 주문 애그리거트의 주문 금액을 계산한다. 이 두 로직은 각각 애그리거트를 변경하고 애그리거트의 값을 계산하는 도메인 로직이다. 도메인 로직이면서 한 애그리거트에 넣기 적합하지 않으므로 이 두 로직은 도메인 서비스로 구현하게 된다. 119 | 120 | ## 도메인 서비스의 패키지 위치 121 | 122 | 도메인 서비스는 도메인 로직을 실행하므로 다른 도메인 구성 요소와 동일한 패키지(domain)에 위치한다. 123 | 124 | 도메인 서비스의 개수가 많거나 엔티티나 밸류와 같은 다른 구성요소와 명시적으로 구분하고 싶다면 domain 패키지 밑에 domain.model, domain.service, domain.repository와 같이 하위 패키지를 구분해서 위치시켜도 된다. 125 | 126 | ## 도메인 서비스의 인터페이스와 클래스 127 | 128 | 도메인 서비스의 구현이 특정 기술에 종속되면 인터페이스와 구현 클래스로 분리한다. 129 | 130 | 도메인 서비스의 구현이 특정 구현 기술에 의존적이거나 외부 시스템의 API를 실행한다면 도메인 영역의 도메인 서비스는 인터페이스로 추상화해야 한다. 이를 통해 도메인 영역이 특정 구현에 종속되는 것을 방지할 수 있고 도메인 영역에 대한 테스트가 수월해진다. 131 | -------------------------------------------------------------------------------- /DDD Start!/8장 애그리거트 트랜잭션 관리/CHAPTER 8.md: -------------------------------------------------------------------------------- 1 | # 8장. 애그리거트 트랜잭션 관리 2 | 3 | - 애그리거트의 트랜잭션 4 | - 애그리거트 잠금 기법 5 | 6 | ## 애그리거트와 트랜잭션 7 | 8 | 한 애그리거트를 두 사용자가 거의 동시에 변경할 때 트랜잭션이 필요하다. 9 | 10 | 두 사용자가 하나의 애그리거트를 동시에 변경하고 반영할때 DBMS 가 지원하는 트랜잭션과 함께 애그리거트를 위한 추가적인 트랜잭션 처리 기법이 필요한다. 11 | 12 | 대표적인 트랜잭션 처리 방식에는 선점(Pessimistic) 잠금과 비선점(Optimistic)잠금이 있다. 13 | 14 | ## 선점 잠금(Pessimistic Lock) 15 | 16 | 선점 잠금은 먼저 애그리거트를 구한 스레드가 애그리거트 사용이 끝날 떄까지 다른 스레드가 해당 애그리거트를 수정하는 것을 막는 방식이다. 17 | 18 | ![img_1.png](image/img_1.png) 19 | 20 | 한 스레드가 애그리거트를 구하고 수정하는 동안 다른 스레드가 수정할 수 없으므로 동시에 애그리거트를 수정할 때 발생하는 데이터 충돌 문제를 해소할 수 있다. 21 | 22 | 선점 잠금은 보통 DBMS가 제공하는 행 단위 잠금을 사용해서 구현한다. 23 | 24 | 오라클을 비롯한 다수 DBMS가 for update와 같은 쿼리를 사용해서 특정 레코드에 한 사용자만 접근할 수 있는 잠금 장치를 제공한다. 25 | 26 | JPA의 EntityManager는 LockModeType을 인자로 받는 find() 메서드를 제공하는데, LockModeType.PESSIMISTIC_WRITE를 값으로 전달하면 해당 엔티티와 매핑된 테이블을 이용해서 선점 잠금 방식을 적용할 수 있다. 27 | 28 | 하이버네이트의 경우 잠금 모드로 사용하면 `for update`쿼리를 사용해서 선점 잠금을 구현한다. 29 | 30 | ## 선점 잠금과 교착 상태 31 | 32 | 선점 잠금 기능을 사용할 떄는 잠금 순서에 따른 `deadlock`이 발생하지 않도록 주의해야 한다. 33 | 34 | 이런 문제가 발생하지 않도록 하려면 잠금을 구할 떄 최대 대기 시간을 지정해야 한다. 35 | 36 | JPA의 경우 힌트를 사용해서 최대 대기 시간을 지정해 줄 수 있지만, 힌트를 사용할 때 주의할 점은 DBMS에 따라 힌트가 적용되지 않을 수도 있다. 37 | 38 | ## 비선점 잠금(Optimistic Lock) 39 | 40 | ![img.png](image/img.png) 41 | 42 | 위의 과정에서 운영자가 배송지 정보를 조회하고 배송 상태로 변경하는 사이에 고객이 배송지를 변경하는 작업은 선점 잠금 방식으로 해결할 수 없다. 43 | 44 | 이때 필요한 것이 비선점 잠금이다. 비선점 잠금 방식은 잠금을 해서 동시에 접근하는 것을 막는 대신 변경한 데이터를 실제 DBMS에 반영하는 시점에 변경 가능 여부를 확인하는 방식이다. 45 | 46 | 또한 비선점 잠금을 구현하기 위해서는 애그리거트에 버전으로 사용할 프로퍼티를 추가하고, 애그리거트를 수정할 때마다 값이 1씩 증가해야한다. 47 | 48 | JPA는 버전을 이용한 비선점 잠금 기능을 지원한다. 버전으로 사용할 필드에 `@version` 에노테이션을 붙이고 매핑되는 테이블에 버전을 저장할 칼럼을 추가하면 된다. 49 | 50 | 응용 서비스는 버전에 대해 알 필요가 없으며 리포지터리에서 필요한 애그리거트를 구하고 알맞은 기능만 실행하면 된다. 51 | 52 | 비선점 잠금을 위한 쿼리를 실행할 때 쿼리 실행 결과로 수정된 행의 개수가 0이면 이미 누군가 앞서 데이터를 수정한 것이다. 53 | 54 | 이는 트랜잭션이 충돌한 것이므로 트랜잭션 종료 시점에 `OptimisticLockingFailureException`이 발생한다. 55 | 56 | 응용 서비스 코드에서 발생시키는 `VersionConflictException`은 `OptimisticLockingFailureException`과 비교하여 개발자 입장에서 트랜잭션 충돌이 발생한 시점이 다른 것을 명확하게 해준다. 57 | 58 | `VersionConflictException`은 이미 누군가가 애그리거트를 수정했다는 것을 의미하며 `OptimisticLockingFailureException`은 누군가가 거의 동시에 애그리거트를 수정했다는 것을 의미한다. 59 | 60 | ## 강제 버전 증가 61 | 62 | 애그리거트에 애그리거트 루트 외에 다른 엔티티가 존재하는데 기능 실행 도중 루트가 아닌 다른 엔티티의 값만 변경된다고 하자. 63 | 64 | 그런데 연관된 엔티티의 값이 변경된다고 해도 루트 엔티티 자체의 값은 바뀌는 것이 없으므로 버전 값을 갱신하지 않는다. 65 | 66 | 따라서 애그리거트 내에 어떤 구성요소의 상태가 바뀌면 루트 애그리거트의 버전 값을 증가해야 비선점 잠금이 올바르게 동작한다. 67 | 68 | JPA는 이런 문제를 처리할 수 있도로 EntityManager.find() 메서드로 엔티티를 구할때 강제로 버전 값을 증가시키는 잠금 모드를 지원하고 있다. 69 | 70 | ## 오프라인 선점 잠금 71 | 72 | ![img_2.png](image/img_2.png) 73 | 74 | 더 엄격하게 데이터 충돌을 막고 싶다면 누군가 수정화면을 보고 있을 경우 수정 화면 자체를 실행하지 못하도록 해야 한다. 75 | 76 | 한 트랜잭션 범위에서만 적용되는 선점 잠금 방식이나 나중에 버전 충돌을 확인하는 비선점 잠금 방식으로는 이를 구현할 수 없다. 77 | 78 | 이때 필요한 것이 오프라인 선점 잠금 방식이다. 오프라인 선점 잠금은 여러 트랜잭션에 걸쳐 동시 변경을 막는다. 79 | 80 | 잠금을 해제하기 전까지 다른 사용자는 잠금을 구할 수 없다. 81 | 82 | 오프라인 선점 잠금은 크데 잠금 선점 시도,잠금 확인, 잠금 해제, 락 유효 시간 연장의 네가지 기능을 제공해야 한다. -------------------------------------------------------------------------------- /DDD Start!/8장 애그리거트 트랜잭션 관리/image/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Today-I-Learn/dev-reading-record/0414ccac602b41162f43982db84abe2c887d7824/DDD Start!/8장 애그리거트 트랜잭션 관리/image/img.png -------------------------------------------------------------------------------- /DDD Start!/8장 애그리거트 트랜잭션 관리/image/img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Today-I-Learn/dev-reading-record/0414ccac602b41162f43982db84abe2c887d7824/DDD Start!/8장 애그리거트 트랜잭션 관리/image/img_1.png -------------------------------------------------------------------------------- /DDD Start!/8장 애그리거트 트랜잭션 관리/image/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Today-I-Learn/dev-reading-record/0414ccac602b41162f43982db84abe2c887d7824/DDD Start!/8장 애그리거트 트랜잭션 관리/image/img_2.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

읽고 공유하며 성장합니다.

5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 | ### 책을 읽읍시다 📚 13 | 14 | * [자바봄 스터디의 책을 읽읍시다](https://github.com/Java-Bom/ReadingRecord)를 모티브로 진행하는 스터디입니다 15 | * 참가자들의 합의하에 정독할 도서를 한권 선정합니다. 16 | * 모든 참가자는 책을 읽는 `Leader`이자 `Reader`로 활동합니다. 17 | * `Leader`는 매주 돌아가면서 선정하며 책의 분량을 정하며 `Reader`가 해당 부분을 읽다가 궁금하거나, 기록하고 싶은 사항, 질문등에 대한 피드백을 작성합니다. 18 | * `Reader`는 `Leader`가 선정한 범위의 책을 정독하며 해당 범위내에서 궁금한 사항이나, 기록하고 싶은 것들, 질문사항 등을 이슈로 등록합니다. 19 | * `Leader`는 스터디 시작하기 전까지 `Reader`가 작성한 이슈에 대한 피드백을 작성합니다. 20 | * 해결하지 못한 이슈에 대해서는 자유롭게 토론할 수 있으며 해결이 완료될 때까지 `continue` 태그를 부착합니다. 21 | 22 |
23 | 24 | ### 우리는 자유로운 규칙안에서 지식을 공유합니다 👀 25 | 1. 누가 시켜서 책을 읽는 것이 아닙니다 때문에 각자의 일정과 사정을 고려해 책읽는 것을 강제하지는 않습니다. 26 | * 하지만 매주 스터디에 참여하실 때에는 책을 읽으신 분에 한에서 참여가 가능합니다. 27 | * 누군가 시켜서 할당량을 채우는 것이 아니라 자신의 발전과 지식을 얻기위한 활동의 의미에서 참가해주세요. 28 | 2. 자유롭게 의견을 제시하고 나눌 수 있지만 서로의 기분을 상하게 할 수 있는 공격적인 언행과 피드백은 삼가해주세요. 29 | * 뛰어난 역량과 기술보다 앞서 커뮤니케이션은 개발자들의 문화에 있어서 가장 중요한 요소라고 생각합니다. 30 | * 자신의 의견을 잘 말하는 것과 다른 사람의 의견을 잘 듣는 것 뿐만 아니라 듣는 상대방의 입장을 고려한 것까지 커뮤니케이션의 일부입니다. 31 | * 질문을 하실때에도 또 의견을 나누고 피드백을 해주는 모든 과정에서 기본적인 예의와 상대방을 배려한 커뮤니케이션을 해주세요. 32 | 33 |
34 | 35 | ### 스터디 진행 현황 36 | * [토비의 스프링 3.1]() 37 | * [자바 ORM 표준 JPA 프로그래밍]() 38 | * [모던 자바 인 액션]() 39 | * [DDD Start!]() 40 | * [Real MySQL]() 41 | -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/12장 스프링 데이터 JPA/12.1 ~ 12.11.md: -------------------------------------------------------------------------------- 1 | - 대부분의 데이터 접근 계층은 CRUD 코드를 반복해서 개발해야 하며 JPA를 사용해서 데이터 접근 계층을 개발할 때에도 이와 같은 문제가 발생한다. 2 | - 이러한 문제를 해결하기 위해서 제네릭과 상속을 적절히 사용해서 공통 부분을 처리하는 부모 클래스를 만들면되지만 이 방법은 공통 기능을 구현한 부모 클래스에 종속되고 구현 클래스 상속이 가지는 단점에 노출된다. 3 | 4 |
5 | 6 | # 12.1 스프링 데이터 JPA 소개 7 | 8 | - 스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다. 9 | - 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결한다. 10 | - 우선 CRUD를 처리하기 위한 공통 인터페이스를 제공하고 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 11 | - 따라서 데이터 접근 계층을 개발할 떄 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다. 12 | - 일반적인 CRUD 메서드는 `JpaRepository` 인터페이스가 공통으로 제공할 뿐만 아니라 스프링 데이터 JPA는 메서드 이름을 분석해서 JPQL을 실행한다. 13 | 14 |
15 | 16 | # 12.2 스프링 데이터 JPA 설정 17 | 18 | - 스프링 데이터 JPA를 사용하기 위해서는 `@EnableJpaRepositories` 어노테이션을 추가하고 속성인 `basePackages` 에는 검색할 패키지의 위치를 작성한다. 19 | - 스프링 데이터 JPA는 애플리케이션을 실행할때 `basePackages` 에 있는 리포지토리 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록한다. 20 | - 스프링 부트를 사용하는 경우 `@EnableJpaRepositories` 를 명시적으로 사용하지 않아도 스프링 부트가 이를 자동으로 설정해준다. 21 | 22 |
23 | 24 | ## 스프링 부트의 @EnableJpaRepositories 설정 25 | 26 | - `@SpringBootApplication` 어노테이션의 `@EnableAutoConfiguration` 어노테이션의 동작은 `spring-boot-autoconfigure` 의 `META-INF` 하위의 `spring.factories` 라는 메타파일을 읽어온다. 27 | - `spring.factories` 를 살펴보면 Key와 Value의 쌍으로 이루어진 여러 설정 파일들이 정의되어있는 것을 확인할 수 있다. 28 | - 그 중에서 `org.springframework.boot.autoconfigure.EnableAutoConfiguration` 를 키 값으로 하는 하위 모든 값들이 Auto Configuration 대상이며 이 파일들을 빈으로 등록하는 과정을 거친다. 29 | - 그 중에서는 `org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration` 라는 값을 확인할 수 있는데 이 클래스를 확인해보면 다음과 같은 구조를 가지고 있다. 30 | 31 |
32 | 33 | ```java 34 | @Import(JpaRepositoriesRegistrar.class) 35 | @AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class }) 36 | public class JpaRepositoriesAutoConfiguration { 37 | ``` 38 | 39 | - 여기서 임포트하고 있는 `JpaRepositoriesRegistrar` 클래스를 다시 살펴보면 다음과 같이 되어있는 것을 확인할 수 있다. 40 | 41 |
42 | 43 | ```java 44 | class JpaRepositoriesRegistrar extends 45 | AbstractRepositoryConfigurationSourceSupport { 46 | ... 47 | 48 | @EnableJpaRepositories 49 | private static class EnableJpaRepositoriesConfiguration { 50 | 51 | } 52 | } 53 | ``` 54 | 55 | - 아래 메서드를 확인해보면 `@EnableJpaRepositories` 을 포함하고 있는 메서드를 확인할 수 있는데 이러한 과정을 거쳐서 스프링 부트는 `@EnableJpaRepositories` 를 자동으로 등록해준다. 56 | 57 |
58 | 59 | # 12.3 공통 인터페이스 기능 60 | 61 | - 스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 `JpaRepository` 인터페이스를 제공한다. 62 | - 스프링 데이터 JPA를 사용하는 가장 단순한 방법은 이 인터페이스를 상속받는 것이며 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하면 된다. 63 | 64 |
65 | 66 | ## JpaRepostiory 인터페이스의 계층 구조 67 | 68 | ![https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bb439519-ecca-4261-b2a7-18131732519f/Untitled.png](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fbb439519-ecca-4261-b2a7-18131732519f%2FUntitled.png?table=block&id=2ae29dcc-4577-44a3-b334-0849a106cb58&spaceId=7bf4105e-471a-416e-8171-751ccdb35ff5&width=780&userId=&cache=v2) 69 | 70 | - `JpaRepository` 의 계층구조를 살펴보면 스프링 데이터 모듈 안에 `Repository` , `CrudRepository` , `PagingAndSortingRepository` 가 존재하는데 이는 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스이다. 71 | - 스프링 데이터 JPA가 제공하는 `JpaRepository` 인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다. 72 | 73 |
74 | 75 | ## JpaRepository의 주요 메서드 76 | 77 | - `save()` : 엔티티 식별자 값이 없으면 즉, `null` 이면 새로운 엔티티로 판단해서 `persist()` 를 호출하고 식별자 값이 있으면 기존에 존재하는 엔티티로 판단해서 `merge()` 를 호출한다. 78 | - 필요하다면 스프링 데이터 JPA의 기능을 확장해서 신규 엔티티 판단 전략을 변경할 수 있다. 79 | - `delete()` : 엔티티 하나를 삭제하며 내부에서 `remove()` 를 호출한다. 80 | - `findOne()` : 엔티티 하나를 조회하며 내부에서 `find()` 를 호출한다. 81 | - `getOne()` : 엔티티를 프록시로 조회하며 내부에서 `getReference()` 를 호출한다. 82 | - `findAll()` : 모든 엔티티를 조회하며 정렬이나 페이징 조건을 파라미터로 제공할 수 있다. 83 | 84 |
85 | 86 | # 12.4 쿼리 메서드 기능 87 | 88 | - 쿼리 메서드 기능은 스프링 데이터 JPA가 제공하는 특별한 기능으로 메서드 이름만으로 쿼리를 생성하는 기능이다. 89 | - 인터페이스에 메서드만 선언하면 해당 메서드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행한다. 90 | - 스프링 데이터 JPA가 제공하는 쿼리 메서드 기능을 크게 3가지가 존재한다. 91 | - 메서드 이름으로 쿼리 생성 92 | - 메서드 이름으로 JPA 네임드 쿼리 호출 93 | - `@Query` 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리를 직접 정의 94 | 95 |
96 | 97 | ### 메서드 이름으로 쿼리 생성 98 | 99 | - 스프링 데이터 JPA의 정해진 규칙에 따라서 메서드 이름을 지으면 메서드 이름을 분석해서 JPQL을 생성하고 실행한다. 100 | 101 | [Spring Data JPA - Reference Documentation](https://docs.spring.io/spring-data/jpa/docs/2.5.2/reference/html/#jpa.query-methods) 102 | 103 | - 이 기능을 사용할 떄 주의할 점은 엔티티의 필드명이 변경되면 인터페이스에서 정의한 메서드 이름도 꼭 함께 변경해야한다는 점이다. 104 | - 그렇지 않으면 애플리케이션을 실행하는 시점에 오류가 발생하게 된다. 105 | 106 |
107 | 108 | ### JPA 네임드 쿼리 109 | 110 | - 스프링 데이터 JPA는 메서드 이름으로 JPA 네임드 쿼리를 호출하는 기능을 제공한다. 111 | - 네임드 쿼리는 어노테이션이나 XML에 쿼리를 정의할 수 있으며 메서드 이름만으로 네임드 쿼리를 호출할 수 있다. 112 | - 스프링 데이터 JPA는 우선적으로 메서드 이름에 해당하는 네임드 쿼리를 찾아서 실행하며 만약 실행할 네임드 쿼리가 없다면 메서드 이름으로 쿼리 생성 전략을 사용한다. 113 | 114 |
115 | 116 | ### @Query, 리포지토리 메서드에 쿼리 정의 117 | 118 | - `@Query` 어노테이션을 사용해서 실행할 메서드에 정적 쿼리를 직접 작성할 수 있으며 이는 이름없는 네임드 쿼리라고 할 수 있다. 119 | - 또한 JPA 네임드 쿼리 처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점을 가지고 있다. 120 | - JPQL이 아닌 네이티브 SQL을 사용하려면 `nativeQuery` 속성을 `true` 로 설정하면 되며 이 경우 위치 기반 파라미터가 0부터 시작한다. 121 | 122 |
123 | 124 | ### 파라미터 바인딩 125 | 126 | - 스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩 모두를 지원한다. 127 | 128 |
129 | 130 | ```java 131 | SELECT m FROM Member m WHERE m.username = ?1 // 위치 기반 132 | SELECT m FROM Member m WHERE m.username = :name // 이름 기반 133 | ``` 134 | 135 | - 기본 값은 위치 기반이며 파라미터 순서로 바인딩한다. 136 | - 이름 기반 파라미터 바인딩을 사용하려면 `@Param` 어노테이션을 사용하면 된다. 137 | - 가능하면 코드의 가독성과 유지보수성을 위해 이름 기반 파라미터 바인딩을 사용하는 것이 좋다. 138 | 139 |
140 | 141 | ### 벌크성 수정 쿼리 142 | 143 | - 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 `@Modifying` 어노테이션을 사용하면 된다. 144 | - 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶다면 `clearAutomatically` 속성을 `true` 로 설정하면 되며 기본 값은 `false` 이다. 145 | 146 |
147 | 148 | ### 반환 타입 149 | 150 | - 스프링 데이터 JPA는 유연한 반환 타입을 지원하며 한 건 이상이면 컬렉션 인터페이스를 사용하고 단건이면 반환 타입을 지정한다. 151 | - 만약 조회 결과가 없다면 컬렉션은 빈 컬렉션을 반환하고 단건은 `null` 을 반환한다. 152 | - 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 NonUniqueResultException 예외가 발생한다. 153 | - 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 `getSingleResult()` 메서드를 호출하는데 이 메서드를 호출했을 때 조회결과가 없으면 NoResultException 예외가 발생한다. 154 | - 이 예외는 개발자 입장에서 상당히 다루기가 불편하기 떄문에 스프링 데이터 JPA는 예외가 발생하면 예외를 무시하고 `null` 을 반환하는 것이다. 155 | 156 |
157 | 158 | ### 페이징과 정렬 159 | 160 | - 스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬 기능을 사용할 수 있도록 `Sort` 와 `Pageable` 파라미터를 제공한다. 161 | - `Pageable` 을 사용하면 반환 타입으로 `List` 또는 `Page` 를 사용할 수 있으며 `Page` 를 사용하는 경우 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 카운트 쿼리를 추가로 호출한다. 162 | 163 |
164 | 165 | ### 힌트 166 | 167 | - JPA에서 쿼리 힌트를 사용하려면 `@QueryHints` 어노테이션을 사용하면 되며 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트이다. 168 | 169 |
170 | 171 | ### Lock 172 | 173 | - 쿼리 수행시 락을 걸려면 `@Lock` 어노테이션을 사용하면 된다. 174 | 175 |
176 | 177 | # 12.6 사용자 정의 레포지토리 구현 178 | 179 | - 스프링 데이터 JPA로 리포지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않지만 다양한 이유로 메서드를 직접 구현해야할 때도 존재한다. 180 | - 스프링 데이터 JPA는 이러한 문제를 우회해서 필요한 메서드만 구현할 수 있는 방법을 제공한다. 181 | - 먼저 직접 구현할 메서드를 위한 사용자 정의 인터페이스를 작성하고 사용자 정의 인터페이스를 구현한 클래스를 작성해야한다. 182 | 183 |
184 | 185 | ```java 186 | public interface MemberRepositoryCustom { 187 | public List findMemberCustom(); 188 | } 189 | ``` 190 | 191 | - 이 때 이름을 짓는 규칙이 존재하는데 리포지토리 인터페이스 이름에 `Impl` 을 붙여야 한다. 192 | - 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식하게 된다. 193 | 194 |
195 | 196 | ```java 197 | public class MemberRepositoryImpl implements MemberRepositoryCustom { 198 | @Override 199 | public List findMemberCustom() { ... } 200 | } 201 | ``` 202 | 203 | - 마지막으로 다음과 같이 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다. 204 | 205 |
206 | 207 | ```java 208 | public interface MemberRepository extends JpaRepository, 209 | MemberRepositoryCustom { 210 | 211 | } 212 | ``` 213 | 214 | - 만약 사용자 정의 구현 클래스 이름에 `Impl` 대신 다른 이름을 붙이고 싶다면 `@EnableJpaRepositories` 의 `repository-impl-postfix` 속성을 변경하면된다. 215 | 216 |
217 | 218 | # 12.7 Web 확장 219 | 220 | - 스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능인 식별자로 도메인 클래스를 바인딩 해주는 도메인 클래스 컨버터 기능과 페이징과 정렬 기능을 제공한다. 221 | - 스프링 데이터가 제공하는 웹 확장 기능을 활성화 하려면 `SpringDataWebConfiguration` 을 스프링 빈으로 등록하면 된다. 222 | - 이후 `@EnableSpringDataWebSupport` 어노테이션을 사용하면 되는데 설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 `HandlerMethodArgumentResolver` 가 스프링 빈으로 등록된다. 223 | - 등록되는 도메인 클래스 컨버터는 `DomainClassConverter` 클래스이다. 224 | 225 |
226 | 227 | ## 도메인 클래스 컨버터 기능 228 | 229 | - 도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다. 230 | - 특정 회원을 수정하는 것을 예시로 들면 컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해서 리포지토리를 통해 회원 엔티티를 조회해야한다. 231 | 232 |
233 | 234 | ```java 235 | @Controller 236 | public class MemberController { 237 | 238 | @Autowired MemberRepository memberRepository; 239 | 240 | @RequestMapping("member/memberUpdateForm") 241 | public String memberUpdateForm(@RequestParam("id") Long id, Model model) { 242 | //회원을 찾는다. model.addAttribute("member", member); 243 | Member member = memberRepository.findOne(id); 244 | return "member/memberSaveForm"; 245 | } 246 | } 247 | ``` 248 | 249 | - 도메인 컨버터를 사용하면 다음과 같이 사용했을 때 HTTP 요청으로 회원 아이디를 받지만 도메인 클래스가 중간에 동작하여 아이디를 회원 엔티티 객체로 변환해서 넘겨준다. 250 | 251 |
252 | 253 | ```java 254 | @Controller 255 | public class MemberController { 256 | 257 | @RequestMapping("member/memberUpdateForm") 258 | public String memberUpdateForm(@RequestParam("id") Member member, Model model) { 259 | model.addAttribute("member", member); 260 | return "member/memberSaveForm"; 261 | } 262 | } 263 | ``` 264 | 265 | - 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다. 266 | 267 |
268 | 269 | ## 도메인 클래스 컨버터의 주의사항 270 | 271 | - 도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 데이터베이스에는 반영되지 않는다. 272 | - 이것은 영속성 컨텍스트의 동작 방식과 관련있는데 OSIV를 사용하지 않으면 조회한 엔티티는 준영속 상태이고 변경 감지 기능이 동작하지 않는다. 273 | - 이 때 수정한 내용을 데이터베이스에 반영하고 싶으면 `merge()` 를 사용해야한다. 274 | - OSIV를 사용하면 조회한 엔티티는 영속 상태이지만 OSIV 특성상 컨트롤러와 뷰에서는 영속성 컨텍스트를 플러시하지 않는다. 275 | - 따라서 수정한 내용을 데이터베이스에 반영하지 않는데 만약 수정한 내용을 데이터베이스에 반영하고 싶다면 트랜잭션을 시작하는 서비스 계층을 호출해야한다. 276 | - 해당 서비스 계층이 종료될 때 플러시와 트랜잭션 커밋이 일어나서 영속성 컨텍스트의 변경 내용이 데이터베이스에 반영될 것이다. 277 | 278 |
279 | 280 | ## 페이징과 정렬기능 281 | 282 | - 스프링 데이터가 제공하는 페이징과 정렬기능을 제공할 수 있도록 `HandlerMethodArgumentResolver` 를 제공하며 각각을 제공하는 것은 다음과 같다. 283 | - 페이징 기능 : `PageableHandlerMethodArgumentResolver` 284 | - 정렬 기능 : `SortHandlerMethodArgumentResolver` 285 | - 이와 같이 사용하면 파라미터로 `Pageable` 을 받아서 사용할 수 있으며 `Pageable` 은 다음의 요청 파라미터 정보로 만들어진다. 286 | - `page` : 현재 페이지, 0부터 시작한다. 287 | - `size` : 한 페이지에 노출할 데이터 건수를 나타낸다. 288 | - `sort` : 정렬 조건을 정의하며 정렬 속성과 방향을 `,` 로 구분할 수 있으며 `sort` 파라미터를 추가해서 정렬 조건을 추가할 수 있다. 289 | 290 |
291 | 292 | ### 접두사 293 | 294 | - 사용해야 할 페이징 정보가 둘 이상이면 접두사를 이용해서 구분할 수 있으며 `@Qualifier` 어노테이션을 사용하여 접두사명 + `_` 로 구분한다. 295 | 296 | ```java 297 | @Qualifier("member") Pageable memberPageable, 298 | @Qualifier("order") Pageable orderPageable, ... 299 | 300 | /members?member_page=0&order_page=1 301 | ``` 302 | 303 |
304 | 305 | ### 기본값 306 | 307 | - `Pageable` 의 기본값은 `page` 는 0 `size` 는 20이며 기본값을 변경하고 싶으면 `@PageableDefault` 어노테이션을 사용하면된다. 308 | 309 |
310 | 311 | # 12.8 스프링 데이터 JPA가 사용하는 구현체 312 | 313 | - 스프링 데이터 JPA가 제공하는 공통 인터페이스는 `SimpleJpaRepository` 클래스가 구현한다. 314 | 315 |
316 | 317 | ```java 318 | @Repository 319 | @Transactional(readOnly = true) 320 | public class SimpleJpaRepository 321 | implements JpaRepository, JpaSpecificationExecutor { 322 | 323 | @Transactional 324 | public S save(S entity) { 325 | if (entityInformation.isNew(entity)) { 326 | em.persist(entity); 327 | return entity; 328 | } else { 329 | return em.merge(entity); 330 | } 331 | } 332 | 333 | ... 334 | } 335 | ``` 336 | 337 | - `@Repository` : JPA 예외를 스프링이 추상화한 예외로 변환한다. 338 | - `@Transsactional` : JPA의 모든 변경은 트랜잭션 안에서 이루어져야 하며 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경하는 모든 메서드에 `@Transactional` 처리가 되어있다. 때문에 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작하며 기존의 트랜잭션이 있으면 해당 트랜잭션을 전파받아서 그대로 사용한다. 339 | - `@Transactional(readOnly=true)` : 데이터를 조회하는 메서드에 읽기 전용 옵션이 적용되어있으며 데이터를 변경하지 않는 트랜잭션에서는 이 옵션을 사용하여 플러시를 생략해서 약간의 성능 향상을 얻을수 있다. 340 | -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/13장 웹 애플리케이션과 영속성 관리/13.1 ~ 13.5.md: -------------------------------------------------------------------------------- 1 | # 13장 웹 애플리케이션과 영속성 관리 2 | 3 | 컨테이너 환경에서 JPA가 동작하는 내부 동작 방식을 이해하고, 컨테이너 환경에서 웹 애플리케이션을 개발할 때 발생할 수 있는 다양한 문제점과 해결 방안을 알아본다. 4 | 5 | ## 13.1 트랜잭션 범위의 영속성 컨텍스트 6 | 7 | ### 13.1.1 스프링 컨테이너의 기본 전략 8 | 9 | **트랜잭션 범위의 영속성 컨텍스트** 전략을 기본으로 사용 ! 10 | 11 | - 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료함 12 | - 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근함 13 | - 다른 트랜잭션일 경우에는 항상 다른 영속성 컨텍스트를 사용함 14 | - 스프링 컨테이너는 스레드 마다 각각 다른 트랜잭션을 할당하기 때문에 멀티스레드 상황에 안전하다 15 | 16 | 스프링 트랜잭션 AOP 는 대상 메소드를 호출하기 직전에 트랜잭션을 시작하고, 대상 메소드가 정상 종료되면 트랜잭션을 커밋하는데 이때 JPA는 먼저 영속성 컨텍스트를 플러시해서 변경 내용을 데이터베이스에 반영한 후에 데이터베이스 트랜잭션을 커밋하게 된다. 17 | 18 | 만약, 예외가 발생하면 트랜잭션을 롤백하고 종료하는데 이때는 플러시를 호출하지 않는다. 19 | 20 | ## 13.2 준영속 상태와 지연 로딩 21 | 22 | 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용하면 보통 서비스 계층에서 트랜잭션이 시작하고 종료되므로 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다. 따라서 변경 감지와 지연 로딩이 동작하지 않는다. 23 | 24 | ### 준영속 상태와 변경 감지 25 | 26 | 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층(트랜잭션 범위)까지만 동작하고 영속성 컨텍스트가 종료된 프레젠테이션 계층에서는 동작하지 않는다. 27 | 28 | - 애플리케이션 계층의 책임을 확실하게 할 수 있고, 데이터 변경에 대해서 프레젠테이션 계층까지 찾아 볼 필요가 없다. - 유지 보수 용이함 29 | - 비즈니스 로직은 서비스 계층에서 끝내고 프리젠테이션 계층은 데이터를 보여주는 데 집중해야 한다! 30 | 31 | ### 준영속 상태와 지연 로딩 32 | 33 | 준영속 상태는 영속성 컨텍스트가 없으므로 지연 로딩을 할 수 없다. 아직 초기화 하지 않은 프록시 객체를 사용하면 실체 객체를 불러오려고 초기화를 시도하는데 이때 문제가 발생한다(하이버네이트는 LazyInitializationException 예외 발생, 구현체 마다 다르게 동작함) 34 | 35 | 이런 문제를 해결하기 위한 방법 2가지 36 | 37 | - 뷰가 필요한 엔티티를 미리 로딩 38 | 1. 글로벌 페치 전략 수정 39 | 2. JPQL 페치 조인 40 | 3. 강제로 초기화 41 | - OSIV 를 사용해서 엔티티를 항상 영속 상태로 유지 42 | 43 | 44 | 45 | **뷰가 필요한 엔티티를 미리 로딩하는 방법 상세** 46 | 47 | 1. 글로벌 페치 전략 수정 48 | 49 | 엔티티에서 글로벌 페치 전략을 지연 로딩에서 즉시 로딩(EAGER)으로 변경하면 된다. 50 | 51 | **단점:** 사용하지 않는 엔티티를 로딩함, N+1 문제가 발생함(JPQL 사용시) 52 | 53 | 2. JPQL 페치 조인 - **가장 현실적인 방법** 54 | 55 | 페치 조인을 사용하면 SQL JOIN 을 사용해서 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 발생하지 않는다. 56 | 57 | **단점:** 화면에 fit 한 리포지토리 메소드가 증가함 - 프리젠테이션 계층이 알게 모르게 데이터 접근 계층을 침범 58 | 59 | 무분별한 최적화로 프리젠테이션 계층과 데이터 접근 계층 간에 의존관계가 급격하게 증가하는 것보다는 적절한 선에서 타협점을 찾는 것이 합리적 60 | 61 | 3. 강제로 초기화 62 | 63 | 영속성 컨텍스트가 살아있을 때 프리젠테이션 계층에서 필요한 엔티티(프록시 객체)를 강제로 초기화 해서 반환한다. 64 | 65 | **하이버네이트를 사용하면 initialize() 메소드를 사용해서 프록시를 강제로 초기화할 수 있다.** 66 | 67 | ### 13.2.4 FACADE 계층 추가 68 | 69 | FACADE 계층은 뷰를 위한 프록시 초기화를 담담하는 계층이고 프리젠테이션 계층과 서비스 계층 사이에 정의할 수 있다. 70 | 71 | 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 FACADE 계층에서 트랜잭션을 시작해야 한다. 결과적으로 FACADE 계층을 도입해서 서비스 계층과 프리젠테이션 계층 사이에 논리적인 의존성을 분리할 수 있다. 72 | 73 | **단점:** 중간에 계층이 하나 더 끼어들어 더 많은 코드를 작성해야하고 단순하게 서비스 계층을 호출만 하는 위임 코드가 상당히 많아진다. 74 | 75 | **FACADE 계층의 역할과 특징** 76 | 77 | 1. 프리젠 테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리 78 | 2. 프리젠테이션 계층에서 필요한 프록시 객체 초기화 79 | 3. 서비스 계층을 호출해서 비즈니스 로직 실행 80 | 4. 리포지토리를 직접 호출해서 뷰가 요구하는 엔티티를 찾음 81 | 82 | ### 13.2.5 준영속 상태와 지연 로딩의 문제점 83 | 84 | 1. 오류가 발생할 가능성이 높다. 85 | 86 | : 보통 뷰를 개발할 때는 엔티티 클래스를 보고 개발하지 이것이 초기화되어 있는지 아닌지 확인하는 것은 상당히 번거롭고 놓치기 쉽기 때문이다. 87 | 88 | 2. 애플리케이션 로직과 뷰가 물리적으로는 나누어져 있지만 논리적으로는 서로 의존한다. 89 | 90 | : 물론 FACADE 계층을 통해 이런 문제를 어느 정도 해소할 수는 있지만 상당히 번거롭다. 91 | 92 | **결국 모든 문제는 엔티티가 프리젠테이션 계층에서 준영속 상태이기 때문에 발생한다.** 93 | 94 | → 영속성 컨텍스트를 뷰까지 살아있게 열어둘 수 있는 OSIV 를 사용하면 된다. 95 | 96 | ## 13.3 OSIV 97 | 98 | OSIV(Open Session In View) 는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다. 영속성 컨텍스트가 살아있으면 엔티티는 영속 상태로 유지되고 뷰에서도 지연 로딩을 사용할 수 있다. 99 | 100 | ### 13.3.1 과거 OSIV: 요청 당 트랜잭션(Transaction per request) 101 | 102 | 가장 단순한 구현방법, 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내면 된다. 103 | 104 | **요청 당 트랜잭션 방식 OSIV의 문제점** 105 | 106 | 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다 107 | 108 | → 컨트롤러에서 고객의 정보를 XXX 로 변경해서 렌더링할 뷰에 넘겨주었을 때 개발자의 의도는 단순히 뷰에 노출할 때 XXX로 변경하고 싶은 것 뿐인데, 해당 OSIV 방식은 뷰를 렌더링한 후에 트랜잭션을 커밋하기 때문에 영속성 컨텍스트를 플러시 하게 되고, 이때 영속성 컨텍스트의 변경 감지 기능이 작동해서 변경된 엔티티를 데이터베이스에 반영해버리는 심각한 문제가 발생 !! 109 | 110 | **위의 문제점을 해결하기 위해서 프리젠테이션 계층에서 엔티티를 수정하지 못하게 막는 방법 3가지** 111 | 112 | 1. 엔티티를 읽기 전용 인터페이스로 제공 113 | 114 | 엔티티를 직접 노출하지 않고 읽기 전용 메소드만 제공하는 인터페이스를 프리젠테이션 계층에 제공하는 방법 115 | 116 | 2. 엔티티 레핑 117 | 118 | 엔티티의 읽기 전용 메소드만 가지고 있는 엔티티를 감싼 객체를 만들고 이것을 프리젠테이션 계층에 반환하는 방법 119 | 120 | 3. DTO 만 반환 **- 가장 전통적인 방법** 121 | 122 | 프리젠테이션 계층에 엔티티 대신에 단순히 데이터만 전달하는 객체인 DTO 를 생성해서 반환하는 것 123 | 124 | **단점:** OSIV 를 사용하는 장점을 살릴 수 없고 엔티티를 거의 복사한 듯한 DTO 클래스도 하나 더 만들어야 된다. 125 | 126 | 위의 3가지 방법 모두 코드량이 상당히 증가하는 단점이 있다. 차라리 프리젠테이션 계층에서 엔티티를 수정하면 안된다고 개발자들끼리 합의하는 것이 더 실용적일 수 있다. 127 | 128 | 이러한 과거의 **OSIV: 요청 당 트랜잭션(Transaction per request) 은 앞선 문제점들 때문에 최근에는 거의 사용하지 않는다.** 129 | 130 | ### 13.3.2 스프링 OSIV: 비즈니스 계층 트랜잭션 131 | 132 | 스프링 프레임워크의 spring-orm.jar는 다양한 OSIV 클래스를 제공한다. OSIV 를 서블릿 필터 or 스프링 인터셉터, 어디서 적용할지에 따라 원하는 클래스를 사용하면 된다. 133 | 134 | - 하이버네이트 OSIV 서블릿 필터 135 | - 하이버네이트 OSIV 스프링 인터셉터 136 | - JPA OEIV 서블릿 필터 137 | - JPA OEIV 스프링 인터셉터 138 | 139 | ### 스프링 OSIV 분석 140 | 141 | 스프링 프레임워크가 제공하는 OSIV에서는 트랜잭션은 비즈니스 계층에서만 사용한다. 142 | 143 | **동작 순서** 144 | 145 | 1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성한다. 단 이때 트랜잭션은 시작하지 않는다. 146 | 2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작한다. 147 | 3. 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시한다. 이때 트랜잭션은 끝내지만 영속성 컨텍스트는 종료하지 않는다. 148 | 4. 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속상태를 유지한다. 149 | 5. 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료한다. 이때 플러시를 호출하지 않고 바로 종료한다. 150 | 151 | ### 트랜잭션 없이 읽기(Nontransactional reads) 152 | 153 | - 영속성 컨텍스트는 트랜잭션 범위 안에서 엔티티를 조회하고 수정할 수 있다. 154 | - 영속성 컨텍스트는 트랜잭션 범위 밖에서 엔티티를 조회만 할 수 잇다. 이것을 트랜잭션 없이 읽기(Nontransactional reads)라 한다. 155 | 156 | **스프링 OSIV 특징** 157 | 158 | - 영속성 컨텍스트를 프리젠테이션 계층까지 유지한다. 159 | - 프리젠테이션 계층에는 트랜잭션이 없으므로 엔티티를 수정할 수 없다. 160 | - 프리젠테이션 게층에는 트랜잭션이 없지만 트랜잭션 없이 읽기를 사용해서 지연 로딩을 할 수 있다. 161 | 162 | **스프링 OSIV 주의사항** 163 | 164 | 컨트롤러에서 엔티티를 수정하고 즉시 뷰를 호출한 것이 아니라 트랜잭션이 동작하는 비즈니스 로직을 호출하게되면 문제가 발생한다. 때문에 트랜잭션이 있는 비즈니스 로직을 모두 호출하고 나서 엔티티를 변경해야한다. 165 | 166 | 스프링 OSIV 는 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있으므로 이런 문제가 발생한다. OSIV를 사용하지 않는 트랜잭션 범위의 영속성 컨텍스트 전략은 트랜잭션 생명주기와 영속성 컨텍스트의 생명주기가 같으므로 이런 문제가 발생하지 않는다. 167 | 168 | ### 13.3.3 OSIV 정리 169 | 170 | - 스프링 OSIV의 특징 171 | - 클라이언트의 요청이 들어올 때 영속성 컨텍스트를 생성해서 요청이 끝날 때 까지 같은 영속성 컨텍스트를 유지하므로 한 번 조회한 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다. 172 | - 엔티티 수정은 트랜잭션이 있는 계층에서만 동작하고 트랜잭션이 없는 프리젠테이션 계층은 지연 로딩을 포함해서 조회만 할 수 있다. 173 | - 스프링 OSIV의 단점 174 | - OSIV를 적용하면 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있다. 175 | - 프리젠테이션 계층에서 엔티티를 수정하고나서 비즈니스 로직을 수행하면 엔티티가 수정될 수 있다. 176 | - 프리젠테이션 계층에서 지연 로딩에 의한 SQL 이 실행되므로 성능 튜닝시에 확인해야 할 부분이 비교적 넓다. 177 | - OSIV vs FACADE vs DTO 178 | - OSIV 를 사용하지 않으면 어떤 방법이든 준영속 상태가 되기 전에 프록시를 초기화 해야되므로 OSIV 와 비교적 지루한 코드를 많이 작성해야 한다. 179 | - OSIV를 사용하는 방법이 만능은 아니다 180 | - 예를 들어 많은 테이블을 조인해서 보여주는 복잡한 통계 화면 or 복잡한 관리자 화면의 경우는 엔티티로 조회하기보다는 처음부터 통계 데이터를 구상하기 위한 JPQL을 작성해서 DTO로 조회하는 것이 더 나은 해결책일 수 있다. 181 | - OSIV는 같은 JVM을 벗어난 원격 상황에서는 사용할 수 없다. 182 | - 원격지인 클라이언트에서 연관된 엔티티를 지연 로딩하는 것은 불가능하다. 결국 클라이언트가 필요한 데이터를 모두 JSON으로 생성해서 반환해야 한다. 183 | - 엔티티는 생각보다 자주 변경되는데 엔티티를 JSON 변환 대상 객체로 사용하면 엔티티를 변경할 때 노출하는 JSON API도 함께 변경되기 때문에, 외부 API는 엔티티를 변경해도 완충 역할을 할 수 있는 DTO로 변환해서 노출하는 것이 안전하다. 184 | 185 | ## 13.4 너무 엄격한 계층 186 | 187 | OSIV를 사용하면 영속성 컨텍스트가 프리젠테이션 계층까지 살아있으므로 미리 초기화할 필요가 없어진다. 따라서 단순한 엔티티 조회는 컨트롤러에서 리포지토리를 직접 호출해도 아무런 문제가 없다. 188 | 189 | **OSIV를 사용하면 좀 더 유연하고 실용적인 관점으로 접근하는 것도 좋은 방법일 수 있다.** 190 | 191 | ## 13.5 정리 192 | 193 | 스프링이나 J2EE 컨테이너 환경에서 JPA를 사용하면 **트랜잭션 범위의 영속성 컨텍스트 전략**이 적용된다. 이 전략은 트랜잭션의 범위와 영속성 컨텍스트의 생존 범위가 같고 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다. 트랜잭션이라는 단위로 영속성 컨텍스트를 관리하므로 트랜잭션을 커밋하거나 롤백할 때 문제가 없다. **이 전략의 유일한 단점**은 프리젠테이션 계층에서 엔티티가 준영속 상태가 되므로 **지연 로딩을 할 수 없다는 점**이다. 194 | 195 | **OSIV를 사용하면 이런 문제들을 해결할 수 있다.** 기존 OSIV 는 프리젠테이션 계층에서도 엔티티를 수정할 수 있다는 단점이 있었는데 스프링 프레임워크가 제공하는 OSIV는 기존 OSIV의 단점들을 해결해서 프리젠테이션 계층에서 엔티티를 수정하지 않는다. 196 | -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/14장 컬렉션과 부가기능/14.1 ~ 14.5.md: -------------------------------------------------------------------------------- 1 | # 14장 컬렉션과 부가기능 2 | 3 | JPA가 지원하는 컬렉션의 종류와 중요한 부가기능에 대해 알아본다. 4 | 5 | ## 14.1 컬렉션 6 | 7 | 자바에서 기본으로 제공하는 Collection, List, Map 컬렉션을 value type(`@ElementCollection`), embeddable type, entity collection(`@OneToMany`, `@ManyToMany` 엔티티 매핑시)으로 사용 가능하다. 8 | 9 | 하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 래퍼 컬렉션 (`org.hibernate.collection`) 으로 감싸서 사용한다. 10 | -> 이러한 특성 때문에 컬렉션 사용시 인터페이스 유형으로 선언되어야하고, 하이버네이트는 null과 empty를 구별하지 않기 때문에 즉시 초기화해서 사용하는 것을 권장한다. [자세히...](https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#collections) 11 | 12 | 13 | ### 컬렉션 목록 14 | 15 | |컬렉션 인터페이스|내장컬렉션|중복허용|순서보관| 16 | |------|---|---|---| 17 | |Collection, List|PersistentBag|O|X| 18 | |Set|PersistentSet|X|X| 19 | |List + @OrderColumn|PersistentList|O|O| 20 | 21 | - Collection, List 22 | : 엔티티를 추가할때 중복 엔티티가 있는지 비교하지 않고 단순 저장함. 지연 로딩된 컬렉션을 초기화하지 않는다. 23 | 24 | - Set 25 | : HashSet으로 초기화하면 된다. 엔티티 추가시 `equals()`와 `hashCode()`로 같은 객체가 있는지 확인하여 지연 로딩된 컬렉션을 초기화한다. 26 | 27 | - List + @OrderColumn 28 | : 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다. @OrderCOlumn의 POSITION 값을 준 컬럼에 List의 위치 값을 보관한다. 29 | - 단, 일대다 관계의 특성상 위치 값은 다(N) 쪽에 매핑하여 저장해야하므로, 다에서는 POSITION 값을 확인할 수 없고 다를 INSERT할 떄는 POSITION 값이 저장되지 않아 UPDATE하는 SQL이 추가로 발생한다. 30 | - List 변경시 많은 위치값을 변경해야하거나, 중간에 POSITION 값이 없으면 조회한 List에는 null이 보관되어 NPE가 발생하는 등의 이슈로 실무에서는 잘 사용하지 않는다. 31 | 32 | - @OrderBy 33 | 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 34 | 35 | ## 14.2 @Converter 36 | 37 | 엔티티의 데이터를 변환해서 데이터베이스에 저장한다. 컨버터는 현재 타입과 변환하고자하는 타입의 이름으로 클래스를 선언할 수 있고, 메소드나 클래스 레벨 혹은 글로벌로도 설정할 수 있다. 38 | 39 | ## 14.3 리스너 40 | 41 | 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다. 이벤트는 엔티티에서 직접 적용하거나, 별도의 리스너 등록 혹은 기본 리스너를 사용할 수 있다. 이를 잘 활용하면 대부분의 엔티티에 공통으로 적용하는 등록일자나 수정일자 등의 기록을 리스너 하나로 처리할 수 있다. 42 | 43 | ![image](https://user-images.githubusercontent.com/59992230/126921513-fc145859-bff2-4096-881c-b019494adcd8.png) 44 | 45 | - @PostLoad: 엔티티가 영속성 컨텍스트에 조회 or refresh 호출 직후 (2자캐시에 저장되어있어도 호출) 46 | - @PrePersist: `persist()` 메소드 호출하여 영속성 컨텍스트를 관리하기 직전에 호출되거나 새 인스턴스를 merge할때 수행된다. 식별자 생성전략시 아직 엔티티 식별자가 없다. 47 | - @PreUpdate: flush나 commit 호출해서 엔티티를 DB에 수정하기 직전에 호출 48 | - @PreRemove: `remove()` 메소드를 호출해서 영속성 컨텍스트에 삭제하기 직전에 호출 49 | - @PostPersist: flush나 commit을 호출하여 DB에 저장된 직후 혹은 식별 생성 전략이 IDENTITY면 `persists()` 호출시 DB에 저장되므로 바로 호출된다. 50 | - @PostUpdate: flush나 commit을 호출해서 엔티티를 DB에 수정한 직후에 호출 51 | - @PostRemove: flush나 commit을 호출해서 엔티티를 DB에 삭제한 직후에 호출 52 | 53 | 54 | ## 14.4 엔티티 그래프 55 | 56 | 57 | 엔티티 조회시 연관된 엔티티들을 함께 조회하려면 글로벌 fetch 옵션을 `FetchType.EAGER`로 설정 혹은 JPQL에서 페치 조인을 사용하면 된다. 그러나 글로벌 fetch 옵션은 애플리케이션 전체에 영향을 주고 변경 불가하다는 불편함이 있으며, 페치조인은 연관된 엔티티를 함께 조회하는 기능을 제공하여 이에 따라 다른 JPQL을 사용해야해서 같은 JPQL을 중복 작성하는 경우가 많다는 단점이 있다. 58 | 59 | 이를 보완하기 위해 JPA 2.1에 추가된 엔티티 그래프 기능을 사용하면 엔티티 조회시점에 함께 조회할 연관 엔티티를 선택할 수 있고, JPQL은 데이터를 조회하는 기능만 수행하면 된다. 60 | 61 | 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능이며, 정적으로 정의하는 Named (`@NamedEntityGraph`)엔티티 그래프와 동적으로 정의하는 (`createEnttiyGraph()`) 엔티티 그래프가 있다. 정적은 엔티티에 서브그래프를 엔티티에 직접 명시해주는 방식이고, 동적으로는 만들어진 엔티티 그래프에 속성을 추가해서 서브 그래프를 만들 수 있다. 62 | 63 | 엔티티 그래프는 ROOT에서 시작되어야하며, 이미 영속성 컨텍스트에 해당 엔티티가 로딩되어있으면 적용되지 않는다. (단, 초기화되지 않은 프록시에는 적용) -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/15장 고급 주제와 성능 최적화/15.1 ~ 15.5.md: -------------------------------------------------------------------------------- 1 | # 15장 고급 주제와 성능 최적화 2 | 3 | JPA의 깊이 있는 고급 주제들과 JPA의 성능을 최적화하는 다양한 방안을 알아보자 4 | 5 | ## 15.1 예외처리 6 | 7 | ### JPA 표준 예외 정리 8 | 9 | JPA 표준 예외들은 `javax.persistence.PersistenceException`의 자식 클래스이며 이 예외 클래스는 `RuntimeException` 의 자식이다. 10 | 11 | JPA 표준 예외는 크게 2가지로 나눌 수 있다. 12 | 13 | - 트랜잭션 롤백을 표시하는 예외 14 | - 트랜잭션 롤백을 표시하지 않는 예외 15 | 16 | 트랜잭션 롤백을 표시하는 예외는 심각한 예외이므로 복구해선 안된다. 반면에 트랜잭션 롤백을 표시하지 않는 예외는 심각한 예외가 아니기 때문에 개발자가 트랜잭션을 커밋할지 롤백할지를 판단하면 된다. 17 | 18 | ### 스프링 프레임워크의 JPA 예외 변환 19 | 20 | 서비스 계츨에서 JPA의 예외를 직접 사용하면 JPA에 의존하게된다. 스프링 프레임워크는 이런 문제를 해결하기 위해 데이터 접근 계층에 대한 예외를 추상화해서 개발자에게 제공한다. 21 | 22 | JPA 예외를 스프링 프레임워크가 제공하는 추상화된 예외로 변경하려면 `PersistenceExceptionTranslationPostProcessor`를 스프링 빈으로 등록하면 된다. 23 | 24 | `@Repository`를 사용한 곳에 예외 변환 AOP를 적용해서 JPA 예외를 스프링 프레임워크가 추상화환 예외로 변환해준다. 25 | 26 | ### 트랜잭션 롤백 시 주의사항 27 | 28 | 트랜잭션을 롤백하는 것은 데이터베이스의 반영사항만 롤백하게 되고 수정한 자바 객체까지 원상태로 복구해주지 않는다. 29 | 30 | 기본전략인 트랜잭션당 영속성 컨텍스트 전략은 트랜잭션 AOP 종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료하기 때문에 문제가 발생하지 않는다. 31 | 32 | 반면 OSIV처럼 여러 트랜잭션이 하나의 영속성 컨텍스트를 사용하는 경우 트랜잭션을 롤백해서 영속성 컨텍스트에 이상이 발생해도 다른 트랜잭션이 해당 영속성 컨텍스트를 그대로 사용하여 문제가 발생할 수 있다. 33 | 34 | 따라서 롤백시 영속성 컨텍스트를 초기화해서 잘못된 영속성 컨텍스트를 사용하는 문제를 예방한다. 35 | 36 | ## 15.2 엔티티 비교 37 | 38 | 영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 존재하며, 이것은 영속성 컨텍스트와 생명주기를 같이 한다. 39 | 40 | - 같은 트랜잭션 범위 안에서 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다. 41 | 42 | - 동일성 : `==` 비교가 같다. 43 | - 동등성 : `equals()` 비교가 같다. 44 | - 데이터베이스 동등성 : `@id` 인 데어베이스 식별자가 같다. 45 | 46 | - 영속성 컨텍스트가 다를때 엔티티 비교는 다음과 같다. 47 | 48 | - 동일성 : `==` 비교가 실패한다. 49 | - 동등성 : `equals()` 비교가 만족한다. 단 `equals()` 를 구현해야 한다. 50 | - 데이터베이스 동등성 : `@id` 인 데이터베이스 식별자가 같다. 51 | 52 | 동일성 비교는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용할 수 있다. 그렇지 않은 떄는 비즈니스 키를 사용한 동등성 비교를 해야한다. 53 | 54 | ## 15.3 프록시 심화 주제 55 | 56 | ### 영속성 컨텍스트와 프록시 57 | 58 | - 프록시 조회 후 원본 엔티티 조회하는 경우 59 | 60 | 영속성 컨텍스트는 `em.getReference()`를 통해 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다. 61 | 62 | 프록시로 조회해서 영속성 컨텍스트는 영속 엔티티의 동일성을 보장한다. 63 | 64 | - 원본 엔티티 조회후 프록시 조회하는 경우 65 | 66 | 원본 엔티티를 먼저 조회하면 영속성 컨텍스트는 이미 데이터베이스에서 조회했기 때문에 프록시를 반환할 이유가 없다. 67 | 68 | 따라서 `em.getReference()`를 호춣도 원본 엔티티를 반환한다. 69 | 70 | 이경우에도 영속성 컨텍스트는 자신이 관리하는 영속 엔티티의 동일성을 보장한다. 71 | 72 | ### 프록시 타입 비교 73 | 74 | - 프록시로 조회한 엔티티의 타입을 비교할때는 `==` 비교를 하면 안되고 대신에 `instanceof`를 사용해야한다. 75 | 76 | - 프록시의 멤버변수에 직접 접근하면 안되고 대신에 접근자 메소드를 사용해야 한다. 77 | 78 | ### 상속관계와 프록시 79 | 80 | 프록시를 부모 타입으로 조회하면 문제가 발생한다. 프록시를 부모타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되어 하위 타입과 다른 타입으로 생성되고 하위 타입으로 다운캐스팅 할 수 없고 `instanceof`연산을 사용할 수 없다. 81 | 82 | 상속관계에서 발생하는 프록시 문제를 해결하기 위한 방법 83 | 84 | - JPQL로 대상 직접 조회 85 | 86 | - 처음부터 자식 타입을 직접 조회해 필요한 연산을 수행 87 | - 이 방법의 경우 다형성을 활용할 수 없다. 88 | 89 | - 프록시 벗기기 90 | 91 | - 하이버네이트가 제공하는 기능을 통해 원본 엔티티를 가져옴 92 | - 이 방법의 경우 프록시에서 원본 엔티티를 직접 꺼내기 때문에 프록시와 원본 엔티티의 동일성 비교가 실패하게 된다. 93 | - 원본 엔티티가 꼭 필요한 상황에 사용하고 다른 곳에서 사용되지 않도록 하는 것이 중요하다. 94 | 95 | - 기능을 위한 별도의 인터페이스 제공 96 | 97 | - 특정 기능을 제공하는 인터페이스를 생성 98 | - 다형성을 사용하는 좋은 방법이다. 99 | - 프록시의 특징 때문에 프록시의 대상이 되는 타입에 인터페이스를 적용해야 한다. 100 | 101 | - 비지터 패턴 사용 102 | 103 | - `Visiotr` 와 `Visitor`를 받아들이는 대상 클래스로 구성된다. 104 | - 비지터 패턴을 사용하면 프록시에 대한 걱정 없이 안전하게 원본 엔티티에 접근할 수 있고 `instanceof`나 타입캐스팅 없이 코드를 구현할 수 있다. 105 | - 새로운 기능이 추가될 떄 기존 코드 구조 변경 없이 `Visitor`만 추가하면 된다. 106 | 107 | ## 15.4 성능 최적화 108 | 109 | JPA로 애플리케이션을 개발할 때 발생하는 다양한 성능 문제와 해결 방안 110 | 111 | ### N+1 문제 112 | 113 | 즉시로딩 전략은 `N+1` 문제는 물론 비즈니스 로직에 따라 필요하지 않은 엔티티를 로딩해야하는 상황이 자주 발생한다. 또한 성능 최적화가 어렵다. 114 | 115 | 따라서 모두 지연로딩으로 설정하고 성능 최적화가 꼭 필요한 곳에는 JPQL 페치 조인을 사용하자. 116 | 117 | - `@OneToOne`, `@ManyToOne` : 기본 페치 전략은 즉시 로딩 118 | - `@OneToMany`, `@ManyToMany` : 기본 페치 전략은 지연 로딩 119 | 120 | 다른 방법으로는 하이버네이트 `@BatchSize`를 사용하여 지정해둔 size만큼 해당 엔티티를 조회할때 SQL의 IN절을 사용하는 방법이 있다. 121 | 122 | 또한 하이버네이트 `@Fetch(FetchMode.SUBSELECT)` 를 사용하여 연관된 데이터를 조회할때 서브 쿼리를 사용해서 `N+1` 문제를 해결할 수 있다. 123 | 124 | ### 읽기 전용 쿼리의 성능 최적화 125 | 126 | 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 더 많은 메모리를 사용하는 단점이 있다. 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화할 수 있다. 127 | 128 | - 스칼라 타입으로 조회 129 | - 엔티티가 아닌 스칼라 타입으로 모든 필드를 조회한다. 130 | 131 | - 읽기 전용 쿼리 힌트 사용 132 | - 하이버네이트 전용 힌트인 `org.hibernate.readOnly`를 사용해 엔팉를 읽기 전용으로 조회한다. 133 | - 스냅샷을 보관하지 않기 때문에 엔티티를 수정해도 데이터베이스에 반영되지 않는다. 134 | 135 | - 읽기 전용 트랜잭션 사용 136 | - `@Transactional(readOnly = true)` 어노테이션을 사용하여 트랜잭션을 읽기 전용 모드로 설정할 수 있다. 137 | - 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않으며 트랜잭션을 커밋해도 영속성 컨텍스트를 플러시하지 않는다. 138 | - 플러시할 때 일어나는 스냅샷 비교와 같은 로직을 수행하지 않기 때문에 성능이 향상된다. 139 | 140 | - 트랜잭션 밖에서 읽기 141 | - 트랜잭션 없이 엔티티를 조회한다는 뜻이다. 142 | - 데이터 변경이 불가능하기 때문에 조회가 목적일때만 사용해야 한다. 143 | 144 | 읽기 전용 트랜잭션과 읽기 전용 쿼리 힌트를 동시에 사용하는것이 가장 효과적이다. 145 | 146 | ### 배치 처리 147 | 148 | 수백만 건의 데이터를 처리해야하는 경우 일반적인 방식으로 엔티티를 계속 조회하면 영속성 컨텍스트에 아주 많은 엔티티가 쌓이면서 메모리 부족으로 오류가 발생한다. 149 | 150 | 따라서 이런 배치 처리는 적절한 단위로 영속성 컨텍스트를 초기화해야한다. 또한 2차 캐시를 사용하고 있다면 2차 캐시에 엔티티를 보관하지 않도록 주의해야 한다. 151 | 152 | - JPA 등록 배치 153 | 154 | 수천에서 수만 건 이상의 엔티티를 한 번에 등록할 때 주의할 점은 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 데이터베이스에 플러시하고 영속성 컨텍스트를 초기화해야 한다. 155 | 156 | 배치 처리는 아주 많은 데이터를 조회해서 수정하는 데 수많은 데이터를 한 번에 메모리에 올려둘 수 없어서 2가지 방법을 주로 사용한다. 157 | 158 | - 페이징 처리 159 | 160 | - 커서 : JPA는 JDBC 커서를 지원하지 않는다. 하이버네이트 세션을 사용해야 한다. 161 | 162 | 데이터베이스 커서(Cursor)는 일련의 데이터에 순차적으로 액세스할 때 검색 및 "현재 위치"를 포함하는 데이터 요소이다. 하이버네이트는 scroll 이라는 이름으로 JDBC 커서를 지원한다. 163 | 164 | - 하이버네이트 무상태 세션 사용 165 | 166 | 하이버네이트는 무상태 세션이라는 특별한 기능을 제공한다. 영속성 컨텍스트를 만들지 않고 심지어 2차 캐시도 사용하지 않는다. 167 | 168 | 무상태 세션은 영속성 컨텍스트가 없기 때문에 엔티티를 수정하려면 무상태 세션이 제공하는 update()메서드를 직접 호출해야 한다. 169 | 170 | ### SQL 쿼리 힌트 사용 171 | 172 | JPA는 데이터베이스 SQL 힌트 기능을 제공하지 않기 때문에 하이버네이트를 직접 사용해야한다. 173 | 174 | ```java 175 | Session session = em.unwrap(Session.class); //하이버네이트 직접 사용 176 | 177 | List list = session.createQuery("select m from Member m") 178 | .addQueryHint("FULL (MEMBER)") //SQL HINT 추가 179 | .list(); 180 | ``` 181 | 182 | ### 트랜잭션을 지원하는 쓰기 지연과 성능 최적화 183 | 184 | - 트랜잭션을 지원하는 쓰기 지연과 JDBC 배치 185 | 186 | 네트워크 호출 한 번은 단순한 메서드를 수만 번 호출하는 것보다 더 큰 비용이 든다. 187 | 188 | 이것을 최적화하려면 SQL을 모아서 한 번에 데이터베이스로 보내면 된다. 189 | 190 | JDBC가 제공하는 SQL 배치 기능을 사용하면 SQL을 모아서 데이터베이스에 한 번에 보낼 수 있다. 191 | 192 | SQL 배치 최적화 전략은 구현체마다 조금씩 다르다. 193 | 194 | `hibernate.jdbc.batch_size` 속성의 값을 정해 SQL 배치를 실행하게 되며 SQL 배치는 같은 SQL일때만 유효하다. 195 | 196 | 중간에 다른 처리가 들어가면 SQL 매치를 다시 시작한다. 197 | 198 | 엔티티가 영속 상태가 되려면 식별자가 꼭 필요하다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스가 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 Insert SQL이 데이터베이스에 전달된다. 따라서 쓰기 지연을 활용한 성능 최적화를 할 수 없다. 199 | 200 | - 트랜잭션을 지원하는 쓰기 지연과 애플리케이션 확장성 201 | 202 | 데이터베이스 테이블 로우에 락이 걸리는 시간을 최소화 한다는 점이 장점이다. 203 | 204 | 이 기능은 트랜잭션을 커밋해서 영속성 컨텍스트가 플러시하기 전까지는 데이터베이스에 데이터를 등록, 수정, 삭제하지 않는다. 205 | 206 | 따라서 커밋 직전까지 데이터베이스 로우에 락을 걸지 않는다. 207 | 208 | ## 15.5 정리 209 | 210 | - JPA 예외는 트랜잭션 롤백을 표시하는 예외와 표시하지 않는 예외로 나눈다. 211 | - 스프링 프레임워크는 JPA의 예외를 스프링 프레임워크가 추상화한 예외로 변환해준다. 212 | - 같은 영속성 컨텍스트의 엔티티를 비교할 떄는 동일성 비교가 가능하지만 다르면 동일성 비교에 실패한다. 213 | - `N+1` 문제는 주로 페치 조인을 사용해서 해결한다. 214 | - 엔티티를 읽기 전용으로 조회하면 스냅샷을 유지할 필요가 없고 영속성 컨텍스트를 플러시하지 않아도 된다. 215 | - 트랜잭션을 지원하는 쓰기 지연 덕분에 SQL 배치 기능을 사용할 수 있다. -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/16장 트랜잭션과 락, 2차캐시/16.1 ~ 16.3.md: -------------------------------------------------------------------------------- 1 | # 16장 트랜잭션과 락, 2차 캐시 2 | 3 | ## 트랜잭션과 락 4 | 5 | ### 트랜잭션과 격리 수준 6 | 7 | 트랜잭션은 **ACID**라 하는 원자성, 일관성, 격리성, 지속성을 보장해야 한다. 8 | 9 | - 원자성 10 | - 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하든가 모두 실패해야 한다. 11 | - 일관성 12 | - 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야 한다. 13 | - ex) 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야 한다. 14 | - 격리성 15 | - 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. 16 | - ex) 동시에 같은 데이터를 수정하지 못하도록 해야 한다. 격리성은 동시성과 관련된 성능 이슈로 인해 격리 수준을 선택할 수 있다. 17 | - 지속성 18 | - 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다. 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다. 19 | 20 | 트랜잭션은 원자성, 일과성, 지속성을 보장하지만 격리성을 완벽히 보장하려면 트랜잭션을 거의 차례대로 실행해야 한다. 이렇게 하면 동시성 처리 성능이 매우 나빠진다. 따라서 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다. 21 | 22 | 23 | 24 | #### 격리 수준에 따른 문제점 25 | - DIRTY READ 26 | - 커밋하지 않아도 데이터를 읽을 수 있는 것을 의미한다. 이 문제점은 데이터 정합성에 심각한 문제가 발생할 수 있다. 27 | - NON-REPEATABLE READ 28 | - 반복해서 같은 데이터를 읽을 수 없는 상태를 의미한다. 29 | - PHANTOM READ 30 | - 반복 조회 시 결과 집합이 달라지는 것을 의미한다. 31 | 32 | #### 4단계 격리 수준 33 | - READ UNCOMMITED (커밋되지 않은 읽기) 34 | - 커밋하지 않은 데이터를 읽을 수 있다. DIRTY READ가 발생할 수 있다. 35 | - READ COMMITTED (커밋된 읽기) 36 | - 커밋한 데이터만 읽을 수 있다. 따라서 DIRTY READ가 발생하지 않는다. 하지만 NON-REPEATABLE READ는 발생할 수 있다. 37 | - REPEATABLE READ (반복 가능한 읽기) 38 | - 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회된다. NON-REPEATABLE READ는 허용하지 않지만, PHANTOM READ는 발생할 수 있다. 39 | - SERIALIZABLE (직렬화 기능) 40 | - 가장 엄격한 트랜잭션 격리 수준이다. 여기서는 PHANTOM READ가 발생하지 않지만 동시성 처리 능력이 급격히 떨어질 수 있다. 41 | 42 | 순서대로 READ UNCOMMITED의 격리 수준이 가장 낮고 SERIALIZABLE의 격리 수준이 가장 높다. 격리 수준이 낮을수록 동시성은 증가하지만 격리 수준에 따른 다양한 문제가 발생한다. 43 | 애플리케이션 대부분은 동시성 처리가 중요하므로 데이터베이스들은 보통 READ COMMITTED 격리 수준을 기본으로 사용한다. 일부 중요한 비즈니스 로직에 더 높은 격리 수준이 필요하면 데이터베이스 트랜잭션이 제공하는 잠금 기능을 사용하면 된다. 44 | 45 | ### 낙관적 락과 비관적 락 46 | 47 | JPA의 영속성 컨텍스트를 적절히 활용하면 데이터 베이스 트랜잭션이 READ COMMITTED 격리 수준이어도 애플리케이션 레벨에서 반복 가능한 읽기가 가능하다. 물론 영속성 컨텍스트의 관리를 받지 못한다면 반복 가능한 읽기를 할 수 없다. 48 | 49 | **낙관적 락** : 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법. 데이터베이스가 제공하는 락 기능을 사용하는 것이 아니라 JPA가 제공하는 버전 관리 기능을 사용한다. (애플리케이션이 제공하는 락) 낙관적 락은 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다는 특징이 있다. 50 | 51 | **비관적 락** : 트랜잭션의 충돌이 발생한다고 가정하고 우선 락을 걸고 보는 방법. 데이터베이스가 제공하는 락 기능을 사용한다. 대표적으로 `select for update` 구문이 있다. 52 | 53 | #### 두 번의 갱신 분실 문제 54 | 55 | - 마지막 커밋만 인정하기 : A의 내용은 무시하고 마지막에 커밋한 B의 내용만 인정 56 | - 최초 커밋만 인정하기 : A가 이미 수정을 완료 했으므로 B가 수정을 완료할 때 오류 발생 57 | - 충돌하는 갱신 내용 병합하기 : A와 B의 수정사항을 병합 58 | 59 | ### @Version 60 | 61 | @Version 적용 가능 타입 62 | 63 | - Long 64 | - Integer 65 | - Short 66 | - Timestamp 67 | 68 | 버전 관리 기능을 적용하려면 엔티티에 버전 관리용 필드를 하나 추가하고 @Version을 붙이면 된다. 엔티티를 수정할 때 마다 버전이 하나씩 자동으로 증가한다. 엔티티를 수정할 때 조회 시점의 버전과 수정 시점의 버전이 다르면 예외가 발생한다. 따라서 최초 커밋만 인정하기가 적용된다. 69 | 70 | 버전은 엔티티의 값을 변경하면 증가한다. 그리고 값 타입인 임베디드 타입과 값 타입 컬렉션은 논리적인 개념상 해당 엔티티의 값이므로 수정하면 엔티티의 버전이 증가한다. 단, 연관관계 필드는 외래 키를 관리하는 연관관계의 주인 필드를 수정할 때만 버전이 증가한다. 71 | 72 | *벌크 연산은 버전을 무시한다 .벌크 연산에서 버전을 증가하려면 버전 필드를 강제로 증가시켜야 한다.* 73 | 74 | ### JPA 락 사용 75 | 76 | *JPA를 사용할 때 추천하는 전략은 READ COMMITTED + 낙관적 버전 관리 (두 번의 갱신 내역 분실 문제 예방)* 77 | 78 | 락은 다음 위치에 적용할 수 있다. 79 | 80 | - EntityManager.lock(), EntityManager.find(), EntityManager.refresh() 81 | - Query.setLockMode() (TypeQuery 포함) 82 | - @NamedQuery 83 | 84 | ### JPA 낙관적 락 85 | 86 | JPA가 제공하는 낙관적 락은 버전(@Version)을 사용한다. 따라서 낙관적 락을 사용하려면 버전이 있어야 한다. 트랜잭션을 커밋하는 시점에 충돌을 알 수 있다는 특징이 있다. 87 | 88 | - NONE 89 | - 용도 : 조회한 엔티티를 수정할 때 다른 트랜잭션에 의해 변경되지 않아야 한다. 조회 시점부터 수정 시점까지를 보장한다. 90 | - 동작 : 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다. 이 때 DB 버전 값이 아니면 예외가 발생한다. 91 | - 이점 : 두 번의 갱신 분실 문제를 예방한다. 92 | - OPTIMISTIC 93 | - 용도 : 조회 시점부터 트랜잭션이 끝날 때까지 조회한 엔티티가 변경되지 않음을 보장한다. 94 | - 동작 : 트랜잭션을 커밋할 때 버전 정보를 조회해서 현재 버전과 같은지 검증한다. 같지 않다면 에러가 발생한다. 95 | - 이점 : DIRTY READ와 NON-REPEATABLE READ를 방지한다. 96 | - OPTIMISTIC_FORCE_INCREMENT 97 | - 용도 : 논리적인 단위의 엔티티 묶음을 관리할 수 있다. 98 | - 동작 : 엔티티를 수정하지 않아도 커밋할 때 UPDATE 쿼리를 사용해 버전 정보를 강제로 증가시킨다. 이 때 DB버전과 엔티티 버전이 다르면 예외가 발생한다. 추가로 엔티티를 수정하면 버전 UPDATE가 발생한다. 따라서 총 2번의 버전 증가가 나타날 수 있다. 99 | - 이점 : 강제로 버전을 증가해서 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다. 100 | 101 | ### JPA 비관적 락 102 | 103 | 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법이다. 주로 쿼리에 `select for update` 구문을 사용하면서 시작하고 버전 정보는 사용하지 않는다. 주로 PESSIMISTIC_WRITE 모드를 사용한다. 104 | 특징으로는 엔티티가 아닌 스칼라 타입을 조회할 때도 사용할 수 있다. 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있다. 105 | 106 | - PESSIMISTIC_WRITE 107 | - 용도 : 데이터베이스에 쓰기 락을 건다. 108 | - 동작 : `select for update`를 사용해서 락을 건다. 109 | - 이점 : NON-REPEATABLE READ를 방지한다. 락이 걸린 로우는 다른 트랜잭션이 수정할 수 없다. 110 | - PESSIMISTIC_READ 111 | - 데이터를 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용한다. 일반적으로 잘 사용하지 않는다. 112 | - PESSIMISTIC_FORCE_INCREMENT 113 | - 유일하게 버전 정보를 사용한다. 비관적 락이지만 버전 정보를 강제로 증가시킨다. 114 | 115 | ### 비관적 락과 타임아웃 116 | 117 | 비관적 락을 사용하면 락을 획들할 때까지 트랜잭션이 대기하는데 무한정 기다릴 수 없으므로 타임아웃 시간을 줄 수 있다. 118 | 119 | ## 2차 캐시 120 | 121 | ### 1차 캐시와 2차 캐시 122 | 123 | 일반적으로 1차캐시는 트랜잭션을 시작하고 종료할 때까지만 유효하다. OSIV를 사용해도 클라이언트의 요청이 들어올 때부터 끝날때까지만 유효하다. 따라서 애플리케이션 전체로 보면 데이터베이스 접근 횟수를 획기적으로 줄이지는 못한다. 124 | 그러나 하이버네이트를 포함한 대부분의 JPA 구현체들은 애플리케이션 범위의 캐시를 지원한다. 이것을 **공유캐시** 및 **2차캐시**라고 한다. 이것을 활용하면 애플리케이션 조회 성능을 향상시킬 수 있다. 125 | 126 | #### 1차 캐시 127 | 128 | 1. 최초 조회할 떄는 1차 캐시에 엔티티가 없으므로 129 | 2. 데이터베이스에서 엔티티를 조회해서 130 | 3. 1차 캐시에 보관하고 131 | 4. 1차 캐시에 보관한 결과를 반환한다. 132 | 5. 이후 같은 엔티티를 조회하면 DB를 조회하지 않고 1차 캐시의 엔티티를 그대로 반환한다. 133 | 134 | 특징 135 | - 1차 캐시는 같은 엔티티가 있으면 해당 엔티티를 그대로 반환한다. 따라서 객체 동일성 (a==b)을 보장한다. 136 | - 1차 캐시는 기본적으로 영속성 컨텍스트 범위의 캐시다. (컨테이너 환경에서는 트랜잭션 범위의 캐시, OSIV를 적용하면 요청 범위의 캐시다.) 137 | 138 | #### 2차 캐시 139 | 140 | 2차 캐시는 애플리케이션을 종료할 때까지 캐시가 유지된다. 분산 캐시나 클러스터링 환경의 캐시는 애플리케이션보다 더 오래 유지될 수도 있다. 141 | 142 | 1. 영속성 컨텍스트는 엔티티가 필요하면 2차 캐시를 조회한다. 143 | 2. 2차 캐시에 엔티티가 없으면 데이터베이스를 조회해서 144 | 3. 결과를 2차 캐시에 보관한다. 145 | 4. 2차 캐시는 자신이 보관하고 있는 엔티티를 복사해서 반환한다. 146 | 5. 2차 캐시에 저장되어 있는 엔티티를 조회하면 복사본을 만들어 반환한다. 147 | 148 | 2차 캐시는 동시성을 극대화하려고 캐시한 객체를 직접 반환하지 않고 복사본을 만들어서 반환한다. 149 | 150 | 특징 151 | - 2차 캐시는 영속성 유닛 범위의 캐시다. 152 | - 2차 캐시는 조회한 객체를 그대로 반환하는 것이 아니라 복사본을 만들어 반환한다. 153 | - 데이터베이스 기본 키를 기준으로 캐시하지만 영속성 컨텍스트가 다르면 객체 동일성 (a==b)을 보장하지 않는다. 154 | 155 | ### JPA 2차 캐시 기능 156 | 157 | 캐시 모드 설정 158 | 159 | - @Cacheable 160 | 161 | 캐시 조회, 저장 방식 설정 162 | 163 | - javax.persistence.CacheRetrieveMode : 캐시 조회 모드 설정 옵션 164 | - USE : 캐시에서 조회 165 | - BYPASS : 캐시를 무시하고 데이터베이스에 직접 접근 166 | - javax.persistence.CacheStoreMode : 캐시 보관 모드 설정 옵션 167 | - USE : 조회한 데이터를 캐시에 저장. 이미 캐시에 있다면 최신 상태로 갱신하지 않는다. 168 | - BYPASS : 캐시에 저장하지 않는다. 169 | - REFRESH : USE 전략에 추가로 데이터베이스에서 조회한 엔티티를 최신 상태로 다시 캐시한다. 170 | 171 | #### JPA 캐시 관리 API 172 | 173 | JPA는 캐시를 관리하기 위한 javax.persistence.Cache 인터페이스를 제공한다. 174 | 175 | ### 하이버네이트와 EHCACHE 적용 176 | 177 | 1. 엔티티 캐시 : 엔티티 단위로 캐시한다. 178 | 2. 컬렉션 캐시 : 엔티티와 연관된 컬렉션을 캐시한다. 컬렉션이 엔티티를 담고 있으면 식별자 값만 캐시한다. 179 | 3. 쿼리 캐시 : 쿼리와 파라미터 정보를 키로 사용해서 캐시한다. 결과가 엔티티면 식별자 값만 캐시한다. 180 | 쿼리 캐시를 적용하려면 영속성 유닛을 설정에 `hibernate.cache.use_query_cache` 옵션을 꼭 `true`로 설정해야 한다. 181 | 쿼리 캐시를 잘 활용하면 극적인 성능 향상이 있지만 빈번하게 변경이 있는 테이블에 사용하면 오히려 성능이 저하된다. 182 | 183 | ### 쿼리 캐시와 컬렉션 캐시의 주의점 184 | 185 | 엔티티 캐시를 사용해서 엔티티를 캐시하면 엔티티 정보를 모두 캐시하지만 쿼리 캐시와 컬렉션 캐시는 결과 집합의 식별자 값만 캐시한다. 이 값을 하나씩 엔티티 캐시에서 조회해서 실제 엔티티를 찾는다. 186 | 187 | 문제는 쿼리 캐시나 컬렉션 캐시만 사용하고 대상 엔티티에 엔티티 캐시를 적용하지 않으면 심각한 문제가 발생 할 수 있다. 즉, 최악의 상황에 결과 집합의 개수만큼의 SQL이 실행될 수 있다. 따라서 쿼리 캐시나 컬렉션 캐시를 사용하면 결과 대상 엔티티에는 꼭 엔티티 캐시를 적용해야 한다. 188 | -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/1장 JPA 소개/1.1 ~ 1.4.md: -------------------------------------------------------------------------------- 1 | # 1장 JPA 소개 2 | 3 | ## SQL 을 직접 다룰 때 발생하는 문제점 4 | 5 | - SQL 에 의존적인 개발로 SQL 중심의 의존 관계가 생김 6 | - 엔티티를 신뢰할 수 없음 7 | - DAO 를 열어서 어떤 SQL 이 실행되고 어떤 객체가 함께 조회되는지 일일이 확인해야함 8 | - 진정한 의미의 계층 분할이 어려움 9 | - 논리적으로 엔티티오 아주 강한 의존관계를 가짐 10 | 11 | ## 패러다임의 불일치 12 | 13 | ### 상속 14 | 15 | - 객체는 상속이라는 기능을 가지고 있지만 테이블은 상속이라는 기능이 없음 16 | - JPA 는 상속과 관련된 패러다임의 불일치 문제를 개발자가 대신 해결해줌 17 | - 개발자는 마치 자바 컬렉션에 객체를 저장하듯이 JPA 에게 객체를 저장하면 됨 18 | 19 | ### 연관관계 20 | 21 | - 객체는 참조를 사용해서 다른 객체와 연관관계를 가지고 참조에 접근해서 연관된 객체를 조회함 22 | - 테이블은 외래 키를 사용해서 다른 테이블과 연관관계를 가지고 조인을 사용해서 연관된 테이블을 조회함 23 | - 객체는 참조가 있는 방향으로만 조회할 수 있는데, 테이블은 외래 키 하나로 양방향으로 모두 조회가 가능함 24 | - JPA 는 참조를 외래키로 변환해서 적절한 SQL을 데이터베이스에 전달하고, 외래 키를 참조로 변환하는 일도 처리해줌 25 | 26 | ### 객체 그래프 탐색 27 | 28 | SQL 을 직접 다루면 처음 실행하는 SQL 에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이것은 객체지향 개발자에겐 너무나 큰 제약이고 SQL 에 의존적인 개발이 될 수 밖에 없다. 29 | 30 | JPA 는 연관된 객체를 즉시 함께 조회할지 아니면 실제 사용되는 시점에 지연해서 조회할지를 간단하게 설정할 수 있고, 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 31 | 32 | 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미뤘다가 조회하는것을 **지연 로딩** 이라 한다. 33 | 34 | ## JPA 란 무엇일까? 35 | 36 | JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준이다. JPA 는 애플리케이션과 JDBC 사이에서 동작한다. 여기서 ORM(Object-Relational Mapping) 이란 이름 그대로 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑해서 적절한 SQL도 대신 생성해주고, 패러다임의 불일치 문제를 개발자 대신 해결해준다. 덕분에 개발자는 데이터 중심인 관계형 데이터베이스를 사용해도 객체지향 애플리케이션 개발에 집중할 수 있다. 37 | 38 | ### JPA 소개 39 | 40 | JPA 는 자바 ORM 기술에 대한 API 표준 명세다. 쉽게 이야기해서 인터페이스를 모아둔 것이다. 따라서 JPA 를 사용하려면 JPA 를 구현한 ORM 프레임워크를 선택해야 한다. JPA 를 구현한 ORM 프레임워크는 하이버네이트, EclipseLink, DataNucleus 가 있는데 이 중에 하이버네이트가 가장 대중적이다. 41 | 42 | ### 왜 JPA 를 사용해야 하는가? 43 | 44 | - 생산성 45 | 46 | JPA 를 사용하면 자바 컬렉션에 객체를 저장하듯이 JPA 에게 저장 할 객체를 전달하면 된다. 따라서 지루하고 반복적인 코드와 CRUD 용 SQL 을 개발자가 직접 작성하지 않아도 된다. 이런 기능들을 사용하면 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전시킬 수 있다. 47 | 48 | - 유지보수 49 | 50 | 필드를 추가하거나 삭제할때 개발자가 작성해야 했던 SQL 과 JDBC API 코드를 JPA 가 대신 처리해주므로 유지보수해야 하는 코드가 줄어든다. 또한, JPA 가 패러다임의 불일치 문제를 해결해주므로 객체지향 언어가 가진 장점들을 활용해서 유연하고 유지보수하기 좋은 도메인 모델을 편리하게 설계할 수 있다. 51 | 52 | - 패러다임의 불일치 해결 53 | 54 | JPA 는 상속, 연관관계, 객체 그래프 탐색, 비교하기와 같은 패러다임의 불일치 문제를 해결해준다. 55 | 56 | - 성능 57 | 58 | JPA 는 애플리케이션과 데이터베이스 사이에서 다양한 성능 최적화 기회를 제공한다. 애플리케이션과 데이터베이스 사이에 계층을 하나 더 두면서 최적화 관점에서 시도해볼 수 있는게 많아지게 된다. 59 | 60 | - 데이터 접근 추상화와 벤더 독립성 61 | 62 | JPA 는 애플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 한다. 만약 데이터베이스를 변경하면 JPA 에게 다른 데이터베이스를 사용한다고 알려주기만 하면 된다. 63 | 64 | - 표준 65 | 66 | JPA 는 자바 진영의 ORM 기술 표준이다. 표준을 사용하면 다른 구현 기술로 손쉽게 변경할 수 있다. -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/2장 JPA 시작/2.1 ~ 2.7.md: -------------------------------------------------------------------------------- 1 | # 2장 JPA 시작 2 | 3 | ## 객체 매핑 시작 4 | 5 | JPA 를 사용하려면 클래스와 테이블을 매핑해야 한다. 이때 JPA 가 제공하는 매핑 어노테이션을 사용할 수 있다. 6 | 7 | ```java 8 | @Entity 9 | @Table(name="MEMBER") 10 | public class Member { 11 | 12 | @Id 13 | @Column(name = "ID") 14 | private String id; 15 | 16 | @Column(name = "NAME") 17 | private String username; 18 | 19 | // 매핑 정보가 없는 필드 20 | private Integer age; 21 | } 22 | ``` 23 | 24 | 여기서 @Entity, @Table, @Column 이 매핑 정보다. JPA 는 매핑 어노테이션을 분석해서 어떤 객체가 어떤 테이블과 관계가 있는지 알아낸다. 25 | 26 | - @Entity 27 | 28 | 이 클래스를 테이블과 매핑한다고 JPA 에게 알려준다. @Entity 가 사용된 클래스를 엔티티 클래스라 한다. 29 | 30 | - @Table 31 | 32 | 엔티티 클래스에 매핑할 테이블 정보를 알려준다. 이 어노테이션을 생략하면 클래스 이름을 테이블 이름으로 매핑한다(더 정확히는 엔티티 이름을 사용한다.) 33 | 34 | - @Id 35 | 36 | 엔티티 클래스의 필드를 테이블의 기본 키에 매핑한다. @Id 가 사용된 필드를 식별자 필드라 한다. 37 | 38 | - @Column 39 | 40 | 필드를 컬럼에 매핑한다. 41 | 42 | - 매핑 정보가 없는 필드 43 | 44 | 매핑 어노테이션을 생략하면 필드명을 사용해서 컬럼명으로 매핑한다. 45 | 46 | ## persistence.xml 설정 47 | 48 | JPA 는 persistence.xml 을 사용해서 필요한 설정 정보를 관리한다. 이 설정 파일이 META-INF/persistence.xml 클래스 패스 경로에 있으면 별도의 설정 없이 JPA 가 인식할 수 있다. 49 | 50 | ### 데이터베이스 방언 51 | 52 | JPA 는 특정 데이터베이스에 종속적이지 않은 기술이다. 따라서 다른 데이터베이스로 손쉽게 교체할 수 있다. SQL 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능을 JPA 에서는 방언(Dialect)이라 한다. 하이버네이트는 다양한 방언을 제공하기 때문에 데이터베이스가 변경되어도 애플리케이션 코드를 변경할 필요 없이 데이터베이스 방언만 교체하면 된다. 53 | 54 | ## 애플리케이션 개발 55 | 56 | ### 엔티티 매니저 설정 57 | 58 | - 엔티티 매니저 팩토리 생성 59 | 60 | Persistence 클래스를 사용해서 엔티티 매니저 팩토리를 생성하고 JPA 를 사용할 수 있게 준비한다. 61 | 62 | 이때 persistence.xml 의 설정 정보를 읽어서 JPA를 동작시키기 위한 기반 객체를 만들고 JPA 구현체에 따라서는 데이터베이스 커넥션 풀도 생성하므로 엔티티 매니저 팩토리를 생성하는 비용은 아주 크다. 따라서 엔티티 매니저 팩토리는 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용해야 한다. 63 | 64 | - 엔티티 매니저 생성 65 | 66 | 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다. 엔티티 매니저를 사용해서 엔티티를 데이터베이스에 등록/수정/삭제/조회할 수 있다. 엔티티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드간에 공유하거나 재사용하면 안 된다. 67 | 68 | - 종료 69 | 70 | 마지막으로 사용이 끝난 엔티티 매니저는 반드시 종료해야 한다. 애플리케이션을 종료할 때 엔티티 매니저 팩토리도 종료해야 한다. 71 | 72 | ### 트랜잭션 관리 73 | 74 | JPA 를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 한다. 트랜잭션 없이 데이터를 변경하면 예외가 발생한다. 트랜잭션 API 를 사용해서 비즈니스 로직이 정상 동작하면 트랜잭션을 커밋(commit)하고 예외가 발생하면 트랜잭션을 롤백(rollback)한다. 75 | 76 | ### 비즈니스 로직 77 | 78 | ```java 79 | public static void logic(EntityManager em) { 80 | 81 | String id = "id1"; 82 | Member member = new Member(); 83 | member.setId(id); 84 | member.setUsername("지한"); 85 | member.setAge(2); 86 | 87 | //등록 88 | em.persist(member); 89 | 90 | //수정 91 | member.setAge(20); // em.update가 아닌 .set엔티티 값만 변경 92 | 93 | //한 건 조회 94 | Member findMember = em.find(Member.class, id); 95 | System.out.println("findMember=" + findMember.getUsername() + ", age=" + findMember.getAge()); 96 | 97 | //목록 조회 98 | List members = em.createQuery("select m from Member m", Member.class).getResultList(); 99 | System.out.println("members.size=" + members.size()); 100 | 101 | //삭제 102 | em.remove(member); 103 | } 104 | ``` 105 | 106 | 비즈니스 로직을 보면 등록, 수정, 삭제, 조회 작업이 엔티티 매니저(em)를 통해서 수행되는 것을 알 수 있다. 107 | 108 | - 등록 109 | 110 | 엔티티를 저장하려면 엔티티 매니저의 persist() 메소드에 저장할 엔티티를 넘겨주면 된다. 111 | 112 | - 수정 113 | 114 | JPA 는 어떤 엔티티가 변경되었는지 추적하는 기능을 갖추고 있다. 따라서 엔티티의 값만 변경하면 알아서 UPDATE SQL 을 생성해서 데이터베이스에 값을 변경한다. 115 | 116 | - 삭제 117 | 118 | 엔티티를 삭제하려면 엔티티 매니저의 remove() 메소드에 삭제하려는 엔티티를 넘겨주면 된다. 119 | 120 | - 한 건 조회 121 | 122 | find() 메소드는 조회할 엔티티 타입과 @Id로 데이터베이스 테이블의 기존 키와 매핑한 식별자 값으로 엔티티 하나를 조회하는 가장 단순한 조회 메소드다. 123 | 124 | ### JPQL 125 | 126 | 애플리케이션이 필요한 데이터만 데이터베이스에서 불러오려면 결국 검색 조건이 포함된 SQL 이 필요한데, JPA 는 JPQL(Java Persistence Query Language)이라는 쿼리 언어로 이런 문제를 해결한다. 127 | 128 | JPQL 은 SQL 을 추상화한 객체지향 쿼리 언어로 JPA 가 제공하고, SQL 과 문법이 거의 유사하다. 주의할 점은 JPQL 에서는 엔티티 객체를 대상으로 쿼리한다. 클래스와 필드를 대상으로 쿼리하기 때문에 JPQL 은 데이터베이스 테이블을 전혀 알지 못한다. -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/3장 영속성 관리/3.1 ~ 3.7.md: -------------------------------------------------------------------------------- 1 | # 3장 영속성 관리 2 | 3 | # 3장 영속성 관리 4 | 5 | ## 엔티티 매니저 팩토리와 엔티티 매니저 6 | 7 | 데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성한다. 8 | 9 | ![https://blog.kakaocdn.net/dn/tb4f6/btqQIVOT2M6/6ieKwNTeU9F8Ss2XESNTR1/img.png](https://blog.kakaocdn.net/dn/tb4f6/btqQIVOT2M6/6ieKwNTeU9F8Ss2XESNTR1/img.png) 10 | 11 | - 고객의 요청이 오면 엔티티 매니저 팩토리에서 엔티티 매니저를 생성한다. 12 | - 생성된 앤티티 매니저는 내부적으로 데이터베이스 커넥션을 사용해서 DB를 사용하게 된다. 13 | - 엔티티 매니저는 각 고객의 요청마다 생성된다. 14 | 15 | 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안된다. 16 | 17 | ## 영속성 컨텍스트란? 18 | 19 | **`JPA` 를 이해하는데 가장 중요한 용어** 20 | 21 | **`"엔티티를 영구 저장하는 환경"` 이라는 뜻** 22 | 23 | ```java 24 | entityManage.persist(entity); 25 | ``` 26 | 27 | 위 코드는 DB에 entity를 **저장한다는 것이 아니라** 영속성 컨텍스트를 통해서 **entity를 영속한다**는 뜻이다. 28 | 29 | - entity를 DB에 저장하는 것이 아니라 영속성 컨텍스트에 저장한다는 것이다. 30 | - 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고, 관리할 수 있다. 31 | 32 | ## 엔티티의 생명주기 33 | 34 | - 비영속 (new/tansient) 35 | 36 | 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 37 | 38 | - 영속 (menaged) 39 | 40 | 영속성 컨텍스트에 관리되는 상태 41 | 42 | - 준영속 (detached) 43 | 44 | 영속성 컨텍스트에 저장되었다가 분리된 상태 45 | 46 | - 삭제 (removed) 47 | 48 | 삭제된 상태 49 | 50 | --- 51 | 52 | ### 비영속 53 | 54 | ![https://blog.kakaocdn.net/dn/b7d345/btqQDFMWQci/cJKsKlSgiaTDzaRAB6nwA1/img.png](https://blog.kakaocdn.net/dn/b7d345/btqQDFMWQci/cJKsKlSgiaTDzaRAB6nwA1/img.png) 55 | 56 | ``` 57 | // 객체 생성 (비영속) 58 | Entity entity = new Entity(); 59 | entity.setId("entity1"); 60 | entity.setName("지토"); 61 | ``` 62 | 63 | --- 64 | 65 | ### 영속 66 | 67 | ![https://blog.kakaocdn.net/dn/cGnefO/btqQJEe13ze/PXGdoBQohQtFOhSfyekyE1/img.png](https://blog.kakaocdn.net/dn/cGnefO/btqQJEe13ze/PXGdoBQohQtFOhSfyekyE1/img.png) 68 | 69 | ``` 70 | // 객체 생성 (비영속) 71 | Entity entity = new Entity(); 72 | entity.setId("entity1"); 73 | entity.setName("지토"); 74 | 75 | EntityManager em = emf.createEntityManager(); // emf : 엔티티 매니저 팩토리 76 | em.getTransaction().begin(); 77 | 78 | // 객체를 저장 (영속) 79 | em.persist(entity); 80 | ``` 81 | 82 | --- 83 | 84 | ### 준영속 85 | 86 | ``` 87 | // 객체 생성 (비영속) 88 | Entity entity = new Entity(); 89 | entity.setId("entity1"); 90 | entity.setName("지토"); 91 | 92 | EntityManager em = emf.createEntityManager(); // emf : 엔티티 매니저 팩토리 93 | em.getTransaction().begin(); 94 | 95 | // 객체를 저장 (영속) 96 | em.persist(entity); 97 | 98 | // 엔티티를 영속성 컨텍스트에서 분리 (준영속) 99 | emdetach(entity); 100 | ``` 101 | 102 | --- 103 | 104 | ### 삭제 105 | 106 | ``` 107 | // 객체 생성 (비영속) 108 | Entity entity = new Entity(); 109 | entity.setId("entity1"); 110 | entity.setName("지토"); 111 | 112 | EntityManager em = emf.createEntityManager(); // emf : 엔티티 매니저 팩토리 113 | em.getTransaction().begin(); 114 | 115 | // 객체를 저장 (영속) 116 | em.persist(entity); 117 | 118 | // 객체를 삭제 (삭제) 119 | em.remove(entity); 120 | ``` 121 | 122 | ## 영속성 컨텍스트의 특징 123 | 124 | - 영속 상태는 식별자 값이 반드시 있어야 한다. 125 | - JPA 는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이것을 플러시(flush)라 한다. 126 | - 영속성 컨텍스트가 엔티티를 관리해서 생기는 이점 127 | 1. 1차 캐시 128 | 2. 동일성(identity) 보장 129 | 3. 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind) 130 | 4. 변경 감지 (Dirty Checking) 131 | 5. 지연 로딩 (Lazy Loading) 132 | 133 | ### 엔티티 조회, 1차 캐시 134 | 135 | ![https://blog.kakaocdn.net/dn/bp3cUr/btqQDGrta5w/nX9pOWECBPRiedFJMdtqLk/img.png](https://blog.kakaocdn.net/dn/bp3cUr/btqQDGrta5w/nX9pOWECBPRiedFJMdtqLk/img.png) 136 | 137 | ``` 138 | // 객체 생성 (비영속) 139 | Entity entity = new Entity(); 140 | entity.setId("entity1"); 141 | entity.setName("지토"); 142 | 143 | // 객체를 저장 (영속) 144 | em.persist(entity); 145 | ``` 146 | 147 | `persist`를 하면 해당 엔티티를 1차 캐시에 저장한다 ( 영속 ) 148 | 149 | --- 150 | 151 | ### 1차 캐시에서 조회 152 | 153 | ![https://blog.kakaocdn.net/dn/rTpXC/btqQIXsqwhW/toqe7NjiJtKZDs0QKziC30/img.png](https://blog.kakaocdn.net/dn/rTpXC/btqQIXsqwhW/toqe7NjiJtKZDs0QKziC30/img.png) 154 | 155 | ``` 156 | // 객체 생성 (비영속) 157 | Entity entity = new Entity(); 158 | entity.setId("entity1"); 159 | entity.setName("지토"); 160 | 161 | // 1차 캐시에 저장됨 162 | em.persist(entity); 163 | 164 | // 1차 캐시에서 조회 165 | Entity findEntity = en.find(Entity.class, "entity1"); 166 | ``` 167 | 168 | `1차 캐시`에 조회하려는 id 값의 엔티티가 있을 경우에는 해당 값을 반환한다. 169 | 170 | --- 171 | 172 | ### 데이터베이스에서 조회 173 | 174 | ![https://blog.kakaocdn.net/dn/9O5pa/btqQIXTuemE/hyektBnWc5hZqypDIE0ddK/img.png](https://blog.kakaocdn.net/dn/9O5pa/btqQIXTuemE/hyektBnWc5hZqypDIE0ddK/img.png) 175 | 176 | ``` 177 | Entity findEntity2 = en.find(Entity.class, "entity2"); 178 | ``` 179 | 180 | 1차 캐시에 조회하려는 엔티티의 id가 없을 경우 DB에서 조회를 해와서 1차 캐시에 저장을 하고 해당 객체를 반환한다. 181 | 182 | 이후에 entity2를 조회하면 1차 캐시에 값이 있기 때문에 1차 캐시에서 조회한다. 183 | 184 | 하지만 해당 시나리오(1차 캐시)는 `트랜잭션 단위로 공유되는 캐시`이기 때문에, 그렇게 큰 성능의 이점을 얻는다 라는 장점은 거의 없다. 185 | 186 | --- 187 | 188 | ### 영속 엔티티의 동일성 보장 189 | 190 | ``` 191 | Entity findEntity1 = en.find(Entity.class, "entity1); 192 | Entity findEntity2 = en.find(Entity.class, "entity2"); 193 | 194 | assertThat(findEntity1 == findEntity2).isTrue(); // 테스트 성공 195 | ``` 196 | 197 | 1차 캐시로 반복 가능한 읽기(`REPEATABLE READ`) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공 198 | 199 | --- 200 | 201 | ### 엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind) 202 | 203 | ``` 204 | EntityManager em = emf.createEntityManager(); 205 | EntityTransaction transaction = em.getTransaction(); 206 | transaction.begin(); // 트랜잭션 시작 207 | 208 | em.persist(entity1); 209 | em.persist(entity2); 210 | 211 | // 커밋하는 순간 데이터베이스에 INSERT QUERY를 보냄 212 | transaction.commit(); // 트랜잭션 커밋 213 | ``` 214 | 215 | 기본적으로 `JPA`에서는 트랜잭션을 시작하고 `persist`를 실행하면 insert sql을 db에 보내지 않고 `JPA`가 쌓아두고 있는다. 216 | 217 | `commit 하는` 순간에 쌓아둔 `Query`들을 보낸다. 218 | 219 | --- 220 | 221 | ### 엔티티 수정 - 변경 감지 (Dirty checking) 222 | 223 | ``` 224 | EntityManager em = emf.createEntityManager(); 225 | EntityTransaction transaction = em.getTransaction(); 226 | transaction.begin(); // 트랜잭션 시작 227 | 228 | // 영속 엔티티 조회 229 | Entity entity = em.find(Entity.class, "entity"); 230 | 231 | // 영속 엔티티 데이터 수정 232 | entity.setName("지토"); 233 | 234 | // em.update(entity) <-- 이런 코드가 없는데?? 235 | 236 | transaction.commit(); // 트랜잭션 커밋 237 | ``` 238 | 239 | 1. `JPA`는 commit 시점에 내부적으로 `flush`가 호출이 된다. 240 | 2. `영속 컨텍스트(entityManager)`에서 엔티티와 스냅샷을 비교한다. (여기서 스냅샷은 해당 entity가 처음에 영속상태가 됐을 때 첫 시점의 entity 상태이다.) 241 | 3. 이때 entity가 변경이 된 값이 있다면 `쓰기 지연 SQL 저장소`에 update쿼리를 만들고 DB에 반영을 하게 된다. 242 | 243 | 위와 같은 동작방식을 가지기 때문에 commit 시점 이전에 엔티티에 대한 변경이 아무리 많았어도 최종적인 변경에 대한 update 쿼리가 생성된다. 244 | 245 | **JPA의 기본 전략은 엔티티의 모든 필드를 업데이트 한다.** 246 | 247 | **모든 필드를 업데이트하면 데이터베이스에 보내는 데이터 전송량이 증가하는 단점이 있지만, 아래와 같은 장점으로 인해 모든 필드를 업데이트 한다.** 248 | 249 | - 모든 필드를 사용하면 수정 쿼리가 항상 같다. 따라서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다. 250 | - 데이터베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다. 251 | 252 | --- 253 | 254 | ### 엔티티 삭제 255 | 256 | ``` 257 | // 삭제 대상 엔티티 조회 258 | Entity entity = em.find(Entity.class, "entity"); 259 | 260 | em.remove(entity); // 엔티티 삭제 261 | ``` 262 | 263 | `변경 감지 메커니즘과` 똑같은 메커니즘으로 해당 엔티티가 삭제가 된다고 보면 된다. 264 | 265 | ## 플러시 266 | 267 | 플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. 268 | 269 | **플러시 실행시 일어나는 일** 270 | 271 | 1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하고 수정 된 엔티티를 찾는다. 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다. 272 | 2. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.(등록, 수정, 삭제 쿼리). 273 | 274 | **영속성 컨텍스트를 플러시 하는 방법은 3가지다.** 275 | 276 | 1. em.flush() 를 직접 호출한다. 277 | 278 | 테스트나 다른 프레임워크와 JPA 를 함께 사용할 때를 제외하고 거의 사용하지 않는다. 279 | 280 | 2. JPA 는 트랜잭션 커밋 시 플러시가 자동 호출된다. 281 | 3. JPQL 쿼리 실행 시 플러시가 자동 호출된다. 282 | 283 | 식별자를 기준으로 조회하는 find() 메소드를 호출할 때는 플러시가 실행되지 않는다. 284 | 285 | **플러시라는 이름으로 인해 영속성 컨텍스트에 보관된 엔티티를 지운다고 생각하면 안 된다. 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 하는 것이 플러시다.** 286 | 287 | ### JPQL 쿼리 실행 시 플러시가 자동 호출되는 이유 ! 288 | ```java 289 | entityManager.persist(member1); 290 | entityManager.persist(member2); 291 | entityManager.persist(member3); 292 | 293 | query = entityManager.createQuery("SELECT m FROM Member m", Member.class); 294 | ``` 295 | 위와 같은 코드가 실행될 때 entityManager.createQuery 를 통해 SELECT를 하게되는데, 296 | 만약 JPQL 실행 시 자동 flush() 호출 동작이 없다면, 해당 시점에서 flush() 를 직접 호출하거나 트랜잭션이 끝나지 않았기 때문에 실제 데이터베이스에 297 | member1, member2, member3 에 대한 데이터가 존재하지 않아 조회가 의도한대로 실행되지 않을 것 이다. 298 | 299 | JPA 같은 경우에 이런 문제를 사전에 방지하기 위해 JPQL을 실행할 때 플러시를 자동으로 호출하게 되고, 300 | 따라서 위의 코드를 실행하였을때는 member1, member2, member3 에 대한 결과가 포함된다. 301 | 302 | 303 | ## 준영속 304 | 305 | 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된(detached)것을 준영속 상태라 한다. 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다. 306 | 307 | **영속 상태의 엔티티를 준영속 상태로 만드는 법은 3가지가 존재한다.** 308 | 309 | 1. em.detach(entity) 310 | 311 | 특정 엔티티만 준영속 상태로 전환한다. 312 | 313 | 이 메소드를 호출하는 순간 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거된다. 314 | 315 | 2. em.clear() 316 | 317 | 영속성 컨텍스트를 완전히 초기화해서 해당 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만든다. 318 | 319 | 이것은 영속성 컨텍스트를 제거하고 새로 만든 것과 같다. 320 | 321 | 3. em.close() 322 | 323 | 영속성 컨텍스트를 종료한다. 324 | 325 | 영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다. 326 | 327 | ### 준영속 상태의 특징 328 | 329 | - 거의 비영속 상태에 가깝다. 330 | - 식별자 값을 가지고 있다. 331 | - 지연 로딩을 할 수 없다. 332 | 333 | ### 병합: merge() 334 | 335 | 준영속 상태의 엔티티를 다시 영속 상태로 변경할때 사용한다. 336 | 337 | merge() 메소드는 준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다. 338 | 339 | 파라미터로 넘어온 엔티티는 병합 후에도 준영속 상태로 남아 있다. 340 | 341 | 병합은 준영속, 비영속을 신경 쓰지 않는다. 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 조회할 수 없으면 새로 생성해서 병합한다. 따라서 병합은 save or update 기능을 수행한다. 342 | 343 | --- -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/4장 엔티티 매핑/4.1 ~ 4.8.md: -------------------------------------------------------------------------------- 1 | # 4장 엔티티 매핑 2 | 3 | ## @Entity 4 | 5 | JPA 를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야한다. 6 | 7 | JPA 가 엔티티 객체를 생성할 때 기본 생성자를 사용하기 때문에 @Entity 적용시 기본 생성자는 필수이며 final 클래스, enum, interface, inner 클래스에는 사용할 수 없다. 8 | 또한 저장할 필드에 final 을 사용하면 안된다. 9 | 10 | ## @Table 11 | 12 | @Table 은 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다. 13 | 14 | ## 데이터베이스 스키마 자동 생성 15 | 16 | JPA 는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다. 17 | 18 | ```xml 19 | 20 | ``` 21 | 위와 같은 코드를 `persistence.xml`에 추가하면 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다. 22 | HBM2DDL 의 주의사항으로는 운영 서버에서는 create, create-drop, update 처럼 DLL 을 수정하는 옵션을 사용하면 안된다. 23 | 이 옵션들은 운영 중인 데이터베이스의 테이블이나 칼럼을 삭제할 수 있기 때문이다. 24 | 25 | ## DDL 생성 기능 26 | 27 | @Column 의 length 와 nullable 같은 속성들은 DDL 을 자동 생성할 때만 사용되고 JPA 의 실행 로직에는 영향을 주지 않는다. 28 | 하지만 이러한 기능을 사용하면 개발자가 엔티티만 보고도 제약조건을 파악 할 수 있다는 장점이 있다. 29 | 30 | ## 기본 키 맵핑 31 | 32 | 데이터베이스마다 기본 키를 생성하는 방식이 서로 다르기 때문에 JPA 는 이런 문제를 해결하기 위해 다양한 기본 키 생성 방식을 제공한다. 33 | 34 | ### 기본 키 직접 할당 전략 35 | 36 | 기본 키를 직접 할당하려면 @Id로 매핑하면 된다. 37 | 38 | ```java 39 | // 기본 키 직접 매핑 40 | @Id 41 | @Column(name = "id") 42 | private String id; 43 | ``` 44 | 45 | 기본 키 직접 할당 전략은 `em.persist()`로 엔티티를 저장하기 전에 직접 기본 키를 할당하는 방법이다. 46 | 47 | ### IDENTITY 전략 48 | 49 | IDENTITY 는 기본 키 생성을 데이터베이스에 위임하는 전략으로 주로 `AUTO_INCREMENT` 기능을 사용하는 DB에 사용한다. 50 | 51 | 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용하며 `em.persist()` 호출시 INSERT SQL 이 즉시 DB에 전달된다. 52 | 따라서 엔티티에 식별자 값을 할당하려면 JPA 는 추가적인 DB 조회가 필요하다. 엔티티가 영속 상태가 되려면 식별자가 반드시 필요하기 때문에 IDENTITY 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다. 53 | 54 | ```java 55 | // IDENTITY 매핑 56 | @Entitiy 57 | public class Board{ 58 | 59 | @Id 60 | @GeneratedValue(strategy = GenerationType.IDENTITY) 61 | private Long id; 62 | ... 63 | } 64 | ``` 65 | 66 | ### SEQUENCE 전략 67 | 68 | 데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. 69 | 70 | SEQUENCE 전략은 시퀀스를 지원하는 Oracle, PostgreSQl, H2 DB 에서 사용할 수 있다. 71 | 72 | ```java 73 | // 시퀀스 매핑 74 | @Entitiy 75 | @SequenceGenerator( 76 | name = "BOARD_SEQ_GENERATOR", 77 | sequenceName = "BOARD_SEQ", //매핑할 데이터베이스 시퀀스 이름 78 | initialValue = 1, aloocationSize =1) 79 | public class Board{ 80 | 81 | @Id 82 | @GeneratedValue(strategy = GenerationType.SEQUENCE, 83 | generator = "BOARD_SEQ_GENERATOR") 84 | private Long id; 85 | ... 86 | } 87 | ``` 88 | SEQUENCE 전략은 IDENTITY 전략과 사용하는 코드는 동일하지만 내부 동작 방식이 다르다. 89 | 90 | SEQUENCE 전략 `em.persist()`를 호출할 때 데이터베이스 시퀀스를 통해 시퀀스를 조회한 후 조회된 식별자를 엔티티에 할당해 엔티티를 영속 상태로 바꾸고 영속성 컨텍스트에 저장한다. 91 | 이후 트랜잭션을 커밋해 플러시가 발생하면 DB에 해당 엔티티가 저장된다. 92 | 93 | SEQUENCE 전략을 최적화하는데 사용되는 allocationSize 는 시퀀스를 한 번 호출할 때 증가하는 수를 의미한다. 94 | 데이터베이스에 저장하기 전 시퀀스 조회가 먼저 일어나야하기 때문에 DB 와의 통신이 총 2번 발생한다. 95 | 이때 allocationSize 가 50이라면 한 번 조회될 때 시퀀스를 50을 증가시키고 이후 1~50까지는 메모리에서 식별자를 할당하게 된다. 96 | 이런 방법은 시퀀스 값을 선점하므로 JVM이 동시에 동작해도 기본 키 충돌이 발생하지 않는다는 장점이 있다. 97 | 98 | ### TABLE 전략 99 | 100 | TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 칼럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 101 | 102 | 테이블을 사용하기 때문에 이전 전략들과는 달리 모든 데이터베이스에 적용할 수 있다. 103 | 104 | ```java 105 | // TABLE 전략 매핑 106 | @Entitiy 107 | @TableGenerator( 108 | name = "BOARD_SEQ_GENERATOR", 109 | table = "MY_SEQUENCES", //매핑할 데이터베이스 시퀀스 이름 110 | pkColumnValue = "BOARD_SEQ", aloocationSize =1) 111 | public class Board{ 112 | 113 | @Id 114 | @GeneratedValue(strategy = GenerationType.TABLE, 115 | generator = "BOARD_SEQ_GENERATOR") 116 | private Long id; 117 | ... 118 | } 119 | ``` 120 | TABLE 전략은 시퀀스 대신에 테이블을 사용한다는 차이점을 제외하고 내부 동작방식이 동일하다. 121 | 122 | TABLE 전략은 시퀀스 값을 조회해서 사용한 후 다음 값으로 증가시키기 위해 SEQUENCE 전략보다 한번 더 DB와 통신하는 단점이 있다. 123 | 이런 문제를 최적화하기 위해 SEQUENCE 전략과 동일한 allocationSize 를 사용할 수 있다. 124 | 125 | ### AUTO 전략 126 | 127 | AUTO 전략은 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동을 선택한다. 128 | 129 | AUTO 전략의 장점은 특정 데이터베이스 벤더에 종속적이지 않고 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다. 130 | 131 | ```java 132 | // AUTO 전략 매핑 133 | @Entitiy 134 | public class Board{ 135 | 136 | @Id 137 | @GeneratedValue(strategy = GenerationType.AUTO) 138 | private Long id; 139 | ... 140 | } 141 | ``` 142 | 143 | ### 기본 키 매핑 정리 144 | 145 | 영속성 컨텍스트는 엔티티를 식별자 값으로 구분하므로 엔티티를 영속 상태로 만들려면 식별자 값이 반드시 있어야 한다. 146 | em.persist()를 호출한 직후에 발생하는 일을 식별자 할당 전략별로 정리하면 다음과 같다. 147 | 148 | 직접 할당 : em.persist()를 호출하기 전에 애플리케이션에서 직접 식별자 값을 할당해야 한다. 만약 식별자 값이 없으면 예외가 발생한다. 149 | 150 | SEQUENCE : 데이터베이스 시퀀스에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다. 151 | 152 | TABLE : 데이터베이스 시퀀스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다. 153 | 154 | IDENTITY : 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텓스트에 저장한다 (IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.) 155 | 156 | ### 권장하는 식별자 선택 전략 157 | 158 | 데이터베이스 기본 키는 null 값을 허용하지 않고 유일해야하며 변해서는 안된다는 조건을 만족해야한다. 159 | 160 | 기본키를 선택하는 전략으로는 비즈니스에 의미가 있는 자연 키와 비즈니스와 관련 없는 임의의 키 대리 키가 있다. 161 | 162 | 자연 키의 경우 현재 상황에서는 개인 키 역할을 적절히 수행하며 기본 키의 조건을 모두 만족할 수 있지만 추후 정부 정책 변경으로 인한 데이터 저장에 대한 이슈가 발생할 경우 시스템이 전체적으로 수정을 필요로 하는 경우가 발생할 수 있다. 163 | 비즈니스에 대한 요소는 계속해서 변화해가기 때문에 외부의 영향을 받지 않는 대리 키를 선택하여 기본 키로 사용하는것이 권장하는 방법이다. -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/5장 연관관계 매핑 기초/5.1 ~ 5.7.md: -------------------------------------------------------------------------------- 1 | # 5장 연관관계 매핑 기초 2 | 3 | 엔티티들은 대부분 다른 엔티티와 연관관계가 존재한다. 객체는 참조를 사용해 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 4 | 5 | 이 둘은 완전히 다른 특징을 갖기 때문에 객체 관계 매핑에서 가장 어려운 부분이 객체 연관관계와 테이블 연관관계를 매핑하는 일이다. 6 | 7 | 연관관계 매핑을 이해하기 위한 핵심 키워드는 `방향`, `다중성` 그리고 `연관관계의 주인`이다. 8 | 9 | ## 단방향 연관관계 10 | 11 | #### 객체 연관관계 12 | - 객체는 단방향 관계다. 13 | - 객체의 필드로 다른 객체와 연관관계를 맺는다. 14 | 15 | #### 테이블 연관관계 16 | - 양방향 관계이다. 17 | - 테이블은 외래 키로 다른 테이블과 연관관계를 맺는다. 18 | - 두 테이블의 외래 키를 통해서 서로 조인할 수 있다. 19 | 20 | #### 객체 연관관계와 테이블 연관관계의 가장 큰 차이 21 | 참조를 통한 연관관계는 언제나 단방향이다. 22 | 객체간에 연관관계를 양뱡항으로 만들기 위해서는 반대쪽에도 필드를 추가해 참조를 보관해야 한다. 23 | 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 하지만 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개인 것이다. 24 | 반면에 테이블은 외래 키 하나로 양방향으로 조인할 수 있다. 25 | 26 | ```java 27 | // 단방향 연관관계 28 | class A{ 29 | B b; 30 | } 31 | class B{} 32 | 33 | //양방향 연관관계 34 | class A{ 35 | B b; 36 | } 37 | 38 | class B{ 39 | A a; 40 | } 41 | ``` 42 | 43 | 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 `객체 그래프 탐색`이라 한다. 44 | 45 | #### 객체 관계 매핑 46 | 47 | ```java 48 | // 연관관계 매핑 49 | @ManyToOne 50 | @JoinColumn(name="TEAM_ID") 51 | private Team team; 52 | ``` 53 | 54 | - @ManyToOne 55 | - 다대일(N:1) 관계라는 매핑 정보 56 | - 연관관계를 매핑할 때 다중성을 나타내는 어노테이션은 필수로 사용해야한다. 57 | - 다대일과 비슷한 일대일(@OneToOne) 관계도 존재한다. 58 | 단방향 관계를 매핑할 때 둘 중 어떤 것을 사용해야 할지는 반대편 관계에 달려있다. 59 | 반대편이 일대다 관계면 다대일을 사용하고 반대편이 일대일 관계면 일대일을 사용하면 된다. 60 | 61 | - @JoinColumn(name="TEAM_ID") 62 | - 조인 칼럼은 외래 키를 매핑할 때 사용 63 | - name 속성에 매핑할 외래 키 이름을 지정 64 | - 해당 어노테이션을 생략하는 경우 외래 키를 찾을 때 기본 전략을 사용 65 | (기본 전략: 필드명 + _ + 참조하는 테이블의 칼럼명) 66 | 67 | ## 연관관계 사용 68 | 69 | #### 저장 70 | 71 | JPA 에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다. 72 | 73 | ```java 74 | member1.setTeam(team1); // 연관관계 설정 member1 -> team1 75 | em.persist(member1); //저장 76 | ``` 77 | 78 | JPA 는 참조한 팀의 식별자를 외래 키로 사용해서 적절한 등록 쿼리를 생성하게 된다. 79 | 80 | #### 조회 81 | 82 | 연관관계가 있는 엔티티를 조회하는 방법은 객체 그래프 탐색, 객체지향 쿼리 사용(JPQL)로 크게 2가지다. 83 | 84 | ```java 85 | Team team = member.getTeam(); //객체 그래프 탐색 86 | ``` 87 | 88 | SQL 은 연관된 테이블을 조인해서 검색조건을 사용하면 된다. JPQL 역시 문법은 약간 다르지만 조인을 지원한다. 89 | 90 | ```java 91 | String jpql = "select m from Member m join m.team t where t.name=:teamName"; 92 | ``` 93 | 94 | JPQL 의 `from Member m join m.team t` 부분을 보면 회원이 팀과 관계를 가지고 있는 `m.team`을 통해서 `Member`와 `Team`을 조인한다. 95 | 96 | 실행된 SQL 과 JPQL 을 비교하면 JPQL 은 객체를 대상으로 하고 SQL 보다 간결하다. 97 | 98 | #### 수정 99 | 100 | 수정은 em.update() 같은 메소드가 없기 때문에 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 101 | 102 | 연관관계를 수정할 때도 참조하는 대상을 변경하면 나머지는 JPA 가 자동으로 처리한다. 103 | 104 | #### 삭제 105 | 106 | - 연관관계 삭제 107 | 108 | 연관관계를 null 로 설정하며 기존 연관관계를 제거할 수 있다. 109 | 110 | ```java 111 | member1.setTeam(null); //연관관계 제거 112 | ``` 113 | 114 | - 연관된 엔티티 삭제 115 | 116 | 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생한다. 117 | 118 | ```java 119 | member1.setTeam(null); //연관관계 제거 120 | member2.setTeam(null); //연관관계 제거 121 | em.remvoe(team); 122 | ``` 123 | 124 | (참고) 외래 키 제약조건 125 | 126 | 외래 키 제약조건은 두 테이블 사이의 관계를 선언함으로써 데이터의 무결성을 보장해 주는 역할을 한다. 127 | 128 | 외래키 관계를 설정하면 하나의 테이블이 다른 테이블에 의존하게 된다. 129 | 130 | 외래 키 테이블이 참조하는 테이블이 열은 반드시 primary key 이거나 unique 제약조건이 설정되어 있어야한다. 131 | 132 | ## 양방향 연관관계 133 | 134 | 회원과 팀이 있을때, 회원과 팀은 다대일 관계하고 하자, 그럼 반대로 팀에서 회원은 일대다 관계다. 135 | 136 | 일대다 관계는 여러건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야한다. 팀은 Team.members 를 List 컬렉션으로 추가했다. 137 | 138 | JPA 는 List, Set, Map 과 같은 다양한 컬렉션을 지원하지만 데이터베이스 테이블은 외래 키 하나로 양방향 조회가 가능하기 때문에 처음부터 양방향 관계이다. 139 | 140 | ```java 141 | @OneToMany(mappedBt = "team") 142 | private List members = new ArrayList<>(); 143 | ``` 144 | 145 | mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 146 | 147 | ## 연관관계의 주인 148 | 149 | 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하지만 양방향으로 매핑하면 `회원 -> 팀` , `팀 -> 화원` 처럼 두곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트가 2곳으로 늘어나게 되고 객체의 참조는 둘인데 외래 키는 하나이기 때문에 둘 사이에 차이가 발생한다. 150 | 이러한 차이로 인해 JPA 에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야하는데 이것을 연관관계의 주인이라고 한다. 151 | 152 | 양방향 연관관계 매핑시 두 연관관계 중 하나를 주인으로 정해야하고, 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 할 수 있다. 153 | 주인이 아닌쪽은 읽기만 가능하다. 154 | 155 | - 주인은 mappedBy 속성을 사용하지 않는다. 156 | - 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야한다. 157 | 158 | 연관관계의 주인을 정한다는 것은 외래 키 관리자를 선택하는 것이라고 생각하면 된다. 159 | 책의 예시에서 회원테이블에 있는 `TEAM_ID` 외래 키를 관리할 관리자로 팀 엔티티에 있는 `Team.members` 를 선택하게 된다면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. 160 | `Team.members`는 `TEAM` 엔티티에 존재하고 해당 엔티티는 `TEAM` 테이블에 매핑되어 있는데 외래 키는 다른 테이블에 존재하기 때문이다. 161 | 162 | 따라서 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 예시에서는 회원 테이블이 외래 키를 갖고 있기 떄문에 `Memebet.team`이 주인이 된다. 163 | 164 | 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 `@ManyToOne`은 항상 연관관계의 주인이 되기 때문에 mappedBy 속성을 설정할 수 없고, 해당 속성이 존재하지 않는다. 165 | 166 | ## 양뱡향 연관관계 저장 167 | 168 | 양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다. 169 | 170 | 따라서 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않고 데이터베이스에 저장할 때 무시된다. 171 | 172 | ## 양방향 연관관계의 주의점 173 | 174 | 양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌곳에 값을 입력하는 경우이다. 175 | 176 | 연관관계의 주인만이 외래 키를 관리할 수 있기 때문에 값이 정상적으로 저장되지 않는다면 가장 우선적으로 의심해봐야한다. 177 | 178 | 주인에만 값을 저장하고 아닌곳에는 저장을 하지 않아도 되지만 사실 객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전한 방법이다. 179 | 180 | 만일 JPA 를 사용하지 않는 순수한 객체 상태에서는 양쪽 모두 값이 입력되지 않은 경우 문제가 발생할 수 있기 때문이다. 181 | 182 | 항상 양쪽에 값을 저장해 주기 위해는 각각 메소드를 호출해야하는데 이런 경우 실수를 방지하기 위해 `연관관계 편의 메소드`를 만들어 사용한다. 183 | 184 | ```java 185 | public class Member{ 186 | private Team team; 187 | 188 | //연관관계 편의 메소드 189 | public void setTeam(Team team){ 190 | 191 | //기존 팀과 관계를 제거 192 | if(this.team != null){ 193 | this.team.getMembers().remove(this); 194 | } 195 | this.team = team; 196 | team.getMembers().add(this); 197 | } 198 | ... 199 | } 200 | ``` 201 | 202 | 위의 연관관계 편의 메소드를 통해 한번에 양방향 관계를 설정할 수 있고 `if(this.team != null)` 해당 조건을 통해 연관관계가 변경될 때 기존에 설정되어 있던 연관관계를 삭제해 줄 수 있다. 203 | 204 | 기존에 설정되어 있는 연관관계를 제거하지 않아도 외래 키를 변경하는데에는 문제가 없다. 연관관계의 주인의 참조를 변경했기 때문에 주인이 아닌쪽도 정상적으로 반영되기 때문이다. 205 | 206 | 이후 새로운 영속성 컨텍스트에서 관계를 조회하더라도 이전 관계는 끊어져 있으므로 조회되지 않는다. 207 | 208 | 하지만 관계를 변경한 후 영속성 컨텍스트가 살아있는 상태에서 주인이 아닌쪽에서 관계를 호출하게 되면 변경 전의 관계가 반환되게 된다. 따라서 `if(this.team != null)` 조건을 통해 항상 이전 관계를 제거하는 로직이 필요하다. 209 | 210 | ## 정리 211 | 212 | 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이라는 점이며 양방향은 여기에 주인이 아닌 연관관계를 하나 추가했을 뿐이다. 213 | 214 | 결국 단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다. 215 | 216 | - 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다. 217 | - 단방향을 양방향으로 만들면 반대 방향으로 객체 그래프 탐색 기능이 추가된다. 218 | - 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다. 219 | 220 | 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다. -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/6장 다양한 연관관계 매핑/6.1~6.5.md: -------------------------------------------------------------------------------- 1 | # 6장 다양한 연관관계 매핑 2 | 3 | 엔티티의 연관관계를 매핑할 때에는 다중성, 단방향 및 양방향, 연관관계의 주인 세 가지를 고려해야한다. 4 | 5 | **다중성**은 다대일, 일대다, 일대일, 다대다의 연관관계가 존재하고 보통 다대다는 실무에서 사용하지 않는다. 6 | 7 | 외래 키 하나로 두 테이블이 연관관계를 맺을때 연관관계를 관리하는 포인트는 외래 키 하나다. 반면, 객체 관계에서는 참조용 필드로 연관된 객체를 조회할 수 있으므로 한쪽만 참조하는 **단방향**, 양쪽이 서로 참조하는 **양방향** 관계가 존재한다. 8 | 9 | A->B, B->A 두 곳에서 서로 참조할 때 연관관계를 관리하는 포인트는 두 가지가 되는 것이다. 이 때, JPA는 두 가지 중 하나를 정해서 외래 키를 관리하는데, 이를 **연관관계의 주인**이라고 한다. 주인이 아닌 방향은 외래 키 변경이 불가능하고, 읽기만 가능하다. 또한 주인이 아닌 경우 `mappedBy`로 주인 필드 이름을 값으로 입력해야한다. 10 | 11 | 다중성, 단방향, 양방향을 고려한 모든 연관관계를 *왼쪽을 연관관계의 주인*으로 정하여 살펴본다. 12 | 13 | ## 다대일과 일대다 14 | - 다대일 관계 <-> 일대다 관계 15 | - 일(1)과 다(N) 관계에서 외래 키는 항상 다쪽에 있다. 16 | - 하나의 팀에 소속된 여러 명의 멤버 간의 관계를 살펴본다. 17 | 외래 키(FK, TEAM_ID)는 항상 멤버에게 있으며, 실선이 연관관계의 주인이다. 18 | 19 | 20 | ### 다대일 단방향 [**N**:1] 21 | ![image](https://user-images.githubusercontent.com/59992230/123551153-91c63e80-d7ab-11eb-8979-096164a6da4b.png) 22 | 23 | - 멤버에서만 팀을 조회할 수 있다. 24 | - 그림에서 보이는 **[연관관계 매핑]** 이 설정된 객체가 연관관계의 주인이다. 25 | 26 | ### 다대일 양방향 [**N**:1, 1:N] 27 | ![image](https://user-images.githubusercontent.com/59992230/123551160-97bc1f80-d7ab-11eb-84ca-ff986dd07b8f.png) 28 | 29 | - 팀에서도 멤버를 조회할 수 있으며, 외래 키와 관련된 작업은 읽기전용이다. 30 | 31 | - 양방향 연관관계는 항상 서로를 참조해야한다. 32 | -> 항상 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋다. 이 때, 무한 루프에 빠지지 않도록 주의해야한다. 33 | 34 | ### 일대다 단방향 [**1**:N] 35 | ![image](https://user-images.githubusercontent.com/59992230/123551167-9be83d00-d7ab-11eb-8df2-25e3bc3297c0.png) 36 | 37 | - 연관관계의 주인인 팀에서만 멤버를 조회할 수 있다. 38 | - 반드시 `@JoinColumn`을 명시해야한다. 39 | - 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것을 권장한다. 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있기 때문이다. 이로 인해 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야하는 등의 성능 문제가 발생할 수 있다. 40 | 41 | ### 일대다 양방향 [1:N, N:1] 42 | ![image](https://user-images.githubusercontent.com/59992230/123551180-a9052c00-d7ab-11eb-8817-0313cf32bf87.png) 43 | 44 | - `@OneToMany`는 연관관계의 주인이 될 수 없고, `@ManyToOne`에는 `mappedBy` 속성이 없다. 다 쪽에 외래 키가 존재해야하기 때문이다. 45 | 46 | - 이를 구현하려면 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다. 다만, 일대다 단방향에서의 단점을 그대로 가지므로 가능하면 다대일 양방향을 사용하는 것이 좋다. 47 | 48 | 49 | ## 일대일 [1:1] 50 | - 일대일 <-> 일대일 51 | - 주 테이블이나 대상 테이블 둘 중 어느곳이나 외래키를 가질 수 있다. 52 | 주 테이블이 외래 키를 가질 경우, 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지를 알 수 있다. 53 | 대상 테이블이 가질 경우, 테이블 관계를 1:1 -> 1:N 으로 변경할 때 테이블 구조를 그대로 유지할 수 있다. 54 | 55 | - 주 테이블인 Member에 외래 키가 있는 경우(LOCKER_ID)와 대상 테이블 Locker에 외래 키가 있는 경우 (MEMBER_ID)를 살펴본다. 56 | 57 | 58 | ### 주 테이블에 외래 키 단방향 59 | ![image](https://user-images.githubusercontent.com/59992230/123551737-0601e180-d7ae-11eb-9a82-adc89927521f.png) 60 | 61 | - `@OneToOne`을 사용한다는 점 외에는 다대일 단방향과 거의 비슷하다. 62 | 63 | ### 주 테이블에 외래 키 양방향 64 | ![image](https://user-images.githubusercontent.com/59992230/123551741-09956880-d7ae-11eb-80d7-7556b364cc6c.png) 65 | 66 | - Member가 외래키를 지니고 있으므로 Member.locker가 연관관계의 주인이다. 67 | 68 | ### 대상 테이블에 외래 키 단방향 69 | ![image](https://user-images.githubusercontent.com/59992230/123551745-0e5a1c80-d7ae-11eb-9421-c41241f28d10.png) 70 | 71 | - 대상 테이블에 외래 키가 있는 단방향 관계는 지원하지 않는다. (일대다는 JPA 2.0부터 지원) 72 | - 단방향 관계를 Member <- Locker로 수정(Locker가 주 테이블)하거나, 양방향으로 만들어 Locker를 연관관계의 주인으로 설정해야한다. 73 | 74 | ### 대상 테이블에 외래 키 양방향 75 | ![image](https://user-images.githubusercontent.com/59992230/123551748-10bc7680-d7ae-11eb-9128-b3d535c5bb7c.png) 76 | 77 | - Locker를 연관관계의 주인으로 만들어 외래키를 관리한다. 78 | 79 | ## 다대다[N:N] 80 | 81 | - 회원들이 상품들을 주문하는 상황을 고려한다. 82 | 83 | 관계형 데이터베이스는 테이블 2개로는 표현 불가능하므로 회원-상품 테이블을 추가하여 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다. 84 | 85 | ![image](https://user-images.githubusercontent.com/59992230/123582878-8f9ac900-d819-11eb-81cf-aa835d4d1771.png) 86 | 87 | 반면 객체로는 두 가지로 다대다 관계를 만들 수 있다. 서로 컬렉션을 사용하여 참조하면 된다. 88 | 89 | ![image](https://user-images.githubusercontent.com/59992230/123582922-a4775c80-d819-11eb-9012-43ca44818f7e.png) 90 | 91 | 92 | - `@ManyToMany`를 활용하면 연결 테이블을 자동 처리할 수 있다. 다만, 위의 상황에서 연결 테이블에 주문 수량이나 주문한 날짜 같은 컬럼을 추가해야하는 경우가 생긴다면 더이상 매핑할 수 없어 사용할 수 없다. 결국 회원상품 연결 엔티티를 추가하여 일대다, 다대일 관계로 풀어야한다. 93 | 94 | 다대다를 풀어내기 위한 연결 테이블을 만들 때 부모 테이블의 기본키였던 `MEMBER_ID`와 `PRODUCT_ID`를 받아 이를 기본 키 + 외래 키로 사용하게 되는 것을 **식별 관계**라고 한다. 95 | 이러한 두 가지의 기본 키를 받아 복합 기본 키를 사용할 때엔 몇 가지 지켜야하는 사항이 있다. 96 | 97 | - 복합 키는 별도의 식별자 클래스로 만들어야 한다. 98 | - Serializable을 구현해야 한다. 99 | - equals와 hashCode를 구현해야 한다. 100 | - 기본 생성자가 있어야 한다. 101 | - 식별자 클래스는 public이어야한다. 102 | 103 | 104 | 부모로부터 받아온 식별자를 외래 키로만 사용하고 새로운 식별자를 추가해서 구성(예: `ORDER_ID`)하는 관계를 **비식별 관계**라고 한다. 이러한 방법은 복합 키를 위한 식별자 클래스를 만들지 않아도 된다는 장점이 있다. 105 | 106 | ![image](https://user-images.githubusercontent.com/59992230/123623855-e23fa980-d848-11eb-8698-45c440ca6e18.png) 107 | 108 | -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/7장 고급 매핑/7.1~7.6.md: -------------------------------------------------------------------------------- 1 | # 7장 고급 매핑 2 | 3 | ## 상속 관계 매핑 4 | 5 | 객체의 상속 개념은 데이터베이스의 슈퍼타입 서브타입 관계라는 모델링 기법이 가장 유사하다. 이러한 논리적인 모델을 구현하는 방법으로는 세 가지가 있다. 6 | 7 | ### 조인 전략(Joined Strategy) 8 | 9 | - `@InheritanceType.JOINED` 10 | - 각각을 모두 테이블로 만들고, 부모 테이블의 기본 키를 자식 테이블이 기본 키 + 외래 키로 사용하여 조회할 때 조인을 사용한다. 단, 타입을 구분하는 컬럼이 필요하다. 11 | 12 | - 테이블이 정규화 되고, 외래 키 참조 무결성 제약조건을 활용할 수 있으며, 저장공간을 효율적으로 사용하는 장점이 있다. 다만, **조회할 때 조인이 많이 사용**되므로 성능이 저하될 수 있고 조회 쿼리가 복잡하며, 데이터를 등록할 때 INSERT SQL을 두 번 실행해야한다는 단점이 있다. 13 | 14 | ### 단일 테이블 전략 15 | 16 | - `@InheritanceType.SINGLE_TABLE` 17 | - 이름 그대로 테이블을 하나만 사용하고, 구분 컬럼(`@DiscriminatorColumn`)을 반드시 사용하여 어떤 자식 데이터가 저장되었는지 구분한다. 18 | 19 | - 조인이 필요 없어 조회 성능이 빠르고 조회 쿼리가 단순해지나, 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 하고, 테이블이 커져서 조회 성능이 오히려 느려지는 상황이 생길 수 있다. 20 | 21 | ### 구현 클래스마다 테이블 전략 22 | 23 | - `@InheritanceType.TABLE_PER_CLASS` 24 | - 서브 타입마다 하나의 테이블을 만든다. 조인 전략과 달라진 점은 부모 테이블의 공통적인 부분을 자식 테이블이 모두 포함하도록 만든다는 점이다. 다른 두 가지와 달리 구분 컬럼을 사용하지 않는다. 25 | 26 | - 서브 타입을 구분해서 처리할 때 효과적이고, not null을 사용할 수 있다. 다만 여러 자식 테이블을 함께 조회할 때 성능이 느려지고 (UNION 사용), 통합해서 쿼리하기 어렵다. 27 | 28 | 29 | ## `@MappedSuperclass` 30 | 31 | - 부모 클래스는 실제 테이블과 매핑하지 않고 상속 받아 여러 엔티티에서 공통으로 사용하는 매핑 정보(예: 등록일, 수정일)만 상속받고 싶을 때 사용한다. 32 | 33 | - 추상 클래스와 비슷한 역할로, `@Entity`가 상속할 수 있는 수단이지만 진정한 상속 매핑은 앞서 이야기한 데이터베이스의 슈퍼타입 서브타입 관계와 매핑하는 것이다. 34 | 35 | ## 복합 키와 식별 관계 매핑 36 | 37 | - **식별 관계**(Identifying Relationship)란 부모 테이블의 기본 키를 내려받아 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다. 38 | 39 | - **비식별 관계**(Non-Identifying Relationship)란 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다. 그중에서도 외래 키에 NULL을 허용하지 않는 **필수적 비식별 관계(Mandatory)** 와 허용하는 **선택적 비식별 관계(Optional)** 로 나뉜다. 최근에는 비식별을 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다. 40 | 41 | 42 | ### 비식별 관계의 복합 키 43 | 44 | - JPA는 영속성 컨텍스트에 엔티티를 보관할 때 식별자를 키로 사용하고, 이를 구분하기 위해 `equals`와 `hashCode`를 사용해서 동등성을 비교한다. 식별자 필드가 2개 이상이면 식별자 클래스를 만들고 그곳에 `equals`와 `hashCode`를 구현해야한다. 45 | 46 | - 기본 키가 2개 이상인 부모 클래스로부터 비식별 관계로 자식이 전달받는 상황을 가정한다. 47 | 48 | - `@IdClass` 49 | - RDB에 가까운 방법 50 | - 식별자 클래스(Id class)의 속성명과 엔티티(부모 클래스)에서 사용하는 식별자의 속성명이 같아야한다. 51 | - 엔티티를 저장할 땐 식별자 클래스를 사용하지 않아도 되지만(`.setId()`로 식별자를 직접 등록), 실제 `em.persist()` 호출시 영속성 컨텍스트에서 엔티티 등록 전에 내부에서 식별자 클래스를 생성하고 키로 사용한다. 52 | - 조회시 식별자 클래스를 사용한다. 53 | - 자식 테이블에서는 외래 키 매핑시 여러 컬럼을 매핑해야하므로 `@JoinColumns` 를 사용하고, 각각을 `@JoinColumn`으로 매핑한다. 54 | 55 | 56 | - `@EmbeddedId` 57 | - 객체지향에 가까운 방법 58 | - 식별자 클래스와 엔티티에 동일한 속성명을 지닌 id1, id2로 매핑하던 `@IdClass`와는 달리 `@EmbeddedId` 어노테이션을 붙인 엔티티에는 식별자 클래스를 참조하고, 식별자 클래스에는 `@Embeddable` 어노테이션을 사용하여 기본 키를 직접 매핑한다. 59 | 60 | - 저장 및 조회시에 식별자 클래스를 직접 생성해서 사용한다. 61 | 62 | - 특정 상황에 JPQL이 조금 더 길어질 수 있다. 63 | 64 | - 공통적으로 만족해야하는 특징 65 | - Serializable 구현 66 | - equals, hashCode 구현 67 | - 기본 생성자 68 | - 식별자 클래스는 public 69 | - 복합키는 `@GenerateValue`를 사용할 수 없다. 70 | 71 | ### `equals()`와 `hashCode()` 72 | 73 | - 순수 자바코드에서는 일반적으로 `eqauls()`를 오버라이딩하지 않으면 기본으로 Object를 상속받아 기본 `equals()`는 인스턴스 참조 값 비교인 == 비교 (동일성 비교)를 한다. 74 | 75 | - 영속성 컨텍스트는 엔티티의 식별자를 키로 사용하여 엔티티를 관리하고, 이 때 식별자 비교시에 `equals`와 `hashCode`를 사용한다. 고로 객체의 동등성(`equals` 비교)이 지켜지지 않으면 엔티티 관리에 문제가 발생하기 때문에 복합키는 이를 필수로 구현해야한다. 76 | 77 | 78 | ### 식별 관계에서 비식별관계로 79 | 80 | ![image](https://user-images.githubusercontent.com/59992230/123735026-65a8db80-d8d9-11eb-83f1-e2cf97e4d1e1.png) 81 | 82 | - 다음의 예시는 부모, 자식, 손자까지 계속 기본 키를 전달하는 식별 관계다. 83 | 84 | - `@IdClass`와 `@EmbeddedId`를 사용하여 식별자를 매핑해야하는데, `@EmbeddedId` 사용시 `@MapsId`를 사용해야한다. 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다. 85 | ```java 86 | @Entity 87 | public class Child { 88 | ... 89 | 90 | @MapsId("parentId") 91 | @ManyToOne 92 | @JoinColumn(name="PARENT_ID") 93 | public Parent parent; 94 | } 95 | 96 | ... 97 | @Embeddable 98 | public class ChildId implements Seralizable { 99 | private String parentId; //@MapsId("parentId")와 매핑 100 | ... 101 | } 102 | ``` 103 | 104 | - 이를 비식별 관계로 변경하면 다음과 같이 변경된다. 105 | 106 | ![image](https://user-images.githubusercontent.com/59992230/123735193-ba4c5680-d8d9-11eb-8838-74127e03bad4.png) 107 | 108 | 복합 키를 사용하지 않아도 되어 복합 키 클래스를 별도로 만들 필요도 없고, 코드도 단순해진다. 109 | 110 | 111 | ### 일대일 식별 관계 112 | 113 | - 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용하는 관계이다. 앞선 부모-자식의 예시보다는 부모로부터 뻗어나온 세부 정보들을 예시로 들기 쉽다. (예: 게시판 - 게시판 상세정보 테이블 관계에서 게시판 상세정보의 기본 키는 게시판의 기본 키값만을 참조하여 가진다.) 114 | 115 | - 자식은 `@MapsId`를 사용하여 부모 테이블을 참조한다. 116 | 117 | ### 어떤 식별 관계를 선택해야할까 118 | 119 | - 데이터베이스의 설계 관점 (비식별 선호) 120 | - 식별 관계는 자식 테이블의 기본 키 컬럼이 점점 늘어나고, 복합 기본 키를 만들어야 하는 경우가 많다. 121 | - 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많기 때문에 자식에 손자까지 전파되어있으면 변경하기 어렵다. 122 | 123 | - 객체지향 매핑의 관점 (비식별 선호) 124 | - 일대일을 제외하곤 2개 이상의 복합 기본 키를 사용하는데, JPA에서는 별도 클래스를 만들어서 사용해야하는 번거로움이 있다. 125 | - 비식별의 기본 키는 주로 대리키를 사용하는데, JPA에서는 `@GenerateValue`로 편리하게 사용할 수 있다. 126 | 127 | - 식별 관계는 기본 키 인덱스를 활용하기 좋고, 특정 상황에 조인 없이 하위 테이블만으로도 검색할 수 있는 장점이 있으니 필요한 곳에 적절하게 사용하는 것이 좋다. 128 | 129 | - 권장하는 방법은 비식별 관계를 사용하고, 기본 키는 비즈니스 의미와는 관련 없는 Long 타입의 대리 키를 사용하는 것이다. 또한 필수적 관계로 설정하여 NOT NULL로 항상 관계가 있다는 것을 보장해 내부 조인만 사용하게 만드는 것이 좋다. 130 | 131 | 132 | 133 | ## 조인 테이블 134 | 135 | - 데이터베이스의 연관관계를 설계하는 방법은 외래 키와 같은 조인 컬럼을 사용하는 방법과 조인 테이블을 사용하는 방법이 있다. 136 | 137 | - 조인 컬럼을 사용하는 선택적 비식별 관계를 살펴보며 이를 조인 테이블로 바꾸는 방법에 대해 알아본다. 138 | 139 | ![image](https://user-images.githubusercontent.com/59992230/123737961-e3231a80-d8de-11eb-8e70-66dd44c3a0b5.png) 140 | 141 | 다음의 그림은 회원과 사물함과의 관계를 설명하고 있다. 회원은 원할 때 사물함을 등록할 수 있는 상황이라면, 등록하기 전까지는 사물함 번호를 NULL 값으로 두어야하고 OUTER JOIN을 사용해야한다는 단점이 있다. 142 | 143 | ![image](https://user-images.githubusercontent.com/59992230/123738083-1c5b8a80-d8df-11eb-9bf2-fd7a12e78c2d.png) 144 | 145 | 이를 조인테이블을 활용하여 관리하게 될 경우 MEMBER와 LOCKER 테이블에는 각각 외래 키 컬럼이 존재하지 않고, 회원이 등록할 때마다 조인 테이블에 값을 추가해주면 되어서 편리하다. 반면, 테이블이 하나 늘어난다는 점과 조인을 할 땐 세 테이블을 해주어야한다는 단점이 있다. 146 | 147 | 148 | - 조인 컬럼은 `@JoinColumn`으로 매핑하고 조인 테이블은 `@JoinTable`로 매핑한다. 149 | 150 | 151 | ## 엔티티 하나에 여러 테이블 매핑 152 | 153 | - `@SecondaryTable`을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다. 세 개 이상의 테이블 매핑시 `@SecondaryTables`로 감싸서 선언해준다. 154 | 155 | - 이러한 방법은 하나의 엔티티를 조회할 때 여러 테이블을 조회해야하기 때문에 최적화하기 어렵기 때문에 가급적 사용하지 않는 편이 좋다. -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/8장 프록시와 연관관계 관리/8.1 ~ 8.7.md: -------------------------------------------------------------------------------- 1 | # 8장 프록시와 연관관계 관리 2 | 3 | 핵심 키워드 4 | 1. 프록시 5 | 1. 즉시로딩, 지연로딩 6 | 1. 영속성 전이와 고아 객체 7 | 8 | ## 프록시 9 | 10 | ### 프록시란? 11 | - 간단히 정의해서 실제 엔티티를 상속받는 가짜 객체를 의미한다. 12 | 13 | ### 프록시를 사용하는 이유 14 | 엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. 15 | 사용하지 않는 엔티티까지 데이터베이스에서 함께 조회해 두는 것은 효율적이지 않은데 이런 문제를 해결하기 위해 JPA에는 **지연 로딩**이라는 방법이있다. 16 | 지연 로딩이라는 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 **가짜 객체(프록시 객체)** 가 필요하다. 17 | 18 | ### 프록시 기초 19 | JPA에서 식별자로 엔티티 하나를 조회할 때 EntityManager.find()를 사용한다. 이 메소드는 영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회한다. 20 | 21 | ```Member member = em.find(Member.class, "member1");``` 22 | 23 | 이렇게 직접 조회하면 엔티티를 사용하던 사용하지 않던 데이터베이스를 조회한다. 이 엔티티를 실제로 사용하는 시점까지 조회를 미루고 싶다면 EntityManager.getReference()를 사용하면 된다. 24 | 25 | ```Member member = em.getReference(Member.class, "member1");``` 26 | 27 | 이 메소드는 엔티티를 실제로 사용하기 전까지는 데이터베이스를 조회하지도 실제 엔티티 객체를 생성하지도 않는다. 28 | 대신 위에서 설명한 프록시 객체를 반환한다. 29 | 30 | #### 프록시 특징 1 31 | - 프록시 객체는 실제 엔티티를 상속 받아서 만들어지므로 겉 모양이 같다. 32 | - 사용하는 입장에서는 이것이 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. 33 | - 프록시 객체는 실제 객체에 대한 참조(target)를 보관한다. 34 | - 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다. 35 | 36 | #### 프록시 객체의 초기화 37 | 38 | ![image](https://user-images.githubusercontent.com/40006670/124211066-7b134500-db27-11eb-82c5-f1c8e5993cde.png) 39 | 1. 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회한다. 40 | 2. 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는데 이것을 초기화라고 한다. 41 | 3. 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성한다. 42 | 4. 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버변수에 조회한다. 43 | 5. 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환한다. 44 | 45 | #### 프록시 특징 2 46 | - 프록시 객체는 처음 사용할 때 한 번만 초기화된다. 47 | - 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다. 48 | 초기화되면 프록시 객체를 통해 실제 엔티티에 접근 가능하다. 49 | - 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시에 주의해야한다. 50 | 51 | ex) == 는 사용하면 안되고 instanceof를 사용해야 한다. 52 | ``` 53 | Member m1 = em.find(Member.class, member1.getId()); 54 | Member m2 = em.getReference(Member.class, member2.getId()); 55 | 56 | System.out.println("m1 = " + (m1 instanceof Member)); 57 | System.out.println("m2 = " + (m2 instanceof Member)); 58 | System.out.println("m1 == m2 : " + (m1 == m2)); 59 | ``` 60 | 61 | m1 = true 62 | m2 = true 63 | m1 == m2 : false 64 | 65 | - 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 프록시 초기화를 해도 실제 엔티티가 반환된다. 66 | ex) 67 | ``` 68 | Member findMember = em.find(Member.class, member1.getId()); 69 | Member refMember = em.getReference(Member.class, member1.getId()); 70 | 71 | System.out.println("findMember.getClass() = " + findMember.getClass()); 72 | System.out.println("refMember.getClass() = " + refMember.getClass()); 73 | ``` 74 | 75 | findMember.getClass() = class hellojpa.Member 76 | refMember.getClass() = class hellojpa.Member 77 | 78 | ※ 이유 79 | JPA는 한 트랜잭션 안에서 같은 것에 대해 보장해준다. 80 | 따라서 같은 엔티티 즉, 한 영속성 컨텍스트에서 가져온 것, PK가 똑같으면 JPA는 항상 true를 반환한다. 81 | 이것은 JPA가 기본적으로 제공하는 메커니즘이다. 82 | 83 | 반대로 해도 find와 ref는 같은 값을 가진다. 84 | 85 | refMember.getClass() = class hellojpa.Member$HibernateProxy$gZOWv4aq 86 | findMember.getClass() = class hellojpa.Member$HibernateProxy$gZOWv4aq 87 | 88 | 89 | - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제 발생 90 | ex) 하이버네이트는 org.hibernate.LazyInitializationException 예외 발생 91 | 코드 92 | ``` 93 | Member member1 = new Member(); 94 | member1.setUsername("member1"); 95 | em.persist(member1); 96 | 97 | em.flush(); 98 | em.clear(); 99 | 100 | Member refMember = em.getReference(Member.class, member1.getId()); 101 | System.out.println("refMember.getClass() = " + refMember.getClass()); 102 | 103 | /* 준영속상태로 만들기 위함 */ 104 | em.detach(refMember); 105 | em.clear(); 106 | 107 | em.close(); // 하이버네이트 5.4.1.Final 이후로 예외 발생하지 않음. 108 | 109 | refMember.getUsername(); 110 | ``` 111 | [김영한님 답변](https://www.inflearn.com/questions/53733) 112 | 113 | 114 | ### 프록시 확인 115 | - 프록시 인스턴스의 초기화 여부 확인 116 | `emf.getPersistenceUnitUtil().isLoaded(refMember)` 117 | - 프록시 클래스 확인 방법 118 | `refMember.getClass()` 119 | - 프록시 강제 초기화 120 | `Hibernate.initialize(refMember);` 121 | 122 | ## 지연로딩과 즉시로딩 123 | - 즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다. 124 | `@ManyToOne(fetch = FetchType.EAGER)` 125 | - 지연 로딩 : 연관된 엔티티를 **실제 사용할 때** 조회한다. 126 | `@ManyToOne(fetch = FetchType.LAZY)` 127 | 128 | #### NULL 제약조건과 JPA 조인 전략 129 | JPA는 NULL값에 의한 데이터 손실을 방지하기 위해 외부 조인을 사용한다. 130 | 하지만 외부 조인보다 내부 조인이 성능과 최적화에서 더 유리하다. 131 | 따라서 내부 조인을 사용하려면 외래 키에 NOT NULL 제약 조건을 설정해서 값이 있는 것을 보장해 주면 된다. 132 | `@JoinColumn(name = "TEAM_ID", nullable = false)` 을 설정해 주면 내부 조인을 사용한다. 133 | 134 | 정리하자면 JPA는 선택적 관계면 외부 조인을 사용하고 필수 관계면 내부 조인을 사용한다. 135 | 136 | ### 즉시로딩 사용할 때 주의 137 | - 가급적 지연 로딩만 사용 138 | - 컬렉션을 하나 이상 즉시 로딩하는 것을 권장하지 않는다. 139 | - 즉시 로딩은 JPQL에서 N + 1 문제를 일으킨다. 140 | - SQL : select * from member; 141 | - SQL : select * from team where TEAM_ID = X; 142 | -> member를 가져왔더니 team이 즉시로딩이 걸려있으므로 member 개수만큼 SQL을 보낸다. 143 | @ N + 1을 해결하는 방법 (뒤 챕터에서 설명) 144 | 1. fetch join 145 | 1. 어노테이션을 이용 146 | 1. 배치 사이즈 147 | - 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다. 148 | - @ManyToOne, @OneToOne은 기본이 즉시로딩으로 설정되어 있다. 지연 로딩인 LAZY로 변경해 주어야 함 149 | - @OneToMany, @ManyToMany는 기본이 지연 로딩 150 | 151 | ### 영속성 전이 : CASCADE 152 | - 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다. 153 | ex) 부모 엔티티를 저장할 때 자식 엔티티도 저장 154 | 155 | ### CASCADE - 주의 156 | - 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음. 157 | - 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다. 158 | - 다른 엔티티와 연관되어 있으면 쓰면 안된다. 159 | - 두가지 조건이 만족할 때 사용해도 된다. 160 | - 소유자가 하나일 때 161 | - parent와 child의 라이프 사이클이 똑같을 때 162 | 163 | ### CASCADE 종류 164 | 1. ALL : 모두 적용 165 | 2. PERSIST : 영속 (저장) 166 | 3. REMOVE : 삭제 167 | 4. MERGE : 병합 168 | 5. REFRESH 169 | 6. DETACH 170 | 171 | ## 고아 객체 172 | - 고아 객체 제거 : 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제 173 | 174 | ### 고아 객체 주의 175 | - 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능 176 | - 참조하는 곳이 하나일 때 사용해야 한다. 177 | - 특정 엔티티가 개인 소유할 때 사용 178 | - @OneToOne, @OneToMany만 가능 179 | - 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 180 | 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE 처럼 동작한다. 181 | 182 | ### 영속성 전이 + 고아 객체 183 | - CascadeType.ALL + orphanRemovel = true; 184 | - 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거 185 | - 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음 186 | - [도메인 주도 설계(DDD)의 Aggregate Root 개념](http://martinfowler.com/bliki/DDD_Aggregate.html) 을 구현할 때 유용하다. 187 | - 188 | -------------------------------------------------------------------------------- /자바 ORM 표준 JPA 프로그래밍/9장 값 타입/9.1 ~ 9.5.md: -------------------------------------------------------------------------------- 1 | - JPA의 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다. 2 | - 엔티티 타입은 `@Entity` 로 정의하는 객체이고, 값 타입은 `int` , `Integer` , `String` 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 의미한다. 3 | - 엔티티 타입은 식별자를 통해 지속적으로 추적 가능하지만 값 타입은 식별자가 없고 숫자나 문자 속성만 있기 때문에 추적이 불가능하다. 4 | 5 |
6 | 7 | # 9.1 기본값 타입 8 | 9 | - `String` , `int` 와 같이 엔티티 내부에서 사용되는 자바 기본타입을 기본 값타입이라고 한다. 10 | - 기본값 타입은 식별자도 없고 생명주기도 엔티티에 의존하기 때문에 엔티티 인스턴스를 제거하면 값타입의 값도 제거된다. 11 | - 값 타입은 공유하면 안되며 만약 이를 공유하게 될경우 값 타입의 변경 포인트가 여러곳에 발생하게되어 유지보수하기 어려워지고 코드를 복잡하게 만든다. 12 | 13 |
14 | 15 | # 9.2 임베디드 타입 : 복합 값 타입 16 | 17 | - JPA에서는 새로운 값 타입을 직접 정의해서 사용할 수 있는데 이것을 임베디드 타입이라고 한다. 18 | - 임베디드 타입도 `int` , `Stirng` 과 같은 기본값 타입과 마찬가지로 값 타입에 속한다. 19 | 20 |
21 | 22 | ## 임베디드 타입의 활용 23 | 24 | - 다음과 같이 근무 시작일과 종료일, 주소, 도시, 우편번호를 가지는 `Member` 엔티티가 있다. 25 | 26 |
27 | 28 | ```java 29 | @Entity 30 | public class Member { 31 | ... 32 | 33 | LocalDateTime startDate; 34 | LocalDateTime endDate; 35 | 36 | private String city; 37 | private String street; 38 | private String zipcode; 39 | } 40 | ``` 41 | 42 |
43 | 44 | - 위와 같은 코드는 단순히 정보룰 풀어둔 것으로 서로 아무 관련이 없는 정보들을 풀어두는 것 보다는 다음과 같이 서로 관련있는 정보들을 의미있는 단위로 묶어주는 것이 좋다. 45 | 46 | ```java 47 | @Entity 48 | public class Member { 49 | ... 50 | 51 | @Embedded Period workPeriod; 52 | @Embedded Address homeAddress; 53 | } 54 | ``` 55 | 56 | - 위와 같이 변경할 경우 객체지향적으로 엔티티를 좀 더 의미있고 응집력있게 변경할 수 있다. 57 | - 뿐만 아니라 새로 정의한 값 타입들은 재사용 가능하며 해당 값 타입만 사용 가능한 의미있는 메서드도 만들 수 있다. 58 | 59 |
60 | 61 | 임베디드 타입을 사용하기 위한 제약 조건은 다음과 같다. 62 | 63 | - `@Embeddable` 어노테이션을 값 타입을 정의하는 곳에 표시하고 `@Embedded` 어노테이션을 값 타입을 사용하는 곳에 표시한다. 64 | - 두 어노테이션 중 하나를 생략해도 괜찮지만 코드의 가독성을 위해 둘 다 표시하는 것을 권장한다. 65 | - 임베디드 타입은 기본 생성자를 필수로 가지고 있어야한다. 66 | 67 |
68 | 69 | 임베디드 타입을 포함한 모든 값 타입은 엔티티의 생명주기에 의존하기 때문에 엔티티와 임베디드 타입의 관계는 컴포지션 관계가 된다. 70 | 71 |
72 | 73 | ## 임베디드 타입의 특징 74 | 75 | - 결과적으로 임베디드 타입을 사용한 것과 일반 값 타입을 사용한 것의 테이블 매핑 결과는 동일하지만 임베디드 타입 덕분에 객체와 테이블을 좀 더 세밀하게 매핑하는 것이 가능해진다. 76 | - 임베디드 타입은 또 다른 값 타입을 포함하거나 엔티티를 참조하는 것이 가능하다. 77 | - `@AttributeOverride` 를 이용해서 임베디드 타입에 정의한 매핑 정보를 재정의하는 것이 가능하지만 이를 남용하면 코드가 지저분해진다. 78 | - 임베디드 타입이 `null` 인 경우 매핑한 컬럼값이 전부 `null` 이 된다. 79 | 80 |
81 | 82 | # 9.3 값 타입과 불변 객체 83 | 84 | - 값 타입은 복잡한 객체 세상을 조금이라도 단순화하기 위해 만든 개념이기 때문에 단순하고 안전하게 다룰 수 있어야 한다. 85 | 86 |
87 | 88 | ## 값 타입 공유 참조 89 | 90 | - 임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유해서 사용하면 위험하다. 91 | - 값 타입을 공유하고있는 여러 엔티티에서 동시에 값 타입을 수정하거나 또는 다른 엔티티에 의해서 참조하고 있는 값 타입이 수정되는 경우와 같이 의도하지 않는 변경으로 인하여 버그가 발생할 가능성이 존재한다. 92 | - 공유 참조로 인해 발생하는 버그는 찾기 어려우며 여러가지 사이드 이펙트를 발생시킬 수 있는데 이를 막으려면 값을 복사해서 사용해야한다. 93 | 94 |
95 | 96 | ## 값 타입의 복사 97 | 98 | - 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하기 때문에 인스턴스를 복사해서 사용해야한다. 99 | - 항상 값을 복사해서 사용하게되면 공유 참조로 인해 발생하는 사이드 이펙트를 피할 수 있다. 100 | - 하지만 문제는 자바는 대입하려는 것이 값 타입인지 아닌지는 신경쓰지 않기 때문에 복사하지 않고 원본의 참조값을 직접 넘기는 것을 막을 방법이 없다. 101 | - 때문에 객체의 공유 참조에서 발생하는 문제를 해결하기 위한 근본적인 해결방법이 필요한데 이는 객체를 불변 객체로 만들어 객체의 값을 수정하지 못하도록 막으면된다. 102 | 103 |
104 | 105 | ## 불변 객체 106 | 107 | - 값 타입은 사이드 이펙트 걱정 없이 사용할 수 있어야 하는데 사이드 이펙트가 발생하면 값 타입이라고 할 수 없다. 108 | - 객체를 불변객체로 만들면 값을 수정할 수 없기 때문에 사이드 이펙트를 사전에 방지할 수 있으며 도리 수 있으면 값 타입은 불변 객체로 설계해야 한다. 109 | - 불변 객체를 구현하는 방법에는 다향한 방법이 있지만 가장 간단한 방법은 생서자로만 값을 설정하고 수정자를 생성하지 않는 것이다. 110 | 111 |
112 | 113 | # 9.4 값 타입의 비교 114 | 115 | - 자바에서 제공하는 객체 비교는 동일성 비교와 동등성 비교 두 가지가 존재한다. 116 | - 값이 동일하지만 서로 다른 인스턴스인 두 값 타입을 동일성 `==` 비교 하게되면 결과는 `false` 가 나오게 된다. 117 | - 값 타입은 인스턴스가 달라고 그 안에 값이 같으면 같은 것으로 보아야하기 때문에 `equals()` 메서드를 이용하여 동등성 비교를 수행해야 한다. 118 | - 값 타입의 `equals()` 메서드를 재정의할 때는 보통 모든 필드값을 비교하도록 구현하며 이와 동시에 `hashCode()` 도 함께 재정의하는 것이 안전하다. 119 | 120 |
121 | 122 | # 9.5 값 타입 컬렉션 123 | 124 | - 값 타입을 하나 이상 저장하려면 컬렉션이 보관하고 `@ElementCollection` , `@CollectionTable` 어노테이션을 사용하면된다. 125 | 126 |
127 | 128 | ```java 129 | @Entity 130 | public class Member { 131 | 132 | ... 133 | 134 | @ElementCollection 135 | @CollectionTable(name = "FAVORITE_FOODS", 136 | joinColumns = @JoinColumn(name = "MEMBER_ID")) 137 | @Column(name = "FOOD_NAME") 138 | private Set favoriteFoods = new HashSet<>(); 139 | 140 | @ElementCollection 141 | @CollectionTable(name = "ADDRESS", 142 | joinColumns = @JoinColumn(name = "MEMBER_ID")) 143 | private List
addressHistory = new ArrayList<>(); 144 | 145 | } 146 | ``` 147 | 148 |
149 | 150 | - 위의 예시에서 회원 엔티티는 값 타입 컬렉션을 사용하는데 이것을 관계형 데이터베이스 테이블로 매핑할 때 테이블은 컬럼 내부에 컬렉션을 포함할 수 없기 때문에 별도의 테이블을 추가해서 추가한 테이블과 매핑을 수행해야한다. 151 | 152 | ![https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9cf5252c-5f56-4f06-8dcf-fd4534eadfc1/Untitled.png](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F3a1e326b-c6c2-4a5b-9816-1fa1fca012fb%2FUntitled.png?table=block&id=e7f337b8-4134-4252-b43c-767d55601eb6&spaceId=7bf4105e-471a-416e-8171-751ccdb35ff5&width=590&userId=&cache=v2) 153 | 154 |
155 | 156 | ## 값 타입 컬렉션 사용 157 | 158 | ```java 159 | Member member = new Member(); 160 | 161 | //임베디드 값 타입 162 | member.setHomeAddress(new Address("통영","몽돌해수욕장","660-123")); 163 | 164 | //기본값 타입 컬렉션 165 | member.getFavoriteFoods().add("짬뽕"); 166 | member.getFavoriteFoods().add("짜장"); 167 | member.getFavoriteFoods().add("탕수육"); 168 | 169 | //임베디드 값 타입 컬렉션 170 | member.getAddressHistory().add(new Address("서울","강남","123-123")); 171 | member.getAddressHistory().add(new Address("서울","강북","000-000")); 172 | 173 | em.persist(member); 174 | ``` 175 | 176 |
177 | 178 | - 위와 같은 코드를 실행한다고 했을 때 실제로는 회원 엔티티만 영속화했지만 JPA는 엔티티의 값 타입도 함께 저장한다. 179 | - 때문에 실제로 데이터베이스에 실행되는 SQL은 다음과 같다. 180 | - 회원 엔티티 삽입 SQL 1번 181 | - 회원의 집주소에 해당하는 값 타입 삽입 SQL은 회원 엔티티 삽입 SQL에 포함된다. 182 | - 회원의 좋아하는 음식에 해당하는 값 타입 삽입 SQL 3번 183 | - 회원의 주소 기록에 해당하는 값 타입 삽입 SQL 2번 184 | - 회원 엔티티만 영속화를 수행했지만 회원 엔티티를 제외한 총 5개의 SQL이 추가적으로 수행됨을 확인할 수 있다. 185 | - 값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다. 186 | - 값 타입 컬렉션 조회도 `fetch` 전략을 선택할 수 있으며 기본 `fetch` 전략은 `LAZY` 이다. 187 | 188 |
189 | 190 | ### 값 타입의 수정과 값 타입 컬렉션의 수정 191 | 192 | - 임베디드 값 타입을 수정하는 경우 엔티티 테이블과 직접 매핑되기 때문에 엔티티 테이블만 업데이트하며 이는 엔티티를 수정하는 것과 동일하다. 193 | - `String` 과 같은 기본값 타입 컬렉션을 수정하는 경우 자바의 `String` 은 불변객체이기 때문에 수정하려는 `String` 객체를 제거하고 새로운 값을 추가해야한다. 194 | - 임베디드 값 타입 컬렉션을 수정하는 경우 값 타입은 불변해야하는 원칙에 따라서 기본 값 타입과 동일하게 수정하려는 값 타입 객체를 컬렉션에서 제거하고 새로운 값 타입 객체를 저장해야한다. 195 | 196 |
197 | 198 | ## 값 타입 컬렉션의 제약사항 199 | 200 | - 엔티티는 식별자가 존재하기 떄문에 엔티티의 값을 변경해도 식별자로 데이터베이스에 저장된 원본 데이터를 쉽게 찾아서 변경할 수 있다. 201 | - 반면 값 타입은 식별자라는 개념이 없고 단순한 값들의 모음이기 떄문에 값을 변경하면 데이터베이스에 저장된 원본 데이터를 찾기 어렵다. 202 | - 특정 엔티티 하나에 소속된 값 타입은 값이 변경되어도 자신이 소속된 엔티티를 데이터베이스에서 찾고 값을 변경하면 되지만 문제는 값 타입 컬렉션이다. 203 | - 값 타입 컬렉션에 보관된 값 타입들은 별도의 테이븡레 보관되며 보관된 값이 변경되면 데이터베이스에 존재하는 원본 데이터를 찾기 어렵다는 문제가 있다. 204 | - 이러한 문제로 인하여 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다. 205 | - 추가로 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야하는데 데이터베이스의 기본키 제약조건으로 인해 컬럼에 `null` 을 입력할 수 없고 같은 값을 중복해서 저장할 수 없다는 제약 사항도 존재한다. 206 | - 떄문에 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 또는 값 타입의 여러가지 제약사항으로 인한 문제점을 해결하기 위해서는 값 타입 컬렉션 대신 1:N 관계를 사용하면된다. 207 | - 1:N 관계로 새로운 엔티티를 생성하고 영속성 전이와 고아 객체 제거 기능을 적용하면 값 타입 컬렉션과 동일하게 사용할 수 있다. 208 | -------------------------------------------------------------------------------- /토비의 스프링 3.1/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Today-I-Learn/dev-reading-record/0414ccac602b41162f43982db84abe2c887d7824/토비의 스프링 3.1/.DS_Store -------------------------------------------------------------------------------- /토비의 스프링 3.1/1장 오브젝트와 의존관계/1.1 ~ 1.5.md: -------------------------------------------------------------------------------- 1 | 스프링은 자바로 기반으로 한 기술로 스프링이 가장 중요하게 가치를 두는 것은 객체지향 프로그래밍입니다. 즉, EJB를 사용함으로써 잃어버렸던 객체지향의 진정한 가치를 회복시키고 객체지향이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것이 스프링의 핵심 철학입니다. 2 | 3 | 때문에 스프링은 객체에 가장 많은 관심을두고 있으며 스프링을 이해하기 위해서는 객체에 깊은 관심을 가져야 합니다. 4 | 5 | - 애플리케이션에서 객체가 생성되고 관계를 맺고 사용되고 소멸되기까지의 전반적인 과정은 어떻게 이루어지는지 6 | - 객체는 어떻게 설계되어야 하며 어떤 단위로 만들어지고 어떤 과정을 통해 존재를 드러내고 등장해야하는지 7 | 8 | 결국 이러한 객체에 대한 관심은 기술적인 특징과 사용 방법을 넘어선 설계의 측면으로 발전하게 됩니다. 때문에 객체지향 설계, 디자인 패턴, 리팩토링, 단위 테스트와 같은 객체의 설계와 구현에 관한 여러가지 응용 기술과 지식이 요구되어집니다. 9 | 10 | 스프링은 객체를 어떻게 효과적으로 설계하고 구현하고 사용하고 이를 개선해 나갈 것인가에 대한 명쾌한 기준을 마련해줌과 동시에 객체지향 기술과 설계, 구현에 관한 실용적인 전략과 검증된 베스트 프랙티스를 제공합니다. 11 | 12 |
13 | 14 | ## 1.1 초난감 DAO 15 | 16 | 초난감 DAO란 여러 관심사와 책임이 하나의 클래스에 혼재되어 높은 결합도를 가지고 있는 클래스를 의미합니다. 토비의 스프링 1장에서는 이러한 초난감 DAO가 가지고 있는 문제점을 하나하나 개선해 나아갑니다. 그 과정에서 다음의 질문들에 초점을 맞춰서 생각을 해봅시다. 17 | 18 | - 초난감 DAO 클래스 코드의 문제점은 무엇일까? 19 | - 왜 이 코드에 문제가 많다고 하는 것일까? 20 | - 잘 동작하는 코드를 굳이 수정하고 개선해야하는 이유는 무엇일까? 21 | - 초난감 DAO 클래스 코드를 개선했을 때의 장점은 무엇인가? 22 | - 당장에 얻을 수 있는 장점과 더불어 미래에 어떠한 이점을 가져다 주는가? 23 | - 객체지향 설계 원칙과 초난감 DAO 클래스를 개선하는 것과는 어떠한 상관관계가 있는가? 24 | - 이러한 초난감 DAO를 그대로 사용한 경우와 개선한 경우 스프링에서의 차이는 무엇인가? 25 | 26 |
27 | 28 | ## 1.2 관심사의 분리 29 | 30 | 토비의 스프링 1장에서 가장 인상깊게 읽은 부분을 꼽으라면 1.2의 관심사 분리에 나온 지문을 소개하고 싶습니다. 31 | 32 | ``` 33 | 세상에는 변하는 것과 변하지 않는 것이 있다. 하지만 객체지향의 세계에서는 모든 것이 변한다. 34 | 여기서 변한다는 것은 변수나 오브젝트 필드 값이 변한다는 게 아니다. 오브젝트에 대한 설계와 이를 구현한 35 | 코드가 변한다는 뜻이다. 36 | 37 | ... 38 | 39 | 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끊임없이 바뀌고 발전한다. 애플리케이션이 기반을 두고 40 | 있는 기술도 시간이 지남에 따라 바뀌고, 운영되는 환경도 변환한다. 41 | 42 | 그래서 개발자가 객체를 설계할 때 가장 염두에 둬야 할 사항은 바로 미래릐 변화를 어떠헥 대비할 것인가이다. 43 | 44 | 토비의 스프링 3.1 p.61 45 | ``` 46 | 47 | 책의 구문처럼 객체지향 세계에서는 모든 것이 변화합니다. 때문에 개발자가 이를 설계하고 개발할 때 가장 핵심적으로 고려해야할 사항은 바로 미래에 대한 변화를 어떻게 대비할 것인지 어떻게 확장성 있고 유지보수하기 쉬운 설계를 할 것인가 입니다. 48 | 49 | 객체지향 프로그래밍이 절차지향 프로그래밍 패러다임에 비해 초기에 조금 더 많은 번거로운 작업들을 요구하는 이유는 객체지향 기술 자체가 지니는 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문입니다. 50 | 51 | 이러한 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고 이를 자유롭고 편리하게 변경, 발전, 확장 시킬 수 있다는 것이 객체지향의 진정한 의미입니다. 52 | 53 | 그렇다면 미래의 변화에 어떻게 대비할 것인가라는 과제가 남았는데요 이에 대한 가장 좋은 대책은 변화의 폭을 최소한으로 줄여주는 것입니다. 54 | 55 | 어떻게 변경이 일어날 때 필요한 작업을 최소화하고 변경이 다른 곳에 문제를 잃으키지 않게 할 수 있을까요? 이것을 가능하게 해주는 것은 바로 분리와 확장을 고려한 설계입니다. 56 | 57 |
58 | 59 | ### 분리 60 | 61 | 먼저 분리의 관점에서 생각해봅시다. 변경과 발전에 대한 요청은 한번에 한가지 관심사항에 집중해서 일어나게 된다는 것을 알 수 있습니다. 하지만 문제가 되는 것은 변화는 대체로 집중된 한가지 관심에 대해서 일어나지만 그에 따른 작업은 여러 곳에서 동시다발적으로 발생해야 한다는 것에서 시작됩니다. 62 | 63 | 만약 하나의 설계나 기능을 변경하기 위해 수백 개의 코드를 수정하고 구조를 변경해야 한다면 하나의 변화를 이루기 위해서 지불해야 하는 대가가 너무나도 클 것입니다. 64 | 65 | 떄문에 변화가 한번에 한 가지 관심에 집중되어 일어난다면 개발자가 할 일은 한 가지 관심이 한 군데에 집중할 수 있도록 하는 것입니다. 즉, 관심이 같은 것 끼리는 모으고 관심이 다른 것 끼리는 떨어지게 하는 것이 필요합니다. 66 | 67 | 이러한 관심사의 분리를 통해 관심이 같은 것 끼리는 하나의 객체 안으로 모으고 관심이 다른 것은 가능한 따로 떨어져서 영향을 주지 않도록 분리하는 것을 통해 여러 종류의 관심사를 적절하게 구분하고 같은 관심에 효과적으로 집중할 수 있게 되는 것입니다. 68 | 69 | 토비의 스프링 3.1에 나오는 `UserDao` 클래스의 관심사 분리는 다음의 과정을 거쳐서 진행됩니다. 70 | 71 | - 데이터베이스 연결, SQL 실행, 리소스 관리에 대한 관심사를 적절하게 분리하기 72 | - 중복된 코드를 하나의 메서드를 통해 분리 73 | - 데이터베이스 커넥션을 가져오는 중복된 코드를 하나의 메서드로 분리 74 | - 데이터베이스 연결과 관련된 부분에 변경이 일어나더라도 하나의 메서드의 코드만 수정하면 되기 때문에 변화에 유연하게 대처가 가능하다. 75 | - 상속을 통한 확장 76 | - 변경될 가능성이 존재하는 데이터베이스 커넥션을 생성하는 방식을 상속을 통해 자유롭게 확장 77 | - 서브 클래스에서 구체적인 객체의 생성 방법을 결정하게 하는 팩토리 메서드 패턴을 적용함으로써 관심 사항이 다른 코드를 분리해내고 서로 독립적으로 변경 및 확장이 가능하다. 78 | 79 |
80 | 81 | ## 1.3 DAO의 확장 82 | 83 | 앞서서 `UserDao` 클래스를 상속을 통해 확장하였지만 상속 관계를 통한 확장은 여전히 다음과 같은 문제를 가지고 있습니다. 84 | 85 | - 슈퍼 클래스와 서브 클래스가 긴밀한 결합을 가지고 있다. 86 | - 때문에 슈퍼 클래스 내부에 변경이 일어나게되면 모든 서브 클래스를 수정하거나 다시 개발해야 할 수도 있다. 87 | - 확장된 기능의 코드를 다른 클래스에 적용할 수 없다. 88 | - 때문에 매번 상속을 통해 DAO 클래스들이 계속해서 만들어진다면 상속을 통해 만들어진 해당 구현코드가 매번 DAO 클래스마다 중복되서 나타날 것이다. 89 | 90 | 이러한 문제가 발생한 원인은 다음의 서로다른 변화의 성격을 고려하지 않은 확장 때문입니다 91 | 92 | - 데이터 엑세스 로직을 어떻게 만들 것인가 93 | - 데이터베이스 연결을 어떻게 할 것인가 94 | 95 | 때문에 관심사를 메서드를 분리한 코드를 또 다시 변화의 성격이 다른 것들을 다른 클래스로 분리해 두 개의 관심사를 독립시키면서 동시에 손쉽게 확장할 수 있도록 변경하기 위해서는 먼저 두 가지 문제를 해결해야합니다. 96 | 97 | - 구현 클래스에서 서로다른 메서드를 제공하는 경우 구현 클래스를 변경할 경우 변화에 대한 변경사항이 여러 곳에서 동시다발적으로 발생하는 것 98 | - 데이터베이스 커넥션을 제공하는 클래스가 어떤 것인지 구체적으로 알고있어야 하기 때문에 구현 클래스와 직접적으로 의존관계를 맺고 있는 것 99 | 100 |
101 | 102 | ### 인터페이스의 도입 103 | 104 | 이러한 문제를 해결하기 위해서는 두 개의 클래스가 긴밀하게 연결되어 있지 않도록 중간에 추상적인 느슨한 연결고리르 만들어주는 작업이 필요합니다. 105 | 106 | 때문에 인터페이스를 도입하여 추상화를 통해 공통적인 성격을 뽑아내어 분리하고 구현한 클래스에 대한 구체적인 정보는 모두 감추어 이를 사용하는 측에서는 일관된 방법으로 인터페이스를 통해 접근할 수 있게 됩니다. 이러한 구조의 장점은 구체적인 구현 클래스의 변경 또는 변화가 발생하더라도 클라이언트는 이에 관심을 가지고 신경쓸 필요가 없다는 점입니다. 107 | 108 | 인터페이스를 도입했지만 여전히 남아있는 문제는 구체적인 구현 클래스와 직접적으로 의존하고 있다는 것입니다. 이러한 문제는 객체 생성에 대한 책임이라는 관심사가 여전히 클래스에 존재하기 때문입니다. 이를 해결하기 위해서는 객체 생성에 대한 책임을 클라이언트로 위임하여 구현 클래스를 선택하고 선택한 클래스의 오브젝트를 생성하는 방식을 통해서 분리할 수 있습니다. 109 | 110 | 인터페이스를 통한 추상화와 다형성을 이용한 다이나믹한 의존관계 설정, 그리고 런타임 오브젝트 의존관계를 설정하는 책임을 분리함으로써 상속을 사용했을 때 보다 더 유연하고 확장가능한 코드로 개선할 수 있었습니다. 111 | 112 |
113 | 114 | ### 원칙과 패턴 115 | 116 | 위에서의 리팩토링 과정은 객체지향의 5가지 원칙 중 하나인 개방 폐쇄 원칙을 적극 확용하고 있습니다. 117 | 118 | 개방 폐쇄 원칙 `OCP, Open-Closed Principle` 은 클래스나 모듈의 확장에는 열려있어야하고 변경에는 닫혀있어야 한다는 것을 의미합니다. 119 | 120 | 즉, 이제까지 `UserDao` 클래스를 분리해오면서 데이터베이스 연결 방법이라는 기능을 유연하게 확장할 수 있으며 `UserDao` 클래스에 영향을 주지 않고 이를 확장하고 동시에 `UserDao` 는 변화에 영향을 받지 않고 유지할 수 있다고 말할 수 있습니다. 121 | 122 |
123 | 124 | ### 결합도와 응집도 125 | 126 | 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발원리로도 설명이 가능합니다. 127 | 128 | 응집도가 높다는 의미는 하나의 모듈, 클래스가 하나의 책임과 관심사에 집중되어있다는 의미로 불필요하거나 직접 관련이 없는 외부의 관심과 책임에 얽혀있지 않고 공통 관심사는 한 클래스에 모여있다는 것입니다. 129 | 130 | 높은 응집도의 장점은 변경이 일어날 때 모듈의 많은 부분이 동시에 변경된다는 점입니다. 때문에 모듈 전체 중 어느 부분이 바뀌고 바뀌지 않는지를 파악하고 변하는 부분으로 인해 다른 부분에 영향을 미치지 않는지 확인해야 하는 부담을 줄일 수 있습니다. 131 | 132 | 낮은 결합도는 높은 응집도 보다 더 민감한 원칙으로 책임과 관심사가 다른 객체 또는 모듈과는 느슨하게 연결된 형태를 유지하는 것을 의미합니다. 낮은 결합도를 위해서 관계를 유지하는데 최소한의 방법만 간접적인 형태로 제공하고 나머지는 서로 독립적으로 알 필요 없게 만들어주는 것이 필요합니다. 133 | 134 | 낮은 결합도의 장점은 변화에 대응하는 속도가 높아지고 구성이 깔끔해진다는 점에 있습니다. 또한 이러한 장점들을 통해 좀 더 확장에 유연하도록 구성할 수 있게 됩니다. 135 | 136 |
137 | 138 | ### 전략 패턴 139 | 140 | 전략 패턴은 `OCP` 의 실현과도 가장 잘 들어맞는 패턴으로 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴입니다. 141 | 142 | 즉, 인터페이스를 통해 느슨한 연관관계와 이를 통한 확장에 유연한 구조를 만드는 것이 전략 패턴의 목표입니다. 143 | 144 | 토비의 스프링에 나오는 개선된 `UserDao` 클래스는 초기의 초난감 `UserDao` 에서 시작하여 관심사를 분리하고 인터페이스를 이용한 느슨한 연관관계와 연관관계 설정에 대한 책임을 외부로 분리함을 통해서 `OCP` 원칙을 잘 따르고 결합도가 낮고 응집도가 높으며 전략패턴을 잘 활용한 사례로 볼 수 있습니다. 145 | 146 | `UserDao` 의 기능은 그대로지만 내부 구현을 변경함을 통해 좀 더 변화와 확장에 유연하고 유지보수하기 쉬운 구조로 변경할 수 있었습니다. 147 | 148 |
149 | 150 | ## 1.4 제어의 역전 (IoC) 151 | 152 | 기존의 인터페이스를 이용한 확장 방식에서 클라이언트 코드에 객체 생성과 구체적인 구현클래스를 결정하는 책임을 위임하는 것을 통해 확장에 유연한 구조를 만들 수 있었습니다. 153 | 154 | 하지만 이 방법의 문제는 또 다시 클라이언트 측면에서 보면 클라이언트 코드가 가지고 있는 책임 이외에도 구현 클래스를 결정하는 책임을 가지고 있다는 것입니다. 때문에 지금까지 적용해왔던 분리를 통해 이를 또 한번 분리시켜줄 필요성이 존재합니다. 155 | 156 |
157 | 158 | ### 팩토리 159 | 160 | 팩토리는 객체 생성의 방법을 결정하고 그렇게 만들어진 객체를 돌려주는 역할을 가진 클래스를 의미합니다. 팩토리 클래스는 오브젝트를 생성하는 쪽과 생성된 오브젝트를 사용하는 쪽의 역할과 책임을 분리하려는 목적으로 사용되어집니다. 161 | 162 | 때문에 위의 코드에서 팩토리를 적용하여 객체 생성의 책임을 분리함으로써 컴포넌트의 역할과 애플리케이션 구조를 결정하는 객체를 분리할 수 있습니다. 163 | 164 |
165 | 166 | ### 제어의 역전 (IoC, Inversion of Control) 167 | 168 | 제어의 역전은 간단하게 프로그램의제어 흐름 구조가 뒤바뀌는 것을 의미합니다. 일반적으로 프로그램의 흐름은 다음과 같이 이루어집니다. 169 | 170 | - `main()` 메서드와 같이 프로그램의 엔트리 포인트에서 사용할 객체를 결정하고 171 | - 결정된 객체를 생성하고 172 | - 만들어진 객체의 메서드를 호출하고 173 | - 객체의 메서드 안에서 다음에 사용할 것들을 결정하고 호출하는 식의 작업의 반복 174 | 175 | 이런 구조에서 객체는 프로그램 흐름을 결정하거나 사용할 객체를 구성하는 작업에 능동적으로 참여합니다. 초기의 테스트코드 예제를 보면 `UserDao` 클래스의 객체를 직접 생성하고 만들어진 오브젝트의 메서드를 사용하고 `UserDao` 클래스 또한 자신이 사용할 구체적인 데이터베이스 연결에 대한 구현 클래스를 결정하여 필요한 시점에 생성하고 사용합니다. 176 | 177 | 즉, 모든 종류의 작업을 사용하는 쪽에서 제어하는 구조입니다. 제어의 역전은 이런 제어 흐름의 개념을 거꾸로 뒤집는 것을 의미합니다. 제어의 역전에서는 객체가 자신이 사용할 오브젝트를 스스로 선택하지 않습니다. 또한 객체 자신도 어떻게 만들어지고 어디서 사용되는지 알 수 없습니다. 178 | 179 | 이런 것이 가능한 이유는 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이빈다. 프로그램의 시작을 담당하는 `main()` 함수와 같은 엔트리 포인트를 제외하면 모든 객체는 위임받은 제어 권한을 갖는 특별한 객체에 의해 결정되고 만들어지게 됩니다. 180 | 181 | 사실 제어의 역전이란 개념은 서블릿과 서블릿 컨테이너, 템플릿 메서드 패턴, 프레임워크 등 다양한 부분에서 이미 사용되고 있습니다. 182 | 183 |
184 | 185 | ### 라이브러리와 프레임워크 186 | 187 | 프레임워크는 제어의 역전이 적용된 대표적이 기술입니다. 프레임워크가 어떤 것인지 이해하기 위해서는 먼저 라이브러리와 프레임워크가 어떻게 다른지를 알아야 합니다. 188 | 189 | 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어합니다. 단지 동작하는 중간에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐입니다. 190 | 191 | 반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용되는 형태를 가지고 있습니다. 프레임 워크 위에 개발한 클래스를 등록하고 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만든 방식입니다. 192 | 193 | 즉, 이 둘의 가장 큰 차이는 제어권이 어디에 있는지 인데 프레임워크에는 분명한 제어의 역전 개념이 적용된 것이어야만 합니다. 194 | 195 | 앞서 팩토리를 통해 만든 구조에서도 제어의 역전이 적용되어 있습니다. `UserDao` 클래스도 팩토리에 의해 수동적으로 만들어지고 사용할 객체들도 팩토리가 공급해주는 것을 수동적으로 사용할 뿐입니다. 196 | 197 | 자연스럽게 관심을 분리하고 책임을 나누고 유연하게 확장하는 구조로 만들기 위해 팩토리를 도입했던 과정이 바로 `IoC` 를 적용하는 작업으로 이를 통해 설계가 깔끔해지고 유연한 확장이 가능한 구조가 가능해졌습니다. 198 | 199 |
200 | 201 | ## 1.5 스프링의 IoC 202 | 203 | 대표적인 `IoC` 컨테이너라고 불리는 스프링에서 가장 핵심적인 기능을 담당하는 것은 애플리케이션 컨텍스트라고 불리는 스프링 `IoC` 컨테이너 입니다. 204 | 205 | 스프링에서는 스프링이 제어권을 직접 가지고 관계를 부여하는 객체를 스프링 빈이라고 부릅니다. 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 `IoC` 가 적용된 객체를 가리키는 단어입니다. 206 | 207 | 스프링에서 제어를 담당하는 `IoC` 객체를 빈 팩토리라고 하며 이를 확장한 애플리케이션 컨텍스트를 주로 사용합니다. 애플리케이션 컨텍스트는 별도의 정보를 참고하여 빈을 생성하고 관계를 설정하는 등의 제어 작업을 총괄합니다. 208 | 209 | 애플리케이션 컨텍스트에서 사용할 설정정보르를 담당하는 클래스는 `@Configuration` 이라는 어노테이션을 붙이고 메서드에는 `@Bean` 이라는 어노테이션을 붙여서 이를 스프링이 빈 팩토리를 위한 객체 설정을 담당하는 클래스라고 인식할 수 있습니다. 210 | 211 |
212 | 213 | ### 애플리케이션 컨텍스트의 동작방식 214 | 215 | 팩토리가 특정 클래스에 대한 객체를 생성하고 관계를 맺어주는 제한적인 역할을 하는 반면에 애플리케이션 컨텍스트는 애플리케이션에서 `IoC` 를 적용해서 관리할 모든 객체에 대한 생성과 관계설정을 담당합니다. 216 | 217 | 뿐만아니라 애플리케이션 컨텍스트에는 객체 생성 및 관계를 맺어주는 코드가 직접적으로 존재하지 않고 별도의 설정정보를 통해 다음과 같은 과정으로 객체의 생성과 관리에 대한 정보들을 얻습니다. 218 | 219 | - `@Configuration` 및 `@Bean` 어노테이션이 붙은 클래스를 스캔하여 빈 설정 정보를 통해 빈 목록을 생성합니다. 220 | - 클라이언트의 요청에 의해 빈 목록에 요청한 이름이 있는지 찾고 있다면 이를 찾고 생성하는 메서드를 호출해 이를 전달합니다. 221 | 222 | 애플리케이션 컨텍스트를 통해 기존의 팩토리의 기능을 그대로 적용하면서도 범용적이고 유연한 방법으로 `IoC` 기능을 확장할 수 있습니다. 뿐만아니라 팩토리를 사용할 때와 비교하여 다음의 장점들을 얻을 수 있습니다. 223 | 224 | - 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다. 225 | - 클라이언트가 필요한 객체를 가져오려면 어떠한 팩토리 클래스를 사용해야 할지 알고 필요할 때마다 팩토리 오브젝트를 생성하는 번거로움이 있지만 애플리케이션 컨텍스트를 사용하면 일관된 방식으로 원하는 객체를 가져올 수 있습니다. 226 | - 애플리케이션 컨텍스트는 종합 `IoC` 서비스를 제공한다. 227 | - 애플리케이션 컨텍스트의 역할을 단지 객체 생성과 관계 설정만이 아니라 객체가 만들어지는 방식, 시점, 전략을 다르게 가져갈 수도 있으며 자동생성, 후처리, 정보의 조합, 설정 방식의 다변화, 인터셉팅 등 객체를 효과적으로 사용할 수 있는 다양한 기능을 제공한다. 228 | - 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다. 229 | - 애플리케이션 컨텍스트는 빈의 이름 뿐만 아니라, 타입 또는 특별한 어노테이션로 설정되어 있는 빈을 찾을 수 있는 기능을 제공한다. 230 | -------------------------------------------------------------------------------- /토비의 스프링 3.1/1장 오브젝트와 의존관계/1.6 ~ 1.9.md: -------------------------------------------------------------------------------- 1 | ## 1.6 싱글톤 레지스트리와 오브젝트 스코프 2 | 3 | DaoFactory를 직접 사용하는것과 @Configuration 애노테이션을 추가하여 스프링의 애플리케이션 컨텍스트를 통해 사용하는 것에는 차이가 있다. 4 | 5 | 우선 DaoFactory의 userDao() 메소드를 두번 호출하여 리턴되는 UserDao 오브젝트를 비교하면 다른 값을 가진 오브젝트임을 알 수 있다. 6 | 반면 스프링 애플리케이션 컨텍스트에 DaoFactory를 설정 정보로 등록하고 getBean() 메소드를 이용해 userDao() 로 등록된 두개의 오브젝트르 비교하면 7 | 같은 값을 가지는 것을 볼 수 있다. 8 | 9 | 10 | >__java 오브젝트의 동일성과 동등성__ 11 | > 12 | > __1. 동일성 비교__ 13 | > - 두개의 오브젝트가 완전히 동일한 경우, == 연산자를 이용하여 비교 14 | > - 두 오브젝트가 동일하다면 하나의 오브젝트만 존재하는 것이고, 두개의 오브젝트 레퍼런스 변수를 가진다. 15 | > 16 | > __2. 동등성 비교__ 17 | > - 동일한 정보를 담고 있는 오브젝트일 경우, equals() 메소드를 이용하여 비교 18 | 19 | ### 1.6.1 싱글톤 레지스트리로서의 애플리케이션 컨텍스트 20 | - 애플리케이션 컨텍스트는 IoC 컨테이너이며 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이다. 21 | - 스프링은 기본값으로 내부에서 생성하는 빈 오브젝트를 싱글톤으로 만든다. 22 | - 스프링의 싱글톤은 디자인 패턴에서 나오는 싱글톤과 비슷한 개념이지만 구현 방법이 다르다. 23 | 24 | #### 스프링이 싱글톤으로 빈을 만드는 이유 25 | - 스프링은 주로 대규모의 엔터프라이즈 서버 환경에서 이용되어 높은 성능이 요구된다. 26 | - 매번 클라이언트 요청이 올때마다 각 로직을 담당하는 오브젝트를 새로만든다면 부하가 발생하게된다. 27 | - 따라서 서비스 오브젝트라는 개념을 사용하여 서블릿 클래스당 하나의 오브젝트만 만들어, 28 | 사용자의 요청을 담당하는 여러 스레드에서 하나의 오브젝트를 공유해 동시에 사용하게 한다. 29 | 30 | #### 싱글톤 패턴의 한계 31 | 1. private 생성자를 갖고 있어 상속 불가 32 | - 생성자를 private 로 제한하여 오직 싱글톤 클래스 자신만이 오브젝트를 만들수 있다. 33 | - 따라서 객체지향의 장점인 상속과 다형성을 적용할 수 없다. 34 | 2. 싱글톤은 테스트하기 어렵다 35 | - 싱글톤은 만들어지는 방식이 제한적이기 때문에 테스트에서 사용될 때 목 오브젝트 등으로 대체가 힘들다. 36 | 3. 서버환경에서 싱글톤이 하나만 만들어지는것을 보장할 수 없다. 37 | - 서버에서 클래스 로더를 구성하는 방법에 따라 싱글톤 클래스의 오브젝트가 하나이상 만들어질 수 있다. 38 | - 여러개의 JVM에서 분산돼 서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생겨 싱글톤으로서의 가치가 떨어진다. 39 | 4. 싱글톤의 사용은 전역 상태를 만들 수 있어 바람직하지 못하다. 40 | - 싱글톤의 스태틱 메소드를 이용해 누구든 사용할 수 있기 때문에 전역 상태로 사용되기 싶다. 41 | 42 | #### 싱글톤 레지스트리 43 | 스프링은 자바의 싱글톤 구현 방식의 단점을 해결하기 위해 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공하며 이를 싱글톤 레지스트리라고 한다. 44 | - 장점 45 | - 스태틱, private 생성자를 사용하지 않고 자바 클래스를 싱글톤으로 활용하게 해준다. 46 | - 평범한 자바 클래스라도 IoC 방식의 컨테이너를 사용하여 생성과 관계설정, 사용 등에 대한 제어권을 컨테이너에 넘겨 싱글톤 방식으로 만들어져 관리되게 할 수 있다. 47 | - 싱글톤 방식으로 사용될 어플리케이션 클래스라도 public 생성자를 가질 수 있어 테스트를 할 수 있다. 48 | 49 | - 정리 50 | - 싱글톤 패턴과는 달리 스프링이 지지하는 객체지향적인 설게 방식과 원칙, 디자인 패턴을 적용하는데 제약이 없다. 51 | - 스프링은 IoC 컨테이너일 뿐만 아니라, 싱글톤을 만들고 관리해주는 싱글턴 레지스트리다. 52 | 53 | ### 1.6.2 싱글톤과 오브젝트의 상태 54 | 싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있기 때문에 상태 관리에 주의해야한다. 그렇기 때문에 stateless 방식으로 만들어져야 한다. 55 | 상태가 없는 방식으로 클래스를 만드는 경우, 각 요청 정보, DB 서버 등의 정보를 다루기 위해서는 파라미터와 로컬 변수 리턴 값 등을 이용하여 해결한다. 56 | 57 | ### 1.6.3 스프링 빈의 스코프 58 | - 빈 스코프 59 | - 스프링이 관리하는 오브젝트인 빈의 생명주기와 적용 범위를 의미힌다. 60 | - 기본적으로 싱글톤으로 오브젝트를 생성하여 강제로 제거하지 않는 한 계속 유지된다. 61 | - 프로토타입 스코프 62 | - 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어준다. 63 | 64 | ## 1.7 의존관계 주입(DI) 65 | 66 | ### 1.7.1 제어의 역전(IoC)과 의존관계 주입 67 | 스프링 IoC 컨테이너는 객체를 생성하고 관계를 맺어주느 등의 작업ㅇ르 담당하는 기능을 일반화한 것이다. 68 | IoC 라는 용어의 정의의 범위는 매우 넓어 스프링이 제공하는 IoC 방식을 의존관계 주입이라고 한다. 69 | 70 | ### 1.7.2 런타임 의존관계 설정 71 | 72 | 의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 오브젝트이다. 73 | 이 오브젝트는 관계설정 책임을 가진 코드를 분리하여 만든 오브젝트이다. 74 | 스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 것들이다. 75 | 76 | ### 1.7.3 의존관계 검색과 주입 77 | 의존관계 검색은 의존관계 주입과 달리 외부가 아닌 스스로 검색을 통해 의존관계를 맺는다. 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생서 작업은 외부 컨테이너에게 IoC로 맡기지만, 78 | 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다. 79 | 80 | 의존관계 검색은 의존관계 주입의 장점을 포함하고 있어 더 좋아보이지만, 코드의 복잡성과 애플리케이션 컴포넌트가 컨테이너와 같이 성격이 다른 오브젝트에 의존하게 되는 것이므로 좋지 않다. 81 | 하지만 스태틱 메소드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문에 의존관계 검색을 사용해야한다. 그리고 서버에서 사용자의 요청을 받을 때마다 서블릿에서 스프링 컨테이너가 담긴 오브젝트를 82 | 사용하기 위해서는 의존관계 검색을 사용해 오브젝트를 가져와야 한다. 83 | 84 | 85 | 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다. 하지만 의존관계 주입에서는 DI가 적용되려면 반드시 컨테이너가 만드는 빈 오브젝트여야 한다. 86 | 87 | ### 1.7.5 메소드를 이용한 의존관계 주입 88 | - 수정자 메소드를 이용한 주입 89 | 90 | 수정자 메소드(set)는 외부에서 오브젝트 내부의 애트리뷰트 값을 변경하려는 용도로 사용된다. 91 | 수정자 메소드는 외부로부터 제공받은 오브젝트 레퍼런스를 저장해뒀다가 내부의 메소드에서 사용하게 하는 DI 방식에서 활용하기 좋다. 92 | 93 | 94 | - 일반 메소드를 이용한 주입 -------------------------------------------------------------------------------- /토비의 스프링 3.1/2장 테스트/2.1 ~ 2.6.md: -------------------------------------------------------------------------------- 1 | # 토비의 스프링 3.1 - 2장 테스트 2 | 3 | 스프링이 개발자에게 제공하는 가장 중요한 가치 두가지는 객체지향과 테스트이다. 4 | 테스트 기술은 만들어진 코드를 확신할 수 있게 해주고, 변화에 유연하게 대처할 수 있는 자신감을 준다. 5 | 6 | ## 2.1 UserDaoTest 다시 보기 7 | 8 | ### 2.1.1 테스트의 유용성 9 | 만든 코드는 어떤 방법으로든 테스트해야 한다. 10 | 코드만 만들어놓고 잘 돌아가겠거니 하고 무책임하게 개발을 마치는 개발자는 아마도 없을 것이다. 11 | 12 | 처음과 동일한 기능을 수행함을 보장해줄 수 있는 방법에는 테스트를 통해 직접 기능을 동작시켜서 결과를 확인하는 방법이 유일하다. 13 | 테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 14 | 최종적으로 테스트가 성공하면 구현했던 코드의 모든 결함이 제거됐다는 확신을 얻을 수 있다. 15 | 16 | ### 웹을 통한 DAO 테스트 방법의 문제점 17 | 웹 화면을 통해 입출력 및 기능에 대한 결과를 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다. 18 | DAO뿐만 아니라 서비스 클래스, 컨트롤러, JSP 뷰 등 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다는점이 가장 큰 문제다. 19 | 그리고 정작 테스트할 대상뿐만 아니라 다른 코드 때문에 에러가 나거나 테스트가 실패할 수도 있다. 20 | 테스트하고 싶었던 건 UserDao였는데 다른 계층의 코드와 컴포넌트, 심지어 서버의 설정 상태까지 모두 테스트에 영향을 줄 수 있기 때문에 21 | 이런 방식으로 테스트하는 것은 번거롭고, 오류가 있을 때 빠르고 정확하게 대응하기가 힘들다는 문제가 있다. 22 | 23 | ### 작은 단위의 테스트 24 | 테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다. 25 | 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다. 26 | 이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 **단위 테스트(unit test)** 라고 한다. 27 | 28 | 단위 테스트가 필요한 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서다. 29 | 확인의 대상과 조건이 간단하고 명확할수록 좋다. 그래서 작은 단위로 제한해서 테스트하는 것이 편리하다. 30 | 31 | ### 자동수행 테스트 코드 32 | 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다. 33 | 자주 반복할 수 있다는 장점이 있다. 때문에 언제든 코드를 수정하고 나서 테스트를 해볼 수 있다. 34 | 35 | ### 지속적인 개선과 점진적인 개발을 위한 테스트 36 | 기능을 더 추가해가면서 그에 대한 테스트도 함께 추가하는 식으로 점진적인 개발이 가능하다. 37 | 새로운 기능도 기대한 대로 동작하는지 확인할 수 있고, 기존에 만들어둔 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 잘 동작하는지를 확인할 수도 있다. 38 | 39 | ## 2.2 UserDaoTest 개선 40 | 41 | ### 2.2.1 테스트 검증의 자동화 42 | 개발 과정에서, 또는 유지보수를 하면서 기존 애플리케이션 코드에 수정을 할 때 마음의 평안을 얻고, 자신이 만지는 코드에 대해 항상 자신감을 가질 수 있으며, 43 | 새로 도입한 기술의 적용에 문제가 없는지 확인할 수 있는 가장 좋은 방법은 빠르게 실행 가능하고 스스로 테스트 수행과 기대하는 결과에 대한 확인까지 해주는 코드로 된 자동화 테스트를 만들어두는 것이다. 44 | 45 | ### 2.2.2 테스트의 효율적인 수행과 결과 관리 46 | 이미 자바에는 실용적인 테스트를 위한 도구가 여러가지 존재한다. 47 | 그중 JUnit은 유명한 테스트 지원 도구이고 자바로 단위 테스트를 만들 때 유용하게 쓸 수 있는 프레임워크이다. 48 | 49 | ## 2.3 개발자를 위한 테스팅 프레임워크 JUnit 50 | 51 | ### JUnit 프레임워크 사용 조건 52 | - 메소드가 public으로 선언돼야 한다. 53 | - 메소드에 @Test 애노테이션을 붙혀줘야 한다. 54 | 55 | 두가지 조건은 꼭 지켜져야 된다. 그리고 이왕이면 테스트의 의도가 무엇인지 알 수 있는 이름을 메소드명으로 작성하면 좋다. 56 | JUnit은 예외가 발생하거나 asserthat()에서 실패하지 않고 테스트 메소드의 실행이 완료되면 성공했다고 인식한다. 57 | 58 | ### 2.3.1 JUnit 테스트 실행 방법 59 | 개발자 개인별는 IDE에서 JUnit 도구를 활용해서 테스트를 실행하는게 가장 편리하다. 60 | 61 | 여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때는, 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다. 62 | 이때는 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 그 결과를 메일 등으로 통보받는 방법을 사용하면 된디. 63 | 64 | ### 2.3.2 테스트 결과의 일관성 65 | 반복적으로 테스트를 했을 때 테스트가 실패하기도 하고 성공하기도 한다면 이는 좋은 테스트라고 할 수가 없다. 66 | 67 | 코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다. 68 | DB에 남아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하는 것은 물론이고, 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다. 69 | 70 | ### 2.3.3 포괄적인 테스트 71 | 테스트를 안 만드는 것도 위험한 일이지만, 성의 없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 건 더 위험하다. 72 | 특히 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. 73 | 74 | 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. 그렇다면 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다. 75 | 76 | ### 2.3.4 테스트가 이끄는 개발 77 | 78 | #### 테스트 주도 개발 79 | 만들고자 하는 기능의 내용을 담고, 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 80 | 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법을 **테스트 주도 개발(TDD : Test Driven Development)** 라고 한다. 81 | 82 | 코드에 대한 피드백을 매우 빠르게 받을 수 있고, 작성한 코드에 대한 확신을 가질 수 있다. 83 | 개발자는 자신감과 마음의 여유를 가질 수 있다. 84 | 85 | TDD에서는 테스트 작성과 이를 성공시키는 코드 작성의 주기를 가능한 한 짧게 가져가도록 권장한다. 86 | 개발한 코드의 오류는 빨리 발견할수록 좋고, 미리미리 쉽게 발견할 수 있었던 사소한 문제도 나중에 많은 코드와 얽혀서 돌아가는 상황에서는 쉽게 찾지 못하는 경우가 많기 때문이다. 87 | 88 | 테스트를 작성하느라 개발시간이 늦춰진다고 생각할 수 있지만, 테스트 덕분에 오류를 빨리 잡아낼 수 있어서 전체적인 개발 속도는 오히려 빨라진다. 89 | 90 | ### 2.3.5 테스트 코드 개선 91 | 테스트 코드 자체가 이미 자신에 대한 테스트이기 때문에 테스트 결과가 일정하게 유지된다면 얼마든지 리팩토링을 해도 좋다. 92 | 93 | ### JUnit 프레임워크가 테스트를 수행하는 방식 (간단하게 7단계로 정리) 94 | 1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다. 95 | 2. 테스트 클래스의 오브젝트를 하나 만든다. 96 | 3. @Before가 붙은 메소드가 있으면 실행한다. 97 | 4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다. 98 | 5. @After가 붙은 메소드가 있으면 실행한다. 99 | 6. 나머지 테스트 메소드에 대해 2~5번을 반복한다. 100 | 7. 모든 테스트의 결과를 종합해서 돌려준다. 101 | 102 | JUnit은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다. 103 | 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해서 이런 방식을 사용한다. 104 | 105 | ### 픽스쳐(fixture) 106 | 테스트를 수행하는 데 필요한 정보나 오브젝트를 **픽스쳐(fixture)** 라고 한다. 107 | 108 | ## 2.4 스프링 테스트 적용 109 | 테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다. 110 | 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다. 111 | 112 | ### 2.4.1 테스트를 위한 애플리케이션 컨텍스트 관리 113 | 114 | #### 테스트 메소드의 컨텍스트 공유 115 | 하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용할 수 있다. 116 | 117 | 스프링의 JUnit 확장기능(@RunWith)은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 118 | 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다. 119 | 120 | #### 테스트 클래스의 컨텍스트 공유 121 | 여러개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면, 122 | 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다. 123 | 124 | ### 2.4.2 DI와 테스트 125 | 126 | #### DI를 적용하는 이유 127 | - 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다. 128 | - 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다. 129 | - 단지 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다. 130 | DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는 데 중요한 역할을 한다. 131 | 132 | #### 테스트 코드에 의한 DI 133 | 테스트 코드 내에서 직접 DI를 해도 된다. 134 | 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 하기 위해서 @DirtiesContext 애노테이션을 추가해주는게 좋다. 135 | 136 | #### 테스트를 위한 별도의 DI 설정 137 | 테스트 코드 내에서 직접 DI를 하는 방법은 장점보다 단점이 많다. 138 | 때문에, 아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용해도 된다. 139 | 140 | 테스트 환경에 적합한 구성을 가진 설정파일을 이용해서 테스트를 진행하면 애플리케이션 컨텍스트도 한 개만 만들어서 모든 테스트에서 공유할 수 있다. 141 | 142 | #### DI를 이용한 테스트 방법 선택 143 | - 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 144 | 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다. 145 | - 여러 오브젝트와 복잡한 의존관계를 갖는 오브젝트에 대한 테스트시에는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다. 146 | - 예외적인 의존관계를 강제로 구성해서 테스트 해야 할 때는 테스트 코드 내에서 직접 DI 를 하는 방법을 이용하면 된다. 147 | 148 | ## 2.5 학습 테스트로 배우는 스프링 149 | 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 작성하는 테스트를 **학습테스트(learning test)** 라고 한다. 150 | 151 | ### 2.5.1 학습 테스트의 장점 152 | - 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다. 153 | - 학습 테스트 코드를 실제 개발에서 샘플 코드로 참고할 수 있다. 154 | - 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다. 155 | - 테스트 작성에 대한 좋은 훈련이 된다. 156 | - 문서를 읽는 것 보다 직접 코드를 작성해보면서, 새로운 기술을 공부하는 과정이 즐거워진다. 157 | 158 | ### 2.5.3 버그 테스트 159 | 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 **버그 테스트(bug test)** 라고 한다. 160 | 161 | 버그 테스트는 버그가 원인이 되서 실패하도록 만들어야 한다. 그러고 나서 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다. 162 | 테스트가 성공하면 버그는 해결된 것이다. 163 | 164 | #### 버그 테스트의 장점 165 | - 불충분했던 테스트를 보완해줘서 테스트의 완성도를 높여준다. 166 | - 테스트를 작성하면서 버그의 내용을 명확하게 분석하게 해준다. 167 | - 기술적인 문제를 해결하는 데 도움이 된다. 168 | 169 | ## 2.6 정리 170 | - 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다. 171 | - main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다. 172 | - 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안 된다. 173 | - 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것 보다 나쁠 수 있다. 174 | - 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다. 175 | - 테스트하기 쉬운 코드가 좋은 코드이다. 176 | - 테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유용하다. 177 | - 테스트 코드도 애플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다. 178 | - @Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다. 179 | - 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다. 180 | - 동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유한다. 181 | - @Autowired를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있다. 182 | - 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하자. 183 | - 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다. -------------------------------------------------------------------------------- /토비의 스프링 3.1/3장 템플릿/3.1 ~ 3.7.md: -------------------------------------------------------------------------------- 1 | # 토비의 스프링 3.1 - 3장 템플릿 2 | 3 | 객체지향 설계의 핵심 원칙은 개발 폐쇄 원칙(OCP)는 변화의 특성이 다른 부분을 구분해주고, 각가 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조로 만들어 주는 것이다. 4 | 5 | 템플릿이란 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로 독립시켜 활용하는 방법이다. 6 | 7 | 8 | 9 | ## 3.1 다시 보는 초난감 DAO 10 | 11 | 일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리한다. 12 | 13 | DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 그런데 이런 식으로 오류가 날 때마다 미처 반횐되지 못한 Connection이 계속 쌓이면 어느 순간에 커넥션 풀에 여유가 없어지고 리소스가 모자란다는 심각한 오류를 내며 서버가 중단될 수 있다. 그래서 JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장하고 있다. 14 | 15 | 어느 시점에서 예외가 발생했는지에 따라서 close()를 사용할 수 있는 변수가 달라질 수 있기 때문에 finally에서는 반드시 Connection과 PreparedStatment가 null이 아닌지 먼저 확인한 후에 close() 메소드를 호출해야 한다. 16 | 17 | 18 | 19 | ## 3.2 변하는 것과 변하지 않는 것 20 | 21 | JDBC try/catch/finally 코드를 효과적으로 다루기 위한 핵심은 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업이다. 22 | 23 | ### 분리와 재사용을 위한 디자인 패턴 적용 24 | 25 | #### 메소드 추출 26 | 27 | ``` 28 | public void deleteAll() throws SQLException { 29 | ... 30 | try { 31 | c = dataSource.getConnection(); 32 | 33 | ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 만들었다. 34 | 35 | ps.executeUpdate(); 36 | } catch(SQLException e) 37 | ... 38 | } 39 | 40 | private PreparedStatement makeStatement(Connection c) throws SQLException { 41 | PreparedStatement ps; 42 | ps = c.prepareStatement("delete from users"); 43 | return ps; 44 | } 45 | ``` 46 | 47 | 메소드 추출은 변하는 부분을 메소드로 빼는 것이다. 하지만 보통 메소드 추출 리펙토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문에 당장 이득이 없어 보이게 된다. 48 | 49 | 50 | 51 | #### 템플릿 메소드 패턴의 적용 52 | 53 | 템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다. 54 | 55 | ``` 56 | abstract protected PreparedStatement makeStatement(Connection c) throws SQLException; 57 | ``` 58 | 59 | ``` 60 | public class UserDaoDeleteAll extends UserDao { 61 | 62 | protected PreparedStatement makeStatement(Connection C) throws SQLException { 63 | PreparedStatment ps = c.prepareStatement("delete from users"); 64 | return ps; 65 | } 66 | } 67 | ``` 68 | 69 | 템플릿 메소드 패턴 적용을 통해 UserDao 클래스의 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있고, 확장 때문에 기존의 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙(OCP)을 어느정도 지킬 수 있지만 , 가장 큰 문제는 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다. 70 | 71 | 변하지 않는 코드를 가진 UserDao의 JDBC try/catch/finally 블록과 변하는 PreparedStatement를 담고 있는 서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있다. 따라서 그 관계에 대한 유연성이 떨어진다. 72 | 73 | 74 | 75 | #### 전략 패턴의 적용 76 | 77 | 개방 폐쇄 원칙(OCP)을 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다. 전략 패턴은 OCP 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다. 78 | 79 | deleteAll()은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 맥락(context)을 갖는다. deleteAll()의 컨텍스트를 정리해보면 다음과 같다. 80 | 81 | - DB 커넥션 가져오기 82 | - PreparedStatement를 만들어줄 외부 기능 호출하기 83 | - 전달받은 PreparedStatement 실행하기 84 | - 예외가 발생하면 이를 다시 메소드 밖으로 던지기 85 | - 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기 86 | 87 | 두번째 작업에서 사용하는 PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다. 전략 패턴의 구조를 따라 이 기능을 인터페이스로 만들어두고 인터페이스 이 메소드를 통해 PreparedStatement 생성 전략을 호출해주면 된다. 여기서 눈여겨볼 것은 이 PreparedStatement를 생성하는 전략을 호출할 때는 이 컨텍스트 내에서 만들어둔 DB 커넥션을 전달해야 한다는 점이다. 88 | 89 | PreparedStatement를 만드는 전략의 인터페이스는 컨텍스트가 만들어준 Connection을 전달받아서, PreparedStatement를 만들고 만들어진 PreparedStatement 오브젝트를 돌려준다. 이 내용을 인터페이스로 정의하면 다음과 같다. 90 | 91 | ``` 92 | public interface StatementStrategy { 93 | PreparedStatement makePreparedStatement(Connection c) throws SQLException; 94 | } 95 | ``` 96 | 97 | StatementStrategy 인터페이스를 상속해서 실제 전략 클래스를 만들고 이 전략 클래스를 이용한 전략 패턴을 적용한 코드는 다음과 같다. 98 | 99 | ``` 100 | public void deleteAll() throws SQLException { 101 | ... 102 | try { 103 | c = dataSource.getConnection(); 104 | 105 | StatementStrategy strategy = new DeleteAllStatement(); 106 | ps = strategy.makePreparedStatement(c); 107 | 108 | ps.executeUpdate(); 109 | } catch (SQLException e) { 110 | ... 111 | } 112 | } 113 | ``` 114 | 115 | 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있다는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 컨텍스트가 StatementStrategy 인터페이스 뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없는 문제가 발생한다. 116 | 117 | 118 | 119 | #### DI 적용을 위한 클라이언트/컨텍스트 분리 120 | 121 | 전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는게 일반적이다. Client가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 Context에 전달하는 것이다. 122 | 123 | 결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 ObjectFactory이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)이었다. 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다. 124 | 125 | 이 패턴 구조를 코드에 적용하는데 중요한 것은 컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시켜야 한다는 것이다. 126 | 127 | ``` 128 | public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { 129 | connection c = null; 130 | PreparedStatement ps = null; 131 | 132 | try { 133 | c = dataSource.getConnection(); 134 | 135 | ps = stmt.makePreparedStatement(c); 136 | 137 | ps.executeUpdate(); 138 | } catch (SQLException e) { 139 | throw e; 140 | } finally { 141 | if (ps != null) { try { ps.close(); } catch (SQLException e) {} } 142 | if (c != null) { try { c.close(); } catch (SQLException e) {} } 143 | } 144 | } 145 | ``` 146 | 147 | 이 메소드는 컨텍스트의 핵심적인 내용을 잘 담고 있다. 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다. 148 | 149 | 다음은 클라이언트에 해당하는 부분이다. 컨텍스트를 별도의 메소드로 분리했으니 deleteAll() 메소드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다. 150 | 151 | ``` 152 | public void deleteAll() throws SQLException { 153 | StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성 154 | jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달 155 | } 156 | ``` 157 | 158 | 클라이언트롸 컨텍스트는 클래스를 분리하지 않았지만, 의존관계와 책임으로 볼 때 이상적인 클라이언트/컨텍스트 관계를 갖고 있으며, 클라이언트가 컨텍스트가 사용할 전략을 정해서 전달하는 면에서 DI 구조라고 이해할 수도 있다. 159 | 160 | > **마이크로 DI** 161 | > 162 | > 의존관계 주입(DI)은 다양한 형태로 적용할 수 있다. DI의 가장 중요한 개념은 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다. 163 | > 164 | > 일반적으로 DI는 의존관계에 있는 두 개의 오브젝트와 이 관계를 다이내믹하게 설정해주는 오브젝트 팩토리(DI 컨테이너), 그리고 이를 사용하는 클라이언트라는 4개의 오브직트 사이에서 일어난다. 하지만 때로는 원시적인 전략패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다. 165 | > 166 | > 이런 경우에는 DI가 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다. 이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI 또는 코드에 의한 의미로 수동 DI라고 부른다. 167 | 168 | 169 | 170 | ## 3.3 JDBC 전략 패턴의 최적화 171 | 172 | ### 전략과 클라이언트의 동거 173 | 174 | 지금까지 개선한 작업의 경우 다음과 같은 개선사항이 발생하게 된다. 175 | 176 | - DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 것. 177 | 178 | - DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다는 것. 179 | 180 | 181 | 182 | #### 로컬 클래스 183 | 184 | 클래스 파일이 많아지는 문제를 해결하는 간단한 방법은 StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao 클래스 안에 내부 클래스로 정의해버리는 것이다. 185 | 186 | > **중첩 클래스의 종류** 187 | > 188 | > 다른 클래스 내부에 정의되는 클래스를 중첩 클래스(nested class)라고 한다. 중첩 클래스는 독립적으로 오브젝트로 만들어질 수 있는 스태틱 클래스(static class)와 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 내부 클래스(inner class)로 구분된다. 189 | > 190 | > 내부 클래스는 다시 범위(scope)에 따라 세가지로 구분된다. 191 | > 192 | > - 멤버 내부 클래스 : 멤버 필드처럼 오브젝트 레벨에 정의된다. 193 | > - 로컬 클래스 : 메소드 레벨에 정의된다. 194 | > - 익명 내부 클래스 : 이름을 갖지 않는 익명 클래스이다. 익명 내부 클래스의 범위는 선언된 위치에 따라서 다르다. 195 | 196 | 로컬 클래스의 장점은 클래스가 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근할 수 있다는 것이다. 내부 메소드는 자신이 정의된 메소드의 로컬 변수에 직접 접근할 수 있기 때문이다. 다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해줘야 한다. 197 | 198 | 199 | 200 | #### 익명 내부 클래스 201 | 202 | > **익명 내부 클래스** 203 | > 204 | > 익명 내부 클래스(anonymous inner class)는 이름을 갖지 않는 클래스다. 클래스 선언과 오브젝트 생성이 결합된 상태로 만들어지며, 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 다음과 같은 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다. 205 | > 206 | > new 인터페이스이름() { 클래스 본문 }; 207 | 208 | 익명 내부 클래스는 선언과 동시에 오브젝트를 생성한다. 이름이 없기 때문에 클래스 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장할 수 있다. 209 | 210 | 211 | 212 | ## 3.4 컨텍스트와 DI 213 | 214 | ### 클래스 분리 215 | 216 | 분리해서 만들 클래스의 이름을 JdbcContext라고 하자. JdbcContext에 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다. 그런데, 이렇게 하면 DataSource가 필요한 것은 UserDao가 아니라 JdbcContext가 돼버린다. DB 커넥션을 필요로 하는 코드는 JdbcContext 안에 있기 때문이다. 따라서 JdbcContext가 DataSource에 의존하고 있으므로 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다. 217 | 218 | 219 | 220 | ### 빈 의존관계 변경 221 | 222 | 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 없다. 따라서 인터페이스를 구현하지 않고, 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다. 223 | 224 | 225 | 226 | ### 스프링 빈으로 DI 227 | 228 | 인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유는 다음과 같다. 229 | 230 | 1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다. 231 | 2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다. JdbcContext는 dataSource 프로퍼티를 통해 JdbcContext 오브젝트를 주입받도록 되어 있다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다. 스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 때문이다. 232 | 233 | 인터페이스가 없다는 건 두 클래스 간의 매우 긴밀한 관계가 존재하고 강하게 결합되어있다는 의미이다. 234 | 235 | 236 | 237 | ## 3.5 템플릿과 콜백 238 | 239 | 전략 패턴은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합한 구조다. 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다. 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다. 240 | 241 | > **템플릿** 242 | > 243 | > 템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로 이뤄진다. 244 | > 245 | > **콜백** 246 | > 247 | > 콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 자바에서는 메소드 자체를 파라미터로 전달할 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 펑서녈 오브젝트(functional object)라고도 한다. 248 | 249 | 250 | 251 | ### 템플릿/콜백의 동작원리 252 | 253 | 템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름이다. 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다. 254 | 255 | 256 | 257 | #### 템플릿/콜백의 특징 258 | 259 | 여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다. 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면된다. 260 | 261 | 템플릿/콜백 패턴의 일반적인 작업 흐름은 다음과 같다. 262 | 263 | - 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공하는 것이다. 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다. 264 | - 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메소드를 호출한다. 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다. 265 | - 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다. 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다. 266 | 267 | 일반적인 DI라면 템플릿에 인스턴스 변수를 만들어두고 사용할 의존 오브젝트를 수정자 메소드로 받아서 사용하지만 템플릿/콜백 방식에서는 매소드 단위로 오브젝트를 전달받는 것이 특징이다. 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다. 268 | 269 | 270 | 271 | ### 편리한 콜백의 재활용 272 | 273 | 템플릿/콜백 방식에서 한 가지 아쉬운 점은 DAO 메소드에서 매번 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다는 점이다. 재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다. 274 | 275 | 276 | 277 | ### 템플릿/콜백의 응용 278 | 279 | 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 길러야한다. 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 통해 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 사이에 두고 분리해서 전략패턴을 적용하고 DI로 의존관계를 관리하도록 만든다. 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 이번엔 템플릿/콜백 패턴을 적용하는 것을 고려해볼 수 있다. 280 | 281 | 가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다. 282 | 283 | 템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 가장 중요하다. 그에 따라 콜백의 인터페이스를 정의해야 하기 때문이다. 284 | 285 | 클래스 이름이 Template으로 끝나거나 인터페이스 이름이 Callback으로 끝난다면 템플릿/콜백이 적용된 것이라고 보면 된다. 286 | 287 | 288 | 289 | ## 3.6 스프링의 JDBCTEMPLATE 290 | 291 | 스프링은 JDBC 코드 작성을 위해 JdbcTemplate 을 기반으로 하는 다양한 템플릿과 콜백을 제공한다. 292 | 293 | ### 테스트 보완 294 | 295 | 성공적인 테스트 결과를 보면 다음 기능으로 넘어가고 싶어지지만 현명한 개발자가 되기 위해서는 예외사항에 대한 테스트인 네거티브 테스트를 항상 생각해야한다. 296 | 297 | 정상적인 조건의 테스트부터 만들면 테스트가 성공하는 것을 보고 쉽게 만족해서 예외적인 상황은 빼먹고 넘어가기 쉽기 때문에 미리 예외상황에 대한 일관성 있는 기준을 정해두고 이를 테스트로 만들어 검증하는 작업이 필요하다. 298 | 299 | 300 | 301 | ## 3.7 정리 302 | 303 | - JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다. 304 | - 일정한 작업 흐름이 반복되면서 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분을 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다. 305 | - 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다. 306 | - 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면 클래스를 분리해서 공유하도록 만든다. 307 | - 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다. 308 | - 콜백의 코드에도 일정한 패천이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다. 309 | - 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭을 이용한다. 310 | - 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다. 311 | - 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야한다. -------------------------------------------------------------------------------- /토비의 스프링 3.1/4장 예외/4.1 ~ 4.3.md: -------------------------------------------------------------------------------- 1 | # 잘못된 예외처리 방법 2 | 3 | - 예외를 아무것도 하지 않고 별문제가 없는 것 처럼 넘어가 버리는 것은 매우 위험한 일이다 이것은 원치않는 예외가 발생하는 것보다 훨씬 나쁜 일이다. 4 | 5 |
6 | 7 | ## 처리되지 않은 예외 8 | 9 | - 프로그램 실행 중에 어디선가 오류가 있어서 예외가 발생했는데 그것을 무시하고 계속 진행해버린다는 의미이다. 10 | - 결국 이러한 동작방식은 발생한 예외로 인하여 비정상 동작이나 메모리 리소스가 소진되거나 예상치 못한 다른 문제를 일으킬 잠재적 위험성을 가지고 있다. 11 | - 뿐만 아니라 오류나 이상한 결과의 원인이 무엇인지 찾아내기가 매우 힘들다는 점이다. 12 | 13 |
14 | 15 | ## 단순히 예외를 출력하는 처리방법 16 | 17 | ```java 18 | try { 19 | ... 20 | } catch(Exception e) { 21 | e.printStackTrace(); 22 | } 23 | ``` 24 | 25 | 이렇게 예외를 단순히 `printStackTrace` 나 `print` 를 통해 그냥 콘솔에 출력해버리는 방법도 문제가 있다. 26 | 27 | - 다른 로그나 메세지에 금방 묻혀버리면 놓치기 쉬울 뿐더러 누군가는 이를 계속 모니터링 해야한다. 28 | - 결국 누군가에 의해서 지속적으로 모니터링 되고 처리되지 않는한 이 예외는 심각한 잠재적 위험성을 가지고 남아있을 것이다. 29 | 30 | 예외에서 가장 중요한 것은 발생한 예외가 반드시 어떠한 형태로든 처리되어야 한다는 것이다. 단순히 메세지를 출력한 것은 예외를 처리한 것이 아니다. 31 | 32 | - 모든 예외는 적절하게 복구되던지 아니면 작업을 중단시키고 개발자 또는 운영자에게 통보되어야 한다. 33 | 34 |
35 | 36 | ## 무의미 무책임한 throws 37 | 38 | ```java 39 | public void method() throws Exception { 40 | ... 41 | } 42 | ``` 43 | 44 | 위와 같이 메서드 선언에 `throws Exception` 을 기계적으로 붙여 모든 예외를 무조건 던져버리는 무책임한 `throws` 선언도 문제가 있다. 45 | 46 | - 위와 같은 방법은 메서드 선언에서 예외적인 상황이 발생할 수 있는지 아니면 습관적으로 붙여넣은 것인지 의미있는 정보를 파악할 수가 없다. 47 | - 이러한 방식은 적절한 처리를 통해 복구될 수 있는 예외상황도 기회를 박탈당하게된다. 48 | 49 |
50 | 51 | # Java의 예외의 종류와 특징 52 | 53 | - Java에서 `throw` 를 통해 발생시킬 수 있는 예외는 3가지 종류가 존재한다. 54 | 55 |
56 | 57 | ## Error 58 | 59 | - 비정상적인 상황이 발생했을 경우 사용되며 주로 JVM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안된다. 60 | - 왜냐하면 OutOfMemoryError 또는 ThreadDeath와 같은 에러는 잡아봤자 애플리케이션 레벨에서 아무런 대응 방법이 존재하지 않는다. 61 | 62 |
63 | 64 | ## Exception 과 CheckedException 65 | 66 | - 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용되며 Exception은 다시 CheckedException과 UncheckedException으로 구분되어진다. 67 | - UncheckedException의 경우에는 Exception의 서브 클래스이면서 RuntimeException을 상속한 클래스들을 의미한다. 68 | - CheckedException이 발생할 수 있는 메서드를 사용할 경우 반드시 예외를 처리하는 코드를 작성해야 한다. 69 | - 만약 사용할 메서드가 CheckedException을 던진다면 이를 `catch` 를 이용해서 잡던지 아니면 `throws` 를 정의해서 메서드 밖으로 던져야한다. 70 | - IOException, SQLException 등이 대표적인 CheckedException에 속한다. 71 | 72 |
73 | 74 | ## RuntimeException 과 UncheckedException 75 | 76 | - RuntimeException을 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 UncheckedException으로 불린다. 77 | - RuntimeException은 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것들로 NullPointerException, IllegalArgumentException 등이 이에 해당된다. 78 | - 이러한 예외들은 피할 수 있지만 개발자의 부주의에 의해서 발생할 수 있는 경우에 발생하도록 설계되었으며 예상하지 못했던 예외상황에서 발생한 것이 아니기 때문에 굳이 `catch` , `throws` 를 사용하여 예외를 처리하도록 강제하지않는다. 79 | 80 | CheckedException은 예외처리를 강제하기 때문에 예외를 제대로 처리하지 않거나 무책임한 `throws` 같은 코드가 남발되었는데 최근 Java API 들은 예상가능한 예외상황을 다루는 예외들을 CheckedException으로 만들지 않는 추세이다. 81 | 82 |
83 | 84 | # 다양한 예외 처리 방법 85 | 86 | - 먼저 예외를 처리하는 일반적인 방법들에 대해서 알아보고 효과적인 예외처리 전략에 대해서 생각해보자. 87 | 88 |
89 | 90 | ## 예외 복구 91 | 92 | - 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법을 의미한다. 93 | - 예외로 인해 기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도를 하는 등의 처리가 이루어져야 한다. 94 | - 예외가 처리되었으면 기능적으로 사용자에게 예외상황으로 비추어져도 애플리케이션에서는 정상적으로 설계된 흐름을 따라 진행되어야 한다. 95 | 96 |
97 | 98 | ### 예외 복구 방법의 예시 99 | 100 | - 사용자가 요청한 파일이 없거나 다른 문제 때문에 IOException이 발생한 경우 101 | - 사용자에게 예외 상황을 알려주고 다른 파일을 이용하도록 안내해서 예외상황을 해결한다. (다른 작업 흐름으로 자연스럽게 유도하는 처리방식) 102 | - 네트워크가 불안정해서 서버 접속이 잘 되지않는 열악한 환경의 시스템에서 원격 데이터베이스 서버에 접속하다 실패해서 SQLException이 발생한 경우 103 | - 네트워크 접속이 원할하지 않아서 SQLException이 발생하는 경우 일정 시간 대기했다가 다시 시도해보는 방법을 통해 예외상황으로부터 복구를 시도한다. 104 | - 만약 정해진 횟수 이상 재시도해서 실패하는 경우 예외복구를 포기하도록 한다. (예외에 대한 적절한 처리를 시도해보도록 요구하는 방법) 105 | 106 | 즉, 예외 복구와 같이 예외처리 코드를 강제하는 CheckedException의 경우 예외를 어떤식으로든 복구할 가능성이 있는 경우에 사용한다. 107 | 108 |
109 | 110 | ## 예외 회피 111 | 112 | - 예외처리를 자신이 수행하지않고 자신을 호출한 쪽으로 던져버리는 방법으로 `throws` 를 선언해서 예외가 발생하면 알아서 던져지게 하거나 `catch` 로 잡은다음 로그를 남기고 다시 예외를 던지는 방식으로 구현할 수 있다. 113 | - 예외처리를 회피하려면 반드시 다른 오브젝트나 메서드가 예외를 대신 처리할 수 있도록 던져주어야 한다. 114 | - 예외를 그냥 던져버리는 것은 무책임한 회피가 될 수 있기 때문에 복구와 마찬가지로 의도가 분명해야 한다. 115 | - 템플릿 콜백 패턴처럼 긴밀한 관계에있는 다른 오브젝트에게 예외처리 책임을 분명하게 지게하거나 자신을 사용하는 쪽에서 예외를 다루게 하는 것이 최선이라는 확신이 있어야 한다. 116 | 117 |
118 | 119 | ### 예외 회피 방법의 예시 120 | 121 | - JdbcContext, JdbcTemplate 등이 사용하는 콜백 오브젝트는 SQLException을 자신이 처리하지 않고 템플릿으로 던져버린다. 122 | - SQLException을 처리하는 일은 콜백 오브젝트의 일이 아니라고 보기 때문에 SQLException에 대한 예외를 회피하고 템플릿 레벨에서 처리하도록 한다. 123 | 124 |
125 | 126 | ## 예외 전환 127 | 128 | - 예외 전환은 회피와 마찬가지로 예외를 복구해서 정상적으로 만들 수 없기 때문에 메서드 밖으로 예외를 던지는 것이다. 129 | - 단 예외를 회피하는 방법과 구별되는 특징은 예외를 그대로 던지는 것이 아니라 적절한 예외로 전환해서 던진다는 특징이 있다. 130 | - 예외 전환을 사용하는 이유에는 여러가지가 있는데 보통 두가지 목적으로 사용된다. 131 | - 내부에서 발생한 예외를 그대로 던지는 것이 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서 132 | - 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하려는 경우 중첩된 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부로 담아서 던지는 방식으로 주로 예외처리를 강제하는 CheckedException을 UncheckedException으로 바꾸기 위해서 133 | 134 |
135 | 136 | ### 예외 전환 방법의 예시 137 | 138 | - 사용자 등록시 중복되는 아이디가 존재해서 데이터베이스 에러가 발생하여 JDBC API가 SQLException이 발생시킨 경우 (적절한 의미를 부여해주지 못하는 예외를 의미가 분명한 예외로 바꾸어 전달하기 위해서) 139 | - DAO의 메서드가 SQLException을 그대로 밖으로 던져버린다면 서비스 계층에서는 왜 SQLExceptio이 발생했는지 쉽게 알 방법이 없다. 140 | - 아이디 중복의 경우 충분히 예상가능하고 복구 가능한 예외상황이기 때문에 SQLException을 해석해서 DuplicateUserIdException 같은 예외로 바꾸어 던져줄 수 있다. 141 | - 이러한 경우 서비스 계층에서 적절한 복구 작업을 시도하는 것이 가능하다. 142 | - CheckedException인 SQLException의 대부분은 애플리케이션 레벨에서 복구가 불가능한 경우가 대부분이다. (CheckedException을 UnCheckedException으로 전환하기 위해서) 143 | - 어짜피 복구가 불가능한 예외라면 가능한 빨리 RuntimeExcption으로 포장해서 던지게해서 다른 계층의 메서드를 작성할 때 불필요한 `throws` 선언이 들어가지 않도록 해주어야한다. 144 | - 대부분의 서버 환경에서 애플리케이션 코드에서 처리하지 않고 전달된 예외들을 일괄적으로 다룰 수 있는 기능을 제공하며 어짜피 복구하지 못할 예외라면 RuntimeException으로 포장해서 던져버리고 예외처리 서비스 등을 이용해 로그를 남기고 관리자에게는 메일로 사용자에게는 안내 메세지로 보여주는 식으로 처리하는 것이 바람직하다. 145 | 146 | 애플리케이션 코드에서 의도적으로 던지는 예외 즉, 비즈니스적인 의미가 있는 예외는 이에 대한 적절한 대응이나 복구 작업이 필요하기 때문에 CheckedException을 사용하는 것이 적절하지만 일반적으로 CheckedException을 계속해서 `throws` 를 사용해 넘기는 것은 무의미하므로 처리하지 못할 예외라면 가능하면 빠르게 RuntimeException으로 전환해서 다른 계층으로 예외가 확산되지 않게 하는 것이 좋다. 147 | 148 |
149 | 150 | # 서버 환경에서 예외를 효과적으로 처리하는 방법 151 | 152 | - 일반적으로 CheckedException이 일반적인 예외를 다루고 UncheckedException은 시스템 장애나 프로그램상의 오류에 사용된다. 153 | - CheckedExcpeiton은 복구할 가능성이 조금이라도 존재하기 때문에 Java에서는 이를 처리하는 `catch` 나 `throws` 선언을 강제하고 있다. 154 | - 하지만 Java 엔터프라이즈 서버 환경은 조금 다르다 수많은 사용자가 동시에 요청을 보내거나 각 요청이 독립적인 작업으로 취급되기 때문에 하나의 요청을 처리하는 중에 예외가 발생하면 해당 작업만 중지시키면 그만이다. 155 | - 서버는 특정 계층에서 예외가 발생했을 경우 작업을 일시 중지하고 사용자와 커뮤니케이션을 통해 예외상황을 복구할 수 있는 방법이 존재하지 않는다. 156 | - 때문에 애플리케이션 차원에서 예외상황을 미리 파악하고 발생하지 않도록 차단하는 것이 좋고 외부 환경으로 인한 예외라면 요청을 취소하고 관리자에게 통보해주는 것이 좋다. 157 | - 즉, 서버 환경으로 이동하면서 점차 CheckedException의 활용도와 가치가 줄어들고 있다는 것으로 대응이 불가능한 CheckedException이라면 가능한 빠르게 RuntimeException으로 전환해서 던지는 것이 낫다. 158 | 159 |
160 | 161 | ## 애플리케이션에서의 예외 162 | 163 | - 애플리케이션 예외는 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고 반드시 `catch` 해서 무엇인가 조치를 취하도록 요구하는 예외이다. 164 | 165 |
166 | 167 | ### 은행계좌에서 출금하는 기능에 대한 예외처리 168 | 169 | - 계좌에서 출금하는 기능의 경우 현재 잔고를 확인해서 허용하는 범위 외의 출금을 요청하는 경우 작업을 중단시키고 적절한 경고를 사용자에게 보내야한다. 170 | - 정상 상태와 비정상 상태의 결과값을 다르게 반환하도록 설계 171 | - 명확한 설계 기준이 없으면 혼란이 생길 수 있으며 표준이 없다면 의사소통 문제 및 결과값을 확인하는 조건문이 자주 등장하여 코드가 지저분해진다. 172 | - 예외상황에만 비즈니스적 의미를 가지는 예외를 던지도록 한다. 173 | - 예외를 만드는 코드와 처리하는 코드를 분리할 수 있으며 이러한 경우 번거로운 조건문을 반복하지 않아도 되기 때문에 설계가 단순해진다. 174 | 175 |
176 | 177 | # SpringFramework에서 SQLException 처리 전략 178 | 179 | - 가장 먼저 고려해야할 점은 SQLException이 과연 복구가 가능한 예외인가라는 점이다. 180 | - SQLException은 거의 대부분의 경우에 코드 레벨에서 복구할 방법이 존재하지 않는다 프로그램의 오류 또는 개발자의 부주의 때문에 발생하거나 통제할 수 없는 외부상황 때문에 발생하는 경우가 대부분이다. 181 | - 이와 같은 상황들은 SQL 문법이 틀렸거나 제약 조건위반, 데이터베이스 서버의 장애 및 네트워크 불안정, 데이터베이스 커넥션 풀의 포화로 인한 커넥션을 가져올 수 없는 경우 등이 해당한다. 182 | - 대부분의 SQLException은 복구가 불가능하기 때문에 가능한 빠르게 RuntimeException으로 전환해주어야 한다. 183 | - JdbcTemplate은 이러한 예외처리 전략을 잘 따르고 있는데 템플릿과 콜백 안에서 발생하는 모든 SQLException은 RuntimeException인 DataAccessException으로 포장해서 던져준다. 184 | 185 |
186 | 187 | ## JDBC의 한계점 188 | 189 |
190 | 191 | ### 비표준 SQL 구문 192 | 193 | - SQL은 어느정도 표준화된 언어이고 표준 규약이 존재하지만 대부분의 데이터베이스는 표준을 따르지 않는 비표준 문법과 기능이 존재한다. 194 | - 비표준 SQL은 결국 특정 데이터베이스에 종속적인 코드를 만들게 되는데 이는 곧 다른 데이터베이스로 변경을 시도할 경우 많은 부분을 수정해야함을 의미한다. 195 | 196 |
197 | 198 | ### 호환성 없는 SQLException 데이터베이스 에러 정보 199 | 200 | - SQLException이 발생할 수 있는 원인은 수백가지가 있는데 JDBC는 데이터 처리중에 발생하는 예외를 모두 SQLException에 담아서 던진다. 201 | - 예외정보를 확인하기 위해서는 에러코드와 SQL 상태정보를 모두 확인해야하는데 데이터베이스마다 서로다른 고유한 에러코드를 사용하기 때문에 이와 같은 방법은 특정 데이터베이스에 종속적인 코드를 만들게 된다. 202 | - JDBC에서는 특정 상태코드를 제공하여 표준화하려고 하지만 각 데이터베이스 벤더에서 이를 정확하게 만들어주지는 않는다. 203 | - 때문에 SQL 상태코드만 믿고 결과를 파악하는 것은 위험하며 SQLException 만으로는 데이터베이스에 독립적이고 유연한 코드를 작성하는 것이 불가능하다. 204 | 205 |
206 | 207 | ## DataAccessException 예외 전환 208 | 209 | - 예외 전환의 목적은 위에서 언급한 것 처럼 RutnimeException으로 포장하기 위함과 의미있고 추상화된 예외로 바꾸어서 던져주기 위함이다. 210 | - DataAccessException은 RuntimeException으로 SQLException을 포장해주는 역할을 할 뿐만 아니라 상세한 예외정보를 의미있고 일관성있는 예외로 전환해서 추상화해주려는 용도로 사용된다. 211 | - DataAccessException은 SQLException을 대체할 수 있는 RuntimeException을 정의하고있을 뿐만 아니라 서브 클래스로 세분화된 예외 클래스들을 정의하고있다. 212 | - 데이터 엑세스 상황 중에 발생할 수 있는 예외상황을 수십가지로 분류하고 이를 추상화해서 정의한 다양한 예외 클래스를 제공하며 데이터베이스별 에러코드를 분류해서 스프링이 정의한 예외 클래스와 매핑하는 매핑정보 테이블을 이용하고있다. 213 | - DataAccessException은 데이터 엑세스 기술에 상관없이 일관된 예외가 발생하도록 만들어주기 때문에 데이터 엑세스 기술에 독립적인 추상화된 예외를 제공한다. 214 | - 이러한 예외 추상화는 JDBC, JPA, Hibernate 등에 상관없이 데이터 엑세스 기술에 관계없이 비슷한 성격의 예외를 추상화된 예외로 던져주게되기 때문에 일관성 있는 예외를 던질 수 있도록 해준다. 215 | 216 |
217 | 218 | ### DataAccessException의 한계점 219 | 220 | - 데이터 엑세스 기술에 따라서 세분화된 정도가 다르기 때문에 좀 더 디테일한 예외 대신에 포괄적인 예외로 처리할 수 밖에 없다. 221 | - 때문에 다른 상황에서도 동일한 예외가 발생할 가능성이 있기 때문에 예외에 대한 이용가치가 떨어지게된다. 222 | -------------------------------------------------------------------------------- /토비의 스프링 3.1/5장 서비스 추상화/5.1 ~ 5.2.md: -------------------------------------------------------------------------------- 1 | # 토비의 스프링 3.1 - 5장 서비스 추상화 2 | 3 | 5장에서는 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 이를 일관된 방법으로 사용할 수 있도록 지원하는지 살펴봅니다. 4 | 5 | --- 6 | ## 5.1 사용자 레벨 관리 기능 추가 7 | 8 | 5.1의 내용은 주로 5장에서의 새로운 개념을 다루기 보단, 새로운 요구사항에 따른 기능을 추가구현 하는 과정(코딩)과 9 | 이후에 어떻게 테스트하고 리팩토링할 수 있는지 작성되어 있습니다. 10 | 11 | 코드위주의 설명이 많은 관계로, 이 부분의 전반적인 내용은 책과 코드를 참고하는 편이 이해에 도움이 될 것 같습니다. 12 | 13 | ### 5.1.5 코드 개선 14 | 15 | 작성된 코드를 살펴볼 때는 다음과 같은 질문을 해볼 필요가 있다. 16 | - 코드에 중복된 부분은 없는가? 17 | - 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가? 18 | - 코드가 자신이 있어야 할 자리에 있는가? 19 | - 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가? 20 | 21 | 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다. 22 | 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리이다. 23 | 24 | '이렇게 만들면 코드를 더 이해하기 쉽고 변화에 대응하기 편하구나' 라고 생각하면 좋겠다. 25 | 26 | 항상 코드를 더 깔끔하고 유연하면서 변화에 대응하기 쉽고 테스트하기 좋게 만들려고 노력해야 함을 기억해야한다. 27 | 28 | --- 29 | 30 | ## 5.2 트랜잭션 서비스 추상화 31 | 32 | 트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다. 33 | 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 핵심 속성인 원자성을 의미한다. 34 | 35 | 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다. 이것이 바로 트랜잭션이다. 36 | 37 | ### 5.2.2 트랜잭션 경계설정 38 | 39 | 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다고 믿을 수 있다. 40 | 하지만 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야 하는 경우도 있다. 41 | 트랜잭션을 설명할 때 자주 언급되는 계좌이체라든가 이 장에서 만든 여러 사용자에 대한 레벨 수정 작업 등이 그렇다. 42 | 은행 시스템의 계좌이체 작업은 반드시 하나의 트랜잭션으로 묶여서 일어나야 한다. 43 | 44 | 두 가지 작업이 하나의 트랜잭션이 되려면, 두번째 SQL이 성공적으로 DB에서 수행되기 전에 문제가 발생할 경우에는 45 | 앞에서 처리한 SQL 작업도 취소시켜야 한다. 이런 취소 작업을 **트랜잭션 롤백**이라고 한다. 46 | 47 | 반대로 여러 개의 SQL을 하나의 트랜잭션으로 처리하는 경우에 모든 SQL 수행 작업이 다 성공적으로 마무리됐다고 48 | DB에 알려줘서 작업을 확정시켜야 한다. 이것을 **트랜잭션 커밋**이라고 한다. 49 | 50 | ### JDBC 트랜잭션의 트랜잭션 경계설정 51 | 52 | 모든 트랜잭션은 시작하는 지점과 끝나는 지점이 있다. 53 | 시작하는 방법은 한 가지이지만 끝나는 방법은 두 가지다. 54 | 모든 작업을 무효화하는 **롤백**과 모든 작업을 다 확정하는 **커밋**이다. 55 | 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 **트랜잭션의 경계**라고 부른다. 56 | 57 | 58 | *JDBC를 이용해 트랜잭션을 적용하는 간단한 예제(트랜잭션 처리 부분에 초점을 맞춰 간략하게 만든 코드라 Connection, PreparedStatement를 처리하는 일부분은 생략)* 59 | 60 | ```java 61 | Connection c = dataSource.getConnection(); 62 | 63 | c.setAutoCommit(false); // 트랜잭션 시작 64 | try { 65 | PreparedStatement st1 = c.prepareStatement("update users ..."); 66 | st1.executeUpdate(); 67 | 68 | 69 | PreparedStatement st2 = c.prepareStatement("delete users ..."); 70 | st2.executeUpdate(); 71 | 72 | c.commit(); // 트랜잭션 커밋 73 | } 74 | catch(Exception e) { 75 | c.rollback(); // 트랜잭션 롤백 76 | } 77 | 78 | c.close(); 79 | ``` 80 | 81 | - JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 82 | 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄지기 때문이다. 83 | 84 | - JDBC의 기본설정은 DB 작업을 수행한 직후에 자동으로 커밋이 되도록 되어 있다. 85 | 작업마다 커밋해서 트랜잭션을 끝내버리므로 여러 개의 DB 작업을 모아서 트랜잭션을 만드는 기능이 꺼져 있는 것이다. 86 | JDBC에서는 이 기능(자동커밋 옵션)을 false로 설정해주면 새로운 트랜잭션이 시작되게 만들 수 있다. 87 | 88 | - 일반적으로 작업 중에 예외가 발생하면 트랜잭션을 롤백한다. 89 | 예외가 발생했다는 건, 트랜잭션을 구성하는 데이터 엑세스 작업을 마무리할 수 없는 상황이거나 DB에 결과를 반영하면 안 되는 이유가 생겼기 때문이다. 90 | 91 | setAutoCommit(false)로 트랜잭션의 시작을 선언하고 commit() 또는 rollback() 으로 트랜잭션을 종료하는 작업을 **트랜잭션 경계설정**이라고 한다. 92 | 93 | **트랜잭션의 경계**는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재하는데 이렇게 하나의 DB Connection 안에서 만들어지는 트랜잭션을 **로컬 트랜잭션** 이라고도 한다. 94 | 95 | ### UserService와 UserDao의 트랜잭션 문제 96 | JDBC의 트랜잭션 경계설정 메소드는 모두 Connection 오브젝트를 사용하게 되어 있는데, 97 | JdbcTemplate을 사용하면 템플릿 메소드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫히는 일까지 일어나기 때문에 98 | JdbcTemplate을 사용하는 UserDao는 각 메소드마다 하나씩의 독립적인 트랜잭션으로 실행될 수 밖에 없다. 99 | 100 | 이렇다는건 결국 DAO를 사용하면 비즈니스 로직을 담고 있는 UserService 내에서 진행되는 여러 가지 작업을 하나의 트랜잭션으로 묶는 일이 불가능해진다. 101 | 102 | 트랜잭션은 Connection 오브젝트 안에서 만들어지기 때문에 어떤 일련의 작업이 하나의 트랜잭션으로 묶이려면 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용돼야 하기 때문이다. 103 | 104 | ### 비즈니스 로직 내의 트랜잭션 경계설정 105 | UserService와 UserDao를 그대로 둔 채로 UserService의 upgradeLevels() 메소드 일련의 작업을 하나의 트랜잭션으로 묶으려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다. 106 | upgradeLevels() 메소드의 시작과 함께 트랜잭션이 시작하고 메소드를 빠져나올 때 트랜잭션이 종료돼야 하기 때문이다. 107 | 108 | 메소드를 시작할때 Connection을 생성하고 이를 필요로 하는 모든 곳에 Connection 오브젝트를 파라미터로 전달해서 사용할 수 있다. 109 | 110 | *Connection을 공유하도록 수정한 UserService 메소드* 111 | 112 | ```java 113 | import java.sql.Connection; 114 | 115 | class UserService { 116 | public void upgradeLevels() throws Exception { 117 | Connection c = ...; 118 | ... 119 | try { 120 | ... 121 | upgradeLevels(c, user); 122 | } 123 | } 124 | 125 | protected void upgradeLevel(Connection c, User user) { 126 | user.upgradeLevel(); 127 | userDao.update(c, user); 128 | } 129 | } 130 | 131 | interface UserDao { 132 | public update(Connection c, User user); 133 | ... 134 | } 135 | ``` 136 | 137 | ### UserService 트랜잭션 경계설정의 문제점 138 | 1. DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다. 139 | 2. DAO의 메소드와 비즈니스 로직을 담고 있는 UserService의 메소드에 Connection 파라미터가 추가돼야 한다. 140 | - 트랜잭션이 필요한 작업에 참여하는 UserService의 메소드는 Connection 파라미터로 지저분해질 것 이다. 141 | 3. Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao는 더 이상 데이터 엑세스 기술에 독립적일 수가 없다. 142 | 4. DAO 메소드에 Connection 파라미터를 받게 하면 이전에 작성했던 테스트 코드에도 영향을 미친다. 143 | 144 | ### 5.2.3 트랜잭션 동기화 145 | 스프링은 위에서 살펴본 문제점(딜레마)을 해결할 수 있는 멋진 방법을 제공해준다. 146 | 147 | ### Connection 파라미터 제거 148 | UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 **트랜잭션 동기화** 방법을 사용할 수 있다. 149 | 150 | 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다. 151 | 152 | 이렇게 트랜잭션 동기화 방법을 적용하기만 하면, 더 이상 로직을 담은 메소드에 Connection 타입의 파라미터가 전달될 필요도 없고, UserDao의 인터페이스에도 일일이 JDBC 인터페이스인 Connection을 사용한다고 노출할 필요가 없다. 153 | 154 | ### 트랜잭션 동기화 적용 155 | 스프링은 JdbcTemplate과 더불어 이런 트랜잭션 동기화 기능을 지원하는 간단한 유틸리티 메소드를 제공하고 있다. 156 | 157 | *트랜잭션 동기화 방식을 적용한 UserService* 158 | ```java 159 | private DataSource dataSource; 160 | 161 | // Connection을 생성할 때 사용할 DataSource를 DI 받도록 함 162 | public void setDataSource(DataSource dataSource) { 163 | this.dataSource = dataSource; 164 | } 165 | 166 | public void upgradeLevels() throws Exception { 167 | // 트랜잭션 동기화 관리자를 이용해 동기화 작업을 초기화한다. 168 | TransactionSynchronizationManager.initSynchronization(); 169 | 170 | // DB 커넥션을 생성하고 트랜잭션을 시작한다. 171 | // 이후의 DAO 작업은 모두 여기서 시작한 트랜잭션 안에서 진행된다. 172 | // (DB 커넥션 생성과 동기화를 함께 해주는 유틸리티 메소드) 173 | Connection c = DataSourceUtils.getConnection(dataSource); 174 | c.setAutoCommit(false); 175 | 176 | try { 177 | List users = userDao.getAll(); 178 | for (User user : users) { 179 | if (canUpgradeLevel(user)) { 180 | upgradeLevel(user); 181 | } 182 | } 183 | c.commit(); // 정상적으로 작업을 마치면 트랜잭션 커밋 184 | } catch (Exception e) { 185 | c.rollback(); 186 | throw e; 187 | } finally { 188 | // 스프링 유틸리티 메소드를 이용해 DB 커넥션을 안전하게 닫는다. 189 | DataSourceUtils.releaseConnection(c, dataSource); 190 | 191 | // 동기화 작업 종료 및 정리 192 | TransactionSynchronizationManager.unbindResource(this.dataSource); 193 | TransactionSynchronizationManager.clearSynchronization(); 194 | } 195 | } 196 | ``` 197 | 198 | 스프링이 제공하는 트랜잭션 동기화 관리 클래스는 **TransactionSynchronizationManager**다. 199 | 200 | 해당 클래스를 사용할 때 작업 순서는 이러하다. 201 | 1. 트랜잭션 동기화 작업을 초기화하도록 요청 202 | 2. DB 커넥션을 생성 203 | 3. 트랜잭션 동기화에 사용하도록 저장소에 바인딩 204 | 4. DAO의 메소드를 사용하는 트랜잭션 내의 작업을 진행 205 | 5. 일련의 작업은 처음에 만든 Connection 오브젝트를 사용하고 같은 트랜잭션에 참여 206 | 6. 작업을 정상적으로 마치면 트랜잭션을 커밋 207 | 7. 커넥션을 닫고 트랜잭션 동기화를 마치도록 요청 208 | 8. 만약 예외가 발생하면 트랜잭션을 롤백 209 | - 이때도 7번 작업(DB 커넥션을 닫는 것과 동기화 작업 중단)은 동일하게 진행 210 | 211 | 이렇듯 JDBC의 트랜잭션 경계설정 메소드를 사용해 트랜잭션을 이용하는 전형적인 코드에 간단한 트랜잭션 동기화 작업만 붙여줌으로써, 지저분한 Connection 파라미터의 문제를 깔끔히 해결할 수 있다. 212 | 213 | ### JdbcTemplate과 트랜잭션 동기화 214 | JdbcTemplate은 미리 생성된 DB 커넥션이나 트랜잭션이 없는 경우엔 직접 DB 커넥션을 만들고 트랜잭션을 시작하고 작업을 진행한다. 215 | 216 | 반면에 미리 트랜잭션 동기화를 시작해놓았다면 저장소에 있는 DB 커넥션을 가져와서 사용하고 이를 통해 이미 시작된 트랜잭션에 참여하는 것이다. 217 | 218 | 따라서 JdbcTemplate 을 사용하는 DAO 에서 트랜잭션이 굳이 필요 없다면 바로 호출해서 사용해도 되고, 219 | DAO 외부에서 트랜잭션을 만들고 이를 관리할 필요가 있다면 미리 DB 커넥션을 생성한 다음 트랜잭션 동기화를 해주고 사용하면 된다. 220 | 221 | ### 5.2.4 트랜잭션 서비스 추상화 222 | 223 | ### 기술과 환경에 종속되는 트랜잭션 경계설정 코드 224 | 한 개 이상의 DB로의 작업을 하나의 트랜잭션으로 만드는 건 JDBC의 Connection을 이용한 트랜잭션 방식인 **로컬 트랜잭션**으로는 불가능하다. 225 | 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문이다. 226 | 227 | 따라서, 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 **글로벌 트랜잭션**방식을 사용해야 한다. 228 | 글로벌 트랜잭션을 적용해야 트랜잭션 매니저를 통해 여러 개의 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다. 229 | 또 JMS와 같은 트랜잭션 기능을 지원하는 서비스도 트랜잭션에 참여시킬 수 있다. 230 | 231 | 자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인 JTA(Java Transaction API)를 제공하고 있다. 232 | 233 | 자세한 내용은 11장에서 다시 다룰 것 이고, 일단은 하나 이상의 DB가 참여하는 트랜잭션을 만들려면 JTA를 사용해야 한다는 사실만 알고있자. 234 | 235 | ### 트랜잭션 API의 의존관계 문제와 해결책 236 | UserDao가 DAO 패턴을 사용해 구현 데이터 엑세스 기술을 유연하게 바꿔서 사용할 수 있게 했지만 237 | UserService에서 트랜잭션 경계 설정을 해야 할 필요가 생기면서 다시 특정 데이터 엑세스 기술에 종속되는 구조가 되었다. 238 | 239 | 원래 UserService는 UserDao 인터페이스에만 의존하는 구조로 전형적인 OCP 원칙을 지키는 코드였지만 240 | JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService 에서 사용되면서 UserService는 UserDaoJdbc에 간접적으로 의존하는 코드가 돼버렸다. 241 | 242 | 그렇지만, 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조로 사용 방법에 공통점이 있기 때문에 추상화를 생각해볼 수 있다. 243 | 244 | **추상화**란 하위 시스템의 공통점을 뽑아내서 분리시키는 것을 말한다. 그렇게 하면 하위 시스템이 어떤 것일지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수가 있다. 245 | 246 | ### 스프링의 트랜잭션 서비스 추상화 247 | 스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 248 | 249 | *스프링의 트랜잭션 추상화 API를 적용한 upgradeLevels()* 250 | ```java 251 | public void upgradeLevels() { 252 | // JDBC 트랜잭션 추상 오브젝트 생성 253 | PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource); 254 | 255 | // 트랜잭션 시작 256 | TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); 257 | 258 | try { 259 | // 트랜잭션 안에서 진행되는 작업 start 260 | List users = userDao.getAll(); 261 | for (User user : users) { 262 | if (canUpgradeLevel(user)) { 263 | upgradeLevel(user); 264 | } 265 | } 266 | // 트랜잭션 안에서 진행되는 작업 end 267 | 268 | transactionManager.commit(status); // 트랜잭션 커밋 269 | } catch (RuntimeException e) { 270 | transactionManager.rollback(status); // 트랜잭션 롤백 271 | throw e; 272 | } 273 | } 274 | ``` 275 | 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 **PlatformTransactionManager**다. 276 | 277 | PlatformTransactionManager 에서 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 호출하면 트랜잭션이 시작되고 278 | 이렇게 시작된 트랜잭션은 TransactionStatus 타입의 변수에 저장된다. 279 | 280 | TransactionStatus는 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager메소드의 파라미터로 전달해주면 된다. 281 | 282 | 트랜잭션 작업을 모두 수행한 후에는 트랜잭션을 만들 때 돌려받은 TransactionStatus 오브젝트를 파라미터로 해서 PlatformTransactionManager의 commit() 메소드를 호출하면 된다. 예외가 발생하면 rollback() 메소드를 부른다. 283 | 284 | ### 트랜잭션 기술 설정의 분리 285 | 트랜잭션 추상화 API 를 적용한 코드에서는 어떤 트랜잭션 매니저 구현 클래스를 사용할지 정해주기만 하면 쉽게 변경할 수 있다. 286 | 287 | 하지만 Service 에서 어떤 구체적인 구현 클래스를 사용할지 알고있는 것은 DI 원칙에 위배되기 때문에 컨테이너를 통해 외부에서 제공받게 하는 스프링의 DI 방식으로 바꿀 수 있다. 288 | 289 | 여기서 먼저 검토해야 할 것은 스프링의 빈으로 등록할 때 싱글톤 빈으로 만들어져 여러 스레드에서 동시에 사용해도 괜찮은가 하는 점이다. 290 | 스프링이 제공하는 PlatformTransactionManager의 구현 클래스는 싱글톤으로 사용이 가능하기 때문에 안심하고 스프링의 싱글톤 빈으로 등록해도 된다. 291 | 292 | --- 293 | 294 | 이렇게 하면 UserService는 트랜잭션 기술에서 완전히 독립적인 코드가 된다. 295 | DAO를 하이버네이트나 JPA, JDO 등을 사용하도록 수정했다면 그에 맞게 transactionManager의 구현 클래스만 변경해주면 된다. 296 | UserService의 코드는 조금도 수정할 필요가 없다. -------------------------------------------------------------------------------- /토비의 스프링 3.1/5장 서비스 추상화/5.3 ~ 5.5.md: -------------------------------------------------------------------------------- 1 | ## 5.3 서비스 추상화와 단일 책임 원칙 2 | 3 | ### 수직, 수평 계층구조와 의존관계 4 | 5 | 기술과 서비스에 대한 추상화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있다. 6 | 7 | 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구분이든 모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는 데는 스프링의 DI가 중요한 역할을 하고 있다. DI의 가치는 이렇게 관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는 데 있다. 8 | 9 | ### 단일 책임 원칙 10 | 11 | 이런 적절한 분리가 가져오는 특징은 객체지향 설계의 원칙 중의 하나인 단일 책임 원칙(Single Responsibility Principle)으로 설명할 수 있다. 단일 책임 원칙은 하나의 모듈은 한 가지 책임을 가져야 한다는 의미다. 하나의 모듈이 바뀌는 이유는 한 가지여야 한다고 설명할 수도 있다. 12 | 13 | ### 단일 책임 원칙의 장점 14 | 15 | 단일 책임 원칙을 잘 지키고 있다면, 어떤 변경이 필요할 때 수정대상이 명확해진다. 기술이 바뀌면 기술 계층과의 연동을 담당하는 기술 추상화 계층의 설정만 바꿔주면 된다. 데이터를 가져오는 테이블의 이름이 바뀌었다면 데이터 액세스 로직을 담고 있는 UserDao 를 변경하면 된다. 비즈니스 로직도 마찬가지다. 16 | 17 | 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는 반드시 필요하다. 이를 위한 핵심적인 도구가 바로 스프링이 제공하는 DI다. 18 | 19 | 이렇게 스프링의 의존관계 주입 기술인 DI는 모든 스프링 기술의 기반이 되는 핵심엔진이자 원리이며, 스프링이 지지하고 지원하는, 좋은 설계와 코드를 만드는 모든 과정에서 사용되는 가장 중요한 도구다. 스프링을 DI 프레임워크라고 부르는 이유는 외부 설정정보를 통한 런타임 오브젝트 DI 라는 단순한 기능을 제공하기 때문이 아니다. 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다. 20 | 21 | ## 5.4 메일 서비스 추상화 22 | 23 | ### 테스트와 서비스 추상화 24 | 25 | 일반적으로 서비스 추상화라고 하면 트랜잭션과 같은 기능은 유사하거나 사용 방법이 다른 로우레벨의 다양한 기술에 대해 추상 인터페이스와 일관성 있는 접근 방법을 제공해주는 것을 말한다. 26 | 27 | 이를 적용하면 어떤 경우에도 UserService 와 같은 애플리케이션 계층의 코드는 아래 계층에서는 어떤 일이 일어나는지 상관없이 메일 발송을 요청한다는 기본 기능에 충실하게 작성하면 된다. 28 | 29 | 서비스 추상화란 이렇게 원활한 테스트만을 위해서도 충분한 가치가 있다. 기술이나 환경이 바뀔 가능성이 있음에도, JavaMail 처럼 확장이 불가능하게 설계해놓은 API 를 사용해야 하는 경우라면 추상화 계층의 도입을 적극 고려해볼 필요가 있다. 30 | 31 | ### 테스트 대역의 종류와 특징 32 | 33 | 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 이런 오브젝트를 통틀어서 테스트 대역(test double)이라고 부른다. 34 | 35 | 대표적인 테스트 대역은 테스트 스텁(test stub)이다. 테스트 스텁은 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말한다. 36 | 37 | 많은 경우 테스트 스텁이 결과를 돌려줘야 할 때도 있다. MailSender 처럼 호출만 하면 그만인 것도 있지만, 리턴 값이 있는 메소드를 이용하는 경우에는 결과가 필요하다. 이럴 땐 스텁에 미리 테스트 중에 필요한 정보를 리턴해주도록 만들 수 있다. 38 | 39 | 테스트는 보통 어떤 시스템에 입력을 주었을 때 기대하는 출력이 나오는지를 검증한다. 40 | 41 | 테스트는 테스트의 대상이 되는 오브젝트에 직접 입력 값을 제공하고, 테스트 오브젝트가 돌려주는 출력 값, 즉 리턴 값을 가지고 결과를 확인한다. 테스트 대상이 받게 될 입력 값을 제어하면서 그 결과가 어떻게 달라지는지 확인하기도 한다. 문제는 테스트 대상 오브젝트는 테스트로부터만 입력을 받는 것이 아니라는 점이다. 테스트가 수행되는 동안 실행되는 코드는 테스트 대상이 의존하고 있는 다른 의존 오브젝트와도 커뮤니케이션하기도 한다. 42 | 43 | 때론 테스트 대상 오브젝트가 의존 오브젝트에게 출력한 값에 관심이 있을 경우가 있다. 또는 의존 오브젝트를 얼마나 사용했는가 하는 커뮤니케이션 행위 자체에 관심이 있을 수가 있다. 문제는 이 정보는 테스트에서는 직접 알 수가 없다는 것이다. 이때는 테스트 대상과 의존 오브젝트 사이에 주고받는 정보를 보존해두는 기능을 가진 테스트용 의존 오브젝트인 목 오브젝트를 만들어서 사용해야 한다. 테스트 대상 오브젝트의 메소드 호출이 끝나고 나면 테스트는 목 오브젝트에게 테스트 대상과 목 오브젝트 사이에서 일어났던 일에 대해 확인을 요청해서, 그것을 테스트 검증 자료로 삼을 수 있다. 44 | 45 | 목 오브젝트를 이용한 테스트라는 게, 작성하기는 간단하면서도 기능은 상당히 막강하다는 사실을 알 수 있다. 보통의 테스트 방법으로는 검증하기가 매우 까다로운 테스트 대상 오브젝트의 내부에서 일어나는 일이나 다른 오브젝트 사이에서 주고받은 정보까지 검증하는 일이 손쉽기 때문이다. 46 | 47 | ## 5.5 정리 48 | 49 | - 비즈니스 로직을 담은 코드는 데이터 엑세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바람직하다. 비즈니스 로직 코드 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메소드로 정리돼야 한다. 50 | - 이를 위해서는 DAO 의 기술 변화에 서비스 계층의 코드가 영향을 받지 않도록 인터페이스와 DI를 잘 활용해서 결합도를 낮춰줘야 한다. 51 | - DAO 를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요하다. 52 | - 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다. 트랜잭션 경계설정은 주로 비즈니스 로직 안에서 일어나는 경우가 많다. 53 | - 시작된 트랜잭션 정보를 담은 오브젝트를 파라미터로 DAO 에 전달하는 방법은 매우 비효율적이기 때문에 스프링이 제공하는 트랜잭션 동기화 기법을 활용하는 것이 편리하다. 54 | - 자바에서 사용되는 트랜잭션 API 의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭션 방법이 변경되면 경계설정 코드도 함께 변경돼야 한다. 55 | - 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO 가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다. 56 | - 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다. 57 | - 서비스 추상화는 로우레벨의 트랜잭션 기술과 API 의 변화에 상관없이 일괄된 API 를 가진 추상화 계층을 도입한다. 58 | - 서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다. 59 | - 테스트 대상이 사용하는 의존 오브젝트를 대체할 수 있도록 만든 오브젝트를 테스트 대역이라고 한다. 60 | - 테스트 대역은 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 테스트를 위해 간접적인 정보를 제공해주기도 한다. 61 | - 테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 목 오브젝트라고 한다. -------------------------------------------------------------------------------- /토비의 스프링 3.1/6장 AOP/6.1 ~ 6.2.md: -------------------------------------------------------------------------------- 1 | # 토비의 스프링 3.1 - 6장 AOP 2 | 3 | AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술 중 하나다. 4 | 5 | AOP를 바르게 이용하려면 OOP를 대체하려고 하는 것처럼 보이는 AOP라는 이름 뒤에 감춰진, 그 필연적인 등장배경과 스프링이 그것을 도입한 이유, 그 적용을 통해 얻을 수 있는 장점이 무엇인지에 대한 충분한 이해가 필요하다. 6 | 7 | 8 | ## 6.1 트랜잭션 코드의 분리 9 | 10 | ```java 11 | public void upgradeLevels() throws Exception { 12 | TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); 13 | try { 14 | 15 | //// 비즈니스 로직 //// 16 | List users = userDao.getAll(); 17 | for (User user : users) { 18 | if (canUpgradeLevel(user)) { 19 | upgradeLevel(user); 20 | } 21 | } 22 | //// 비즈니스 로직 //// 23 | 24 | this.transactionManager.commit(status); 25 | } catch (Exception e) { 26 | this.transactionManager.rollback(status); 27 | throw e; 28 | } 29 | } 30 | ``` 31 | 32 | 얼핏 보면 트랜잭션 경계설정 코드와 비즈니스 로직 코드가 복잡하게 얽혀 있는듯이 보이지만, 자세히 살펴보면 뚜렷하게 두 가지 종류의 코드가 구분되어 있음을 알 수 있다. 비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치하고 있다. 33 | 34 | 또, 이 코드의 특징은 트랜잭션 경계설정의 코드와 비즈니스 로직 코드 간에 서로 주고받는 정보가 없다는 점이다. 다만 이 비즈니스 로직을 담당하는 코드가 트랜잭션의 시작과 종료 작업 사이에서 수행돼야 한다는 사항만 지켜지면 된다. 35 | 36 | ### DI 적용을 이용한 트랜잭션 분리 37 | 38 | 현재 구조는 UserService 클래스와 그 사용 클라이언트 간의 관계가 강한 결합도로 고정되어 있다. 그래서 UserService를 인터페이스로 만들고 기존 코드는 UserService 인터페이스의 구현 클래스를 만들어넣도록 한다. 그러면 클라이언트와 결합이 약해지고, 직접 구현 클래스에 의존하고 있지 않기 때문에 유연한 확장이 가능해진다. 39 | 40 | 이렇게 인터페이스를 이용해 구현 클래스를 클라이언트에 노출하지 않고 런타임 시에 DI를 통해 적용하는 방법을 쓰는 이유는, 일반적으로 구현 클래스를 바꿔가면서 사용하기 위해서다. 41 | 42 | 하지만 꼭 그래야 한다는 제약은 없다. 지금 해결하려고 하는 문제는 UserService에는 순수하게 비즈니스 로직을 담고 있는 코드만 두고 트랜잭션 경계설정을 담당하는 코드를 외부로 빼내려는 것이다. 43 | 44 | 그래서 다음과 같은 구조를 생각해볼 수 있다. UserService를 구현한 또 다른 구현 클래스를 만든다. 이 클래스는 사용자 관리 로직을 담고 있는 구현 클래스인 UserServiceImpl을 대신하기 위해 만든 게 아니다. 단지 트랜잭션의 경계설정이라는 책임을 맡고 있을 뿐이다. 그리고 스스로는 비즈니스 로직을 담고 있지 않기 때문에 또 다른 비즈니스 로직을 담고 있는 UserService의 구현 클래스에 실제적인 로직 처리 작업은 위임하는 것이다. 45 | 46 | ### UserService 인터페이스 도입 47 | 48 | 먼저 기존의 UserService 클래스를 UserServiceImpl로 이름을 변경한다. 그리고 클라이언트가 사용할 로직을 담은 핵심 메소드만 UserService 인터페이스로 만든 후 UserServiceImpl이 구현하도록 만든다. 49 | 50 | UserService 인터페이스의 구현 클래스인 UserServiceImpl은 기존 UserService 클래스의 내용을 대부분 그대로 유지하면 된다. 51 | 52 | ```java 53 | public class UserServiceImpl implements userService { 54 | UserDao userDao; 55 | MailSender mailSender; 56 | 57 | public void upgradeLevels() { 58 | List users = userDao.getAll(); 59 | for (User user : users) { 60 | if (canUpgradeLevel(user)) { 61 | upgradeLevel(user); 62 | } 63 | } 64 | } 65 | ... 66 | } 67 | ``` 68 | 69 | ### 분리된 트랜잭션 기능 70 | 71 | 비즈니스 트랜잭션 처리를 담은 UserServiceTx 는 기본적으로 UserService 를 구현하게 만든다. 그리고 같은 인터페이스를 구현한 다른 오브젝트에게 고스란히 작업을 위임하게 만들면 된다. 72 | 73 | ```java 74 | public class UserServiceTx implements UserService { 75 | UserService userService; 76 | PlatformTransactionManager transactionManager; 77 | 78 | public void setTransactionManager( 79 | PlatformTransactionManager transactionManger){ 80 | this.transactionManager = transactionManager; 81 | } 82 | 83 | // UserService를 구현한 다른 오브젝트를 DI 받는다. 84 | public void setUserService(UserService userService) { 85 | this.userService = userService; 86 | } 87 | 88 | // DI 받은 UserService 오브젝트에 모든 기능을 위임한다. 89 | public void add(User user) { 90 | userService.add(user); 91 | } 92 | 93 | public void upgradeLevels() { 94 | TransactionStatus status = this.transactionManager 95 | .getTransaction(new DefaultTransactionDefinition(); 96 | try{ 97 | 98 | userService.upgradeLevels(); 99 | 100 | this.transactionManager.commit(status); 101 | 102 | } catch(RuntimeException e){ 103 | this.transactionManager.rollback(status); 104 | throw e; 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | UserServiceTx는 UserService 인터페이스를 구현했으니, 클라이언트에 대해 UserService 타입 오브젝트의 하나로서 행세할 수 있다. UserServiceTx는 사용자 관리라는 비즈니스 로직을 전혀 갖지 않고 고스란히 다른 UserService 구현 오브젝트에 기능을 위임한다. 이렇게 준비된 UserServiceTx에 트랜잭션의 경계설정이라는 부가적인 작업의 부여가 가능해진다. 111 | 112 | ### 트랜잭션을 위한 DI 설정 113 | 114 | 클라이언트가 UserService라는 인터페이스를 통해 사용자 관리 로직을 이용하려고 할 때 먼저 트랜잭션을 담당하는 오브젝트가 사용돼서 트랜잭션에 관련된 작업을 진행해주고, 실제 사용자 관리 로직을 담은 오브젝트가 이후에 호출돼서 비즈니스 로직에 관련된 작업을 수행하도록 만든다. 115 | 116 | ### 트랜잭션 경계설정 코드 분리의 장점 117 | 118 | 1. 비즈니스 로직을 담당하고 있는 UserServiceImpl의 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경 쓰지 않아도 된다. 트랜잭션의 적용이 필요한지도 신경 쓰지 않아도 된다. 119 | 2. 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있다. 120 | 121 | 122 | 123 | ## 6.2 고립된 단위 테스트 124 | 125 | 가장 편하고 좋은 테스트 방법은 가능한 한 작은 단위로 쪼개서 테스트하는 것이다. 작은 단위의 테스트가 좋은 이유는 테스트가 실패했을 때 그 원인을 찾기 쉽기 때문이다. 126 | 127 | 기존의 UserService 테스트 코드의 경우 구현 클래스들이 동작하려면 세 가지 타입의 의존 오브젝트가 필요하기 때문에 UserService라는 테스트 대상이 테스트 단위인 것처럼 보이지만 사실 그 뒤의 의존관계에 따라 등장하는 오브젝트와 서비스, 환경 등이 모두 합쳐서 테스트 대상이 되는 문제점을 갖고있다. 따라서 테스트를 준비하기 힘들고 환경에 따라 동일한 결과를 내지 못할 수도 있으며, 수행속도는 느리고 그에 따라 테스트를 작성하고 실핼하는 빈도가 점차적으로 떨어질 수 있는 문제점 또한 갖고 있다. 128 | 129 | ### 테스트 대상 오브젝트 고립시키기 130 | 131 | 테스트의 대상이 환경이나, 외부 서버, 다른 클래스의 코드에 종속되고 영향을 받지 않도록 고립시킬 필요가 있다. 테스트를 의존 대상으로부터 분리해서 고립시키는 방법은 테스트를 위한 대역을 사용하는 것이다. 132 | 133 | ### 테스트를 위한 UserServiceImpl 고립 134 | 135 | 의존 오브젝트나 외부 서비스에 의존하지 않는 고립된 테스트 방식으로 만든 UserServiceImpl 은 아무리 그 기능이 수행되어도 그 결과가 DB 등을 통해서 남지 않으니, 기존의 방법으로는 작업 결과를 검증하기 힘들다. upgradeLevels() 처럼 결과가 리턴되지 않는 경우는 더더욱 그렇다. 136 | 137 | 그래서 이럴 땐 테스트 대상인 UserServiceImpl과 그 협력 오브젝트인 UserDao에게 어떤 요청을 했는지를 확인하는 작업이 필요하다. 테스트 중에 DB에 결과가 반영되지는 않았지만, UserDao의 update() 메소드를 호출하는 것을 확인할 수 있다면, 결국 DB에 그 결과가 반영될 것이라고 결론을 내릴 수 있기 때문이다. UserDao와 같은 역할을 하면서 UserServiceImpl과의 사이에서 주고받은 정보를 저장해뒀다가, 테스트의 검증에 사용할 수 있게 하는 목 오브젝트를 만들 필요가 있다. 138 | 139 | ### 테스트 수행 성능의 향상 140 | 141 | 고립된 테스트를 하면 테스트가 다른 의존 대상에 영향을 받을 경우를 대비해 복잡하게 준비할 필요가 없고 수행 성능도 크게 향상된다. 테스트가 빨리 돌아가면 부담 없이 자주 테스트를 돌려볼 수 있다. 142 | 143 | ### 단위 테스트와 통합 테스트 144 | 145 | 단위 테스트의 단위는 정하기 나름이다. 사용자 관리 기능 전체를 하나의 단위로 볼 수도 있고 하나의 클래스나 하나의 메소드를 단위로 볼 수도 있다. 중요한 것은 하나의 단위에 초점을 맞춘 테스트라는 점이다. 146 | 147 | ‘테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트 하는 것’을 단위 테스트라고 부른다. 반면에 두 개 이상의, 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나, 또는 외부의 DB나 파일, 서비스 등의 리소스가 참여하는 테스트는 통합 테스트라고 부른다. 통합 테스트란 두 개 이상의 단위가 결합해서 동작하면서 테스트가 수행되는 것이라고 보면 된다. 스프링의 테스트 컨텍스트 프레임워크를 이용해서 컨텍스트에서 생성되고 DI된 오브젝트를 테스트하는 것도 통합 테스트다. 148 | 149 | 아래는 단위 테스트와 통합 테스트 중에서 어떤 방법을 쓸지 어떻게 결장할 것인지에 대한 가이드 라인이다. 150 | 151 | - 항상 단위 테스트를 먼저 고려한다. 152 | - 하나의 클래스나 성격과 목적이 같은 긴밀한 클래스 몇 개를 모아서 외부와의 의존관계를 모두 차단하고 필요에 따라 스텁이나 목 오브젝트 등의 테스트 대역을 이용하도록 테스트를 만든다. 단위 테스트는 테스트 작성도 간단하고 실행 속도도 빠르며 테스트 대상 외의 코드나 환경으로부터 테스트 결과에 영향을 받지도 않기 때문에 가장 빠른 시간에 효과적인 태스트를 작성하기에 유리하다. 153 | - 외부 리소스를 사용해야만 가능한 테스트는 통합 테스트로 만든다. 154 | - 단위 테스트로 만들기가 어려운 코드도 있다. 대표적인 게 DAO다. DAO는 그 자체로 로직을 담고 있기보다는 DB를 통해 로직을 수행하는 인터페이스와 같은 역할을 한다. SQL을 JDBC를 통해 실행하는 코드만으로는 고립된 테스트를 작성하기가 힘들다. 작성한다고 해도 가치가 없는 경우가 대부분이다. 따라서 DAO는 DB까지 연동하는 테스트로 만드는 편이 효과적이다. 155 | - DAO 테스트는 DB라는 외부 리소스를 사용하기 때문에 통합 테스트로 분류된다. 하지만 코드에서 보자면 하나의 기능 단위를 테스트하는 것이기도 하다. DAO를 테스트를 통해 충분히 검증해두면, DAO를 이용하는 코드는 DAO 역할을 스텁이나 목 오브젝트로 대체해서 테스트할 수 있다. 충분한 단위 테스트를 거친다면 통합 테스트에서 오류가 발생할 확률도 줄어들고 발생한다고 하더라도 쉽게 처리할 수 있다. 156 | - 여러 개의 단위가 의존관계를 가지고 동작할 때를 위한 통합 테스트는 필요하다. 다만, 단위 테스트를 충분히 거쳤다면 통합 테스트의 부담은 상대적으로 줄어든다. 157 | - 단위 테스트를 만들기가 너무 복잡하다고 판단되는 코드는 처음부터 통합 테스트를 고려해본다. 이때도 통합 테스트에 참여하는 코드 중에서 가능한 한 많은 부분을 미리 단위 테스트로 검증해두는 게 유리하다. 158 | - 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합 테스트다. 가능하면 스프링의 지원 없이 직접 코드 레벨의 DI를 사용하면서 단위 테스트를 하는게 좋겠지만 스프링의 설정 자체도 테스트 대상이고, 스프링을 이용해 좀 더 추상적인 레벨에서 테스트해야 할 경우도 종종 있다. 이럴 땐 스프링 테스트 컨텍스트 프레임워크를 이용해 통합 테스트를 작성한다. 159 | 160 | ### 목 프레임워크 161 | 162 | 단위 테스트를 만들기 위해서는 스텁이나 목 오브젝트의 사용이 필수적이다. 의존관계가 없는 단순한 클래스나 세부 로직을 검증하기 위해 메소드 단위로 테스트할 때가 아니라면, 대부분 의존 오브젝트를 필요로 하는 코드를 테스트하게 되기 때문이다. 163 | 164 | 목 오브젝트를 만드는 일은 번거로울 수 있다. 그러나 이런 번거로운 목 오브젝트를 편리하게 작성하도록 도와주는 다양한 목 오브젝트 지원 프레임워크가 있다. 165 | 166 | ### Mockito 프레임워크 167 | 168 | Mockito라는 프레임워크는 사용하기도 편리하고, 코드도 직관직이라 많은 인기를 끌고 있다. 169 | 170 | Mockito와 같은 목 프레임워크의 특징은 목 클래스를 일일이 준비해둘 필요가 없다는 것이다. 간단한 메소드 호출만으로 다이내믹하게 특정 인터페이스를 구현한 테스트용 목 오브젝트를 만들 수 있다. 171 | 172 | Mockito 목 오브젝트는 다음의 네 단계를 거쳐서 사용하면 된다. 두 번째와 네 번째는 각각 필요할 경우에만 사용할 수 있다. 173 | 174 | - 인터페이스를 이용해 목 오브젝트를 만든다. 175 | - 목 오브젝트가 리턴할 값이 있으면 이를 지정해준다. 메소드가 호출되면 예외를 강제로 던지게 만들 수도 있다. 176 | - 테스트 대상 오브젝트에 DI 해서 목 오브젝트가 테스트 중에 사용되도록 만든다. 177 | - 테스트 대상 오브젝트를 사용한 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증한다. 178 | -------------------------------------------------------------------------------- /토비의 스프링 3.1/6장 AOP/6.6 ~ 6.9.md: -------------------------------------------------------------------------------- 1 | #### 트랜잭션(Transaction) 2 | 3 | * 트랜잭션의 기본 개념이 더 이상 쪼갤 수 없는 최소단위 작업은 어떠한 경우에도 맞는 내용이지만 모든 트랜잭션이 같은 방식으로 동작하진 않는다. 4 | * `DefaultTransactionDefinition` 이 구현되고 있는 `TransactionDefinition` 인터페이스의 동작방식에 영향을 줄 수 있는 여러 속성을 정의하고 있다. 5 | 6 | 7 | 8 | #### 트랜잭션 전파 속성 9 | 10 | * 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을때 또는 없을때 어떻게 동작할 것인가 결정하는 방법 11 | 12 | * `PROPAGATION_REQUIRED` 13 | * 진행중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다. 14 | * `DefaultTransactionDefinition` 의 트랜잭션 전파 속성이 `PROPAGATION_REQUIRED` 이다. 15 | * `PROPAGATION_REQUIRES_NEW` 16 | * 시작된 트랜잭션이 있든 없든 항상 새로운 트랜잭션을 만들어 독자적으로 행동한다 17 | * `PROPAGATION_NOT_SUPPORTED` 18 | * 트랜잭션 없이 동작하도록 만들 수 있다. 19 | * 모든 메소드에 트랜잭션 AOP가 적용되도록 하고, 특정 메소드의 트랜잭션 전파 속성만 `PROPAGATION_NOT_SUPPORTED` 로 설정해서 트랜잭션 없이 동작하게 만드는 용도로 사용된다. 20 | 21 | 22 | 23 | #### 격리수준 24 | 25 | * 모든 DB 트랜잭션은 격리수준을 갖고 있어야 한다. 26 | * 이상적으로는 각각이 독립적으로 진행되어야 하지만 그럴 경우 성능의 저하가 심하기 때문에 적절하게 격리 수준을 조정해서 가능한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않게 하는 제어가 필요하다. 27 | * 기본적으로는 DB나 DataSource에 설정된 디폴트 격리수준을 따르는 편이 좋지만 특별한 작업을 수행하는 메소드의 경우 독자적인 격리수준을 지정할 필요가 있다. 28 | 29 | 30 | 31 | #### 제한시간 32 | 33 | * 트랜잭션을 수행하는 제한시간을 설정할 수 있다. 34 | * `DefaultTransactionDefinition` 의 기본 설정은 제한 시간이 없고 이를 사용하기 위해선 `PROPAGATION_REQUIRED` 나 `PROPAGATION_REQUIRES_NEW` 와 함께 사용해야만 의미가 있다. 35 | 36 | 37 | 38 | #### 읽기전용 39 | 40 | * 읽기전용을 사용하면 트랜잭션 내에 데이터를 조작하는 시도를 막아줄 수 있다. 또한 성능 향상이 될 수도 있다. 41 | 42 | 43 | 44 | #### 트랜잭션 정의를 수정하려면? 45 | 46 | * 트랜잭션의 정의를 바꾸고 싶다면 디폴트 속성을 갖고 있는 `DefaultTransactionDefinition`을 사용하는 대신 외부에서 정의된 `TransactionDefinition `오브젝트를 DI 받아서 사용하도록 만들면 된다. 47 | * 하지만 해당 방식으로는 `TransactionAdvice` 를 사용하는 모든 트랜잭션의 속성이 한꺼번에 바뀐다는 문제가 있다 즉, 원하는 메소드만 선택하여 트랜잭션을 설정해 주기 위해선 `Advice` 의 기능을 확장해야 한다. 48 | 49 | 50 | 51 | #### 메소드마다 별도의 트랜잭션 정의 적용하기 52 | 53 | * `TransactionAdvice` 와 동작방식이 유사한 `TransactionInterceptor` 를 이용한다. `TransactionInterceptor` 는 추가적으로 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가로 제공해준다. 54 | * `TransactionInterceptor` 는 `PlatformTransactionManager` 와 `Properties` 타입의 두 가지 프로퍼티를 갖고 있다. 55 | * 여기서 `Properties` 의 타입인 변수의 이름이 `transactionAttribuites` 로, 트랜잭션 속성을 정의한 프로퍼티이다. 56 | * `TransactionAttribute`를 이용하면 트랜잭션의 부가기능의 동작 방식이 모두 제어가 가능하다 57 | 58 | 59 | 60 | #### 메소드 이름 패턴을 이용한 트랜잭션 속성 지정 61 | 62 | * `Properties` 타입의 `transactionAttributes` 프로퍼티는 메소드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬렉션이다. 63 | 64 | * ``` 65 | PROPAGATION_NAME(전파방식), ISOLATION_NAME(격리 수준), readOnly(읽기 전용), timeout_NNNN(제한시간), -Exception1(예외가 나면 롤백해야할 대상), +Exception2(런타임 예외지만 롤백시키지 않을 대상) 66 | ``` 67 | 68 | * 주의 해야할 점으로 메소드 이름이 하나 이상의 패턴과 일치할 때인데 이때는 메소드 이름 패턴 중에서 가장 정확히 일치하는 것이 적용된다. 69 | 70 | #### tx 네임 스페이스를 이용한 설정 방법 71 | 72 | * 앞에서 얘기한 `TransactionInterceptor` 타입과 `TransactionAttribute` 타입의 속성 정보도 tx 스키마를 통해 정의 될 수 있다. 73 | * 해당 방식을 사용하게 되면 설정 내용을 이해하기가 더 쉽고, 에디터의 도움을 받아 오타 문제도 해결할 수 있다는 장점이 있다. 따라서 해당 방법을 가장 권장한다. 74 | 75 | 참고: `advisor` = `포인트컷(메소드 선정 알고리즘)+ advice(부가기능)` 76 | 77 | ### 6.6.3 포인트컷과 트랜잭션 속성의 적용 전략 78 | 79 | * 트랜잭션 부가기능을 적용할 후보 메소드를 선정하는 작업은 `포인트컷`에 의해 진행된다. 80 | * 포인트컷 표현식과 트랜잭션 속성을 정의할 때 따르면 좋은 전략들을 소개한다. 81 | 82 | 1. 트랜잭션 포인트컷 표현식은 타입패턴이나 빈 이름을 이용한다. 83 | * 단순한 조회 작업만 하는 메소드에는 모두 트랜잭션을 적용하는게 좋다. 84 | * 트랜잭션 포인트컷 표현식에는 메소드나 파라미터, 예외에 대한 패턴을 정의하지 않는게 바람직하다. 85 | 86 | 2. 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다. 87 | 88 | 3. 프록시 방식 AOP는 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다. 89 | 90 | 91 | 92 | ### 6.7 애노테이션 트랜잭션 속성과 포인트컷 93 | 94 | #### @Transactional 95 | 96 | * 메소드, 클래스, 인터페이스에 사용할 수 있다. 97 | * 해당 애노테이션을 사용하면 메소드마다 다르게 트랜잭션을 설정할 수 있으므로 매우 유연한 트랜잭션 속성 설정이 가능해진다. 98 | * 단, @Transaction을 남발할 경우 코드는 지저분해지고, 동일한 속성 정보를 가진 애노테이션을 반복적으로 메소드마다 부여해 주는 바람직 하지 못한 결과를 가져올 수도 있다. 따라서 스프링에서는 이를 대체할 수 있는 정책이 있다. 99 | 100 | 101 | 102 | #### 대체정책 103 | 104 | * 구현 클래스가 바뀌더라도 트랜잭션 속성을 유지할 수 있다는 장점이 있기 때문에 일반적으로 구현체 보다는 인터페이스에 @Transactional을 두는게 좋다. 105 | 106 | 107 | 108 | ### 6.8 트랜잭션 지원 테스트 109 | 110 | #### 6.8.1 선언적 트랜잭션과 트랜잭션 전파 속성 111 | 112 | * 선언적 트랜잭션 : AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법 113 | * 프로그램에 의한 트랜잭션 : `TransactionTemplate` 이나 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법 114 | 115 | 특별한 경우가 아니라면 선언적 트랜잭션을 사용하는게 바람직하다 116 | 117 | 118 | 119 | #### 6.8.3 테스트를 위한 트랙잭션 애노테이션 120 | 121 | * `@ContextConfiguration` : 클래스에 부여하면 테스트를 실행하기 전에 스프링 컨테이너를 초기화한다. 122 | * `@Transactional` : 메소드의 트랜잭션 속성이 클래스의 속성보다 우선한다. 123 | * `@Rollback` : 테스트 코드에 있는 `@Transactional` 과 일반적인 코드에 있는 `@Transactional` 은 동일하게 보이지만 테스트용 코드는 `Rollback`이 된다는 차이점이 있다. 따라서 Rollback을 원하지 않으면 false를 넣어주면 된다. 124 | * `@TransactionConfiguration` : `@Rollback` 은 메소드 레벨에서만 가능하다는 단점이 있는데 `@TransactionConfiguration(defaultRollback = false)` 를 사용하면 클래스레벨에서도 Rollback을 사용한것과 같은 결과를 나타낼 수 있다. 125 | * `@NotTransactional` ,`@Transactional(propagation=Propagation.NEVER)` : 트랜잭션이 안되게 만드려고 할때 적용할 수 있는 어노테이션이다 126 | 127 | 128 | 129 | #### 효과적인 DB 테스트 130 | 131 | * 단위테스트와 통합테스트는 아예 따로 만드는 방법이 효과적이다. 132 | 133 | * 테스트는 어떠한 경우에도 서로 의존하면 안된다. 코드가 바뀌지 않는 한 어떤 순서로 진행되더라도 일정한 결과를 만들어야 한다. 134 | 135 | -------------------------------------------------------------------------------- /토비의 스프링 3.1/8장 스프링이란 무엇인가/8.1 ~ 8.4.md: -------------------------------------------------------------------------------- 1 | # 8.1 스프링의 정의 2 | 3 | - 스프링은 **자바 엔터프라이즈 개발을 편하게** 해주는 **오픈소스** **경량급** **애플리케이션 프레임워크**이다. 4 | 5 |
6 | 7 | ## 애플리케이션 프레임워크 8 | 9 | - 라이브러리나 프레임워크는 특정 업무 분야나 한 가지 기술에 특화된 목표를 가지고 만들어진다. 10 | - 반면에 애플리케이션 프레임워크는 애플리케이션 전 영역을 포괄하는 범용적인 프레임워크로 애플리케이션 개발의 전 과정을 빠르고 편리하며 효율적으로 진행하는데 일차적인 목표를 두는 프레임워크이다. 11 | - 스프링은 단순히 여러 계층의 다양한 기술을 모아놓았기 때문에 애플리케이션 프레임워크라고 불리는게 아니다. 12 | - 애플리케이션 전 영역을 관통하는 일관된 프로그래밍 모델과 핵심 기술을 바탕으로 해서 각 분야의 특성에 맞는 필요를 채워주고있고 이를 통해 애플리케이션을 빠르고 효과적으로 개발할 수 있기 때문이다. 13 | - 스프링의 일차적인 목적은 핵심 기술에 담긴 프로그래밍 모델을 일관되게 적용해서 엔터프라이즈 애플리케이션 전 계층과 영역에 전략과 기능을 제공해줌으로써 애플리케이션 개발을 편리하게 해주는 애플리케이션 프레임워크로 사용되는 것이다. 14 | 15 |
16 | 17 | ## 경량급 18 | 19 | - 스프링 자체가 가볍다는 이야기가 아니고 EJB와 같은 기술에 비해서 상대적으로 단순하고 가볍다는 이야기이다. 20 | - 스프링은 단순한 서버환경인 톰캣이나 제티에서도 완벽하게 동작하며 단순한 개발툴과 기본적인 개발환경으로도 엔터프라이즈 개발에서 필요한 주요기능을 갖춘 애플리케이션을 개발하는 것이 가능하다. 21 | - 스프링 기반의 코드가 가벼운 이유는 코드에 불필요하게 등장했던 프레임워크와 서버환경에 의존적인 부분을 제거해주어 기술과 환경을 지원하기 위해 반복되던 코드가 제거되고 단순하고 가벼운 코드만 남겨두었기 때문이다. 22 | 23 |
24 | 25 | ## 자바 엔터프라이즈 개발을 편리하게 26 | 27 |
28 | 29 | ## 오픈소스 30 | 31 |
32 | 33 | # 8.2 스프링의 목적 34 | 35 | - 스프링의 개발 철학과 궁극적인 목표가 무엇인지 생각해보자 그리고 스프링을 어떻게 적용하고 사용하는 것이 스프링을 제대로 사용한다고 말할 수 있는 것인지. 36 | - 스프링은 개발 표준이 존재하지 않을 뿐더러 스프링의 베스트 프랙티스만 모아다가 그대로 따른다고 해도 스프링을 잘 사용하고 있다고 확신할 수 없다. 37 | - 스프링은 기능과 API 사용법을 잘 안다고 잘 사용할 수 있는 것이 아니다. 38 | - 자바 언어와 JDK 라이브러리가 일종의 편한 도구로써 객체지향 프로그래밍을 좀 더 손쉽게 할 수 있도록 도와주는 것처럼 자바의 근본적인 목적은 객체지향 프로그래밍을 통해 유연하고 확장성 좋은 애플리케이션을 빠르게 만드는 것이다. 39 | - 때문에 자바를 가지고 절차지향 언어처럼 사용한다면 자바를 사용하는 가치를 얻을 수 없다. 40 | - 스프링은 정의에서 처럼 경량급 프레임워크인 스프링을 활용해서 엔터프라이즈 애플리케이션 개발을 편하게 하는 것이 목적이다. 41 | 42 |
43 | 44 | ## 8.2.1 엔터프라이즈 개발의 복잡함 45 | 46 | - 자바 엔터프라이즈 개발이 실패하는 이유는 여러가지가 있지만 그 중에서 가장 대표적인 이유는 엔터프라이즈 시스템이개발이 너무 복잡해서이기 때문이다. 47 | 48 |
49 | 50 | ### 엔터프라이즈 애플리케이션은 왜 복잡한가? 51 | 52 | - 엔터프라이즈 시스템이란 서버에서 동작하며 기업과 조직의 업무를 처리해주는 시스템을 의미한다. 53 | - 엔터프라이즈 시스템은 많은 사용자의 요청을 동시에 처리해야하기 때문에 서버의 자원을 효율적으로 공유하고 분배해서 사용할 수 있어야 한다. 54 | - 또한 중요한 기업의 핵심 정보나 금융 시스템 등을 다루기도 하기 때문에 보안과 안정성, 확장성 면에서도 뛰어나야 한다. 55 | - 엔터프라이즈 시스템을 개발하는데 순수한 비즈니스 로직을 구현하는 것 이외에도 기술적으로 고려할 사항이 많은데 이러한 기술적인 요구사항은 단순히 고가의 애플리케이션 서버나 툴을 사용한다고 충족되는 것이 아니다. 56 | - 따라서 엔터프라이즈 시스템 개발에는 복잡한 비즈니스 로직과 더불어 기술적인 문제를 고려해야하기 때문에 그 복잡성이 증가하게 된다. 57 | - 결국 비즈니스 로직과 엔터프라이즈 기술이라는 두 가지 복잡함이 한데 얽혀있기 때문에 복잡함이 몇배로 가중되는데 서비스를 개발하기 위해 각종 기술적인 API 호출 코드를 비즈니스 로직에 대한 구현코드와 함께 붙여서 만드는 것은 매우 어렵다. 58 | - 만약 적용한 기술을 변경하거나, 특정 로직을 수정하거나, 하나의 수정 요구를 적용하기 위해 복잡하게 얽혀있는 코드를 헤매다보면 정작 수정해야할 대상이 아닌 부분에 영향을 줘서 새로운 버그를 만들어낼 수 있다. 59 | 60 |
61 | 62 | ## 8.2.2 복잡함을 해결하려는 도전 63 | 64 | - 엔터프라이즈 개발에 나타나는 복잡함의 원인은 제거 대상이 아니며 대신 복잡함을 효과적으로 상대할 수 있는 전략과 기법이 필요하다. 65 | - 따라서 가장 먼저 해야할 일은 성격이 다른 두 가지 복잡함을 분리해내는 일이다. 66 | - EJB도 처음 등장했을 당시에는 이 두 가지 복잡함을 분리하는데 목표를 두었지만 EJB라는 환경과 스팩에 종속되는 코드가 결국에는 자바 언어가 가지는 장점을 잃어버리게 만들었고 결국 객체지향적인 특성을 잃어버리게되었다. 67 | - 반면 스프링은 침투적인 EJB 기술과 반대로 스프링 코드가 직접적으로 애플리케이션에 등장하지 않는 비침투적인 방법을 통해 기술적인 복잡함과 비즈니스 로직을 다루는 코드를 깔끔하게 분리해낼 수 있었다. 68 | 69 |
70 | 71 | ## 8.2.3 복잡함을 상대하는 스프링의 전략 72 | 73 | - 스프링의 기본적인 전략은 비즈니스 로직을 담은 애플리케이션 코드와 엔터프라이즈 기술을 처리하는 코드를 분리시키는 것이다. 74 | - 서비스 추상화 : 기술에 대한 접근 방식의 일관성이 없고 특정 환경에 종속적인 문제를 해결했다. 75 | - AOP : 기술적인 처리를 담당하는 코드가 성격이 다른 코드에 섞여서 등장하는 문제를 해결했다. 76 | - 기술적인 코드와 침투적인 기술이 가져온 불필요한 흔적을 제거하고 나면 순수하게 애플리케이션 주요 기능과 비즈니스 로직만 담은 코드만 독립적으로 존재하게 된다. 77 | - 비즈니스 로직의 복잡함은 객체지향 프로그래밍 기법이 주는 유연한 설계와 재사용성을 통해 효과적으로 구현해낼 수 있다. 78 | - 결국 비즈니스 로직의 복잡함을 상대하는 전략은 자바라는 객체지향 기술 그 자체이며 스프링은 단지 객체지향 언어의 장점을 제대로 살리지 못했던 방해요소를 제거하도록 도와줄 뿐이다. 79 | 80 |
81 | 82 | ### 객체지향과 DI 83 | 84 | - 기술과 비즈니스 로직의 복잡함을 해결하는데 스프링이 공통적으로 사용하는 도구는 객체지향이다. 85 | - 때문에 스프링은 자바의 기본인 객체지향에 충실한 설계가 가능하도록 단순한 오브젝트로 개발할 수 있고 객체지향 설계 기법을 잘 적용할 수 있는 구조를 만들기 위해 DI 같은 유용한 기술을 편하게 적용하도록 도와주는 것이 스프링의 전략이다. 86 | - 기술적인 복잡함을 효과적으로 다루게 해주는 기법은 모두 DI를 바탕으로 하고 있는데 서비스 추상화, 템플릿과 콜백, AOP와 같은 스프링 기술은 DI 없이는 존재할 수 없는 것이다. 87 | - DI는 자연스럽게 객체지향 설계와 개발로 이끌어주는 도구이며 성격이다르고 변경의 이유가 다른 후보들을 찾아 DI를 적용해서 오브젝트를 분리하고 인터페이스를 도입하다보면 객체지향 설계원칙의 장점을 잘 살린 설계가 나올 수 있다. 88 | - 기술적인 복잡함을 해결하는 문제나 기술적인 복잡함이 비즈니스 로직에 침범하지 못하도록 분리하는 경우에도 DI가 바탕이된 기법이 활용된다. 89 | - 반면에 비즈니스 로직 자체의 복잡함을 해결하기 위해서는 DI보다는 객체지향 설계 기법이 더 중요하다. 90 | 91 | 결국 스프링은 비즈니스 로직 자체를 기술적인 코드와 분리해서 순수한 비즈니스 로직만 남겨두어 객체지향 설계에서 나온 도메인 모델을 쉽게 적용할 수 있도록 하였다. 92 | 93 | 결국 이러한 장점들을 최대한 활용해서 복잡하고 자주 변하는 업무를 지원하는 시스템을 설계할 때에도 손쉽게 대응이 가능해지는 것이며 스프링의 기술과 전략은 객체지향이라는 자바 언어가 가진 강력한 도구를 극대화해서 사용할 수 있도록 돕는 것이라고 볼 수 있다. 94 | 95 |
96 | 97 | # 8.3 POJO 프로그래밍 98 | 99 |
100 | 101 | ## 8.3.2 POJO란 무엇인가? 102 | 103 | - POJO는 Plain Old Java Object의 약자로 순수한 자바 객체를 의미한다. 104 | - POJO의 탄생배경은 장난스럽지만 POJO가 지향하고 있는 목적은 단순한 오브젝트를 이용해 애플리케이션 비즈니스 로직을 구현하는 편이 EJB처럼 복잡하고 제한이 많은 기술을 사용하는 것보다 낫다는 것을 어필하기 위해서이다. 105 | 106 |
107 | 108 | ## 8.3.3 POJO의 조건 109 | 110 | - POJO는 다음의 세 가지 조건을 충족해야한다. 111 | 112 |
113 | 114 | ### 특정 규약에 종속되지 않는다. 115 | 116 | - POJO는 자바 언어와 필요한 API 외에는 종속되지 않아야 한다. 117 | - 특정 프레임워크의 규약에 종속되지 않고 객체지향 설계의 자유로운 적용이 가능한 오브젝트여야만 POJO라고 불릴 수 있다. 118 | 119 |
120 | 121 | ### 특정 환경에 종속되지 않는다. 122 | 123 | - 특정 환경에 종속적이어야만 동작하는 오브젝트는 POJO라고 할 수 없다. 124 | - 특히 비즈니스 로직을 담고있는 POJO 클래스는 웹이라는 환경정보나 웹 기술을 담고있는 클래스와 인터페이스를 사용해서는 안된다. 125 | - 때문에 비즈니스 로직을 담은 코드에 `HttpServeletRequest` , `HttpSession` 과 관련된 API가 등장하거나 웹 프레임워크의 클래스를 직접 이용하는 부분이 있다면 진정한 POJO라고 볼 수 없다. 126 | 127 |
128 | 129 | ### 객체지향 원리에 충실한 설계원칙을 가진다. 130 | 131 | - 위의 두가지 조건의 충족되었다고 POJO라고 볼수는 없다. 132 | - 만약 책임과 역할이 다른 코드들을 한 클래스에 몰아넣어 하나의 만능 클래스로 만들고, 재사용이 불가능할 정도로 다른 영역과 레이어에 강한 결합을 가지고 만들어지는 경우, 조건문으로 가득 도배가된 긴 메서드로 작성해 놓은경우라면 객체지향 오브젝트, POJO라고 부르기 힘들다. 133 | 134 | 때문에 진정한 POJO란 객체지향 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라서 재활용될 수 있는 방식으로 설계된 오브젝트를 의미한다. 135 | 136 | 또한 그러한 POJO에 애플리케이션의 핵심 로직과 기능을 담아 설계하고 개발하는 방법을 POJO 프로그래밍이라고 한다. 137 | 138 |
139 | 140 | ## 8.3.4 POJO의 장점은 무엇인가? 141 | 142 | - POJO 프로그래밍의 장점은 POJO가 될 수 있는 조건 그자체가 장점이 된다. 143 | - 특정 기술과 환경에 종속되지 않는 오브젝트는 그만큼 깔끔한 코드가 될 수 있다. 144 | - POJO로 개발된 코드는 자동화된 테스트를 작성하는데 매우 유리하다. 145 | - 객체지향 설계를 자유롭게 적용할 수 있다. 146 | 147 |
148 | 149 | ## 8.3.5 POJO 프레임워크 150 | 151 | - 스프링은 POJO를 이용한 엔터프라이즈 애플리케이션 개발을 목적으로 하는 프레임워크라고 한다. 152 | - POJO 프로그래밍이 가능하도록 기술적인 기반을 제공하는 프레임워크를 POJO 프레임워크라고하며 스프링과 하이버네이트가 대표적인 POJO 프레임워크이다. 153 | - 스프링을 이용하면 POJO 프로그래밍의 장점을 그대로 살려서 엔터프라이즈 애플리케이션의 핵심 로직을 객체지향적인 POJO를 기반으로 구현하고 동시에 엔터프라이즈 환경의 각종 서비스와 기술적인 필요를 POJO로 만들어진 코드에 적용할 수 있다. 154 | - 즉, POJO 프레임워크로서 스프링은 자신을 직접 노출하지 않으면서 애플리케이션을 POJO로 개발할 수 있게 지원해준다. 155 | 156 |
157 | 158 | # 8.4 스프링의 기술 159 | 160 | - 기술과 비즈니스 로직을 분리하고 POJO 방식의 애플리케이션 개발을 가능하게 하기 위해서는 스프링과 같은 POJO 프레임워크가 필요하다. 161 | - 스프링은 POJO 프로그래밍을 손쉽게 할 수 있도록 PSA, IoC/DI, AOP라는 세가지 기술을 제공한다. 162 | 163 |
164 | 165 | ## 8.4.1 제어의 역전과 의존관계 주입 166 | 167 | - 스프링의 가장 기본이 되는 기술이자 스프링의 핵심 개발 원칙이며 AOP와 PSA도 IoC/DI에 바탕을 두고 있다. 168 | - 두 개의 오브젝트를 분리하고 인터페이스를 통해 느슨하게 연결한뒤 실제 사용할 대상은 DI를 통해 외부에서 지정하는 방식이 자신이 사용할 오브젝트를 직접 `new` 키워드로 생성해서 사용하는 강한 결합방법보다 나은점에 대해 생각해보자. 169 | - 가장 간단하게는 유연한 확장이 가능하게 하기 위해서라고 할 수 있는데 DI는 개방 폐쇄 원칙이라는 객체지향 설계 원칙으로 가장 잘 설명될 수 있다. 170 | - 결국 각각의 의존관계를 가지는 두 모듈이 독립적으로 확장이 가능하고 각각의 변경이 다른 모듈에 영향을 주지않고 그대로 유지가 가능하다는 것으로 각각의 관점에서 유연한 확장, 변경없는 재사용으로 해석될 수 있다. 171 | 172 |
173 | 174 | ### DI의 활용 방법 175 | 176 | - 전략패턴을 이용해서 의존 대상이 가진 핵심 기능을 변경하는 경우 즉, 의존 대상의 구현을 변경하는 경우이다. 177 | - 매번 다르게 동적으로 의존 오브젝트의 핵심 기능을 변경하는 경우로 다이나믹 라우팅 프록시나, 프록시 오브젝트 기법을 활용하는 것이다. 178 | - 핵심기능은 그대로 둔 채로 부가기능을 추가하는 것으로 데코레이터 패턴을 생각할 수 있는데 이를 통해 핵심 기능과 클라이언트 코드에 영향을 주지 않으면서 부가적인 기능을 얼마든지 추가할 수 있다. 179 | - 사용하려는 오브젝트가 가진 인터페이스가 클라이언트와 호환되지 않은 경우 어댑터 역할을 해주는 레이어를 추가하여 이를 해결할 수 있으며 DI의 응용방법중 하나이자 PSA가 이 경우의 대표적인 예시이다. 180 | - 프록시 패턴의 전형적인 응용방법인 지연로딩의 경우도 DI를 통해 이루어진다. 181 | - 템플릿과 콜백 패턴에서 콜백을 DI를 이용해서 템플릿에 주입하는 방식으로 유연한 확장과 재사용이 가능한 코드를 만들 수 있다. 182 | - DI가 필요한 이유 중 한가지는 DI할 오브젝트의 생명주기를 제어할 수 있다는 것인데 이를 통해 오브젝트의 생성, 관계설정, 이용, 소멸에 이르기까지의 모든 과정을 DI 컨테이너가 주관하기 때문에 오브젝트의 스코프를 자유롭게 제어할 수 있다. 183 | - 테스트에 DI를 적용하면 고립된 단위 테스트를 쉽게 만들 수 있다. 184 | 185 |
186 | 187 | ## 8.4.2 AOP 188 | 189 | - 객체지향 기술만으로 점점 복잡해져가는 애플리케이션 요구조건과 기술적인 난해함을 모두 해결하는데에는 한계가 있다. 190 | - AOP는 이러한 객체지향의 한계와 단점을 극복하도록 도와주는 보조적인 프로그래밍 도구로써 AOP를 통해 OOP를 더욱 OOP 답게 만들 수 있다. 191 | - 때문에 스프링의 목적인 POJO만으로 엔터프라이즈 애플리케이션을 개발하면서 엔터프라이즈 서비스를 선언적으로 제공하는데 필요한 것이 AOP 기술이다. 192 | 193 |
194 | 195 | ### 다이나믹 프록시를 이용한 AOP 적용 196 | 197 | - 스프링과 같이 다이나믹 프록시를 이용하는 방법으로 기존의 코드에 영향을 주지 않고 부가기능을 적용하게 해주는 데코레이터 패턴을 응용할 수 있다. 198 | - 하지만 인터페이스와 DI를 활용하는 데코레이터 패턴을 기반원리로 한 만큼 부가 기능을 부여할 수 있는 곳은 메서드의 호출이 일어나는 지점 뿐이라는 제약이 존재한다. 199 | 200 |
201 | 202 | ### AspectJ와 같은 자바 언어의 한계를 뛰어넘는 언어의 확장을 이용해서 AOP를 적용 203 | 204 | - AspectJ는 강력한 기능을 가진 AOP를 제공하며 프록시 방식의 AOP에서는 불가능한 다양한 조인 포인트를 제공한다. 205 | - 메서드 호출 뿐만아니라 인스턴스 생성, 필드 엑세스, 특정 호출 경로를 가진 메서드의 호출 등에도 부가기능을 제공할 수 있다. 206 | - 이러한 AOP를 적용하기 위해서는 단순히 자바 언어와 JDK의 지원만으로는 불가능하며 별도의 AOP 컴파일러를 이용한 빌드 과정을 거치거나 바이트 코드를 조작하는 위빙과 같은 별도의 방법을 이용해야 한다. 207 | 208 |
209 | 210 | ## 8.4.3 PSA 211 | 212 | - 서비스 추상화는 특정 기술과 환경의 변화에 관계없이 일관된 방식으로 기술에 접근할 수 있게 해주는 기술이다. 213 | - POJO로 개발된 코드는 특정 환경이나 구현 방식에 종속되지 않아야하며 스프링은 PSA를 통해 POJO가 자바 엔터프라이즈 환경에 기술에 직접적으로 노출되지 않도록 해준다. 214 | -------------------------------------------------------------------------------- /토비의 스프링 3.1/README.md: -------------------------------------------------------------------------------- 1 |
2 |

"토비의 스프링 3.1" 을 읽고 토론한 내용을 공유합니다.

3 |
4 | 5 | 6 | 7 | * 1.1 초난감 DAO 8 | * 1.2 DAO의 분리 9 | * 1.3 DAO의 확장 10 | * 1.4 제어의 역전 (IoC) 11 | * 1.5 스프링의 IoC 12 | --------------------------------------------------------------------------------