Skip to content

Latest commit

 

History

History

server

Sir.LOIN Sandbox Server

마지막 수정 일자: 2022년 02월 17일

Sir.LOIN 플랫폼팀 백엔드 개발자용 Sandbox Project 입니다. 이 문서는 IntelliJ IDEA 기준으로 작성했습니다.

Project root 에 IntelliJ IDEA 용 sirloin-coding-sytles.xml 파일이 있습니다. Detekt 설정과 코딩 스타일 충돌로 빌드가 실패한다면 다음 순서로 코딩 스타일 파일을 적용해주세요.

  1. File > Settings 메뉴 진입

  2. Editor > Code Styles 설정 항목 진입

  3. 화면 최상단 Scheme: 옆의 톱니바퀴 아이콘 클릭

  4. Import Scheme > IntelliJ IDEA code style XML 클릭

  5. 파일 선택 창에서 sirloin-coding-sytles.xml 파일 선택

  6. 팝업 창에서 Scheme 이름을 Sirloin Code Styles 로 지정 후 적용 완료

이 프로젝트는 api-main 어플리케이션을 실행하는 프로젝트입니다. 빌드 방법은 다음과 같습니다.

모든 shell 명령은 프로젝트 root 에서 실행하는 것으로 가정하고 설명합니다.

$ ./gradlew :api-main:assemble

api-core, api-core-infra-impl 모듈은 다른 프로젝트에서 라이브러리 형태로도 활용합니다. 라이브러리용 jar 파일을 만들기 위한 task 들은 각각 다음과 같습니다.

  1. jar 및 source file jar 파일 빌드

    $ ./gradlew :api-core:jar :api-core:sourcesJar
  2. test jar 파일 빌드

    $ ./gradlew :api-core:testJar :api-core:testSourcesJar

테스트는 규모에 따라 크게 SmallTest, MediumTest, LargeTest 로 분류합니다. 각각의 테스트 분류 기준은 링크를 눌러 확인해주세요.

  1. 전체 테스트 실행

    $ ./gradlew test
  2. 개별 모듈만 테스트 실행

    전체 테스트는 실행이 오래 걸리기 때문에, 자주 실행하기 어렵습니다. 따라서 모듈명과, 테스트 규모를 구체적으로 입력하면 원하는 테스트만 실행할 수 있습니다.

    $ ./gradlew :<api-main|api-core|api-core-infra-impl>:<test|smallTest|mediumTest|largetTest>

    가령, api-core 의 Small test 들만 실행하려면 아래와 같이 입력하시면 됩니다.

    $ ./gradlew :api-core:smallTest
$ java -jar api-main/build/libs/api-main-<버전명>.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-17 14:36:49.480  INFO 73553 --- [           main] com.sirloin.sandbox.server.api.ApplicationKt                 : Starting ApplicationKt using Java 17.0.2 on localhost with PID 73553 (api-main-0.1.1.jar started by root in /app)
2022-02-17 14:36:50.039  INFO 73553 --- [           main] com.sirloin.sandbox.server.api.appconfig.AppConfig           : Build configurations -
2022-02-17 14:36:50.039  INFO 73553 --- [           main] com.sirloin.sandbox.server.api.appconfig.AppConfig           :   Version:     0.1.37
2022-02-17 14:36:50.039  INFO 73553 --- [           main] com.sirloin.sandbox.server.api.appconfig.AppConfig           :   Fingerprint: eefc698
2022-02-17 14:36:50.039  INFO 73553 --- [           main] com.sirloin.sandbox.server.api.appconfig.AppConfig           :   Profile:     LOCAL

위 문서와 다르게 아무 것도 나오지 않는다면 로그가 출력되지 않아요! 를 참고하시기 바랍니다.

높은 코드 품질을 유지하는 일은 매우 중요합니다. 우리 프로젝트는 일관성 있는 코드 스타일을 유지하기 위해 detekt 라는 도구를 활용합니다.

$ ./gradlew detekt

> Task :api-core:detekt FAILED
api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/UserReadonlyRepository.kt:3:34: The class or object UserReadonlyRepository is empty. [EmptyClassBlock]

empty-blocks - 5min debt
        EmptyClassBlock - [UserReadonlyRepository] at api-core/src/main/kotlin/com/sirloin/sandbox/server/core/domain/user/repository/UserReadonlyRepository.kt:3:34

또한 우리 프로젝트에서는 warning 을 허용하지 않고 있습니다. 경고를 해제하기 위해 @SuppressWarnings(Java), @Suppress(Kotlin) 어노테이션을 쓸 때는 반드시 경고 해제의 이유를 아래와 같은 스타일로 명시해 주시기 바랍니다.

interface User : DateAuditable, Versioned<Long> {
    // ...
    companion object {
        internal data class Model(
            // ...
        )

        // 도메인 객체 생성에 여러 필드가 필요하기 때문에 불가피
        @Suppress("LongParameterList")
        fun create(
            // ...
        ) : User
    }
}

경고 해제의 이유는 모든 사람이 충분히 납득할 수 있어야 합니다.

api-main 모듈 내의 testcase.large 패키지에 @LargeTest 들을 모아뒀습니다. 또한 Large test 과정 동안 실제 API 호출 및 그 결과를 Spring RESTDocs 를 이용해 문서화합니다. 따라서, API 문서를 자동 생성하려면 largeTest 를 함께 실행해야 합니다.

$ ./gradlew :api-main:largeTest :api-main:asciidoctor

> Configure project :
:com.sirloin.sandbox.server: No 'buildConfig' property is specified - 'local' is used by default

> Configure project :api-main
Building for 'local' environment

> Task :api-main:asciidoctor

BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed

asciidoc 문서 템플릿은 src/asciidoc 디렉토리 아래에 있습니다.

문서 생성 및 자세한 동작 과정은 테스트 코드를 참고하시기 바랍니다.

  1. api-main: Spring boot 어플리케이션을 실행하기 위한 모듈입니다. 아래 기술들을 활용하고 있습니다.

    • spring-boot-starter

    • spring-boot-starter-validation

    • spring-boot-starter-web

    • spring-boot-starter-undertow

    • spring-security-web

    • spring-boot-starter-test

    • spring-restdocs-core

    • spring-restdocs-restassured

    • spring-restdocs-asciidoctor

  2. api-core: 핵심 비즈니스 로직을 담아둔 모듈입니다. 재활용을 위해 이식성이 높은 코드를 작성해야 합니다.

    • sirloin-jvmlibs 시리즈

  3. api-core-infra-impl: api-core 가 실제 동작하는 인프라스트럭쳐 코드 모음입니다.

    • spring-boot-starter

    • spring-boot-starter-validation

    • spring-data-jdbc

    • spring-tx

    • HikariCP

프로젝트 최초 시작 후, 루트 디렉토리의 application.yml.sample 을 복사해서 application.yml 로 파일을 생성해주세요. 그리고, 아래의 logback 설정을 확인해 주시기 바랍니다.

logging:
  level:
    ROOT: INFO
    com.sirloin.sandbox.api: DEBUG

이 단락은 mysql 이용자를 root, 비밀번호를 test1234 로 설정했다고 가정합니다.

application.yml 의 datasource 항목을 다음과 같이 수정한 뒤에,

spring:
  datasource:
    password: test1234

앱을 처음 실행하면 아래와 같은 mysql 오류가 발생합니다.

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.3)

2022-02-17 14:36:49.480  INFO 73553 --- [           main] com.sirloin.sandbox.server.api.ApplicationKt                 : Starting ApplicationKt using Java 17.0.2 on localhost with PID 73553 (api-main-0.1.1.jar started by root in /app)
2022-02-25 22:16:14.062  INFO 79419 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-02-25 22:16:15.177 ERROR 79419 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Exception during pool initialization.
2022-02-25 22:16:15.199 ERROR 79419 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.sql.SQLException: Access denied for user 'root'@'172.17.0.1' (using password: YES)
    ... 109 common frames omitted
Process finished with exit code 1

이 단락에서는 문제 발생 원인과, 해결책을 설명합니다.

개발 장비에 docker 를 설치한 후, run_mysql.sh 파일을 실행하면 아래와 같은 메시지가 출력되며 테스트용 docker mysql container 를 생성합니다.

$ ./run_mysql.sh test1234
container 내의 mysqld 실행 완료시까지 대기합니다
${sirloin-sandbox-mysql} 컨테이너 실행 완료. Local database 에 여전히 접근할 수 없다면 이 스크립트를 한번 더 실행해주세요.

위 스크립트를 실행하고 나면 아래의 docker 명령으로 mysql container 에 접속할 수 있습니다.

$ docker exec -it sirloin-sandbox-mysql mysql -h localhost -P 3306 --user=root --password=test1234
mysql: [Warning] Using a password on the command line interface can be insecure.

mysql>

SHOW DATABASES 를 입력해 sirloin_sandbox 데이터베이스가 있는지 확인해봅시다.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sirloin_sandbox    |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

준비는 모두 끝났네요. 그럼 컨테이너가 아니라 로컬 개발환경에서 docker container 에 접속해 볼까요?

$ mysql -h localhost -P 8306 --user=root --password=test1234 --protocol=tcp
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 1045 (28000): Access denied for user 'root'@'172.17.0.1' (using password: YES)

처음 실행하면 아마 위와 같은 오류가 발생하며 접근이 되지 않을 겁니다. 왜냐면 docker 는 우리의 개발 장비 ip 주소를 (별 다른 설정을 하지 않으면) 172.17.0.1 로 잡는데, mysql 컨테이너를 처음 실행하면 172.17.0.1 호스트로부터의 root 이용자 접근 권한이 없기 때문에 발생하는 문제입니다.

컨테이너를 최초 실행한 뒤 이용자의 접근 권한을 확인해 보면 아래와 같습니다.

mysql> USE mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SELECT host, user FROM user;
+-----------+------------------+
| host      | user             |
+-----------+------------------+
| %         | root             |
| localhost | mysql.infoschema |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+
5 rows in set (0.00 sec)

따라서 문제를 해결하려면 root 이용자를 172.17.0.1` 로부터 접속할 수 있도록 권한을 추가해 줘야 합니다. 방법은 다음과 같습니다.

  1. docker 명령을 이용해 실행중인 mysql 컨테이너에 접속

    $ docker exec -it sirloin-sandbox-mysql mysql -h localhost -P 3306 --user=root --password=test1234
    
    mysql>
  2. 172.17.0.1 호스트의 root 이용자 접근 권한을 추가

    mysql> CREATE USER 'root'@'172.17.0.1' IDENTIFIED WITH mysql_native_password BY 'test1234';
    Query OK, 0 rows affected (0.00 sec)
  3. 172.17.0.1 호스트의 root 이용자에게 데이터베이스의 모든 권한 부여

    mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'172.17.0.1' WITH GRANT OPTION;
    Query OK, 0 rows affected (0.02 sec)
  4. 권한 정보를 모두 기록 후 종료

    mysql> FLUSH PRIVILEGES;
    Query OK, 0 rows affected (0.01 sec)
    
    mysql> EXIT;
    Bye

위의 step 대로 실행한 뒤, 다시 개발 장비에서 docker mysql container 로 접근해 봅시다. 아래처럼 제대로 접속되는 것을 확인하실 수 있습니다.

$ mysql -h localhost -P 8306 --user=root --password=test1234 --protocol=tcp
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 22
Server version: 8.0.28 MySQL Community Server - GPL

mysql>

이제 앱을 다시 실행해 보면 문제없음을 확인할 수 있습니다.

우리 프로젝트의 DB의 패스워드는 환경 변수로 받고 있습니다. 그렇기에 하드코딩된 패스워드를 사용하지 않는다면 빌드 환경에서 패스워드를 설정해주어야 합니다.

환경 변수 명(DB Password) : PLATFORM_SANDBOX_SECRET

Tip
IntelliJ : Edit Configurations → Environment Variables → 환경 변수 작성
Jenkins : Jenkins 관리 → 시스템 구성 → Global properties → Environment variables 체크 → 환경 변수 작성

mysql의 스크립트에서 설치되는 mysql의 이미지가 mac의 m1과 호환이 되지 않아서 발생한 문제입니다. run_mysql.sh에서 image를 변경합니다.

 DOCKER_IMAGE="arm64v8/mysql:8.0.28-oracle"

Fixture monkey(혹은 AutoParams)에서는 프로젝트 내의 데이터 오브젝트를 참조해서 mock 객체를 생성한다. 코틀린에서는 자바에서는 없는 internal 접근제어자나 자바와는 다른 데이터 오브젝트의 생성 방식등으로 데이터 오브젝트에 접근 혹은 생성이 어렵기 때문에 코틀린 프로젝트에서는 Fixture Monkey(혹은 AutoParams)를 사용하기 어려운 점이 있다. 그러나 Faker는 클래스내 필요할 것이라 예상되는 필드들을 라이브러리 내에 직접 가지고 mock data를 생성하기 때문에 프로젝트내의 데이터 오브젝트를 의존하지 않는 점이 있어서 위의 제약사항에서 상대적으로 자유로운 점이 있다.

internal data class Model(
    override val uuid: UUID,
    override var nickname: String,
    override var profileImageUrl: String,
    override var deletedAt: Instant?,
    override val createdAt: Instant,
    override var updatedAt: Instant,
    override val version: Long
    )
Cannot access 'Model': it is internal in 'Companion'

Fixture Monkey에서 접근 가능한 data class 를 필요로 한다. internal class 설정이 되어있는 데이터 오브젝트를 사용하면 위와 같은 에러가 발생한다.

class org.hibernate.validator.internal.util.privilegedactions.NewInstance cannot access a member of class com.sirloin.sandbox.server.api.validation.UnicodeCharsLengthValidator with modifiers "public"
private class UnicodeCharsLengthValidator : ConstraintValidator<UnicodeCharsLength, CharSequence> {
    private var min = 0
    private var max = Int.MAX_VALUE

    override fun initialize(constraintAnnotation: UnicodeCharsLength?) = constraintAnnotation?.let {
        this.min = it.min
        this.max = it.max
    } ?: Unit

Fixture Monkey는 데이터 오브젝트를 참조하여 mock data를 생성하는데 class 내부에 private 같은 접근제어자가 있으면 Fixture Monkey에서 접근이 되지 않아 문제가 생길 수 있다.

fun CreateUserRequest.Companion.random(
    nickname: String? = null,
    profileImageUrl: String? = null
): CreateUserRequest = with(KFixtureMonkey.create()) {
    val actual = this.giveMeBuilder(MockUser::class.java)
        .set(
            "nickname", Arbitraries.strings()
                .ofMinLength(User.NICKNAME_SIZE_MIN)
                .ofMaxLength(User.NICKNAME_SIZE_MAX)
                .alpha()
        )
        .set("profileImageUrl", Arbitraries.strings().alpha())
        .sample()

생성될 mock data의 세부 조건을 설정할 때 data class 의 field 명을 하드코딩해야할 수도 있다.

        .set(MockUser::profileImageUrl.toString(), Arbitraries.strings().alpha())
        .sample()
Tip
위와 같은 방법으로 해결할 수는 있다.

코틀린의 데이터 오브젝트는 클래스의 파라미터에 변수를 선언해주는 것으로 생성된다. 이것은 내부적으로 자바 코드에서 클래스의 생성자를 통해 데이터 오브젝트를 생성하는 방식과 동일하다. 그러나 Fixture Monkey에서는 Setter를 이용해 mock data를 생성하기 때문에 코틀린의 생성자로 데이터 오브젝트를 생성하는 방식으로는 mock 객체를 만들어줄수 없다.

class MockUser(
    val nickname: String,
    val profileImageUrl: String
    )

코틀린에서 주로 쓰이는 데이터 오브젝트 생성 방법이다. (class 앞에 data를 붙여서 쓴다. - hashcode, toString, equals 메서드가 생성된다) 위의 class는 자바 코드에서 생성자를 통해 필드에 데이터를 넣어주는 코드로 변환된다.

testcase.large.endpoint.v1.user.MockUser.<init>()
java.lang.NoSuchMethodException: testcase.large.endpoint.v1.user.MockUser.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3585)
	at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754)
--more

생성자 방식으로 Fixture Monkey를 사용했을 때 발생하는 에러 로그이다. 위의 내용은 존재하지 않는 메서드를 호출했을 때 발생한다. 이 케이스는 존재하지 않는 생성자를 호출했을 경우이다.

public final class MockUser {
   @NotNull
   private final String nickname;
   @NotNull
   private final String profileImageUrl;

   @NotNull
   public final String getNickname() {
      return this.nickname;
   }

   @NotNull
   public final String getProfileImageUrl() {
      return this.profileImageUrl;
   }

   public MockUser(@NotNull String nickname, @NotNull String profileImageUrl) {
      Intrinsics.checkNotNullParameter(nickname, "nickname");
      Intrinsics.checkNotNullParameter(profileImageUrl, "profileImageUrl");
      super();
      this.nickname = nickname;
      this.profileImageUrl = profileImageUrl;
   }
}
Note
코틀린 코드를 자바 코드로 디컴파일한 상태

자바 코드로 변환된 상태이다. 생성자를 통해 필드에 값을 넣어주는 것을 알 수 있다.

코틀린의 클래스내에서 var 로 변수로 선언하게 되면 자바 코드에서는 생성자가 아닌 getter/setter 코드로 변환이 된다.

class MockUser {
    var nickname: String = ""
    var profileImageUrl: String = ""
}

Fixture Monkey에서는 위의 코드가 동작한다.

public final class MockUser {
   @NotNull
   private String nickname = "";
   @NotNull
   private String profileImageUrl = "";

   @NotNull
   public final String getNickname() {
      return this.nickname;
   }

   public final void setNickname(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.nickname = var1;
   }

   @NotNull
   public final String getProfileImageUrl() {
      return this.profileImageUrl;
   }

   public final void setProfileImageUrl(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.profileImageUrl = var1;
   }
}
Note
코틀린 코드를 자바 코드로 디컴파일한 상태

자바 코드로 변환된 상태이다. 생성자는 사라지고 getter/setter 코드가 있는 것을 알 수 있다.

Tip
서드파티 라이브러리 중에서 kotlin을 위한 Fixture Monkey가 있다. 해당 라이브러리를 사용하면 생성자를 통한 데이터 오브젝트 생성이 가능하다.