Web/Spring 2019. 10. 23. 21:33

응답으로 나가는 Object에 getter가 존재하지 않아서 Response Content-type을 추론하지 못했다. 그래서 필터 단에서 임의로 DefaultMediaType으로 */*? * 로 넣어버리지만 사실 Content-Type에 wildCard는 들어가지 못한다. 즉, 예외발생!

posted by 여성게
:
Web/Gradle 2019. 10. 21. 20:47

 

이번 포스팅은 간단하게 Gradle Task를 작성하는 방법이다. 모든 경우의 Task 작성 방법을 다루지는 않지만 몇가지 예제를 다루어볼 것이다.

 

1
2
3
4
5
task hello{
    doLast{
        println 'Hello'
    }
}
cs

 

위는 간단하게 'Hello'라는 문자열을 출력하는 태스크이다. 아래 명령으로 실행시킨다.

 

1
2
3
4
5
gradle -q hello
 
result->
Hello
 
cs

 

-q 옵션 같은 경우는 로그 출력없이 결과값만 출력하는 옵션이다. 만약 -q 옵션을 뺀다면 빌드에 걸린 시간등의 로그가 찍히게 된다.

 

디폴트 태스크 정의

gradle -q 라는 명령어로 실행하는 디폴트 태스크를 정의하는 방법이다. 보통 빌드전에 clean, install 등의 작업을 기계적으로 하는 경우가 많은데, 디폴트 태스크로 정의하여 사용하면 보다 간편하다.

 

1
2
//Default Task usecase : gradle -q
defaultTasks 'bye', 'variablePrint'
cs

 

 

태스크간 의존성

다음은 태스크간 의존성을 설정하는 방법이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Task 의존성설정
task bye{
    dependsOn 'hello'
    doLast{
        println 'bye'
    }
}
 
task hello{
    doLast{
        println 'Hello'
    }
}
 
cs

 

위 태스크는 bye를 실행하기 전에 hello Task를 실행시키는 의존성을 설정하였다.

 

1
2
3
4
5
6
gradle -q bye
 
rsult->
Hello
bye
 
cs

 

태스크에서 변수사용 하기

태스크에서 사용자 정의 변수를 정의해서 사용가능하다. 결과값은 생략한다.

 

1
2
3
4
5
6
7
8
9
10
//변수사용방법
task variableTask{
    ext.myProperty = 'testProperty'
}
 
task variablePrint{
    doLast{
        println variableTask.myProperty
    }
}
cs

 

사용자 정의 메서드 사용하기

gradle은 groovy, kotlin 스크립트를 이용한 빌드툴이다. 즉, 변수선언은 물론 메서드를 정의해서 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
//메서드 사용
task methodTask{
    doLast{
        printStr('method args')
    }
}
 
String printStr(String arg){
    println arg
}
cs

 

빌드스크립트 자체에 의존성 라이브러리가 필요할 때

프로젝트가 사용하는 라이브러리는 물론, 빌드 스크립트 자체가 어떠한 외부 라이브러리가 필요할 때가 있다. 왜냐? gradle 자체는 groovy 언어로 작성하기 때문에 다른 외부 라이브러리를 사용가능하다!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
빌드스크립트 코드 자체가 외부 라이브러리를 필요로 할때
멀티 프로젝트 일 경우 루트 build.gradle에 선언하면
모든 하위 프로젝트 build.gradle에 반영된다.
 */
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}
 
import org.apache.*
 
task encode{
    doLast{
        Base64.Encoder encoder = Base64.getEncoder()
        println encoder.encode(variableTask.myProperty.getBytes())
    }
}
cs

 

태스크 수행 순서 제어

태스크의 수행 순서를 제어하는 방법이다.

 

1
2
3
4
5
6
7
8
9
task taskX{
    println 'taskX'
}
 
task taskY{
    println 'taskY'
}
 
taskX.mustRunAfter taskY
cs

 

If문을 이용한 태스크 생략

if문을 이용하여 특정 태스크를 생략할 수 있다.

 

1
2
3
4
5
6
7
8
9
task conditionTask{
    doLast{
        println 'conditinal Task'
    }
}
 
conditionTask.onlyIf{!project.hasProperty('skip')}
 
>gradle -q -Pskip conditionTask
cs

 

태스크 비활성화

태스크를 지우고 싶진 않고, 당장은 사용하지 않아도 추후에 사용될 수도 있다면 비활성화 기능을 사용해보아도 좋을 듯하다.

 

1
2
3
4
5
6
7
task disableTask{
    doLast{
        println 'disabled'
    }
}
 
disableTask.enabled=true
cs

 

 

여기까지 간단하게 Task를 작성하는 방법을 다루어봤다. 사실 반복문등을 사용하는 것도 가능하다. 많은 태스크 예제가 있지만, 여기까지 간단하게 gradle task 작성방법의 맛을 보았다.

posted by 여성게
:
Web/Gradle 2019. 10. 20. 21:03

 

이번 포스팅은 그래들을 이용한 자바 프로젝트 구성에 대해 다루어볼 것이다. 그래들로 자바 프로젝트를 초기화 하는 방법은 이전 포스팅에 있으니 참고 바란다.

 

2019/10/20 - [Web/Gradle] - Gradle - Gradle의 기본

 

Gradle - Gradle의 기본

이번 포스팅은 Gradle에 대한 기본을 다루어볼 것이다. 사실 Gradle이 뭔지 모르는 개발자는 거의 없을 것임으로, 자세한 설명은 하지 않을 것이다. Gradle은 빌드툴이다! (마치 Maven과 같은) Gradle 내부 프로..

coding-start.tistory.com

 

자바 타입으로 그래들 프로젝트를 생성하면 아래와 같은 기본 디렉토리 구조를 가진다.

 

src

    -main

       -java

    -test

       -java

 

그래들의 자바 플러그인의 Task 의존 관계는 아래 그림과 같다.

 

 

build를 실행하게 되면 compileJava와 test Task가 실행된다. 그런데 이전 단계 Task가 실패하면 다음 단계는 진행되지 않는다. 

 

-컴파일시 인코딩 오류가 날 경우

>gradle compileJava 

 

위와 같은 명령으로 컴파일 할때 인코딩 문제가 있다면 아래와 같이 옵션 값을 넣어준다.(build.gradle)

 

compileJava.options.encoding='UTF-8'

 

혹은 gradle.properties 파일에서 그래들의 jvmargs 환경변수로 값을 추가할 수도 있다.

 

org.gradle.jvmargs=-Dfile.encoding=UTF-8

 

 

-컴파일 단게에서 테스트 클래스들을 제외하는 방법

 

1
2
3
4
5
6
7
8
sourceSets{
    main{
        java{
            srcDirs = ['src','extraSrc']
            exclude 'test/*'
        }
    }
}
cs

 

컴파일 태스크가 완료되면 build 디렉토리에 컴파일된 클래스 파일들이 생겨난다. 그런데, 이전에 생성된 파일과 중복되지 않도록 파일을 삭제한 후에 컴파일해야 하는 경우가 있다. 이럴 때는 Clean Task를 사용한다. 해당 태스크를 이용하면 빌드한 결과물이 생성되는 build 디렉토리 내용 전체가 삭제된다. 또한, clean Task는 다른 Task와 조합해서 사용할 수 있다.

 

>gradle clean

 

위 명령을 실행하면 build 디렉토리가 삭제되는 것을 볼 수 있다.

 

-의존성 관리하기

자바는 JVM을 통해 운영체제에 독립적으로 동작하고, 라이브러리를 만들때에도 운영체제별로 만들지 않고 자바 런타임에 호환되도록 만들면 되므로 타 언어에 비해서 오픈소스나 라이브러리들이 많은 편이다. 그래들은 이러한 라이브러리들을 편리하게 관리할 수 있도록 지원한다.

 

1)자바 프로젝트의 라이브러리 스코프

개발을 진행할 때 대표적으로 세 가지의 작업(컴파일, 테스트, 실행)을 한다. 각 작업에서 사용되는 라이브러리가 매우 다양하다. 그래들에서는 사용하는 라이브러리가 중복되지 않도록 스코프를 지원한다. 자바 프로젝트에서 지원하는 스코프는 dependencies 명령으로 확인할 수 있다.

 

>gradle dependencies

 

스코프별로 포함된 라이브러리가 출력되는데, 주로 사용하는 스코프는 compile, testCompile, runtime이다.

 

스코프 Task 설명
compile compileJava 컴파일 시 포함해야 할 때
runtime - 실행시점에 포함해야 할 때
testCompile compileTestJava 테스트를 위한 컴파일 시 포함해야 할때
testRuntime test 테스트를 실행시킬 때

 

2)라이브러리 추가

 

1
2
3
4
5
6
7
8
9
10
11
12
dependencies {
    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'
 
    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:28.0-jre'
 
    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
 
    compile 'com.itextpdf:itextpdf:5.5.5'
}
cs

 

마지막에 compile scope로 라이브러리를 하나 추가하였다. 그리고 아래 Task를 실행하면 compile scope에 라이브러리가 추가된 것을 볼 수 있다.

 

>gradle dependencies

 

이제 컴파일 태스크를 실행하면 위의 의존 라이브러리를 내려받을 것이다.

 

3)패키징하기

Java 플러그인을 사용한 상태에서 build 명령을 사용하면 기본으로 JAR 형태로 팩킹된다. 그래들에서 JAR파일은 libs 디렉토리에 생성되며 기본값으로 설정되어 있다. JAR 파일은 두가지 형태가 있다.

 

  1. 실행하기 위한 형태로 배포하는 JAR
  2. 라이브러리로 사용하는 JAR

만약 라이브러리 용도로 사용하는 JAR라면 별도 설정할 것이 없지만, 이러한 JAR를 실행시키면 아래와 같은 예외 메시지가 나타날것이다.

 

"*.jar에 기본 Manifest 속성이 없습니다."

 

JAR를 gradle task로 실행시키기 위해서는 아래 설정을 build.gradle에 추가하고 run task를 수행하면 된다.

 

apply plugin: 'application'
mainClassName="package.classname"

 

>gradle run

 

gradle run 태스크를 수행하면 jar파일이 실행된다.

 

Maven 프로젝트를 Gradle 프로젝트로 변환

 

>gradle init --type pom

 

 

posted by 여성게
:
Web/Gradle 2019. 10. 20. 19:41

 

이번 포스팅은 Gradle에 대한 기본을 다루어볼 것이다. 사실 Gradle이 뭔지 모르는 개발자는 거의 없을 것임으로, 자세한 설명은 하지 않을 것이다. Gradle은 빌드툴이다! (마치 Maven과 같은)

 

Gradle 내부 프로젝트 인터페이스

Project

>final DEFAULT_BUILD_FILE

>DEFAULT_BUILD_DIR_NAME

>GRADLE_PROPERTIES

>SYSTEM_PROP_PREFIX

>Task task(String name, Closure configureClosure)

 

프로젝트 인터페이스는 그래들로 프로젝트를 설정하고 구성할 때 사용하는 파일로, 그래들 프로젝트를 논리적으로 표현하는 인터페이스이다. default_build_file 파일은 프로젝트 설정에 대한 정보를 담고 있으며, 값은 build.gradle로 정의되어 있다. 그래서 build.gradle 파일에서 빌드에 필요한 기본 설정을 하게 된다. default_build_dir_name은 빌드한 결과물(ex. jar,war..)이 저장되는 디렉토리이다. Maven에서는 target 디렉토리에 저장되는데, 그래들에서는 build 디렉토리에 저장된다. gradle_properties는 그래들에서 key/value 형태로 사용할 수 있는 프로퍼티파일에 대한 속성으로 그래들이 초기화되면 build.gradle 파일과 함께 생성되고 속성을 정의해서 사용할 수 있다. 프로퍼티 파일을 사용할 때 접두어가 SYSTEM_PROP인 이유는 SystemPropertiesHandler에서 프로퍼티 파일을 읽을 때 다른 속성값들과 구분하기 위해 SystemProp\\.(.*)")와 같은 형태를 사용하기 때문이다.

 

그래들 Task 인터페이스

Task

>TASK_NAME

>TASK_TYPE

>TASK_EDPENES_ON

>TASK_ACTION

>getProject(), getDependencies()

 

프로젝트는 Task 인터페이스를 가지고 있어서 Task는 프로젝트를 기준으로 실행된다. Task를 실행할 때 Task 인터페이스에서는 기본적으로 Task의 이름과 Task의 유형, Task의 의존성 여부, Task 액션 등을 속성으로 가지고 있다. 즉, Task 인터페이스를 Action을 가지고 있는 것이다.

 

Gradle Build Lifecycle

그래들은 크게 세 단계의 빌드 라이프 사이클을 가진다.

 

1)초기화(init)

그래들이 초기화될 때 settings.gradle과 build.gradle 파일의 상호작용을 통해서 build.gradle에 있는 프로젝트의 정보를 수집한다. settings.gradle 파일의 정보로 해당 프로젝트를 싱글 프로젝트로 구성할 것인지 멀티 프로젝트로 구성할 것인지 등을 결정하고 프로젝트의 인스턴스를 생성한다. 현재 작업 중인 루트 디렉토리에 settings.gradle 파일이 없으면 상위 디렉토리에 settings.gradle파일이 있는지 확인하고, 상위 디렉토리에도 존재하지 않는다면 해당 프로젝트를 싱글 프로젝트로 인식한다.

 

2)설정

프로젝트 인스턴스화가 끝나면 설정 정보가 프로젝트에 반영되는 단계이다.

 

3)실행

그래들은 이전 단계에서 구성된 Task의 부분집합을 이름으로 구분하고, 그 이름이 Gradle 명령에 전달되어 실행된다.

 

Gradle 설치

Mac OS 기준으로 설치가 진행되었음을 알려드립니다.

 

>brew install gradle

 

설치 끝!

 

빌드 초기화

그래들에는 빌드 초기화를 지원하는 명령이 있다. 또한 init 명령은 다섯 가지 타입을 지원한다.

 

  1. Basic
  2. Groovy-library
  3. Java-library
  4. Pom
  5. Scala-library

 

간단하게 Basic 타입으로 Gradle 프로젝트를 초기화해본다.

 

>gradle init --type basic

>Select build script DSL:

  1: Groovy

  2: Kotlin

>Enter selection (default: Groovy) [1..2] 1

>Project name(default:..) gradleBasic> gradle init --type basic

Select build script DSL:

  1: Groovy

  2: Kotlin

Enter selection (default: Groovy) [1..2] 1

Project name (default: gradleExam): gradleBasic

> Task :init

Get more help with your project: https://guides.gradle.org/creating-new-gradle-builds

BUILD SUCCESSFUL in 16s

2 actionable tasks: 2 executed

 

그래들 초기화가 완료되었다. 생성된 디렉토리를 살펴보면, 아래와 같은 파일들이 존재한다.

 

  1. build.gradle
  2. gradle
  3. gradlew
  4. gradlew.bat
  5. settings.gradle

대부분 설명한 파일들이며 추후에 자세히 다루어볼 것이고, 설명하지 않은 gradlew,gradlew.bat, gradle 디렉토리가 있을 것이다. 우선 gradlew는 그래들을 설치하지 않아도 사용할 수 있게하는 Wrapping된 명령이다. gradle 디렉토리에 들어가면 Wrapper를 이용하여 그래들을 사용할 수 있게 하는 파일들이 존재한다. 다음은 자바 프로젝트로 초기화하는 그래들 초기화 명령이다.

 

> gradle init --type java-library

 

Select build script DSL:

  1: Groovy

  2: Kotlin

Enter selection (default: Groovy) [1..2] 1

Select test framework:

  1: JUnit 4

  2: TestNG

  3: Spock

  4: JUnit Jupiter

Enter selection (default: JUnit 4) [1..4] 1

Project name (default: gradle): gradleJava

Source package (default: gradleJava): com.java.gradle

> Task :init

Get more help with your project: https://docs.gradle.org/5.6.3/userguide/java_library_plugin.html

BUILD SUCCESSFUL in 18s

2 actionable tasks: 2 executed

 

생성된 build.gradle을 살펴보자.

 

 

간단히 설정을 설명하자면, 그래들은 외부 저장소를 설정할 수 있다. 일반적으로 url로 다운로드 경로를 적지만, 위와 같이 jcenter() 혹은 mavenCentral() 메서드로 대체할 수 있다. 라이브러리를 사용할 때는 dependencies 블록 안에 라이브러리 그룹명과 패키지명, 버전을 

"그룹:라이브러리:버전" 형식으로 입력한다.

 

사용자 정의 Task 만들기

이번에는 init 명령으로 생성한 build.gradle 파일을 편집해서 그래들 Task를 만든다. 다음은 "Gradle Hello"을 출력하는 Task이다.

 

task hello {

        println 'Gradle Hello'

}

 

그래들에서 Task를 만들 때는 'task Task명'을 입력한다. 스크립트를 사용할 때 'function 함수명'을 입력하는 것과 유사하다. Task를 만들면 Task 목록에 등록되며 gradle tasks명령으로 확인할 수 있다. 사용자가 정의한 Task는 Other tasks에 표시된다. task를 실행해보자.

 

>gradle hello

 

> Configure project :

Gradle Hello

BUILD SUCCESSFUL in 588ms

 

실행 결과에는 println 메서드로 출력한 문자열이 표시된다.

 

Task 실행 순서 제어하기

 

task cellphone{

        description = 'Display calling message'

        doLast{

                println '통화하기'

        }

        doFirst{

                println '전화걸기'

        }

        doLast{

                println '전화끊기'

        }

}

 

> gradle cellphone

 

> Task :cellphone

전화걸기

통화하기

전화끊기

 

결과를 보면 doFirst 태스크가 먼저 실행된 것을 볼 수 있다. 나머지는 정의된 순서대로 수행되었다. 이렇게 선행관계를 줄 수도 있다. 다음은 디폴트 Task를 지정하는 방법이다.

 

디폴트 Task 지정

그래들에서는 기본으로 실행해야 하는 Task를 디폴트로 지정할 수 있다. 지금까지는 그래들을 실행할 때 'cellphone'과 같은 Task의 이름을 파라미터로 지정하였지만, Task를 디폴트로 지정하면 'gradle'만 입력해도 실행할 수 있다.

 

defaultTasks 'defaultTask'

 

task defaultTask{

        println 'Default Task Run !'

}

 

> gradle cellphone

> Configure project :

Default Task Run !

> Task :cellphone

전화걸기

통화하기

전화끊기

BUILD SUCCESSFUL in 721ms

1 actionable task: 1 executed

=========================

> gradle

> Configure project :

Default Task Run !

BUILD SUCCESSFUL in 537ms

 

특정 태스트를 실행하던, 특정 태스크 이름을 넘기지 않아도 디폴트 Task는 항상 실행되는 것을 볼 수 있다.

 

Task 의존성 설정

Task가 실행될 때 다른 Task도 함께 실행하려면 'depends On'으로 의존관계를 설정하면 된다.

 

task cellphone(dependsOn:'dependTask'){

        description = 'Display calling message'

        doLast{

                println '통화하기'

        }

        doFirst{

                println '전화걸기'

        }

        doLast{

                println '전화끊기'

        }

}

task dependTask{

        println 'Depends On Task'

}

 

여기까지 간단하게 그래들 초기화방법과 태스크를 정의해 실행해보았다. 다음 포스팅은 자바 프로젝트에 초점에 맞춰 더 다양하게 그래들 예제를 다루어볼 것이다.

posted by 여성게
:
Web/Spring 2019. 10. 18. 00:33

 

오늘 다루어볼 포스팅은 ResponseBodyAdvice를 이용하여 컨트롤러 응답값을 가공해보는 예제이다. 사실 HandlerInterceptor의 postHandler 같은 곳에서 응답값을 가공할 수 있을 듯하지만, 사실 인터셉터 단에서 응답 가공은 불가능하다. 하지만 우리는 적절한 값으로 응답을 가공하고 싶을 때가 있는데, 그럴때 사용하는 것이 ResponseBodyAdvice이다.

 

예제 상황은 다음과 같다.

 

"Controller에서 응답값을 Enum class로 둔다. 하지만 실제 클라이언트에는 특정 응답용 객체로 컨버팅 후 내려준다."

 

1
2
3
4
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
32
33
34
35
36
37
38
39
40
41
42
43
44
@RestControllerAdvice
public class EnumResponseCtrlAdvice implements ResponseBodyAdvice<Object> {
 
    @Override
    public boolean supports(MethodParameter returnType, Class<extends HttpMessageConverter<?>> converterType) {
        return true;
    }
 
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return body instanceof EnumController.ResponseEnum ? new Response((EnumController.ResponseEnum) body) : body;
    }
 
    @Data
    @AllArgsConstructor
    class Response{
        EnumController.ResponseEnum status;
    }
}
 
@RestController
@RequestMapping
public class EnumController {
 
    @GetMapping
    public ResponseEnum enumResponse(){
        return ResponseEnum.SUCCESS;
    }
 
    @Getter
    enum ResponseEnum{
        SUCCESS("success"),FAIL("fail");
 
        String value;
        ResponseEnum(String value){this.value=value;}
 
    }
 
}
 
=>최종응답
{
"learnableStatus""LEARNABLE"
}
cs

 

어렵지 않다. 응답값은 분명 Enum class 인데, 클라이언트에 반환된 응답값은 ResponseEnum 타입의 오브젝트로 리턴되었다. 공통적인 응답 가공이 필요할때 사용하면 좋을 듯하다. 

posted by 여성게
:
Tools/Git&GitHub 2019. 10. 15. 17:34

아래 명령어는 특정 브랜치만 clone하는 방법이다.

 

git clone -b {branch_name} --single-branch {git_repository_host}

 

 

posted by 여성게
:
Web/Spring 2019. 10. 12. 12:49

 

이번에 다루어볼 포스팅은 커스텀 어노테이션과 인터셉터를 이용하여 특정 컨트롤러의 매핑 메서드에 전처리(인증 등)를 하는 예제이다. 즉, 커스텀하게 어노테이션을 정의하고 컨트롤러 혹은 컨트롤러 메서드에 어노테이션을 붙여(마치 @RequestMapping과 같은) 해당 컨트롤러 진입전에 전처리(인증)을 하는 예제이다. 물론, Allow Ip 같은 것은 필터에서 처리하는 것이 더 맞을 수 있지만, 어떠한 API는 인증이 필요하고 어떠한 것은 필요없고 등의 인증,인가 처리는 인터셉터가 더 적당한 것 같다. 이번 포스팅을 이해하기 위해서는 Spring MVC의 구동 방식을 개념정도는 알고 하는 것이 좋을 듯하다. 아래 포스팅을 확인하자.

 

 

spring의 다양한 intercept 방법 (Servlet Filter, HandlerInterceptor, PreHandle, AOP)

1. 다양한 intercept 방법들과 주 사용처 Servlet Filter : 인코딩, 인증, 압축, 변환 등 HandlerInterceptor : 세션, 쿠키, 검증 등 AOP : 비즈니스단 로깅, 트랜잭션, 에러처리 등 2. Servlet Filter 와 Handler..

sjh836.tistory.com

 

개발 방향은 컨트롤러에 인증을 뜻하는 어노테이션을 커스텀하게 붙이고, 인터셉터에서 어노테이션의 유무를 판단하여 인증을 시도한다. 특별히 어려운 코드는 없어 간략한 설명만 하고 넘어가겠다.

 

1
2
3
4
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
32
33
34
35
36
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public enum Auth {
 
    NONE,AUTH
 
}
 
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsAuth {
 
    Auth isAuth() default Auth.NONE;
 
}
 
@Slf4j
@Component
public class CommonInterceptor implements HandlerInterceptor {
 
    private Map<String,User> userMap = new HashMap<>();
 
    @PostConstruct
    public void setup() {
        User user = new User("","yeoseong_gae",28);
        userMap.put("yeoseong-gae",user);
    }
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("interceptor");
        String userId = request.getParameter("userId");
 
        IsAuth annotation = getAnnotation((HandlerMethod)handler, IsAuth.class);
 
        Auth auth = null;
 
        if(!ObjectUtils.isEmpty(annotation)){
            auth = annotation.isAuth();
            //NONE이면 PASS
            if(auth == Auth.AUTH){
                if(ObjectUtils.isEmpty(userMap.get(userId))){
                    log.info("auth fail");
                    throw new AuthenticationException("유효한 사용자가 아닙니다.");
                }
            }
        }
        return true;
    }
 
    private <extends Annotation> A getAnnotation(HandlerMethod handlerMethod, Class<A> annotationType) {
        return Optional.ofNullable(handlerMethod.getMethodAnnotation(annotationType))
                .orElse(handlerMethod.getBeanType().getAnnotation(annotationType));
    }
 
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User{
        private String id;
        private String name;
        private int age;
    }
}
 
@Slf4j
@RestControllerAdvice
public class CtrlAdvice {
 
    @ExceptionHandler(value = {Exception.class})
    protected ResponseEntity<String> example(Exception exception,
                                             Object body,
                                             WebRequest request) throws JsonProcessingException {
        log.debug("RestCtrlAdvice");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("message:"+exception.getMessage());
    }
 
}
 
@RestController
public class AuthController {
 
    @IsAuth(isAuth = Auth.AUTH)
    @GetMapping("/auth")
    public boolean acceptUser(@RequestParam String userId){
        return true;
    }
 
    @IsAuth
    @GetMapping("/nonauth")
    public boolean acceptAll(@RequestParam String userId){
        return true;
    }
}
cs

 

마지막으로 인터셉터를 등록하기 위한 Config 클래스이다.

 

1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
 
    private final CommonInterceptor commonInterceptor;
 
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(commonInterceptor);
    }
}
cs

 

커스텀한 어노테이션을 정의하고, 컨트롤러 매핑 메서드중 인증을 하고 싶은 곳에 어노테이션을 등록해준다. 그리고 인터셉터에서 현재 찾은 핸들러의 핸들러메서드에 우리가 등록한 커스텀한 어노테이션이 있는지 혹은 해당 핸들러메서드의 클래스에 붙어있는지 등을 리플렉션을 이용하여 확인한다. 만약 있다면 유효한 사용자인지 판단한다. 참고로 인터셉터에서는 디스패처 서블릿 더 뒷단에서 구동하기 때문에 현재 매핑된 핸들러 메서드의 접근이 가능하다.

 

마지막으로 인증이 실패한다면 예외를 발생시키고, 컨트롤러 어드바이스를 이용하여 예외를 처리한다. 참고로 인터셉터는 앞에서 말한 것과 같이 디스패처 서블릿 뒷단에서 구동되기때문에 ControllerAdvice 사용이 가능하다.

 

현재 다루고 있는 예제는 하나의 예시일 뿐이다. 사실 스프링 시큐리티를 사용한다면 굳이 필요없는 예제일수 있다. 하지만 인증뿐아니라 여러가지로 사용될수 있기에 응용하여 사용하면 될듯하다. 해당 코드는 절대 실무에선 사용될 수없는 로직을 갖고 있다.(예제를 위해 간단히 작성한 코드이기에) 컨셉을 갖고 더 완벽한 코드로 완성해야 한다. 만약 해당 코드가 필요하다면 밑의 깃헙에 소스를 올려놓았다.

 

 

 

yoonyeoseong/useAnnotationAndInterceptorAuth

Contribute to yoonyeoseong/useAnnotationAndInterceptorAuth development by creating an account on GitHub.

github.com

 

posted by 여성게
:
Web/Spring 2019. 10. 10. 00:22

 

오늘 포스팅할 내용은 간단하게 Springboot Test이다. 아주 자세하게 다루어보진 않지만, 유용하고 자주 사용하는 부분이다. 간단하게 유닛테스트하는 내용은 이전 포스팅을 참고하고, 이번 내용은 Springboot 환경에서의 테스트 내용을 다룰것이다.

 

TDD - 테스트 주도 개발(Test Driven Development)

작성하려는 코드가 있다면 항상 먼저 어떻게 그 코드를 테스트할지 고민해야한다. 코드를 작성한 후에 어떻게 테스트할지 고민하기보다 작성할 코드를 묘사하는 테스트를 설계해야 한다. 이것이 테스트 주도 개발..

coding-start.tistory.com

 

다음 코드는 Springboot에서 빈으로 등록된 객체들을 사용할수 있게하는 테스트이다. 즉, ApplicationContext를 띄우는 것이다.

 

1
2
3
4
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
32
33
34
35
36
============application.properties============
test.property=test-property
==============================================
@Service
public class CommonService {
    @Autowired
    private CommonBean commonBean;
 
    public String print(){
        String result = commonBean.print();
        System.out.println(result);
        return result;
    }
}
@Component
public class CommonBean {
    @Value("${test.property}")
    private String testProperty;
    public String print(){
        System.out.println(testProperty);
        return testProperty;
    }
}
 
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestPropertyTest {
    @MockBean
    private CommonBean commonBean;
    @Autowired
    private CommonService commonService;
    @Test
    public void print(){
        commonBean.print();
    }
}
 
cs

 

위 테스트는 성공적으로 통과할 것이다. 하지만 여기서 하나 문제점이 존재한다. 만약 아주 큰 애플리케이션이라 ApplicationContext 로드에 아주 긴 시간이 걸린다면 작은 테스트를 하나 수행할때마다, 긴 시간이 걸릴 것이다. 이러할때, 우리가 테스트에 필요한 빈만 띄울수 있다. 아래 코드를 확인하자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
        CommonService.class,
        CommonBean.class
},initializers = ConfigFileApplicationContextInitializer.class)
public class TestPropertyTest {
 
    @Autowired
    private CommonService commonService;
 
    @Test
    public void print(){
        commonService.print();
    }
 
}
cs

 

2가지 방법이 존재한다. 첫번째 방법은 @ContextConfiguration 어노테이션을 이용하는 방법이다. 인자로 빈으로 띄울 클래스를 넘긴다. 여기서 중요한것을 관련된 클래스도 모두 넣어줘야한다는 것이다. 위를 예를 들면 CommonService 클래스를 빈으로 등록할 것인데, 해당 클래스의 내용을 보면 CommonBean을 DI받고 있으므로, CommonBean도 빈으로 띄워줘야하는 것이다. 나중에 다루어볼 것이지만 위와같이 빈으로 띄울 클래스를 명시적으로 등록해도되지만, 굳이 테스트에 사용하지 않으면 Mock 빈으로 등록해주어도 된다. 두번째 인자는 initializers이다. 만약 이 인자가 없다면, CommonBean에서 @Value로 DI 받고있는 프로퍼티값을 주입받지 못한다. 즉, application.properties의 내용을 읽지 못하는 것이다. 내부적으로 @Value를 받기위한 빈이 뜨지 못해서 그런 것 같다. 이와 같은 문제를 해결하기 위해서 initializers = ConfigFileApplicationContextInitializer.class를 등록해주므로써 프로퍼티값을 읽어 올 수 있게 하였다. 

 

두번째 방법은 아래와 같다. @SpringBootTest를 이용하여 더 간단한 설정으로 SpringBoot Test를 위한 설정을 할 수 있게된다. 또한 테스트에만 사용할 프로퍼티를 정의할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {CommonBean.class,CommonService.class}
,properties = {"test.property=dev-property"})
public class TestPropertyTest {
 
    @Autowired
    private CommonService commonService;
 
    @Test
    public void print(){
        commonService.print();
    }
 
}
cs

 

두번째는 Mock빈을 이용하는 방법이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = {
        CommonService.class
},initializers = ConfigFileApplicationContextInitializer.class)
public class TestPropertyTest {
 
    @MockBean
    private CommonBean commonBean;
    @Autowired
    private CommonService commonService;
 
    @Test
    public void mockBeanTest(){
        given(commonBean.print()).willReturn("Test Result");
        commonService.print();
    }
 
}
cs

 

CommonService의 비즈니스 로직을 테스트하고 싶은데, 굳이 CommonBean은 띄울 필요가 없을때, 위와 같이 MockBean을 등록해서 임의로 Mock 빈이 리턴할 값을 명시할 수 있다. 위 소스는 CommonService만 테스트를 위한 빈으로 띄우는데, 내부적으로 관련있는 CommonBean은 띄우지 않고 Mock 빈으로 띄운다. 그리고 CommonBean이 리턴하는 값을 임의로 "Test Result"라는 값으로 지정해주었다. 이렇게 테스트할 빈만 집중하여 테스트가 가능해지며 사이드 코드의 양을 줄일 수 있게 된다.

 

다음은 간단하게 Controller를 테스트할 수 있는 코드이다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RunWith(SpringRunner.class)
@WebMvcTest(controllers = CommonTestController.class)
@ContextConfiguration(classes = {
        CommonTestController.class,
        CommonBean.class,
        CommonService.class}
        ,initializers = ConfigFileApplicationContextInitializer.class)
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void resultTest() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

굳이 스프링을 실행시키지 않고도 컨트롤러를 테스트할 수 있다. @WebMvcTest 어노테이션을 이용하여 스프링 컨트롤러 테스트가 가능한 설정을 띄워준다. 즉, MVC관련 빈만 띄워 더욱 가볍게 스프링 컨트롤러 테스트가 가능하다. 위와 같은 어노테이션도 가능하며 아래와 같은 어노테이션 설정도 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = {
        CommonTestController.class,
        CommonBean.class,
        CommonService.class
})
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void resultTest() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

@AutoConfigureMockMvc 어노테이션을 이용하여 MVC 테스트를 위한 설정을 AutoConfiguration하게 할수 있다. 물론, MVC테스트를 위하여 내부적으로 내/외부 연동코드가 있다면 내/외부연동을 하지 않기 위해 Mock 빈등을 이용하여 테스트 가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = {
        CommonTestController.class
})
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private CommonBean commonBean;
    @MockBean
    private CommonService commonService;
 
    @Test
    public void resultTest() throws Exception {
        given(commonBean.print()).willReturn("test");
        given(commonService.print()).willReturn("test");
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

마지막으로 테스트만을 위한 application.properties를 사용는 방법이다. 우선, application-test.properties를 classpath 밑의 resources 폴더 안에 넣어준다.(기존 application.properties 경로와 동일) 그리고 아래와 같이 @ActiveProfile 어노테이션을 이용하여 프로파일을 이용하여 읽어올 프로퍼티파일을 명시한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = {
        CommonTestController.class,
        CommonBean.class,
        CommonService.class
})
@ActiveProfiles("test")
public class TestControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void resultTest() throws Exception {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/test/common")).andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
 
}
cs

 

위 코드는 기존 application.properties가 아닌, application-test.properties 파일을 읽어 컨텍스트를 구성할 것이다.

 

아주 간단하게만 springboot test를 다루어봤는데, 추후 포스팅에서 지금까지 다룬 내용과 더 자세한 테스팅관련 기술을 다루어볼 것이다.

posted by 여성게
: