SpringBoot (20)

 

 

 

 

 

🍙 애플리케이션 빌드

 

 

🧁 IntelliJ IDE를 이용한 빌드

 

Spring Boot는 Gradle 빌드 툴을 이용해서 애플리케이션을 빌드할 수 있는 툴을 지원한다.

IntelliJ 에서 Gradle 탭을 누르고

Task ➡️ build ➡️ bootjar or build

로 손쉽게 빌드할 수 있다.

 

빌드가 정상적으로 완료되면

build/libs 디렉토리에 Jar 파일이 생성된다.

생성된 Jar 파일이 로컬에서 애플리케이션을 실행가능한 실행파일이다.

 

 

🧁 Gradle Task를 이용한 빌드

 

IntelliJ 같은 IDE가 설치되지 않은 환경에서 빌드를 해야하는 경우도 있다.

CLI 환경에서 빌드할 때는 프로젝트 최상위 경로에서

./gradlew build

를 입력해주면 빌드된다.

BUILD SUCCESSFUL

라고 적힌다면 빌드가 완료된 것이다.

 

 

 

🍙 애플리케이션 실행

 

java -jar [jar파일명].jar

입력하면 애플리케이션이 실행된다.

 

 

 

🍙 애플리케이션 배포

 

Spring Boot 기반의 실행 가능한 Jar(Executable Jar)을 서버에 배포해보자

 

 

🧁 전통적인 서버 배포 방법

 

Spring Boot 기반의 Executable jar 파일을 서버에 배포하기

scp, sftp 같은 표준 유닉스 툴을 사용해서 서버로 간단히 전송한다.

서버로 전송된 Jar파일은 JVM이 설치된 환경이라면 어디서든 쉽게 실행할 수 있다.

 

 

🧁 클라우드 서비스를 위한 배포 방법

 

Executable Jar 파일은 특히 클라우드 환경에서 손쉽게 배포할 수 있다.

 

 

▶️ PaaS(Platform as a Service)

 

대표적 예시 : Cloud Foundry, Heroku

 

▶️ IaaS(Infrastructure as a Service)

  • Executable Jar 는 AWS Elastic Beanstalk, AWS Container Registry, AWS Code Deploy 같은 서비스를 이용해서 손쉽게 배포가능하다.
  • Microsoft에서도 Azure 클라우드 서비스인 Azure Spring Cloud, Azure App Service 에서 배포 가능하다.
  • Google Cloud 에서도 Executable Jar 파일 배포를 위한 여러가지 옵션을 제공한다.

 

▶️ CI / CD 플랫폼을 사용한 배포

  • Github Actions, Circle CI
  • AWS나 Azure 같은 클라우드 서비스에 Executable Jar 파일을 자동 배포하도록 구성할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

API 문서화

 

 

➤ API 문서화란?

 

API 문서화란 클라이언트가 REST API 백엔드 애플리케이션에 요청을 전송하기 위해서 알아야 되는 요청 정보(요청 URL 또는 URI, request body, query parameter 등)를 문서로 잘 정리하는 것을 의미한다.

 

 

▶️ API 요청을 위해 필요한 정보를 문서로 정리해야 하는 이유는 무엇인가?

 

백엔드에서 만든 REST API 기반의 애플리케이션을 클라이언트 쪽에서 사용하려면 API 사용을 위한 정보가 필요하기 때문이다.

 

API 사용을 위한 정보가 담겨 있는 문서를 API 문서 또는 API 스펙(사양, Specification)이라고 한다.

 

 

▶️ API 문서를 만드는 방법

 

🥑 개발자가 수기로 작성 ➡️ 비효율적!, 기능을 빠뜨릴 수 있음!

🥑 애플리케이션 빌드를 통해 API 문서를 자동으로 생성

 

 

 

➤ Spring Rest Docs vs Swagger

 

 

▶️ Swagger의 API 문서화 방식

 

🍋 Swagger는 API 문서 자동화 오픈 소스이다.

🍋 Java 기반의 애플리케이션에서는 전통적으로 Swagger를 많이 사용해왔다.

🍋 많은 양의 애너테이션이 추가된다. ➡️ 가독성이 떨어짐

🍋 코드가 길어지면 그만큼 API 문서화를 위한 코드도 길어진다. ➡️ 유지보수성이 떨어짐

🍋 Controller 뿐만 아니라 DTO 클래스에도 애너테이션을 일일이 추가해주어야 한다.

🍋 Swagger 기반의 API 문서는 [Excute] 라는 버튼하나로 Controller에 요청을 전송할 수 있다. ➡️ API 요청 툴로서 기능을 사용할 수 있다.

 

 

▶️ Spring Rest Docs의 API 문서화 방식

 

🍑 애플리케이션 기능 구현과 관련된 코드에 API 문서 생성을 위한 애너테이션과 같은 정보를 추가하지 않는다.

🍑 Controller 테스트 클래스에 API 문서화를 위한 정보가 추가된다.

🍑 테스트 케이스의 실행결과가 “passed”로 만들지 않으면 API 문서 생성이 완료되지 않는다. ➡️ API 스펙 정보와 API 문서 정보의 불일치로 생길 수 있는 문제 방지

🍑 테스트 케이스에서 전송하는 API 문서 정보와 Controller에서 구현한 request body, response body, query parameter 등의 정보가 하나라도 일치하지 않으면 테스트 케이스 실행 결과가 “failed” 되면서 API 문서가 정상적으로 생성이 되지 않는다.

🍑 Swagger 처럼 API 호출할 수 있는 툴의 역할은 하지 못한다.

 

 

 

 

Spring Rest Docs

 

 

➤ Spring Rest Docs 란?

 

Rest API 문서를 자동으로 생성해주는 Spring 하위 프로젝트이다.

 

 

➤ Spring Rest Docs 의 API 문서 생성 흐름

 

Spring Rest Docs

 

 

1. 테스트 코드 작성

🍒 슬라이스 테스트 코드 작성

🍒 API 스펙 정보(Request Body, Response Body, Query Parameter 등) 코드 작성

 

 

2. test task 실행

🍒 작성된 슬라이스 테스트 코드를 실행한다.

🍒 하나의 테스트 클래스를 실행시켜도 되지만 일반적으로 Gradle의 build task 중 하나인 test task 를 실행시켜서 API 문서 snippet(스니핏*)을 일괄 생성한다.

🍒 테스트 실행 결과가 “passed” 이면 다음 작업을 진행한다.

🍒 실행결과가 “failed” 일 경우 문제를 해결하기 위해 테스트 케이스를 수정한 뒤 다시 테스트를 진행한다.

 

※ 스니핏(snippet)

일반적으로 코드의 조각을 의미한다.

여기선 문서의 일부 조각을 의미한다.

테스트 케이스 하나당 하나의 스니핏이 생성되며, 여러개의 스니핏을 모아서 하나의 API 문서를 생성할 수 있다.

 

 

3. API 문서 스니핏(.adoc 파일) 생성

🍒 API 스펙 정보 코드를 기반으로 API 문서 스니핏이 .adoc 확장자를 가진 파일로 생성된다.

 

 

4. API 문서 생성

🍒 생성된 API 문서 스니핏을 모아서 하나의 API 문서로 생성한다.

 

 

5. API 문서를 HTML로 변환

🍒 생성된 API 문서를 HTML 파일로 변환한다.

🍒 HTML로 변환된 API 문서는 HTML 파일 자체를 공유해도 되고 URL을 통해 해당 HTML에 접속해서 확인할 수 있다.

 

 

 

➤ Spring Rest Docs 설정

 

1. build.gradle 설정

2. API 문서 snippet 을 사용하기 위한 템플릿 API 문서 생성

 

 

▶️ build.gradle 설정

 

API 문서를 자동으로 생성하기 위해 아래 설정 코드 추가

plugins {
	...
	id "org.asciidoctor.jvm.convert" version "3.3.2"
	...
}

...
ext {
	set('snippetsDir', file("build/generated-snippets"))
}

...
configurations {
	asciidoctorExtensions
}

dependencies {
	...
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
	asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    ...
}

...
tasks.named('test') {
	outputs.dir snippetsDir
	useJUnitPlatform()
}

...
tasks.named('asciidoctor') {
	configurations "asciidoctorExtensions"
	inputs.dir snippetsDir
	dependsOn test
}

...
task copyDocument(type: Copy) {
	dependsOn asciidoctor
	from file("${asciidoctor.outputDir}")
	into file("src/main/resources/static/docs")
}

build {
	dependsOn copyDocument
}

...
bootJar {
	dependsOn copyDocument
	from ("${asciidoctor.outputDir}") {
		into 'static/docs'
	}
}

 

 

애플리케이션 실행 후 웹 브라우저에서 아래 주소로 이동하면 API 문서를 눈으로 확인할 수 있다.

http://localhost:8080/docs/index.html

 

 

 

▶️ API 문서 snippet을 사용하기 위한 템플릿(또는 source 파일) 생성

 

API 문서 스니핏이 생성 되었을 때 이 스니핏을 사용해서 최종 API 문서로 만들어 주는 템플릿 문서(index.adoc)을 생성한다.

 

아래 경로에 해당하는 디렉토리 생성 (Gradle 기반 프로젝트)

src/docs/asciidoc/

 

디렉토리 내에 비어있는 템플릿 문서를 생성 (index.adoc)

 

 

 

 

Controller 테스트 케이스에 Spring Rest Docs 적용하기

 

 

➤ API 문서화를 위한 슬라이스 테스트 케이스 작성

 

Controller 슬라이스 테스트 클래스를 작성하고 API 문서화를 위한 코드를 추가해준다.

 

 

▶️ @SpringBootTest -> @WebMvcTest 로 변경

 

🍎 @WebMvcTest 는 Controller를 테스트 하기 위한 전용 애너테이션이다.

🍎 @WebMvcTest 괄호 안에 테스트 대상 Controller 클래스를 지정해준다.

 

 

▶️ @MockBean(JpaMetamodelMappingContext.class)

 

🍎 JPA에서 사용하는 Bean 들을 Mock 객체로 주입해주는 설정이다.

 

Spring Boot 기반의 테스트는 항상 최상위 패키지 경로에 있는 ~Application 클래스를 찾아서 실행한다.

~Application 클래스에 @EnableJpaAuditing 추가

➡️ JPA와 관련된 Bean을 필요로 하기 때문에 @WebMvcTest 애너테이션을 사용해서 테스트를 진행할 경우 JpaMetamodelMappingContext를 Mock 객체로 주입해주어야 한다.

 

 

▶️ 테스트 클래스 위에 @AutoConfigureRestDocs 추가

 

🍎 Spring Rest Docs 에 대한 자동 구성을 해준다.

 

 

▶️ document(..)

 

🍎 API 문서 자동 생성을 위한 API 스펙 정보 추가

🍎 .anDo(..) 메서드 : andExpect() 처럼 어떤 검증 작업을 하는 것이 아닌 일반적인 동작을 정의하고자 할 때 사용한다.

 

 

▶️ @SpringBootTest vs @WebMvcTest

 

🍓 @SpringBootTest

  • @AutoConfigureMockMvc 와 함께 사용되어 Controller 를 테스트할 수 있다.
  • 프로젝트에서 사용하는 전체 Bean을 ApplicationContext 에 등록하여 사용한다.
  • 테스트 환경을 구성하는 것은 편리하지만 실행속도가 상대적으로 느리다.

 

🍓 @WebMvcTest

  • Controller 테스트에 필요한 Bean만 ApplicationContext에 등록한다.
  • 실행 속도가 상대적으로 빠르다.
  • 하지만 테스트 하려는 클래스에서 의존하고 있는 객체가 있다면 해당 객체에 대해서 Mock 객체를 사용하여 의존성을 일일이 제거해주어야 한다.

 

 

 

 

 

 

감사합니다.

오개념에 대한 지적은 늘 환영입니다. 🤩

 

 

 

 

 

 

 

 

Mockito

 

 

➤ Mock이란?

 

mock의 사전적 의미

not real, but intended to be very similar to real situation, substance etc

 

테스트 할 때 Mock의 의미 : 가짜 객체

단위 테스트나 슬라이스 테스트 등에 Mock 객체를 사용하는 것을 Mocking이라고 한다.

 

 

 

➤ 테스트에서 Mock 객체를 사용하는 이유

 

 

▶️ Mock 객체를 사용하지 않고 테스트를 수행할 경우

 

테스트 하고자 하는 계층(클래스)가 다른 계층이나 데이터베이스까지 호출하기 때문에 슬라이스 테스트라고 보기 어렵다.

예를 들어 API 계층을 테스트하기 위해 Controller를 테스트할 때 Mock 객체가 없으면

Controller에 요청을 보내고 Controller는 Service 를 호출하고

Service는 다시 요청에 맞는 메서드를 데이터액세스 계층인 Repository에서 호출할 것이다.

그러면 Repository는 DB에서 해당하는 명령을 수행하고 다시 돌아간다.

이렇게 되면 Controller 뿐 아니라 모든 계층을 원치않게 다 테스트하게 된다.

이러면 테스트에 실패했을 때 어느 부분이 문제인지 제대로 파악하기 어렵다.

 

이런 문제 때문에 Mock 객체가 필요하다.

Mock 객체를 사용하면 Controller를 테스트할 때

Service 객체가 아닌 MockService 객체를 호출하여 더이상 다른 계층을 호출하는 것을 막아준다.

Mock 객체를 사용하면 다른 계층과 단절하여 불필요한 과정을 줄일 수 있게 된다.

 

 

 

➤ Mockito 란?

 

Mock 객체로 Mocking을 할 수 있게 해주는 다양한 오픈 소스 라이브러리가 있다.

Mockito는 그 중에 가장 많이 사용하고 Spring Framework 자체적으로 지원하고 있는 라이브러리이다.

 

Mockito의 Mocking 기능을 이용해서 테스트 하려는 대상 외의 다른 영역을 단절시켜 오로지 테스트 대상에만 집중할 수 있다.

 

 

 

➤ 슬라이스 테스트에 Mockito 적용

 

 

▶️ @MockBean

 

필드에 추가하면 Application Context에 등록되어 있는 Bean에 대한 Mockito Mock 객체를 생성하고 주입(DI)해주는 역할을 한다.

 

 

▶️ Stubbing 메서드

 

Mockito에서 지원하는 메서드 이다.

 

✅ given()

Mock 객체가 특정 값을 리턴하는 동작을 지정하는데 사용한다.

Mockito에서 지원하는 when()과 동일한 기능을 한다.

Mickito.any() 에 매개변수로 실제로 리턴받을 타입을 지정해준다.

 

※ void 타입으로 리턴시에는 doNothing()으로 한번 감싸준다.

ex) doNothing().when(orderService).deleteOrder(anyLong());

 

 

✅ willReturn()

메서드가 리턴할 Stub 데이터이다.

 

 

▶️ Stubbing 이란?

 

테스트를 위해서 Mock 객체가 항상 일정한 동작을 하도록 지정하는 것을 의미한다.

 

 

▶️ @ExtendWith(MockitoExtension.class)

 

Spring을 사용하지 않고, Junit에서 Mockito의 기능을 사용하기위해 test 클래스 이름 위에 추가해준다.

 

 

▶️ @Mock

 

필드에 추가하면 해당 필드의 객체를 Mock 객체로 생성한다.

 

 

▶️ @InjectMocks

 

필드에 추가하면 생성한 Mock 객체를 주입해준다.

 

 

 

 

TDD

 

 

➤ TDD(Test Driven Development) 란?

 

테스트 주도 개발

테스트가 중심으로 하는 개발을 말한다.

테스트 부분을 먼저 만들고 코드 구현은 그 다음에 한다.

 

 

➤ TDD 가 아닌 전통적인 개발 방식

 

TDD 방식이 아닌 방식으로 개발하면 일반적으로 코드를 먼저 작성해서 구현한 후에 테스트를 통해 문제가 있는지 확인한다.

선 구현, 후 테스트

 

 

➤ TDD 방식으로 개발하기

 

메서드 작성 전에 먼저 테스트 클래스 부터 만든다.

존재하지 않는 클래스로 에러가 떠도 일단 테스트를 작성한다.

 

테스트 클래스가 passed 하는 방향으로 코드를 작성한다.

(처음에 클래스가 없어서 나오는 에러도 실행하면 passed 하지 않기 때문에 “failed” 라고 볼 수 있다.)

 

모든 조건에 만족하는 테스트를 먼저 작성 ➡️ 조건에 만족하지 않는 테스트 작성

단계적으로 작성하면서 해결한다.

 

실패하는 테스트 ➡️ 실패하는 테스트를 성공할 만큼의 기능 구현 ➡️ 성공하는 테스트 ➡️ 리팩토링 ➡️ 실패하는 테스트와 성공하는 테스트 확인

 

TDD 개발 방식 모식도

 

 

 

➤ TDD의 장점과 단점

 

 

▶️ 장점

 

  • 테스트를 통과할 만큼만 기능을 구현해서 한번에 너무 많은 기능을 구현할 필요가 없다.
  • 테스트 코드가 추가되면서 검증 범위가 넓어질 수록 기능 구현도 점진적으로 완성된다.
  • 리팩토링 할 부분이 보이면 그때그때 빠르게 진행하기 때문에 리팩토링의 비용이 상대적으로 작아진다.
  • 이미 잘 동작하는 코드를 수정하는 일은 부담스럽지만 TDD 방식은 항상 테스트 케이스가 존재하기 때문에 기존의 코드를 수정해도 상대적으로 심리적 불안감이 줄어든다.
  • 리팩토링을 통해 꾸준히 코드를 개선해서 코드의 품질을 일정 부분 유지할 수 있다.
  • 코드 수정 이후 바로 테스트를 진행할 수 있어서 수정 결과를 빠르게 피드백 받을 수 있다.

 

 

▶️ 단점

 

  • 테스트를 먼저 작성하는 것이 익숙하지 않다.
  • 팀 단위 개발에서 팀원들 간 사전에 협의가 되어야 한다.

 

 

 

 

 

 

감사합니다.

오개념에 대한 지적은 늘 환영입니다. 🤩

 

 

 

 

 

 

 

 

토큰 기반 인증

 

 

➤ 토큰기반 인증을 쓰는 이유?

 

 

세션기반 인증 = 서버(혹은 데이터베이스)에 유저 정보를 담는 방식

매번 요청할 때마다 서버(와 데이터베이스)를 살펴봐야하기 때문에 서버의 부담이 크다.

이 부담을 클라이언트에게 넘겨줄 수 없을까 하는 고민에서 Token 인증 방식이 생겨났다.

대표적 토큰 기반 인증 : JWT(JSON Web Token)

 

 

 

➤ 토큰(Token)이 무엇인가?

 

화폐로 사용하는 토큰

오락실

행사에 입장할 때 사용하는 토큰

 

➡️ 돈을 이미 지불했고 이 서비스를 이용할 수 있다는 정보를 담고 있음

 

 

 

 ➤ 토큰을 클라이언트에 저장하는 것은 위험하지 않을까?

 

토큰을 클라이언트가 가지고 있으면 XSS 공격에 노출되는 위험이 있지 않을까?

토큰은 유저정보를 암호화한 상태로 담을 수 있고, 암호화했기 때문에 클라이언트에 담아도 위험하지 않다.

 

 

 

➤ JWT란?

 

토큰 기반 인증 방식은 다양하다.

그 중에서 가장 많이 사용하는 것이 JWT이다.

JSON Web Token의 약자

JSON 포맷으로 사용자에 대한 속성을 저장하는 웹 토큰이다.

토큰 인증 방식중 가장 일반적으로 사용된다.

 

 

 

➤ JWT의 구조

 

 

JWT 예시

 

. 을 기준으로 세 부분으로 나눠진다.

 

1. Header

토큰의 타입(JSON), 어떤 해시 암호화 알고리즘으로 sign할지가 적혀있다.

➡️ JSON 타입을 base64방식으로 인코딩한다.

 

2. Payload

유저의 정보가 담겨있다.

어떤 정보에 접근 가능한지에 대한 권한을 담을 수도 있고, 사용자의 유저 이름 등 필요한 데이터를 담을 수 있다.

민감한 정보가 들어가선 안된다.

➡️ JSON 타입을 base64방식으로 인코딩한다.

 

3. Signature

Header, Payload를 base64 인코딩한 값과 salt값의 조합으로 암호화된 값

Signature에서는 원하는 비밀키(암호화에 추가할 salt)와 Header에서 지정한 알고리즘을 사용하여 암호화한다.

 

base64 인코딩은 누구나 쉽게 디코딩할 수 있지만 비밀키를 사용해 암호화한 값(시그니처)은 비밀키를 보유하고 있는게 아니라면 해독하는게 엄청 어렵다.

 

ex) HMAC SHA256 알고리즘을 사용한 Signature 생성

HMASHA256(base64urlEncode(header) + “.” + base64urlEncode(payload), secret);

 

 

 

 

➤ JWT의 종류

 

JWT는 다음의 두가지 종류의 토큰을 이용해 인증을 구현한다.

클라이언트는 처음 인증을 받게 될 때 두가지의 토큰을 받는다.

 

 

▶️ 액세스 토큰(Access Token)

 

보호된 정보들(유저의 이메일, 연락처 등)에 접근할 수 있는 권한부여에 사용한다.

실제로 권한을 얻는데 사용하는 토큰이다.

 

 

▶️ 리프레시 토큰(Refresh Token)

 

액세스 토큰이 탈취되는 것을 방지하기 위해서 액세스 토큰은 비교적 짧은 유효기간을 가지고 있다.

이때 액세스 토큰의 유효기간이 만료되면 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받는다. ➡️ 유저가 다시 로그인할 필요 없음

액세스 토큰에 비해 유효기간이 길다.

리프레시 토큰까지 악의적인 유저가 얻어낸다면 큰 문제가 된다.

그렇기 때문에 유저의 편의보다는 정보를 지키는 것이 더 중요한 웹사이트들은 리프레시 토큰을 사용하지 않는 곳이 많다.

 

 

 

➤ 토큰 기반 인증 절차

 

토큰 기반 인증 절차

 

1. 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보낸다.

 

2. 아이디/비밀번호가 일치하는지 확인하고 클라이언트에게 보낼 암호화된 토큰을 생성한다.

액세스 / 리프레시 토큰을 모두 생성한다.

토큰에 담길정보(payload)는 유저를 식별할 정보, 권한이 부여된 카테고리 등이 될 수 있다.

두 종류의 토큰이 같은 정보를 담을 필요는 없다.

 

3. 클라이언트는 토큰을 저장한다.

Local Storage, cookie, Session Storage 등 다양한 공간에 저장 가능

 

4. 클라이언트가 HTTP 헤더(Authorization Header) 또는 쿠키에 토큰을 담아 보낸다.

bearer authentication을 이용한다.

 

5. 서버는 토큰을 해독하여 발급한 토큰과 동일하다고 판단될 경우 클라이언트의 요청을 처리한 후 응답을 보내준다.

 

 

참고) Bearer Token

참고) Bearer Token Details

 

 

 

 

JWT의 장점과 단점

 

 

➤ JWT를 통한 인증의 장점

 

1. Statelessness & Scalability(무상태성 & 확장성)

 

🍒 서버는 클라이언트에 대한 정보를 저장할 필요가 없다. (토큰 해독이 되는지만 판단한다.)

🍒 클라이언트는 요청을 보낼 때마다 토큰을 헤더에 포함시켜서 인증을 한다.

🍒 서버를 여러개 가지고 있는 서비스에서 더 효율적이다. 같은 토큰을 여러 서버에서 인증 가능하기 때문이다. 세션의 경우는 모든 서버가 유저의 정보를 공유하고 있어야 한다.

 

 

2. 안정성

 

🍒 암호화 한 토큰을 사용

🍒 암호화한 키를 노출할 필요가 없음

 

 

3. 어디서나 생산 가능

 

🍒 토큰을 확인하는 서버가 꼭 토큰을 만들지 않아도 됨

🍒 토큰 생성용 서버를 만들거나, 다른 회사에 토큰 생성만 맡기는 등 다양한 활용이 가능하다.

 

 

4. 권한 부여에 용이

 

🍒 토큰의 payload(내용물) 안에 어떤 정보에 접근 가능한지 정의할 수 있다.

 

 

 

➤ JWT를 통한 인증의 단점

 

 

1. Payload를 해독할 수 있음

 

Payload는 base64로 인코딩 되어있기 때문에 토큰을 탈취하여 Payload를 해독하면 데이터를 확인할 수 있다. ➡️ 민감하고 중요한 정보는 Payload에 저장 x

 

 

2. 토큰의 길이가 길어지면 네트워크에 부하를 줄 수 있다.

 

토큰에 저장하는 정보의 양이 길어지면 토큰의 길이가 길어진다. 요청마다 긴 토큰과 함께 전송하면 네트워크에 부하를 줄 수 있다.

 

 

3. 토큰은 자동으로 생성되지 않는다.

 

JWT는 상태를 저장하지 않기 때문에 자동으로 삭제되지 않아서 토큰 유효기간(만료시간)을 반드시 설정해야 한다.

토큰이 탈취되면 기한이 만료될 때까지 대처가 불가능하기 때문에 만료시간을 너무 길게 설정하면 안된다.

 

 

4. 토큰은 어딘가에 저장되어야 한다.

 

토큰은 클라이언트가 가지고 있다가 인증이 필요한 요청을 보낼 때마다 함께 전송할 수 있어야 한다.

 

 

 

 

Spring Security 에서의 JWT 인증

 

 

➤ 프로젝트 사전 준비

 

 

▶️ 의존성 추가

 

  • Spring Boot DevTools
  • Lombok
  • Spring Web
  • Spring Security
  • Spring Data JPA
  • H2 Database

+ JWT 의존성 추가

implementation 'com.auth0:java-jwt:4.0.0'

 

 

▶️ application.yml 설정

 

spring:
  h2:
    console:
      enabled: true
      path: /h2
    datasource:
      url: jdbc:h2:mem:test
    jpa:
      hibernate:
        ddl-auto: create
      show-sql: true

 

 

 

➤ JWT에 필요한 Security 설정

 

 

▶️ 유저 entity에 role 추가

 

유저 entity 필드값에 인가 등급을 저장하기 위한 role 변수 추가

 

 

▶️ SecurityConfig 클래스 생성

 

🍑 JWT를 사용하기 위한 기본 설정들

  • JWT는 headers에 Authorization 값에 토큰을 보내는 방식이다. (-> Bearer)
  • 토큰 정보는 노출되면 안되지만 노출되어도 큰 위험은 없다. (유효시간을 지정했기 때문)

 

 

🍑 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

  • Web은 기본적으로 stateless인데 session이나 cookie를 사용할 수 있다.
  • session / cookie 를 만들지 않고 STATELESS로 진행하겠다는 의미이다.

 

 

🍑 .formLogin().disable()

  • form Login 을 사용하지 않는다.

 

 

🍑 .httpBasic().disable()

  • http 통신을 할 때 headers에 Authorization 값을 ID, Password를 입력하는 방식이다.
  • https를 사용하면 ID와 Password가 암호화되어 전달된다.
  • http 로그인 방식을 사용하지 않는다.

 

 

▶️ CorsConfig 클래스 생성

 

⚠️ 주의) CorsFilter 클래스 import

CorsFilter를 import 해올 때 두가지 경로에 존재하는데 실습데로 구현하려면 아래의 경로에서 import 해와야 한다.

import org.springframework.web.filter.CorsFilter;

 

 

🍈 .setAllowCredentials(true)

  • true : 서버가 응답할 때 json을 자바스크립에서 처리할 수 있게함

 

 

🍈 .addAllowedOrigin(“*”)

  • 모든(*) ip에 응답 허용

 

 

🍈 .addAllowedHeader(“*”)

  • 모든(*) header에 응답 허용

 

 

🍈 .addAllowedMethod(“*”)

  • 모든 http 메서드(post, get, patch, delete) 요청 허용

 

 

 

 

 

JWT Bearer 인증

 

 

➤ Session & Cookie 인증 방식

 

사용자가 로그인 요청을 보내면 사용자를 확인 후 session ID를 발급한다.

발급한 ID를 이용해 다른 요청과 응답을 처리하는 방식이다.

 

 

➤ Token 인증 방식

 

서버에 별도의 저장소가 필요없다.

로그인 시 클라이언트에 토큰을 발급해주고 클라이언트가 요청을 보낼 때마다 발급받은 토큰을 헤더를 통해 전달받아 응답받는 방식이다.

 

 

▶️ 장점

 

🍅 토큰 기반 인증은 쿠키나 세션을 이용한 인증보다 보안성이 강하고 효율적이다.

  • 쿠키 인증은 HTTP 방식 통신을 사용하는 경우 정보가 유출되기 쉽다.
  • 세션 인증은 서버에서 처리해야하기 때문에 추가적인 데이터베이스 공간이 필요하므로 사용자가 점점 많아지면 부담이 될 수 있다.

 

🍅 토큰은 누구나 디코딩하여 데이터가 유출될 수 있어도 Signature 필드가 header와 payload를 통해 만들어져 데이터 변조 후 재전송을 막을 수 있다.

 

🍅 stateless(무상태성) 서버를 만들 수 있다.

  • 서버에 상태를 저장하지 않기 때문이다.

 

🍅 인증정보를 OAuth로 이용할 수 있다.

 

 

일반적으로 토큰은 request header의 Authorization 필드에 담겨져 보내진다.

request header의 Authorization 필드 구조 : Authorization: <type> <credentials>

 

 

 

➤ Bearer 인증 방식

 

JWT or OAuth 토큰을 사용한다.

 

 

➤ Filter 생성

 

Spring Security

 

▶️ addFilterBefore() / addFilterAfter()

 

SecurityFilterChain에 있는 필터 전/후로 필터를 적용해주는 메서드이다.

addFilterBefore(필터, 특정 필터 이름.class)

ex) http.addFilterAfter(new FirstFilter(), BasicAuthenticationFilter.class);

 

 

 

➤ Token 적용

 

▶️ HttpServletRequest

  • ServletRequest를 상속한다.
  • Http 프로토콜의 request 정보를 서블릿에 전달하기 위한 목적으로 사용
  • Header 정보, Parameter, cookie, URI, URL 등의 정보를 읽어들이는 메서드를 가진 클래스
  • Body의 Stream을 읽어들이는 메서드를 가지고 있다.

 

 

▶️ HttpServletResponse

  • ServletResponse를 상속한다.
  • Servlet이 ServletResponse 객체에 Content Type, 응답코드, 응답 메세지 등을 담아서 전송한다.

 

 

 

참고) JWT 공식 사이트

 

 

 

 

 

감사합니다.

오개념에 대한 지적은 늘 환영입니다. 🤩

 

 

 

 

 

 

 

 

 

Hamcrest

 

 

 

➤ Hamcrest 란?

 

 

▶️ Hamcrest란?

 

🍉 Hamcrest 는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework 이다.

🍉 JUnit에서 Assertion을 위한 다양한 메서드를 지원한다.

 

 

▶️ Hamcrest를 사용하는 이유

 

🍉 Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어져서 가독성이 향상된다.

🍉 테스트 실패 메세지를 이해하기 쉽다.

🍉 다양한 Matcher를 제공한다.

 

 

 

➤ JUnit Assertion ➡️ Hamcrest Assertion 적용하기

 

🍇 JUnit Assertion 사용시

public class HelloJUnitTest {
    @DisplayName("Hello JUnit Test")
    @Test
    public void assertionTest() {
        String expected = "Hello, World!";
        String actual = "Hello, JUnit";

        assertEquals(expected, actual);
    }
}

콘솔 출력 결과

expected: <Hello, World!> but was: <Hello, JUnit>
Expected :Hello, World!
Actual   :Hello, JUnit

 

 

🍇 Hamcrest의 Matcher 사용시

public class HelloHamcrestTest {
    @DisplayName("Hello Junit Test using hamcrest")
    @Test
    public void assertionTest1() {
        String expected = "Hello, World!";
        String actual = "Hello, Hamcrest";

        assertThat(actual, is(equalTo(expected)));
    }
}

콘솔 출력 결과

Expected: is "Hello, World!"
     but: was "Hello, Hamcrest"
Expected :Hello, World!
Actual   :Hello, Hamcrest

 

 

▶️ assertThat(actual, is(equalTo(expected)));

 

  • JUnit의 assertEquals(expected, actual) 와 비슷하지만 더 쉽다.

 

 

 

▶️ JUnit의 Assertion vs Hamcrest Assertion 테스트 결과

 

Hamcrest의 Matcher를 사용하면 사람이 읽기 편한 자연스러운 Assertion 문장을 구성할 수 있다. ➡️ 가독성이 많이 높아진다.

테스팅 실패 메시지를 이해하기 더 쉽다.

 

 

더 많은 Hamcrest Matcher

Custom Matcher

 

 

 

 

슬라이스 테스트 (Slice Test)

 

 

➤ 슬라이스 테스트란?

 

각 계층에 구현한 기능들이 잘 동작하는지 특정 계층만 잘라서(Slice) 테스트하는 것

 

 

※ 스모크 테스트(Smoke Test)

소프트웨어의 본격적인 테스트에 앞서, 구축된 테스트 환경에서 테스트가 가능한지를 판단하기 위해 주요 모듈을 간단하게 테스트하는 것이다. 세세한 테스트를 하는 것이 아니라 테스팅 전에 큰 결함은 없는지 확인하는 과정이라고 볼 수 있다.

 

 

➤ API 계층 테스트

 

API 계층 테스트 대상은 대부분 클라이언트의 요청을 받아들이는 핸들러인 Controller 이다.

 

 

▶️ @SpringBootTest

 

Spring Boot 기반의 애플리케이션을 테스트하기 위한 Application Context를 생성한다.

 

 

▶️ @AutoConfigureMockMvc

 

Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해준다.

MocMvc 기능을 사용하기 위해서는 이 애너테이션을 추가해야 한다.

 

 

▶️ MockMvc

 

Controller 테스트 클래스에 DI 로 주입받은 MockMvc 는 Tomcat 같은 서버를 실행하지 않고 Spring 기반 애플리케이션의 Controller를 테스트할 수 있는 완벽한 환경을 지원해준다.

➡️ Spring MVC 테스트 프레임워크

MockMvc 객체를 통해 Controller를 호출해서 손쉽게 테스트를 진행할 수 있다.

 

 

 

▶️ Given

 

🍊 request body 에 포함시키는 요청 데이터와 동일한 역할을 하게끔 작성한다.

🍊 dto의 테스트하고자 하는 객체를 생성한다.

🍊 Gson(JSON 변환 라이브러리) 을 이용하여 dto 객체를 JSON 포맷으로 변환해준다.

 

 

▶️ When

 

🍊 HTTP request 에 대한 정보를 MockMvcRequestBuilders 클래스를 이용해서 빌더 패턴으로 채워준다.

🍊 MockMvc 객체의 perform() 메서드 호출

🍊 post() 메서드 : HTTP POST 메서드와 request URL을 설정한다. (다른 http 메서드도 가능)

🍊 accept() 메서드 : 클라이언트 쪽에서 리턴 받을 응답 데이터 타입으로 JSON 타입 설정

🍊 contentType() 메서드 : 서버 쪽에서 처리가능한 Content Type으로 JSON 타입 설정

🍊 content() 메서드 : request body 데이터를 설정한다. (Gson을 이용해 변환된 Json 문자열)

🍊 perform() 이 최종적으로 ResultActions 타입을 리턴한다.

 

⚠️ post() 메서드 사용하기 위해 import 해올 때 잘못된 경로의 post를 import하지 않도록 주의한다.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;

 

 

▶️ Then

 

🍊 ResultActions 객체를 이용해서 전송한 request 에 대한 검증을 수행한다.

🍊 andExpect() 메서드 : 파라미터로 입력한 매처(Matcher)로 예상되는 기대 결과를 검증한다.

🍊 status().isCreated() : response status 가 201(Created)가 맞는지 검증한다. (is뒤에 원하는 response status를 넣으면 된다.)

🍊 andReturn() : 디버깅 용도로 response로 전달되는 응답 데이터를 출력할 때 사용한다.

🍊 jsonPath() : response body의 각 property 중에서 응답으로 전달 받는 특정 값이 response body로 전송한 값과 동일한지 검증할 수 있다. jsonPath()를 사용하면 JSON 형식의 개별 property에 손쉽게 접근할 수 있다.

 

심화학습) jsonPath()

 

 

Member Cotroller의 getMember 테스트 예시 코드

@Test
void getMemberTest() throws Exception {
        // given
        long memberId = member1.getMemberId();
        Member member = repository.findById(memberId).get();
        String content = gson.toJson(String.valueOf(memberId));

        // when
        ResultActions actions =
                mockMvc.perform(
                        get("/v11/members/" + memberId)
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        // then
        MvcResult result = actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.email").value(member.getEmail()))
                .andExpect(jsonPath("$.data.name").value(member.getName()))
                .andExpect(jsonPath("$.data.phone").value(member.getPhone()))
                .andReturn();
}

 

 

 

✔️ response body 응답 데이터에 한글이 깨질 경우

 

resources 패키지 안에 application.yml에 들어가서 아래의 코드를 추가한다.

server:
  servlet:
    encoding:
      force-response: true

 

 

 

 

➤ 데이터 액세스 계층 테스트

 

 

데이터 액세스 계층 테스트를 위해 테스트하려는 repository를 DI 로 주입한다.

 

 

▶️ @DataJpaTest

 

🥭 Repository 기능을 정상적으로 사용하기 위해 Configuration을 Spring이 자동으로 해주게 된다.

🥭 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 된다.

🥭 여러 개의 테스트 케이스를 한꺼번에 실행시켜도 하나의 테스트 케이스가 종료될 때마다 데이터 베이스의 상태가 초기 상태를 유지한다.

 

참고) @DataJpaTest 애너테이션 doc

 

 

▶️ Repository DI로 주입

 

@Autowired
private MemberRepository memberRepository;

 

 

▶️ Given

 

테스트 할 Entity 정보를 준비한다.

 

 

▶️ when

 

정보를 테스트하고자 하는 메서드를 불러서 테스트한다.

 

 

▶️ Then

 

정보가 잘 저장되었는지 검증(assertion) 한다.

 

 

 

 

테스팅 유닛 예제 코드(깃허브)

 

 

 

 

 

감사합니다.

오개념에 대한 지적은 늘 환영입니다. 🤩

 

 

 

 

 

 

 

 

단위 테스트(Unit Test)

 

 

➤ 단위 테스트(Unit Test) 란?

 

컴퓨터에서 단위 테스트는 소스 코드의 특정 모듈이 의도된 대로 잘 작동하는지 검증하기 위해 모든 함수와 메서드에 대한 테스트 케이스를 작성하는 절차를 말한다.

 

애플리케이션에서 Postman 같은 툴을 사용해서 테스트하면 정확하게 어느 부분에서 문제가 발생하는지 알기 어렵다.

Java에서는 메서드 같이 아주 작은 단위를 가지는 기능들을 테스트할 수 있다.

Spring도 역시 계층별로 테스트할 수 있는 테스트 기법을 지원해준다.

 

일반적으로 단위 테스트는 최대한 독립적이고 작은 단위인 것이 좋다.

 

참고) 유닛테스트 위키

 

 

 

▶️ 기능 테스트

 

  • 테스트 단위가 가장 큰 테스트
  • 애플리케이션을 사용하는 사용자 입장에서 애플리케이션이 제공하는 기능이 올바르게 동작하는지 테스트
  • 주로 테스트하는 주체는 개발자가 될수도 있지만 일반적으로 테스트 전문 부서(QA) 또는 외부 QA 업체가 되기도 한다.
  • API 툴이나 데이터베이스까지 연관되어 있어서 개발한 애플리케이션과 연관된 대상이 많기 때문에 단위 테스트라고 볼 수 없다.

 

 

▶️ 통합 테스트

 

  • 클라이언트 측 툴 없이 개발자가 작성한 테스트 코드를 실행시켜 이루어지는 경우가 많다.
  • 주로 개발자가 테스트의 주체가 된다.
  • 여러 계층이 연관되어 있고 DB까지 연결되어 있어서 독립적인 테스트가 가능하다고 볼 수 없으므로 단위 테스트라고 볼 수 없다.

 

 

▶️ 슬라이스 테스트

 

  • 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트를 의미한다.
  • API 계층, 서비스 계층, 데이터 액세스 계층이 각각 슬라이스 테스트의 대상이 될 수 있다.
  • HTTP 요청이 필요하고, 외부 서비스가 연동되거나 데이터 액세스 계층은 DB와 연동되어 있기 때문에 단위 테스트라고 보기 어렵다.
  • Mock(가짜) 객체를 사용하여 계층별로 끊어서 테스트 할 수 있기 때문에 어느 정도 테스트 범위를 좁힐 수 있다.
  • 애플리케이션의 일부만 테스트하기 때문에 부분 통합 테스트라고 부르기도 한다.

 

 

👉🏻 DB를 사용하면 왜 단위 테스트라고 보기 힘든것일까?

통합 테스트나 슬라이스 테스트에서 DB와 연동된다고 해서 무조건 단위 테스트라고 부르기 어려운 것이 아니다. 데이터베이스의 상태가 테스트 전후로 동일하게 유지될 수 있다면 데이터베이스와 연동된다해도 단위 테스트에 포함될수는 있다.

 

 

 

▶️ 단위 테스트

 

  • 핵심 로직(비즈니스 로직)에서 사용하는 클래스들이 독립적으로 테스트하기 가장 좋은 대상이기 때문에 단위 테스트라고 부른다.
  • 의도한 대로 잘 동작하는지 확인하기 위해서는 메서드를 테스트하는 것이 좋다.
  • 단위 테스트 코드는 대부분 메서드 단위로 작성된다.

 

 

 

➤ 단위 테스트를 해야하는 이유?

 

  • 구현한 코드가 의도한대로 동작하는지 결과를 빠르게 확인할 수 있다.
  • 문제점을 쉽게 찾을 수 있다.
  • 언제라도 단위 테스트를 믿고 리팩토링을 할 수 있다.
  • 버그 리포트를 전달 받을 경우 버그가 발생한 기능의 테스트 케이스를 돌려보면서 문제가 발생한 원인을 단계적으로 찾아가기가 용이하다.

 

 

 

※ 테스트 케이스(Test Case) 란?

 

테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세를 의미한다.

메서드 등 하나의 단위를 테스트하기 위해 작성하는 테스트 코드이다.

 

 

 

➤ F.I.R.S.T 원칙

 

단위 테스트를 위한 테스트 케이스를 작성하기 위한 가이드 원칙으로 F.I.R.S.T 원칙을 참고할 수 있다.

 

 

▶️ Fast(빠르게)

 

일반적으로 테스트 케이스는 빨라야 한다.

자주 돌려야 문제를 빨리 찾는데 너무 느려서 돌리기 힘들면 테스트 케이스를 작성하는 의미가 퇴색될 것이다.

 

 

▶️ Independent(독립적으로)

 

각각의 테스트 케이스는 독립적으로 동작해야 한다.

어떤 테스트 케이스를 먼저 실행해도 실행되는 순서와 상관없이 정상적인 실행이 보장되어야 한다.

 

 

▶️ Repeatable(반복 가능하도록)

 

어떤 환경에서도 반복해서 실행이 가능해야 한다.

외부 리소스나 외부 서비스와 연동하는 경우 동일한 테스트 결과를 보장하지 못하기 때문에 단위 테스트 시에는 외부와의 연동을 끊어주는 것이 바람직하다.

 

 

▶️ Self-validating(셀프 검증이 되도록)

 

성공 또는 실패라는 자체 검증 결과를 보여주어야 한다.

테스트 케이스 스스로가 결과가 옳은지 그른지 판단할 수 있어야 한다.

 

 

▶️ Timely(시기 적절하게)

 

테스트 하려는 기능 구현을 하기 직전에 작성해야 한다.

TDD(테스트 주도 개발) 방식에서는 기능 구현 전에 실패하는 테스트 케이스를 먼저 작성한다.

 

 

 

 

➤ JUnit 없이 비즈니스 로직에 단위 테스트 적용해보기

 

 

JUnit : Java 기반의 소프트웨어를 테스트하기 위한 표준 테스트 프레임워크

 

단위 테스트를 제일 쉽고 빠르게 적용할 수 있는 부분은 헬퍼(helper) 클래스 또는 유틸리티(utility) 클래스 이다.

 

 

 

▶️ Given-When-Then 표현 스타일

 

given-when-then 은 BDD(Behavior Driven Development)에서 사용하는 용어이다.

 

Given

  • 테스트를 위한 준비 과정을 명시한다.
  • 테스트에 필요한 전제 조건들이 포함된다.
  • 테스트 대상에 전달되는 입력 값(테스트 데이터)도 Given에 포함된다.

 

When

  • 테스트 할 동작(대상)을 지정한다.
  • 단위 테스트에선 일반적으로 메서드 호출 통해 테스트를 진행한다.

 

Then

  • 테스트의 결과를 검증하는 영역이다.
  • 예상하는 값과 테스트 대상 메서드의 동작 수행 결과 값을 비교해서 기대한대로 동작을 수행하는지 검증(Assertion)하는 코드가 포함된다.

 

 

✅ Assertion(어써션) 이란?

 

직역하면 단언, 단정 이라는 의미이다.

테스트에서는 어써션이라는 용어는 테스트 결과를 검증할 때 주로 사용한다.

예상하는 결과 값이 참이길 바라는 것이다.

단언문, 단정문이라고 표현하기도 한다.

조건문 등으로 직접 검증할 수 있다.

 

 

 

 

JUnit

 

 

➤ JUnit 이란?

 

Java 언어로 만들어진 애플리케이션을 테스트 하기 위한 Open source Test Framework로

Java의 표준 테스트 프레임워크라고 볼 수 있다.

TestNG라는 경쟁사도 있다.

Spring Boot의 디폴트 테스트 프레임워크는 JUnit이다.

 

 

추가학습) TestNG 란?

  • Cédric Beust가 2004년에 만든 자바 기반 testing framework 이다.
  • JUnit 과 NUnit 에서 영감을 받았다.
  • JUnit 과 오랫동안 경쟁한 테스팅 프레임워크이다.
  • NG는 Next Generation(차세대)라는 의미이다.
  • JUnit 과 다른 TestNG 만의 장점
    • group : Groups 주석으로 사용, 한번 컴파일 한 후엔 같은 그룹끼리 테스트가 가능하다.
    • multithread testing을 지원한다.

 

 

 

➤ JUnit 기본 작성법

 

Spring Boot Initializr 에서 Gradle 기반의 Spring Boot 프로젝트를 생성하고 오픈하면 기본적으로 test 디렉토리가 만들어진다.

그리고 기본적으로 스타터가 포함되고 JUnit도 포함되어 있다.

 

 

▶️ JUnit을 사용한 테스트 케이스의 기본 구조

 

애플리케이션에서 테스트 하고자 하는 대상(Target)이 존재하고

테스트 메서드 위에 @Test 애너테이션을 추가해준다.

 

 

 

▶️ Assertion 메서드 사용하기

 

✔️ assertEquals()

 

org.junit.jupiter.api.Assertions.assertEquals 를 import 해서 assertEquals 메서드를 사용할 수 있다.

 

 

 

✔️ assertNotNull()

 

  • Null 여부 테스트를 진행한다.
  • 첫 번째 파라미터 : 테스트 대상 객체
  • 두 번째 파라미터 : 테스트에 실패했을 때 표시할 메시지(생략 가능)

 

 

✔️ assertThrows()

 

  • 예외(Exception) 테스트
  • 원하는 예외를 던졌을 경우 테스트 passed
  • 결과 예외보다 상위 타입의 예외를 기대해도 동일하게 passed
  • 첫 번째 파라미터 : 발생이 기대되는 예외 클래스 (Exception.class)
  • 두 번째 파라미터 : 람다 표현식으로 테스트 대상 메서드를 호출 (Executable 타입)

 

 

※ Executable 함수형 인터페이스

assertThrows() 의 두 번째 파라미터인 람다 표현식은 JUnit에서 지원하는 Executable 함수형 인터페이스이다.

메서드 하나만 정의되어 있고 리턴값이 없다.

void execute() throws Throwable;

잠재적으로 Throwable 을 던질 수 있는 모든 일반 코드 블럭을 구현하는데 사용된다.

 

참고) Executable 공식문서

 

 

✔️ assertDoesNotThrow()

 

어떤 예외도 던지지 않는지 테스트 한다.

첫 번째 파라미터 : 람다 표현식으로 테스트 대상 메서드를 호출 (Executable 타입)

 

 

 

참고) JUnit5 Assertions 공식문서

 

 

 

▶️ 테스트 케이스 실행 전, 후처리

 

✔️ @BeforeEach

 

이 애너테이션을 추가한 메서드는 테스트 케이스가 각각 실행될 때 마다 케이스 실행 직전에 먼저 실행되어 초기화 작업 등을 진행할 수 있다.

 

 

✔️ @BeforeAll

 

클래스 레벨에서 테스트 케이스를 한꺼번에 실행 시키면 테스트 케이스가 실행되기 전에 한번만 초기화 작업을 한다.

⭐️ @BeforeAll() 에너테이션을 추가한 메서드는 정적 메서드(static method)여야 한다

 

 

 

▶️ 테스트 케이스 실행 후, 후처리

 

@AfterEach, @AfterAll 

@BeforEach, @BeforeAll() 과 동일하게 동작하는데 테스트 케이스 실행이 끝난 시점에 후처리 작업을 할 때 사용한다.

 

 

▶️ Assumption

 

Junit 5에 추가된 기능이다.

Assumption은 말 그대로 ‘가정’할 때 사용한다.

특정 환경에서만 테스트 케이스가 실행되도록 할 수있다.

 

org.junit.jupiter.api.Assumptions 안에서 메서드를 Import 해서 사용한다.

 

 

✔️ assumeTrue()

 

  • 파라미터로 입력된 값이 true 일 때 나머지 아래 로직을 실행한다.
  • 특정 OS 환경 등의 특정 조건에서 선택적인 테스트가 필요할 때 유용하다.

 

※ 실행환경의 OS name 검사하기

System.getProperty(“os.name”).startsWith(“Mac”)

assumeTrue(System.getProperty("os.name").startsWith("Mac"));

getProperty의 파라미터로 os.name을 String 타입으로 전달하면 실행환경의 OS 이름을 리턴한다.

startsWith는 해당 String이 파라미터로 전달하는 String으로 시작하는지 검사한다.

 

 

 

 

 

감사합니다.

오개념에 대한 지적은 늘 환영입니다. 😉

 

 

 

SpringBoot에서 JPA를 사용할 때

application.yml 또는 application.properties 파일에서 설정을 해주어야 한다.

ddl-auto는 JPA 설정중에 빌드시 JPA가 어떻게 자동으로 테이블을 생성해줄지에 대한 설정을 지정한다.

 

 

 

➤ ddl-auto 옵션 종류

 

  • create: 기존테이블 삭제 후 다시 생성 (DROP + CREATE)
  • create-drop: 테이블 생성 후 종료시점에 테이블 DROP
  • update: 변경분만 반영(운영DB에서는 사용하면 안됨)
  • validate: 엔티티와 테이블이 정상 매핑되었는지만 확인
  • none: 사용하지 않음(사실상 없는 값이지만 관례상 none이라고 한다.)

 

 

⚠️ 주의사항

 

  • 운영 장비에서는 절대 crate, create-drop, update를 사용하면 안된다.
  • create는 로컬환경에서만 사용한다
  • 개발 초기 단계에는 create 또는 update 를 사용한다.
  • 테스트 서버는 update 또는 validate 를 사용한다.
  • 스테이징과 운영 서버는 validate 또는 none 을 사용한다.

 

 

 

출처 : https://smpark1020.tistory.com/140

 

 

'SpringBoot > Spring 기초' 카테고리의 다른 글

[Spring] Spring Rest Docs 사용을 위한 설정  (0) 2022.08.16

 

 

✔️ 오류가 난 환경

 

운영체제 : MAC

Gradle 기반의 Spring Boot 프로젝트 내의 .jar 파일을 터미널에서 build 하는 과정에서

 

zsh: ./gradlew: Permission denied

 

 

 

✔️ 해결

 

실행권한이 없어서 실행 거부된 거라서

실행권한을 부여해주면 된다.

아래 명령어를 터미널에 입력하면 해결된다.

 

chmod +x gradlew

 

 

 

'chmod' 명령어에 대한 이전 포스팅

2022.05.03 - [TIL(Today I Learned)] - 5/2 (월) Linux 기초2

 

'+x' : 실행 권한을 부여(+) 해준다.

 

권한을 부여해주고 다시 실행해보면 정상적으로 실행된다.

 

 

 

감사합니다.

도움이 되셨길 바랍니다. 😄

 

 

 

 

 

 

Spring에서의 트랜잭션

 

 

트랜잭션은 크게 로컬 트랜잭션분산 트랜잭션으로 구분된다.

 

Spring에서 사용되는 트랜잭션 방식은 선언형 트랜잭션 방식프로그래밍 코드 베이스 트랜잭션 방식이 있다.

 

트랜잭션은 애플리케이션 내에서 핵심 로직이 아닌 부가 기능이기 때문에 AOP 적용 대상이다.

➡️ 프로그래밍 코드 베이스 방식으로 트랜잭션을 적용하는 방식은 적절하지 않다.

 

 

 

Spring에서 선언형 방식의 트랜잭션 적용

 

Spring에서 선언형 방식으로 트랜잭션을 적용하는 방식은 크게 두가지가 있다.

1. 작성한 비즈니스 로직에 애너테이션을 추가

2. AOP 방식을 이용해서 비즈니스 로직에서 아예 트랜잭션 적용 코드를 감추는 방식

 

 

 

Spring Boot에서의 트랜잭션 설정

 

트랜잭션은 데이터베이스 인터랙션과 관련이 있기 때문에 커넥션 정보를 포함하고 있는 Datasource 가 기본적으로 필요하다.

Spring에서 트랜잭션은 기본적으로 PlatformTransactionManager 인터페이스에 의해 관리된다.

PlatformTransactionManager를 구현해서 유연하게 트랜잭션을 적용할 수 있다.

JPA 데이터 액세스 기술은 PlatformTransactionManager의 구현 클래스인 JpaTransactionManager를 사용한다.

 

 

 

 

애너테이션 방식의 트랜잭션 적용

 

@Transactional

Spring에서 가장 간단하게 트랜잭션을 적용할 수 있는 방법이다.

클래스에 추가하면 모든 메서드에 적용되서 어떤 메서드든 호출하면 하나의 새로운 트랜젝션이 생성된다.

rollback 없이(에러나 예외 없이) 메서드 호출이 끝나면 트랜젝션에서 commit이 일어나고 트랜젝션이 종료된다.

중간에 예외가 발생할 경우 rollback 되서 데이터베이스가 변경됐던 부분이 반영되지 않는다.

 

 

▶️ JPA 로그 레벨 설정

 

application.yml(resources 디렉토리에 있다.)에 해당 코드 추가

 

logging:
  level:
    org:
      springframework:
        orm:
          jpa: DEBUG

 

로그 레벨을 ‘DEBUG’ 레벨로 설정 ➡️ JPA 내부에서 ‘DEBUG’ 로그 레벨을 지정한 부분의 로그를 확인할 수 있다.

 

 

✔️ 참고) 로그 레벨

 TRACE  <  DEBUG  <  INFO  <  WARN  <  ERROR

  • ERROR : 요청을 처리하는 중 오류가 발생한 경우 표시한다.
  • WARN : 처리 가능한 문제, 향후 시스템 에러의 원인이 될 수 있는 경고성 메시지를 표시한다.
  • INFO : 상태변경과 같은 정보성 로그를 표시한다.
  • DEBUG : 프로그램을 디버깅하기 위한 정보를 표시한다.
  • TRACE : Debug보다 훨씬 상세한 정보를 나타낸다.

 

 

 

체크 예외(checked exception)는 rollback이 잘 될까?

 

체크 예외 예시 : Exception, SQLException, DataFormatException,...

체크 예외(checked exception)는 @Transactional 애너테이션만 추가해서는 rollback 되지 않는다.

체크 예외는 캐치(catch)한 후에 어떻게 처리할 지 결정해야 한다.

만일 예외 전략을 짤 필요가 없다면

@Transactional(rollbackFor = {SQLException.class, DataFormatException.class})

같이 체크 예외를 직접 지정해주거나 언체크 예외(unchecked exception)로 감싸서 rollback을 수행한다.

 

 

 

▶️ 메서드 레벨에 @Transactional 적용

 

메서드 레벨에도 @Transactional 에너테이션을 적용할 수 있다.

메서드 위에 작성하고 작성한 메서드만 트랜젝션을 적용한다.

 

Attribute

  • readOnly
    • 값으로 true 를 주면 읽기 전용 메서드가 된다.
    • commit 절차는 진행한다.
    • JPA 내부적으로 영속성 컨텍스트를 flush 하지 않는다. (변경 감지를 위한 스냅샷 생성 x)
    • 내부적으로 불필요한 추가 동작을 줄여서 성능을 최적화 할 수 있다.

 

 

※ @Transactional (readOnly = true)

➡️ readOnly를 찾을 수 없다고 뜨는 경우

Transactional import를 javax.transaction 이 아닌

org.springframework.transaction.annotation.Transactional 에서 해야 한다.

두 개의 라이브러리에 tranasactional 애너테이션이 존재하므로 import 할 때 주의한다.

 

 

▶️ 클래스 레벨과 메서드 레벨의 트랜잭션 적용 순서

 

✔️ 클래스 레벨에만 @Transactional이 적용된 경우

➡️ 클래스 레벨의 @Transactional이 메서드에 일괄 적용된다.

 

✔️ 클래스 레벨과 메서드 레벨에 함께 적용된 경우

메서드 레벨의 @Transactional이 적용된다.

만약 메서드 레벨의 @Transactional이 적용되지 않았다면 클래스 레벨의 @Transactional 이 적용된다.

 

 

 

➤ 트랜잭션 전파(Transaction Propagation)

 

@Transactional(propagation = Propagation.REQUIRED)

 

트랜잭션 Propagation?

트랜잭션의 경계에서 진행 중인 트랜잭션이 존재할 때 또는 존재하지 않을 때, 어떻게 동작할 것인지 결정하는 방식을 의미한다.

 

propagation attribute를 통해 설정할 수 있다.

 

▶️ propagation의 대표적 유형

1. Propagation.REQUIRED

propagation 중 가장 일반적으로 사용된다.

현재 진행 중인 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 존재하지 않으면 새 트랜잭션을 생성하도록 해준다.

 

2. Propagation.REQUIRES_NEW

이미 진행 중인 트랜잭션과 무관하게 새로운 트랜잭션이 시작된다.

기존에 진행중이던 트랜잭션은 새로 시작된 트랜잭션이 종료할 때까지 중지된다.

 

3. Propagation.MANDATORY

진행 중인 트랜잭션이 없으면 예외를 발생시킨다.

 

4. Propagation.NOT_SUPPORTED

트랜잭션을 필요로 하지 않다는 것을 의미한다.

진행 중인 트랜잭션이 있으면 메서드 실행이 종료될 때까지 진행중인 트랜잭션은 중지하며, 메서드 실행이 종료되면 트랜잭션을 계속 진행한다.

 

5. Propagation.NEVER

트랜잭션을 필요로 하지 않다는 것을 의미한다.

진행중인 트랜잭션이 존재할 경우에 예외를 발생시킨다.

 

 

 

➤ 트랜잭션 격리 레벨(Isolation Level)

 

트랜잭션은 다른 트랜잭션에 영향을 주지 않고, 독립적으로 실행되어야 하는 격리성이 보장되어야 한다.(ACID 원칙 중 Isolation)

트랜잭션 Isolation은 동시 트랜잭션에 의해 적용된 변경 사항이 서로에게 어떻게 보이는지 설정한다.

각각의 isolation 레벨은 트랜잭션이 동시에 발생할 때 생길 수 있는 아래 문제들을 예방한다.

  1. Dirty read : 동시 트랜잭션에서 커밋되지 않은 변화를 읽을 수 있다.
  2. Nonrepeatable read : 동시 트랜잭션이 동일한 행을 업데이트하고 커밋하는 경우 행을 다시 읽을 때 다른 값을 얻는다.
  3. Phantom read : 다른 트랜잭션이 범위의 일부 행을 추가하거나 제거하고 커밋하는 경우 쿼리를 다시 실행한 후 다른 행을 가져온다.

Spring 에서 @Transactional 의 attribute로 isolation을 통해 트랜잭션 간의 격리성을 제공한다.

 

1. Isolation.DEFAULT

데이터베이스에서 제공하는 기본 값

 

2. Isolation.READ_UNCOMMITTED

다른 트랜잭션에서 커밋하지 않은 데이터를 읽는 것을 허용한다.

 

3. Isolation.READ_COMMITTED

다른 트랜잭션에 의해 커밋된 데이터를 읽는 것을 허용한다.

 

4. Isolation.REPEATABLE_READ

트랜잭션 내에서 한 번 조회한 데이터를 반복해서 조회해도 같은 데이터가 조회되도록 한다.

 

5. Isolation.SERIALIZABLE

동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션이 수행되지 못하도록 한다.

 

 

https://www.baeldung.com/spring-transactional-propagation-isolation

 

Transaction Propagation and Isolation in Spring @Transactional | Baeldung

Learn about the isolation and propagation settings in Spring's @Transactional

www.baeldung.com

 

 

 

 

 

AOP 방식의 트랜잭션 적용

 

 

1. AOP 방식으로 트랜잭션 적용을 위한 Configuration 클래스 정의

@Configuration 애너테이션 추가

 

2. TransactionManager DI

애플리케이션에 트랜잭션 적용하기 위해서는 TransactionManager라는 객체가 필요하다.

DI(의존성 주입)를 통해 TransactionManager 객체를 받는다.

 

3. 트랜잭션 advice용 TransactionInterceptor 빈 등록

Spring에서 TransactionInterceptor를 이용해서 대상 클래스 / 인터페이스에 트랜잭션 경계를 설정하고 트랜잭션을 적용할 수 있다.

  • 트랜잭션 attribute 지정
  • 트랜잭션을 적용할 매서드에 트랜잭션 애트리뷰트 매핑
  • TransactionInterceptor 객체 생성

 

4. Advisor 빈 등록

  • 포인트 컷 지정
  • Advisor 객체 생성

 

 

 

 

✅ 추가 학습이 필요한 부분

 

✔️ 분산 트랜잭션

서로 다른 데이터 소스를 사용하는 한 개 이상의 데이터베이스를 하나의 트랜잭션으로 묶어서 처리해야 하는 것을 분산 트랜잭션이라고 한다.

분산 트랜잭션을 적용하면 서로 다른 리소스에 대한 작업을 수행하더라도 하나의 작업처럼 관리하기 때문에 원자성을 보장할 수 있다.

WAS가 제공하는 JTA(Java Transaction API) 서비스 이용

Atomikos는 Spring Boot에서 스타터로 지원하는 가장 인기있는 오픈 소스 트랜잭션 관리자이다.

 

 

 

 

 

 

감사합니다.

오개념에 대한 지적은 늘 환영입니다. 😄

 

 

 

 

 

트랜잭션(Transaction)이란?

 

 

트랜잭션 : 여러개의 작업들을 하나의 그룹으로 묶어서 처리하는 처리 단위

두 개 이상의 작업들이 그룹처럼 묶여서 처리될 수 있는데 하나라도 실패할 경우 애플리케이션의 신뢰성이 깨어지는 상황이 발생할 수 있다.

애플리케이션에서 신뢰성이 깨어지는 상황이 발생하면 트랜잭션이라고 부를 수 없다.

 

물리적으로는 여러 개의 작업이지만 논리적으로는 마치 하나의 작업으로 인식하는 경우

전부 성공 또는 전부 실패 (All or Nothing)로만 처리되어야 트랜잭션의 의미를 가진다.

 

 

 

➤ ACID 원칙

 

데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다.

 

 

1. 원자성 (Atomicity)

트랜잭션에서 작업을 더 이상 쪼갤 수 없다는 것을 의미한다.

모두 성공하거나 모두 실패하거나(All or Nothing) 

 

2. 일관성 (Consistency)

트랜잭션이 에러없이 성공적으로 끝날 경우, 비즈니스 로직에서 의도하는대로 일관성있게 저장되거나 변경되는 것을 의미한다.

 

3. 격리성 (Isolation)

여러 개의 트랜잭션이 실행될 경우 각각 독립적으로 실행되어야 함을 의미한다.

 

4. 지속성 (Durability)

트랜잭션이 완료되면 그 결과는 지속되어야 한다는 의미이다.

 

 

 

➤ 트랜잭션 커밋(commit)과 롤백(rollback)

 

 

▶️ commit

  • 모든 작업을 최종적으로 데이터베이스에 반영하는 명령어
  • 커밋을 수행하면 변경된 내용이 DB에 영구적으로 저장된다.
  • 커밋 명령을 수행하면 하나의 트랜젝션 과정은 종료하게 된다.

 

 

▶️ rollback

  • 작업 중 하나라도 문제가 발생했을 경우 트랜잭션 내에서 수행된 작업들을 취소한다.
  • 트랜잭션 시작 이전의 상태로 되돌아간다.

 

 

 

➤ JPA의 Entity Transaction commit() 내부 들여다보기

 

EntityTransaction 객체 commit() 메서드 호출 시 어떤 일이 일어날까?

 

EntityTransaction 인터페이스의 구현 클래스인 TransactionImpl 클래스의 commit()을 호출한다.

-> TransactionDriverControlImpl (물리적인 트랜잭션을 제어하기 위한 로컬 트랜잭션 드라이버 구현 객체) 를 얻은 후 구현 메서드인 commit()을 다시 호출한다.

-> TransactionDriverControlImpl 에서 JDBC Connection의 액세스 방법을 제공하는 JdbcResourceTransaction의 구현객체인 AbstractLogicalConnectionImplementor 의 commit()을 호출한다. (여기까지가 Hibernate ORM에서의 영역)

-> 물리적인 JDBC Connection을 통해 데이터베이스와 인터랙션하기 위해서 JDBC API의 구현체인 JdbcConnection 영역으로 이동한다.

-> (여기부터 JDBC API의 구현체인 H2의 영역) 데이터베이스에 commit 명령을 준비(prepareCommand)한 후, 명령을 실행(executeUpdate)한다.

-> Command 클래스에서 메서드 실행된다.

데이터베이스에 commit 명령을 전달하기 위해 commitIfNonTransactional 메서드 호출한다.

commitIfNonTransactional() 메서드 내부에서 auto commit 여부를 체크한 후

데이터베이스 세션에 해당하는 Session 객체를 통해 commit 명령을 수행한다.

만약에 commitIfNonTransactional() 수행 과정에서 예외가 발생하면 rollback을 수행한다.

-> SessionLocal 클래스에서 트랜잭션에 대한 commit이 수행된다.

 

 

 

 

 

 

 

읽어주셔서 감사합니다.

오개념에 대한 지적은 늘 환영입니다.

 

 

 

1 2