ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring boot - API 서버 만들기 ( ft. Kotlin ) - ( 2 )
    Spring/Spring Boot 2021. 9. 9. 10:45
    반응형

    이전 글에서 ( https://ellune.tistory.com/69 ) 간단하게 설정을 한상태에서 패키지 구성과 간단한 추가 환경 설정에 대해 기록할 예정이다. 

    패키지구성 자체는 사람마다 다 다른편이고 선호하는 방식도 각자 다르다. 개발자의 스타일이 나온다고 해야할까 ? 그렇기 때문에 정해진 정답은 없지만 지켜야할 몇가지 컨벤션들은 정해져 있다. 그중 내가 스스로 지키고 있는 컨벤션 몇가지를 나열하려고한다. 

    1) 대문자를 사용하지 않는다. 

    2) 최대한 명사 단위로 사용한다. 

    3) 모든 패키지는 서비스 단위 도메인별로 그룹핑한다. 

    4) 기본적인 controller , service ,repository 구성을 지킨다. 

    5) 데이터들은 domain 이란 패키지명을 이용하고 그안에서 vo , dto , entity 등을 사용한다. 

    6) 모든 패키지들의 클래스에서 같이 사용할 여지가 있는 기능들은 common 패키지에 모아둔다. 

    7) common 패키지에는 설정, 유틸 등 의 공통영역에 해당 되는 기능들을 모아두는 패키지로 사용한다. 

    8) 테스트 폴더에서는 각 서비스별로 패키지를 나누어서 테스트 코드를 관리한다. 

     

    대강의 예를 들면 위와 같이 구성해서 사용한다. 항상은 아니지만 이와 같은 구성을 유지해서 사용하려고 하지만 때에 따라선 팀의 방침에 따라 유동성있게 구성한다.위와 같이 구성하게 된 이유는 몇가지 들이 있는데 사용하면서 오는 일종의 불편함 때문이다. 

    예전에는 단순하게 controller, service 등 기본적인 패키지 구조로 사용했는데 컨트롤러와 서비스가 점점 많아지면서 스크롤의 압박과 가독성이 떨어지는것을 느꼈다. 그리고 특히 dto 같은경우는 정말 지옥을 경험할정도 어마어마 한 양이 한 패키지에 존재하다보니 이걸 좀더 가독성있게 구성해야 겠다는 생각이 들었고 그래서 서비스별 도메인별로 일단 패키지를 분리해서 하나의 세트로 사용 하는 방식을 선택하기 시작했다. 

     솔직히 그룹핑을 한다해도 dto 가 많아질경우 보기 힘든건 여전했지만 적어도 하나의 패키지에 다 모여있는것보다는 좀더 보기가 좋았고 아직까진 크게 문제가 없어 보인다. 

     

    패키지 구성이 다되면 일단 세부 환경 설정을 마무리 해야한다. 단순하게 라이브러리를 선언하고 yml 에 설정값을 넣어 줬다고 전부다 설정이 되는것은 아니다. 

    해당 패키지에 각각 필요한 설정들을 추가 하였다. 하나하나 살펴볼 예정이다. 

     

    package com.example.demo.common.config
    
    import com.querydsl.jpa.impl.JPAQueryFactory
    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.Configuration
    import javax.persistence.EntityManager
    import javax.persistence.PersistenceContext
    
    @Configuration
    class DatabaseConfiguration {
        @PersistenceContext
        private val entityManager: EntityManager? = null
    
        @Bean
        fun jpaQueryFactory(): JPAQueryFactory? {
            return JPAQueryFactory(entityManager)
        }
    }

    일단 QueryDSL 사용을 위한 설정 부터 해주도록 한다. 별다른것은 없다. 좀더 추가적으로 할게 있다면 이부분에서 설정들을 구성해주면 된다. 

    ackage com.example.demo.common.config
    
    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.Configuration
    import java.sql.SQLException
    
    @Configuration
    class H2ServerConfig {
        /**
         * H2 외부 원격 접속을 위한 세팅
         * @return
         * @throws SQLException
         */
        @Bean
        @Throws(SQLException::class)
        fun h2TcpServer(): org.h2.tools.Server? {
            return org.h2.tools.Server.createTcpServer().start()
        }
    }

    이부분은 필요에 따라 생략할수있는 부분인데 H2 디비 경우 JVM 이 구동되면서 같이 실행되기 때문에 기본적으로 제공하는 Web 이나 내부적으로 어플리케이션만 접속이 가능하다. 하지만 상황에 따라 외부 툴로 접근을 하고 싶을 경우가 있을것이다. 이럴때는 해당 설정을 해줘야 외부 클라이언트 툴에서 접근이 가능하다. 코드를 자세히보면 h2 디비에 포트를 하나 열어주는 것이라고 생각하면 된다. 

    이때 이설정이외에 gradle 설정에서 "implementation("com.h2database:h2")" 을 사용해야한다. 보통 설정을 찾아보면 runtime 으로 많이 블로그들에 기록이 되있는데 그럴경우 외부접속이 불가능하다. 꼭 이설정이 되있는지 확인해야한다. 

     

    package com.example.demo.common.config
    
    import org.springframework.beans.factory.annotation.Value
    import org.springframework.boot.context.properties.ConfigurationProperties
    import org.springframework.context.annotation.Configuration
    import org.springframework.stereotype.Component
    
    /**
     * JWT 설정값
     */
    @Component
    @Configuration
    class SecurityProperties(
        @Value("\${security.jwt.secret-key}")
        var secretKey: String? = null,
        @Value("\${security.jwt.expire-time.access-token}")
        val expireTime: Long? = null,
        val authKey: String? = null
    )

    이 설정도 생략 가능하다. JWT 를 사용하고 싶을떄 사용하기 위한 간단한 데이터 객체이며 여기서 알면 좋은것은 yml 에서 데이터를 불러오는 방법정도 일거 같다. 

    package com.example.demo.common.config
    
    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.Configuration
    import springfox.documentation.builders.ApiInfoBuilder
    import springfox.documentation.builders.PathSelectors
    import springfox.documentation.builders.RequestHandlerSelectors
    import springfox.documentation.service.ApiInfo
    import springfox.documentation.service.ApiKey
    import springfox.documentation.service.AuthorizationScope
    import springfox.documentation.service.SecurityReference
    import springfox.documentation.spi.DocumentationType
    import springfox.documentation.spi.service.contexts.SecurityContext
    import springfox.documentation.spring.web.plugins.Docket
    import springfox.documentation.swagger2.annotations.EnableSwagger2
    import java.util.*
    
    
    @Configuration
    @EnableSwagger2
    class SwaggerConfig {
        /**
         * swagger 기본 메인 설정
         * @return
         */
        @Bean
        fun restAPI(): Docket? {
            return Docket(DocumentationType.SWAGGER_2)
                .useDefaultResponseMessages(false)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.demo"))
                .paths(PathSelectors.any())
                .build()
                .apiInfo(apiInfo())
                .securityContexts(Arrays.asList(securityContext()))
                .securitySchemes(Arrays.asList(apiKey()))
        }
    
        /**
         * swager 기초 정보 설정
         * @return
         */
         fun apiInfo(): ApiInfo? {
            return ApiInfoBuilder()
                .title("Kwak Joo Hyeong Spring Boot REST API")
                .version("1.0.0")
                .description("swagger api 입니다.")
                .build()
        }
    
        /**
         * header 사용을 위한 jwt 설정 자물쇠 노출 설정
         */
         fun apiKey(): ApiKey? {
            return ApiKey("JWT", "token", "header")
        }
    
        /**
         * header 사용을 위한 jwt 설정 자물쇠 노출 설정
         */
         fun securityContext(): SecurityContext? {
            return springfox.documentation.spi.service.contexts.SecurityContext
                .builder()
                .securityReferences(defaultAuth()).forPaths(PathSelectors.any()).build()
        }
    
        /**
         * header 사용을 위한 jwt 설정 자물쇠 노출 설정
         */
        fun defaultAuth(): List<SecurityReference>? {
            val authorizationScope = AuthorizationScope("global", "accessEverything")
            val authorizationScopes: Array<AuthorizationScope?> = arrayOfNulls(1)
            authorizationScopes[0] = authorizationScope
            return listOf(SecurityReference("JWT", authorizationScopes))
        }
    }

    설정이 좀 긴데 swagger 를 사용하기 위한 설정이다.  기본적으로 2.0대 버전을 사용하였고 그에 맞는 설정이기 때문에 3.0 버전이상을 사용하려면 별도로 따로 찾아서 다른방식으로 설정 해주어야 한다. 각각의 역할은 주석을 간단하게 달아놓았으니 참고 하면 될거 같다.  이 설정의 간략한 설명을 하자면 기본적으로 기본베이스를 뎁스를 demo 로 잡아서 모든 컨트롤러들을 다 감지해서 생성하도록 했고 jwt 사용시 헤더 설정의 불편함이 있기 때문에 자물쇠 사용을 가능하도록 설정을 추가한 버전이다 

    package com.example.demo.common.config
    
    import com.example.demo.common.handler.CertificationInterceptor
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.context.annotation.Configuration
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
    
    @Configuration
    class WebConfig(
        val interceptor: CertificationInterceptor
    ) : WebMvcConfigurer{
        override fun addInterceptors(registry: InterceptorRegistry) {
            registry.addInterceptor(interceptor)
                .excludePathPatterns("/api/member/user/**")
                .excludePathPatterns("/api/member/sign-up")
                .addPathPatterns("/api/auth/sign-out")
                .addPathPatterns("/api/member/**")
        }
    }

    이부분은 인터셉터 설정부분인데 사용목적은 일부 api 사용시 헤더를 먼저 확인하게 하기위해 설정한 부분이다. 추가적으로 필터도 설정에 포함시킬까 했지만 일단은 인터셉터만 해놓기로 생각을해서 필터는 생략하였다. 여기서  CertificationInterceptor 클래스는 기본제공되는 클래스가 아니라 jwt 처리를 위해 만든 클래스이다.  다음에 기록할 내용에서 common 패키지에 들어가있는 클래스들 하나하나 볼때 같이 보게 될거 같다. 

     

    common 에서 config 를 봤다면 그다음 각각의 만들어놓은 클래스들과 용도를 간단하게 다음 글에서 기록할 예정이다. 

    단순하게 리마인드 차원에서 프로젝트를 생성하고 빠르게 구성하다보니 여러가지로 허술한 부분들이 있을수 있겠지만 최대한 자세하게 잘 기록 해야겠다. 

    반응형
Designed by Tistory.