| 개요
- 수업 시간에 고객의 계좌 정보에 관한 프로그램을 샘플로 만들며 아래의 내용을 배우기로 했다.
- 저작권상 내용을 여기에 담기는 좀 그래서 전반적인 개념에 추가적인 정보를 담아 스스로의 이해를 위해 정리해보려고 한다.
- 강사님의 강의 흐름이 마치 현장 플젝을 해결하는 것 같이 느껴지는데, 이 흐름만 잘 기억해도, 차후 개인 프로젝트를 기획할 때 많은 도움이 될 것 같다.
no | 흐름 | 특징 |
0 | 개요 | 시스템 한 줄 소개 / 활용 기술 요약 / 프로젝트 엔터티 구조 안내 / 주요 제공 기능(API) - 분류 |
1 | 프로젝트 생성 및 의존성 추가 |
spring.io 또는 인텔리J를 통해 스프링 프로젝트 생성, 의존성 추가 * 어떤 기술을 쓸 건지에 따라 의존성은 달라질 수 있다. |
2 | Lombok | Lombok을 통한 dto 생성 |
3 | HTTP 프로토콜 | HTTP 요청에 따라 Client의 요청사항 분석하고 응답하기 |
4 | H2 DB | 개발을 할 때 또는 테스트 시에 H2 DB를 통해 개발한다. * RDB와의 소통은 JPA를 사용하도록 한다. |
5 | 트랜잭션 | DB의 상태를 변화시키는 작업단위. ACID |
6 | Embeded Redis | 싱글 쓰레드 기반의 일종의 noSql이며 메모리DB를 사용한다. Spinlock으로 동시성을 제어하여 빠르게 휘발적으로 데이터를 사용할 때 Redis를 사용한다. |
7 | 테스트 | 테스트를 왜 할까? TDD와 단위적 테스트의 차이점은? |
1. 의존성 추가하기
- 인텔리j에서 보면, 프로젝트 바로 하위에 "build.gradle"이라는 파일이 있다.
- 앞에서도 잠깐 언급했는데, dependencies 안에 의존성을 추가할 수 있다.
인텔리j에서 플젝 시작할 때 Lombok이나 JDBC 같은 라이브러리를 추가하면 자동으로 의존성이 추가되는데,
별도로 직접 작성해줄 수도 있다.
/* 플러그인의 의존성(library) 관리 */
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'java'
}
group = 'com.zerobase'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
/* 각종 의존성들(libraries)을 어떤 원격 저장소에서 받을 것인지 지정 */
repositories {
mavenCentral() // jcenter로 업로드 설정을 간소화할 수 있다.
}
/* 프로젝트 개발에 필요한 의존성들을 선언하는 곳 */
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
2. Lombok
- Lombok은 DTO(=VO) 객체의 Getter/Setter, 생성자를 자동으로 생성해주거나,
특정 클래스를 지정해서 로그를 자동으로 생성해주는 라이브러리이다.
- 다양한 기능
구분 | 표기 | 내용 |
게터/세터 외 다수 | @Data | 게터/세터/toString/Equals/HashCode 등 한번에 제공 ** @Data의 경우, 사용자의 정보가 toString으로 그대로 노출되는 등의 이슈가 발생할 수 있으므로 사용에 주의하는 것이 좋다. |
게터/세터 | @Getter/@Setter | 게터/세터 자동 생성 |
생성자 | @NoArgsConstructor | 기본 생성자 |
@AllArgsConstructor | 모든 필드 포함 생성자 | |
@RequiredArgsConstructor | 지정 필드 포함 생성자 *맴버 중 private final로 된 맴버 필수 포함 | |
로그 | @Slf34j | 이 클래스의 로그 자동 생성 |
toString | @ToString | Object의 toString |
빌더 패턴 관련 | @Builder | 빌더 패턴 자동 생성 (생성자의 인자가 너무 많을 때, Builder를 쓴다.) ex. SomeClass.builder().name().weight().height().score().build() |
생성자 private화 | @UtilityClass | static 메소드만 제공하는 유틸리티 클래스의 생성자를 private화 |
- Delombok을 통해 Lombok으로 만들어진 생성자 및 메소드 확인하기
: DTO 객체에서 우측 마우스 - [Delombok]을 누르면 생성자 및 메소드가 자동 생성된다.
- 참고로 @Autowired를 사용해도 생성자나 게터, 필드를 만들 때 의존 객체의 타입을 자동으로 주입해주는데,
생성자의 인자가 많이 있거나 수정할 때마다 매번 타이핑을 해야하기 때문에 현재는 사용을 권장하지 않는다.
@Autowired 대신 롬복을 사용하는 것이 좋다.
3. HTTP (Hyper Text Tranfer Protocol)
- 하이퍼텍스트란, 링크를 연결시켜주는 텍스트를 말한다. (마치 이것처럼)
- 과거에는 단순 텍스트 링크(하이퍼텍스트)를 통해서 데이터를 주고 받았었다. - 하이퍼텍스트가 발전하여, HTML, 그리고 JSON 등이 생겨나게 되었다. - 이러한 데이터 전송 방식은 Hyper Text에서 사용하던 프로토콜, 곧 HTTP을 따른다. |
- HTTP 프로토콜에 대한 요약 정리
- 한 컴퓨터에서 다른 컴퓨터로 데이터를 요청할 때에는 데이터를 패킷이라는 단위로 묶어 주는 과정을 거친다. * OSI 7 Layer : Application -> Presentation -> Session -> Transport -> Network -> Data Link -> Physical * TCI/IP : Application -> Transport -> Internet -> Network Interface WWW는 TCP/IP방식을 따르므로, 그를 기준으로 보면 HTTP는 Application 단계(최상위 단계)에 해당된다. - Application단계에서 Client는 URI로 요청사항을 날리고, 그 요청사항은 HTTP의 헤더와 바디에 담겨진 뒤, 나머지 하위 과정에서 포장된다. 포장된 데이터는 여러 네트워크망을 거쳐 Server로 넘어가게 된다. - HTTP는 POST/GET/PUT(PATCH)/DELETE 라는 요청방식을 가진다. - HTTP는 본래 Connectless, 한 번 연결하면 끝나는 일회성 연결 방식을 가졌으나, HTTP 1.1로 버전이 올라가면서 keep-alive를 통해 일정 기간 동안 연결 상태를 유지하게 되었다. * TCP/IP 방식에 따르면, 매번 연결할 때마다 3-way-handshake가 필요하다. 1.1에서는 일정 기간 이걸 하지 않는다. 요즘에는 또 HTTP 2.0 버전이 나왔는데, 가장 큰 특징은 1.1은 한 번에 하나의 파일만 전송 가능했다면, 2.0은 이제 여러개 파일을 병렬처리로 전송하는 Multiplexing이 된다는 것. (그러나 헤더와 바디의 큰 구조는 위 그림처럼 비슷하다) - 추가적으로, HTTP는 본래 Stateless, 통신이 끝나면 상태를 유지하지 않았지만 쿠키와 세션으로 상태를 유지할 수 있다. 특정 정보(ex.로그인 정보)를 쿠키는 브라우저에, 세션은 서버의 id를 이용해 암호화해 서버에 저장 후 브라우저에도 저장한다. * 단, 연결 상태 유지 기간을 설정하면, 데이터는 브라우저가 아닌 로컬에 저장된다. - 정리하면, 오늘날의 HTTP는 URI를 통해 요청을 받는 것은 동일하되, 일정 기간 동안 연결을 유지할 수 있고, 데이터 송수신 시 병렬 처리가 가능해졌으며, 쿠키와 세션을 통해 특정 상태(ex. 로그인 상태)를 브라우저(또는 로컬)에 저장하여 유지시킬 수 있다. |
(1) HTTP Request
POST /path HTTP/1.1 // Header 첫줄 : Method 경로 HTTP버전
Content-Type: application/json // 받은 콘텐츠 타입
Accept: application/json // 보낼 콘텐츠 타입 (응답)
UserInfo: {"userId","test1234"} // 인증키나 회원명 등을 넣는다
{ // Body
"phone" : "010-1234-5678"
}
(2) HTTP Response
HTTP/1.1 200 OK // Header 첫줄 : HTTP버전 상태정보 메세지
Content-type: application/json // content-type부터 연결(keep-alive)유지 기간 등
Transfer-Encoding: chuncked
Date: Mon, 21 Jun 2022 12:11:23 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{ // Body : 응답 받는 Clinet에게 전달할 데이터
"message" : "등록 성공"
}
4. H2 DB를 사용하기
(1) H2 DB의 특징
: 메모리/파일 관계형 DB로, 가볍고 시작할 때마다 자동 삭제-생성 가능하며, 대다수의 DB sql명령어와 호환되어,
: 개발을 하면서 테스트를 하기에 좋은 DB이다.
|
(2) H2 DB를 설정 - application.yml 로 설정하기
: 스프링 플젝을 처음 생성하면, [src] - [java] - [main] - [resources] 안에 "application.properties" 파일이 있다.
: 애플리케이션에 들어가는 설정들을 정리해서 넣는 파일인데, 이 파일의 확장자를 .properties에서 .yml로 바꾼다.
rf. .yml파일은 야믈이라고도 하는 yaml 데이터 타입을 말한다. (+ 자세한 내용은 하단의 더보기에)
: 아래의 내용을 보면, 스프링 설정을 야믈 데이터 타입으로 안내하는 것인데 크게 보면,
[ DB에 대한 설정(위치와 JDBC정보) + H2 콘솔 사용 여부 + JPA 관련 설정(INSERT시작 지점, DB정보, 쿼리 관련 설정) ]
이렇게 세가지를 아래에서 하고 있는 걸 볼 수 있다.
spring: // spring 플젝의 설정할거다!
datasource: // (1) DB의 소스 :
url: jdbc:h2:mem:test // 5가지 정보(url-ip와port와instance),계정,비번)
username: // -- ip와 port대신 mem (memory를 의미)
password:
driverClassName: org.h2.Driver // JDBC 드라이버 정보
h2: // (2) h2 console 사용 여부
console:
enabled: true
jpa: // (3) jpa 관련 설정
defer-datasource-initialization: true // table 생성 후로 resource의 data.sql 입력 미룬다(defer)
database-platform: H2 // db vendor
hibernate: // hibernate (jpa 인터페이스 구현체)
ddl-auto: create-drop // ddl-auto : 자동으로 실행시 drop, create
open-in-view: false // open-view (뷰에서 볼 것인지)
properties: // properties (설정)
hibernate: // hibernate - sql 형식으로 쓰고 볼지
format_sql: true
show_sql: true
[ JPA란 ? ] - 출처 : https://dbjh.tistory.com/77?category=853400 - JPA란, Java Persistence API의 약자로, 말 그대로 RDB와 소통할 때도 자바를 지속적으로 쓰게 해주는 API이다. - 조금 어려운 말로는, 자바에서 ORM(Object-relational Mapping) 기술 표준으로 사용되는 인터페이스 모음이라고 하는데, - 결국 ORM도 객체와 RDB를 연결(매핑)해주는 걸 의미한다. - JPA는 JPA라는 인터페이스가 있고, 그를 hibernate, OpenJPA등이 구현한다. - 장점 : - SQL문이 아닌 자바언어로 DB를 조작하기 때문에, 비즈니스 로직에 맞춰 개발하기 편리하며 가독성이 높다. - 객체 지향적인 코딩이 가능해 진다. (상속 또한 RDB로 자동 표현할 수 있게 했다.) - DB vendor를 변경해도 코드를 수정하지 않아도 되어 vendor에 대한 의존도를 낮추고 리팩토링에 유리하다. - 단점 : - 프로젝트 규모가 크고 복잡한 상태일 때, 설계가 잘못 될 경우, 속도 저하 및 일관성이 떨어질 수 있다. - 복잡하고 무거운 쿼리문의 경우 속도를 위해 별도로 SQL문을 써야할 수 있다. - JPA를 배우는 비용이 비싸다. |
- 설정을 다 하고, 어플리케이션을 이클립스나 인텔리j에서 실행하면, 에디터 콘솔에 Tomcat을 통해 알아서 잘 생성됐다는 메세지가 뜬다.
- 이제, 브라우저에서 아래의 정보를 입력하면 H2 DB 콘솔을 볼 수 있다.
http://localhost:8080/h2-console
- 설정 야믈에 써놓은 URL과 계정명, password를 잘 써주고 연결하면 된다. (JDBC URL이, url이다)
* 처음에 JDBC URL은 jdbc:h2:~/test;로 되어 있는데, 이 부분을 아까 야믈 설정파일에서 설정해두지 않았다면,
매번 실행할 때마다 주소가 자동 할당되어 에디터 콘솔에 뜨고, 그걸 복사해서 위에 붙여주어야만 한다.
[ 이름도 귀여운 야믈은 뭘까? ]
- 데이터를 전송할 때의 규칙, 약속을 우리는 데이터 타입이라고 한다.
- 데이터 타입에는 XML, JSON 등이 있는데
- XML은 <>과 들여쓰기로 각 데이터간 관계성을 나타내고
- JSON은 key와 value로 데이터간의 관계성을 나타낸다면
- YAML은 - 하이픈을 통해 깔끔하게 데이터간의 관계성을 표현한다.
- 야믈이 귀여운 이름만큼 우리 눈과 마음을 참 깔끔하게 해주는 녀석이라고 기억해두면 좋을 것 같다.
(3) 레이어 디자인 패턴으로 패키징을 하고 JPA를 통해 domain 객체를 만든다.
- 앞서서, 스프링은 MVC 3-tier 아키텍처를 따른다고 했다.
* MVC 3-tier 아키텍처는 SOLID의 SRP원칙(단일책임원칙)을 지키도록 해주는데,
[1] 표현층 (Presentation Layer)
[2] 비즈니스 로직층(Business Logic Layer)
[3] 데이터 엑세스층(Data Access Layer)
로 나누어져 있으며, 표현층의 컨트롤러로 비즈니스 로직층으로 넘어간 뒤 DB와 상호작용(Service)하여 처리한다.
- Layered는 n-tier 방식으로 코드를 짜는 디자인 패턴을 말하는 것으로, 각 의존 관계가 마치 레이어처럼 겹겹이 쌓여있다.
* 실습에서는 controller, service, domain(dto), repository로 나누었는데 아래와 같이 나누어볼 수 있었다.
3-tier 패턴의 레이어 | 패키지명 | 특징 | 의존 | Annotation |
Presentation Layer | controller | http 메소드로 관련 service 호출 | service | @RestController @GetMapping 등 |
domain | model : dto(vo), dto에 들어가는 타입(enum) |
- | @Entity @Enumerated |
|
Service Layer | service | repository의 메소드를 구현하여 db에 쿼리 전달 | repository | @Service @Transactional |
Repository Layer | repository | jpa를 통해 db의 table과 직접적으로 연동 | - (db와 소통) | @Repository |
- 현장에서는 다만, 이와 같은 디자인 패턴 보다는 Hexagonal Architecture 디자인 패턴을 더 선호한다고 한다.
(3-1) 테이블 모델
: 레이어 패턴에서는 dto를 domain안에 만들었는데, @Entity를 통해 이 클래스가 테이블임을 표현한다.
: 이렇게 생성한 후에 다시 애플리케이션을 실행하면, H2 console 부분에 테이블이 생성된 게 보인다.
// import 생략
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder // 여기까지 Lombok을 사용해 builder, 게터/세터를 만들게 한다.
@Entity // Entity를 쓰면 이 클래스를 Entity로 설정하겠다는 의미이다.
public class TableName{
@Id // pk인덱스 생성
@GeneratedValue // pk가 Sequence인지, Table인지 등과 세부 제한 설정
private Long id;
private String certainMember1;
private String certainMember2;
}
(3-2) 리포지토리 만들기
- JPA를 통해 DB와 직접적인 소통을 하는 것은 repository interface이다.
- 중요한 건, 이것은 인터페이스이며 클래스로 만들면 안 된다는 것이다.
* 왜 인터페이스로 되어 있을까? 그건 DIP 법칙에 따라 JPA에 너무 의존하지 않게 하려는 것 아닐까? (좀더 생각해보자..)
- JpaRepository<테이블객체명,pk타입>를 구현한 인터페이스를 만든다.
@Repository
public interface CertianRepository extends JpaRepository<TableName, Long> {
}
- JpaRepository의 메소드들은 굉장히 많았는데 그 중에 기본적인 메소드들은 아래와 같았다.
save(S entity) : S saveAll(Iterable<S> entities) : Iterable saveAll |
저장하기 |
findById(ID id) : Optional<T> findAll() : Iterable |
조회하기 |
deleteById(ID id) : void delete(T entity) : void deleteAllById(Iterable<? extends ID> ids) : void |
삭제하기 |
existsById(ID id) : boolean |
존재여부 |
count() : long | 갯수반환 |
(3-3) 서비스 만들기
- 서비스는 리포지토리 인터페이스를 구현한다.
- 바로 위의 표의 메소드들 (save, findById, delete, count 등)을 호출하는 클래스를 만든다.
@Service
@RequiredArgsConstructor
public class CertainService {
private final CertainRepository certainRepository;
@Transactional
public void createOneRow() {
TableName tableName = TableName.builder()
.memberfield("hahaha")
.build();
certainRepository.save(tableName);
}
@Transactional
public TableName getOneRow(Long id) {
return certainRepository.findById(id).get();
}
}
(3-4) 컨트롤러로 URI에 대한 HTTP 처리하기
- 앞서서 Client는 브라우저의 URI를 통해 요청을 보낸다고 했다.
- 이제 어느 경로에서 어떻게 URI를 보내면, 특정 서비스의 메소드를 호출할지를 여기서 정해주면 된다.
@RestController
@RequiredArgsConstructor
public class CertainController {
private final CertainService certainService;
@GetMapping("/create-certain")
public String createCertain() {
certainService.createOneRow();
return "success";
}
@GetMapping("/certain/{id}")
public Account getAccount(@PathVariable Long id) {
return certainService.getOneRow(id);
}
}
5. 트랜잭션
트랜잭션 : 데이터베이스의 상태를 변경시키기 위해 수행하는 작업단위
[출처] 코딩팩토리, https://wonit.tistory.com/462
- 트랜잭션은 DB의 상태를 변경시키는 것으로, CRUD(INSERT, SELECT, UPDATE, DELETE) 행위를 말한다.
(1) Commit과 Rollback
- 트랜잭션은 순차적으로 Commit -저장-을 하며 실패해도 로그를 남겨, 앞의 트랜잭션이 다 끝나야 실제로 반영한다.
- 만약에, 트랜잭션이 비정상적으로 종료한다면 Rollback -철회-을 통해, 트랜잭션 전체 또는 부분적으로 결과를 취소한다.
(2) 트랜잭션의 상태
- 트랜잭션은 실행중(Active)이거나
- 커밋 일부 완료(Partially Commited), 커밋 모두 완료(Commited)
- 실행중 오류 발생으로 실패(Failed), 비정상적 종료로 rollback 수행(Aborted)
- 하는 총 5가지의 상태를 가질 수 있다.
(3) 트랜잭션의 특징
- 트랜잭션에는 4가지의 특징이 존재하는데,
ACID | 내용 |
Atomic (원자성) | All or Nothing : 모든 작업이 실행되거나 or 모두 실행되지 않아야 한다. rf. 결제서비스에 관한 트랜잭션 중 갑자기 오류발생으로 끊긴다면? ㄴ 결제서비스에 관한 트랜잭션 전체는 실행되지 않게 된다. |
Consistency (일관성) | 트랜잭션 작업 결과는 항상 일관적이어야 한다. * 모든 트랜잭션이 종료된 후엔 모든 DB의 제약조건을 지키고 있는 상태여야 한다. rf. 웹툰 결제를 할 때 최소 결제 단위는 100원이어야 한다면, 결제 서비스 트랜잭션이 실행될 때 내 계정엔 100원 이상이 남아있어야 한다. |
Isolation (격리성) | 트랜잭션은 다른 트랜잭션과 독립적이어야 한다. rf. 현실적으로 트랜잭션의 격리성을 지키기가 어렵다 (성능과 안정성의 trade-off) - 격리성의 단계 ) READ_UNCOMMITED > READ_COMMITED > REPEATABLE_READ > SERIALIZABLE - 대체로 현장에서는 REPEATABLE_READ 를 한다고 한다. |
Durability (지속성) | 트랜잭션이 완료되면 영구적으로 결과에 반영되어야 한다. * 트랜잭션은 순차적으로 commit되며, commit이 실패해도 모든 로그를 남겨 db에 반영된다. |
6. Embedded Redis 실행
레디스는 고성능 In-Memory 키-값 저장소로서
문자열, 리스트, 해시, 셋, 정렬된 셋 형식의 데이터를 지원하는 NoSQL이다.
[출처] https://devlog-wjdrbs96.tistory.com/374
- 위의 말을 풀어서 이해했을 때, 레디스는 결국 DB의 데이터를 휘발적으로 메모리에 직접 넣어서 빠르게 처리할 수 있게 해주는 녀석이며, 데이터의 모양은 noSql 중에서도 키-값 형태로 저장되는 녀석인 것 같다.
+ noSql에 대해 더 알고 싶다면 아래의 더보기로
- Redis는 SpinLock을 통해 동시성 제어를 해주며, AOP를 실습할 때에 따라서 자주 사용된다고 한다.
- 나의 경우에는 수업에 따라 인텔리j를 쓰고 있어서 별도로 Redis를 설치하는 곤욕을 겪지 않았는데, 따로 설치해주는 경우도 있는 모양이다. 그게 필요한 경우에 잘 나와 있는 페이지가 있어서 갖고 왔다.
(1) Redis를 dependencies에 추가 & application.yml에도 host/port를 설정해준다.
- Redis도 라이브러리이기 때문에 쓰려면 의존성을 추가해주어야 한다.
- Build.gradle에 들어가서, 아래와 같이 dependencies를 작성해 주었다.
- 아래에서 exclude된 부분은 이미 롬복에서 처리해주는 부분이기 때문에, 제외하지 않으면 오류가 난다고.
그래서 이렇게 제외를 해주는 게 좋다고 한다..
dependencies {
// redis client
implementation 'org.redisson:redisson:3.17.1'
// embedded redis
implementation('it.ozimov:embedded-redis:0.7.3') {
exclude group: "org.slf4j", module: "slf4j-simple"
}
}
- Redis 또한 in-memory 방식의 noSql이기 때문에 다른 DB들과 마찬가지로 host와 port가 필요하다.
- 앞에서 만들어주었던 application. 야믈파일에서 아래와 같이 호스트와 계정을 넣어준다.
* 보통 Redis는 6379 포트를 쓴다고 한다.
spring:
redis:
host: 127.0.0.1
port: 6379
(2) LocalRedis 실행 설정하기
- Config 클래스를 하나 만들어, 포트정보를 통해 RedisServer를 만들어준다.
- RedisServer는 @PostConstruct와 @PreDestroy를 할 수 있는 메소드를 생성해주는데,
Bean의 생성주기에서 빈 생성 및 의존관계 주입 후 @PostConstruct가 이루어지고, 종료 전에 @PreDestroy가 이루어진다.
[ Bean의 생성주기 ]
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백(EVENT) → 앱 본연의 동작 수행 → 소멸전 콜백(EVENT) → 스프링 종료 |
@Configuration
public class LocalRedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
private RedisServer redisServer;
@PostConstruct
public void startRedis() {
redisServer = new RedisServer(redisPort);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if (redisServer != null) {
redisServer.stop();
}
}
}
(3) LocalRepository 등록
- Config 클래스를 하나 만든 후에, 호스트와 포트 맴버 필드를 만들고, RedisClient를 호출하는 메소드를 만든다.
- 이 설정 자체도 너무 어려웠다.. 암기가 필요한 순간.
@Configuration
public class RedisRepositoryConfig {
@Value("${spring.redis.host}") // EL같다. application의 데이터를 가져온다.
private String redisHost;
@Value("${spring.redis.port}") // EL같다. application의 데이터를 가져온다.
public int redisPort;
@Bean
public RedissonClient redissonClient() { // RedissonClient을 반환해주어야한다.
Config config = new Config(); // Config 객체 생성
config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
return Redisson.create(config);
}
}
7. 테스트
- 과거의 경우 어떤 서비스가 만들어지면 그것을 하나하나 검수하는 과정을 거쳤었다.
- 현재에는 자동화된 테스트 코드를 통해 빠르게 테스트를 하고 있다.
- 다양한 테스트 커버리지와 테스트 방법론이 존재한다.
과거의 테스트 | 현재의 테스트 | |
방법 | 기능적으로 하나하나 일일히 검수 | 자동화된 테스트 코드 |
특징 | 다수의 인력, 시간이 너무 많이 든다 | 빠른 속도로 테스트 가능, 코드 수정 후 재검수 용이 |
[ 테스트에 관한 개념 ]
(1) 테스트 커버리지 : 단위 테스트 / 전 구간 테스트 / 통합 테스트
단위 테스트(Unit Test) | 전 구간 테스트(End-to-End Test) | 통합 테스트(Integration Test) | |
크기 | 클래스 또는 메소드 | 현재부터 ~ 배포까지 | 연관된 모든 기능 전반(외부lib까지) |
장점 | - 개발자 관점의 빠른 테스트 - TDD와 함께 할 때 강력하다. |
- 내부 기능까지 테스트하지 않는다 - 사용자 관점에서 E2E(End to End) 형식을 이용해 확인 |
- 개발자가 변경불가한 부분 확인 - 단위 테스트에서 발견이 어려운 버그까지 커버 |
단점 | - 단위를 벗어난 영역의 버그를 확인하기 어려움 | - 테스트를 만들기가 힘들다 - 많은 코드를 테스트해 신뢰성 ↓ |
- 많은 코드를 테스트해 신뢰성 ↓ - 에러 발생 지점을 찾기 어렵다 - 유지보수가 힘들다 |
(2) TDD란 뭘까? Test-Driven-Development의 약자로, "테스트 주도 개발"이라는 테스트 방법론을 의미한다.
: 간단히 요약하자면, 개발 전에 테스트 코드부터 작성하고 개발 후 리팩토링(수정)하는 방법이다.
(3) 테스트는 왜 해야할까? 비즈니스 로직에 맞고, 빠르고 안정적인 개발을 하기 위함
1) 테스트를 하면서 스스로 자신의 코드를 리뷰할 수 있다. 2) 테스트가 잘 되어있으면 리팩토링 하기가 수월하다. - 개발을 하다보면, 기존에 만들어진 코드가 정책에 맞춘 코드인지, 아니면 임의로 만들어진 코드인지 구분을 해야한다. - 테스트 코드를 보면 기획에서 다 알지 못하는 영역까지도 코드 내역을 통해 커버할 수 있다. |
(4) 테스트를 잘 하기 위해서는 뭘 해야할까?
1) 클래스나 메서드가 SRP를 지키며 너무 크지 X 2) 단위 테스트 시, 적절한 Mocking으로 격리성 확보 -- Mocking이란, 카피를 의미 3) 테스트 커버리지를 높혀, 놓치는 구간이 없도록 한다. 4) 테스트 코드 또한 속도 및 방법 등의 면에서 지속적으로 개선해준다. |
[ 테스트를 위한 라이브러리 - JUnit, Mockito ]
JUnit5 | - xUnit이라는 단위 테스트(Unit Test) 프레임워크 - 단위 테스트를 실행 후 전체 결과를 리포트 - 스프링 2.4버전대부터 spring-boot-starter-test에 JUnit5가 포함되어 있다. * 최근 버전에서 JUnit4를 선택하면 테스트 동작안한다. (잦은 테스트 에러) |
Mockito | - Mock(가짜)을 만들어 주는 라이브러리 - 왜 필요한가? : 특정 controller나 service에 연관된 클래스들은 main에 작성되어 있다. : 만약 test를 위해 main과 test를 둘 다 돌리면 port를 두 개의 프로그램이 사용하는 것으로 에러가 난다. : 그렇기에 main에 있는 의존하는 클래스를 test 파일 안에 일일히 생성해주어야 하는데 : 이러한 방식은 품이 너무 많이 든다. --> Mock을 만들어, 의존 클래스를 inject 한다. |
[ JUnit5와 Mockito를 통한 단위 테스트 실습 ]
(1) JUnit5를 사용해보기
(1-1) build.gradle의 dependencies에 "spring-boot-starter-test"가 있는지 확인 (디폴트로 되어있다)
dependencies {
// 생략
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
- 위에 보면 tasks.named('test')가 있는데, 'test'로 된 디렉토리의 테스트들을 JUnit으로 돌리겠다는 의미이다.
(1-2) 테스트하고자 하는 클래스에서 Ctrl+shirt+T로 테스트를 생성한다
- 보면 아래처럼 JUnit5로 라이브러리가 설정되어 있는 걸 볼 수 있다. 이걸 4로 바꾸면 당연히 테스트는 안 된다.
(1-3) @SpringBootTest와 @Autowired를 통해 의존을 주입한다.
// import 생략
@SpringBootTest // 실제 환경과 동일하게 모든 Bean들을 등록
class CertainTest{
@AutoWired // 주입
CertainService certainService;
@BeforeEach // 아래의 두 메소드 각각 시작 전에 행 하나 생성
void init() { certainService.createOneRow(); }
@Test
@DisplayName("Test Name blah blah")
void testGetOneRow(){
TableName tableName = certainService.getOneRow(1L);
assertEquals("010-1234-5678", tableName.getPhone());
assertEqauls(TableName.CertainType, tableName.getType());
}
@Test
@DisplayName("Test Name blah blah2")
void testGetOneRow2(){
TableName tableName = certainService.getOneRow(2L);
assertEquals("010-1234-5678", tableName.getPhone());
assertEqauls(TableName.CertainType, tableName.getType());
}
}
- 그런데 위와 같이 하면 @SpringBootTest로 안 쓰는 클래스까지 가져오면 원하던 테스트에서 방향이 달라질 수 있다.
- 더불어 매번 @AutoWired로 주입을 하게되면 너무 품이 많이 든다.
- 이를 해소하고자 Mockito의 Mock을 사용한다.
(2) Mockito의 Mock을 사용해서 service에서 필요한 클래스만 가짜로 주입하기
@ExtendWith(MockitoExtension.class) // MockitoExtension이라는 클래스를 쓴다.
class CertainServiceTest {
@Mock // 가짜 생성
private CertainRepository certainRepository;
@InjectMocks // 위에서 만든 Mock을 주입
private CertainService certainService;
@Test
@DisplayName("성공")
void testXXX() {
// given
given(certainRepository.findById(anyLong()))
.willReturn(Optional.of(TableName.builder()
.type(TableNameType.TYPE)
.phone("010-1234-5678").build()));
// when
TableName tableName = certainService.getOneRow(1234L);
// then
assertEquals("010-1234-5678", certainService.getPhone());
assertEquals(TableNameType.TYPE, certainService.TableNameType());
}
}
[ Controller 테스트 하기 ]
방법1 | 방법2 |
@SpringBootTest + @AutoConfigMockMvc : 전체 Bean 생성 후 : mockMvc로 HTTP요청 및 검증 |
@WebMvcTest : 필요로하는 MVC관련 Bean만 생성 - Controller, ControllerAdvice, Converter, Filter 등 : 하위 레이어 기능*의 경우, @MockBean으로 mocking - *Controller에 의존하는 Service 및 기타 : 마찬가지로 mockMVC로 HTTP 요청 및 검증 |
@WebMvcTest(AccountController.class)
class AccountControllerTest {
@MockBean
private AccountService accountService;
@MockBean
private RedisTestService redisTestService;
@Autowired
private MockMvc mockMvc;
@Test
void successGetAccount() throws Exception{
// given
given(accountService.getAccount(anyLong()))
.willReturn(Account.builder()
.accountNumber("3456")
.accountStatus(AccountStatus.IN_USE)
.build());
// when
// then
mockMvc.perform(get("/account/876"))
.andDo(print())
.andExpect(jsonPath("$.accountNumber").value("3456"))
.andExpect(jsonPath("$.accountStatus").value("IN_USE"))
.andExpect(status().isOk());
}
}
[ Service 테스트의 다양한 방법 ]
verify
의존하고 있는 Mock이 해당되는 동작을 수행했는지 확인하는 검증
verify(accountRepository, times(1)).save(any<Account>());
verify(accountRepository, times(0)).findById(anyLong());
ArgumentCaptor
의존하고 있는 Mock에 전달된 데이터가 내가 의도하는 데이터가 맞는지 검증
ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class);
verify(accountRepository, times(1)).save(captor.capture());
assertEquals("1234", captor.getValue().getAccountNumber());
assertions
다양한 단언(assertion) 방법들
assertEquals("1234", captor.getValue().getAccountNumber());
assertNotEquals("1234", captor.getValue().getAccountNumber());
assertNull(result);
assertNotNull(result);
assertTrue(resut.getBoolean());
assertFalse(resut.getBoolean());
assertAll(
() -> assertTrue(resut.getBoolean()),
() -> assertFalse(resut.getBoolean())
);
assertThrows
예외를 던지는 로직을 테스트하는 방법
AccountException exception =
assertThrows(AccountException.class, () ->
accountService.getAccount(123L));
assertEquals(ACCOUNT_NOT_FOUND, exception.getErrorCode());
[ 참조 및 출처 ]
부트캠프 수업 후 정리한 내용입니다.
HTTP 메시지 https://developer.mozilla.org/ko/docs/Web/HTTP/Messages
HTTP 2.0 https://velog.io/@taesunny/HTTP2HTTP-2.0-%EC%A0%95%EB%A6%AC
Lombok을 이용해 Builder 패턴 만들기 https://zorba91.tistory.com/298
H2 DB의 특징 https://jamie95.tistory.com/188
야믈 yaml 파일이란 https://www.inflearn.com/questions/16184
JPA란? https://dbjh.tistory.com/77?category=853400
Layer 디자인 패턴 https://4ngeunlee.tistory.com/222
트랜잭션이란? https://wonit.tistory.com/462
단위테스트/통합테스트/인수테스트 https://tecoble.techcourse.co.kr/post/2021-05-25-unit-test-vs-integration-test-vs-acceptance-test/
TDD란? http://clipsoft.co.kr/wp/blog/tddtest-driven-development-%EB%B0%A9%EB%B2%95%EB%A1%A0/
'Framework > Spring' 카테고리의 다른 글
[스프링] Entity 객체를 생성 : 영속성의 개념 + 자동 Auditing (0) | 2022.09.15 |
---|---|
[스프링] 개발을 시작하기 전에 - 요구 사항 분석, 기본 구조 잡기 (1) | 2022.09.15 |
[스프링] 스프링 MVC - 예외처리 (1) | 2022.09.11 |
[스프링] 스프링 MVC - 필터, 인터셉터 (1) | 2022.09.11 |
[스프링] 스프링MVC - HTTP 요청 및 응답 (1) | 2022.09.11 |