반응형

지난 1탄에서는 content-typeapplication/json 일때 문서 작성하는 법에 대해 다뤄보았다. 이번에는 x-www-form-urlencoded 형식의 api 일때 작성하는 법에 대해 다뤄보자.

기본적인 환경설정은 동일하기 때문에 패스하고

 

- 테스트 코드 작성시 사용하는 메소드

- index.adoc 설정법

 

만 다르기 때문에 이것만 다루도록 하겠다.

[1] 테스트코드 작성하기

1. 테스트 대상 코드

x-www-form-urlencoded 방식으로 전달 시 메소드는 POST 로 전달된다. 아래와 같이 테스트할 api를 작성해 준다.

@PostMapping("/tmp2")
fun tmp2(requestDto: TmpRequestDto): TmpResponseDto {
    val recommendEmail = requestDto.name + "@gmail.com"
    val responseDto =
        TmpResponseDto(
            code = 200,
            email = recommendEmail,
            message = "hello, ${requestDto.nickname ?: "guest"}",
        )
    return responseDto
}

 

 

2. 테스트코드 : params

application/json 타입일때에는 요청 파라미터를 정의할때 requestFields 메소드를 사용하였다. 하지만 x-www-form-urlencoded일때에는 params를 사용해야 한다.

// given
val requestDto =
    HealthcheckController.TmpRequestDto(
        userId = 1,
        name = "tester2",
        nickname = "banana",
        age = 25,
        productName = "잘팔리는노트",
        hobby = mutableListOf("클라이밍", "뜨개질"),
        address =
            HealthcheckController.TmpAddress(
                city = "세종특별시 아름동",
                street = "654-321",
                zipcode = 897,
            ),
    )

val xxxRequest =
    LinkedMultiValueMap(
        mapOf(
            "userId" to listOf(requestDto.userId.toString()),
            "name" to listOf(requestDto.name),
            "nickname" to listOf(requestDto.nickname),
            "age" to listOf(requestDto.age.toString()),
            "productName" to listOf(requestDto.productName),
            "hobby" to listOf(requestDto.hobby.joinToString(",")),
            "address.city" to listOf(requestDto.address.city),
            "address.street" to listOf(requestDto.address.street),
            "address.zipcode" to listOf(requestDto.address.zipcode.toString()),
        ),
    )

+) Map<String!, String!>

params의 value 타입에 맞춰 작성해 주어야 한다. 이때 문제가 될만한 것으로는 list 타입의 hobby이다.

콤마 (,) 로 구분하여 string으로 변환하는 작업을 해준 뒤 params의 값으로 작성해야 한다.

 

// when
val result =
    this.mockMvc.perform(
        RestDocumentationRequestBuilders
            .post("$domain/tmp2")
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .params(xxxRequest)
            .accept(MediaType.APPLICATION_JSON),
    )

이제 요청 준비는 다 되었고 결과값을 받아 문서로 만들 코드를 작성한다.

 

// then
result
    .andExpect(MockMvcResultMatchers.status().isOk)
    .andDo(
        document(
            "docs_xxxform",
            documentRequest,
            documentResponse,
            formParameters(
                parameterWithName("userId").description("유저Id").attributes(Attributes.key("type").value("integer")),
                parameterWithName("name").description("이름").attributes(Attributes.key("type").value("string")),
                parameterWithName("nickname").description("닉네임").attributes(Attributes.key("type").value("string")),
                parameterWithName("age").description("나이").attributes(Attributes.key("type").value("integer")),
                parameterWithName("productName").description("상품명").attributes(Attributes.key("type").value("string")),
                parameterWithName("hobby").description("취미").attributes(Attributes.key("type").value("array")),
                parameterWithName("address.city").description("도시").attributes(Attributes.key("type").value("string")),
                parameterWithName("address.street").description("도로명").attributes(Attributes.key("type").value("string")),
                parameterWithName("address.zipcode").description("우편번호").attributes(Attributes.key("type").value("integer")),
            ),
            responseFields(
                fieldWithPath("code").description("code").optional(),
                fieldWithPath("email").description("이메일").optional(),
                fieldWithPath("message").description("메시지").optional(),
            ),
        ),
    )

+) formParameters

요청파라미터를 정의하는 메소드로 formParameters를 사용한다. 객체안의 키는 application/json 형식때와 같이 마침표로 구분하여 작성한다.

 

[2] 문서 생성하기

1. 테스트 실행 : adoc 파일 생성

테스트를 실행하여 docs_xxxform 에 대한 adoc 파일들이 생성되는걸 확인해보자

이전에 생성된 docs_json 과 함께 테스트 성공으로 docs-xxxform 폴더가 생성되었다.

2. rest docs 포맷 설정 : index.adoc

x-www-form-urlencoded 형식에 맞춰 Index.adoc 폼도 수정해 주어야 한다.

 

application/json과 x-www-form-urlencdoed의 차이점!

- request-fields.adoc 은 application/json 에만 있다

- urlencoded에는 form-parameters.adoc 이 있다.

 

위 두가지가 요청 필드에 대한 명세서가 되므로 이에 유의하여 index.adoc파일을 작성해 준다.

 

 

 

 

 

 

# REST API

## [1] REST DOCS EXAMPLE : application/json
### [http : 요청]
include::{snippets}/docs_json/http-request.adoc[]
#### [요청 필드]
include::{snippets}/docs_json/request-fields.adoc[]
### [http : 응답]
include::{snippets}/docs_json/http-response.adoc[]
#### [응답 필드]
include::{snippets}/docs_json/response-fields.adoc[]

## [2] REST DOCS EXAMPLE : x-www-form-urlendcoded
### [http : 요청]
include::{snippets}/docs_xxxform/http-request.adoc[]
#### [요청 필드]
include::{snippets}/docs_xxxform/form-parameters.adoc[]
### [http : 응답]
include::{snippets}/docs_xxxform/http-response.adoc[]
#### [응답 필드]
include::{snippets}/docs_xxxform/response-fields.adoc[]

이제 build를 하고 docs/index.html 경로로 만들어진 문서를 확인해보자

3. /docs/index.html 로 발행된 문서 확인

참고로! 테스트 실행을 하고나서 build 하지 않고, build 시 테스트코드가 자동으로 실행되는 설정이라면 바로 build만 해도된다.

또 로컬환경에서 실행중일때 어라?! 빌드 됐는데 왜 문서 안바뀌지?! 하지 말고 서버도 재실행 해주는걸 잊지말자 🙂

한 페이지에 작성되다 보니 너무 길어서 창 두개에 띄움!

 

이렇게 content-typex-www-form-urlencoded 일때에 spring rest docs 작성도 완성했다! 다음은 이 문서를 보기좋게 커스텀 하는 방법에 대해 다뤄보고자 한다.

반응형
반응형

[1] 공식문서

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#introduction

 

Spring REST Docs

Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.

docs.spring.io

[2] 세팅하기

[build.gradle.kts]

plugins {
	id("org.asciidoctor.jvm.convert") version "3.3.2" // 1번
}

dependencies {
    asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.5.RELEASE") // 2번

    testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") // 3번
    testImplementation("org.springframework.restdocs:spring-restdocs-asciidoctor") // 4번
}

tasks {
    val snippetsDir by extra { file("build/generated-snippets") } // 5번

    test { // 6번 : test Task 의 output 디렉토리 설정
        outputs.dir(snippetsDir)
        useJUnitPlatform()
    }

    asciidoctor { // asciidoctor Task 생성
        configurations(asciidoctorExt.name)
        inputs.dir(snippetsDir) // 7번 : asciidoctor Task 의 input 디렉토리 설정

        dependsOn(test)
        doLast { // 8번
            copy {
                from("build/docs/asciidoc")
                into("src/main/resources/static/docs")
            }
        }
    }


- 1번 : 플러그인 추가

- 2번 : asciidoctor 의존성 추가

- 3번 : spring rest docs 의존성 추가 (mockMvc)

- 4번 : spring rest docs 의존성 추가 (asciidoctor)

- 5번 : snippets 경로 설정

- 6번 : test Task의 output 디렉토리 설정

- 7번 : asciidoctor Task의 input 디렉토리 설정

- 8번 : 테스트 코드 실행 종료 후 파일 복붙하는 설정 (from 하위 파일을 into 폴더 하위로 복제한다)

 

[3] 테스트코드 작성하기

1. abstract class 작성

우아한 형제들 블로그를 참고하여 작성하였다.

package io.my.project

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.ComponentScan
import org.springframework.test.web.servlet.MockMvc

@SpringBootTest
@AutoConfigureRestDocs // REST Docs 설정
@AutoConfigureMockMvc
@ComponentScan(basePackages = ["io.gbike.hwikgo"]) // 컴포넌트 스캔 범위 지정
abstract class ApiDocumentationTest : BaseMockData() {
    @Autowired
    protected lateinit var mockMvc: MockMvc

    @Autowired
    protected lateinit var objectMapper: ObjectMapper
}

 

? SpringBootTest

여기서 SpringBootTest 대신 WebMvcTest 를 사용하여 빌드속도를 좀더 빠르게 할 수 있지만 테스트하려는 Controller 에 연결되어있는 많은 Service들과 Service에서 또 참고하는 유틸성 컴포넌트 등 일일이 주입해주어야 할 의존성이 너무 많은듯 하여 일단 SpringBootTest 어노테이션을 사용하였다.

### : MockMvc

spring REST Docs 문서화를 위해선 MockMvc 의 perform을 수행해야 한다. 동일하게 사용되는 객체이므로 abstract에서 생성했다.

 

2. 테스트 코드 작성

먼저 테스트 코드 작성을 위해 controller에 테스트 대상 코드를 작성해 주었다.

@RestController
class HealthcheckController() {
    @GetMapping("/tmp")
    fun tmp(requestDto: TmpRequestDto): TmpResponseDto {
        val recommendEmail = requestDto.name + "@gmail.com"
        val responseDto =
            TmpResponseDto(
                code = 200,
                email = recommendEmail,
                message = "hello, ${requestDto.nickname ?: "guest"}",
            )
        return responseDto
    }

    data class TmpRequestDto(
        val userId: Int = 1,
        val name: String = "",
        val nickname: String = "",
        val age: Int = 1,
        val productName: String = "",
        val hobby: MutableList<String> = mutableListOf(),
        val address: TmpAddress = TmpAddress(city = "", street = "", zipcode = 0),
    )

    data class TmpAddress(
        val city: String,
        val street: String,
        val zipcode: Int,
    )

    data class TmpResponseDto(
        val code: Int,
        val email: String,
        val message: String = "",
    )
}

그리고 나서 해당 controller 를 호출할 테스트 코드를 작성해 주었다. 이때 호출당시의 content-type 에 따라 사용하는 메소드가 달라진다.

- content-type : x-www-form-urlencoded

요청을 보내는 쪽에서 body가 x-www-form-urlencoded 타입일 경우 formParameters 를 사용한다.

해당 방식은 다음 글에서 다루도록 하겠다.

- content-type : application/json

요청을 보내는 쪽에서 body가 application/json 타입일 경우 requestFields 를 사용한다.

package io.myproject.controller

import io.myproject.ApiDocumentUtils.Companion.documentRequest
import io.myproject.ApiDocumentUtils.Companion.documentResponse
import io.myproject.ApiDocumentationTest
import org.junit.jupiter.api.Test
import org.springframework.http.MediaType
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders
import org.springframework.restdocs.payload.PayloadDocumentation.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

class HealthcheckControllerTest : ApiDocumentationTest() {
    @Test
    fun tmpTest() {
        // given
        val re =
            HealthcheckController.TmpRequestDto(
                userId = 1,
                name = "tester1",
                nickname = "apple",
                age = 30,
                productName = "잘팔리는필통",
                hobby = mutableListOf("수영", "독서"),
                address =
                    HealthcheckController.TmpAddress(
                        city = "서울시 강남구 역삼동",
                        street = "123-456",
                        zipcode = 123,
                    ),
            )

        // when
        val res =
            this.mockMvc.perform(
                RestDocumentationRequestBuilders
                    .get("$domain/tmp")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(re))
                    .accept(MediaType.APPLICATION_JSON),
            )

        res
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andDo(
                document(
                    "docs_json",
                    documentRequest,
                    documentResponse,
                    requestFields(
                        fieldWithPath("userId").description("유저Id").optional(),
                        fieldWithPath("name").description("이름").optional(),
                        fieldWithPath("nickname").description("닉네임").optional(),
                        fieldWithPath("age").description("나이").optional(),
                        fieldWithPath("productName").description("상품명").optional(),
                        fieldWithPath("hobby").description("취미").optional(),
                        fieldWithPath("address").description("주소").optional(),
                        fieldWithPath("address.city").description("도시").optional(),
                        fieldWithPath("address.street").description("도로명").optional(),
                        fieldWithPath("address.zipcode").description("우편번호").optional(),
                    ),
                    responseFields(
                        fieldWithPath("code").description("code").optional(),
                        fieldWithPath("email").description("이메일").optional(),
                        fieldWithPath("message").description("메시지").optional(),
                    ),
                ),
            )
    }
}

 

[4] 문서 생성하기

1. 테스트 실행 : adoc 파일 생성

먼저 작성한 테스트코드가 올바르게 실행되는지 확인한다. 테스트가 성공했다면 build/generated-snippets 에 테스트코드에서 지정한 documentdml identifier 이름의 폴더가 있고 그 아래에 사진과 같은 adoc 파일이 생성되어 있을 것이다.

 

- curl-request.adoc : curl 요청 형식으로 작성된 파일

- form-parameters : 본인의 테스트 코드 request body가 form-data 형식이어서 formParameters를 사용했기에 생성된 파일

- http-request : http 요청 파일

- http-response : http 응답 파일

- httpie-request : http client 요청 파일

- response-fields : 본인 테스트 코드 responseFields에 의해 생성된 파일

 

 

 

 

2. rest docs 포맷 설정 : index.adoc

테스트코드 실행으로 생성된 여러가지 adoc 파일을 활용해 rest docs를 구성해야 한다.

src/docs/asciidoc/index.adoc을 다음과 같이 작성하였다.

# REST API

## REST DOCS EXAMPLE

### [http : 요청]
include::{snippets}/docs_json/http-request.adoc[]

#### [요청 필드]
include::{snippets}/docs_json/request-fields.adoc[]

### [http : 응답]
include::{snippets}/docs_json/http-response.adoc[]

#### [응답 필드]
include::{snippets}/docs_json/response-fields.adoc[]

플러그인을 설치하면 다음와 같이 작성했을때 어떻게 보여지는지 바로 확인할 수 있다.

3. html 문서 확인

이제 프로젝트를 빌드하면 위의 adoc 파일의 내용index.adoc의 형식으로 index.html 파일로 변환된다. 또한  build.gradle.kts 에 작성했던 아래 설정으로 인해 build/docs/asciidoc 에 있는 파일이 src/main/resources/static/docs 로 복제 된다.

tasks {
    ... (생략) ...
    asciidoctor {
    	... (생략) ...
        dependsOn(test)
        doLast {
            copy {
                from("build/docs/asciidoc")
                into("src/main/resources/static/docs")
            }
        }
    }

 

해당 resources 하위에 존재해야지만 url path로 접근 할 수 있는데 이때 application 파일에 아래의 항목이 true 로 되어있어야 스프링 어플리케이션의 기본 정적 리소스 매핑을 사용할 수 있다.

spring:
    web:
        resources:
            add-mappings: true # 정적 리소스 매핑 활성화

 

 

4. /docs/index.html 로 발행된 문서 확인

정적 리소스 매핑으로 도메인 뒤에 /docs/index.html 를 입력하면 main/resources/static/docs/index.html 에 접근 할 수 있다.

참고로 build 로 html문서 생선 전에 localhost:8080/docs/index.html 로 접속하면 그러한 리소스는 없다는 오류가 뜬다.

테스트코드가 성공했다고 하여도 전체적인 build가 성공해야지만 docs가 생성된다는것!!

BUILD SUCCESSFUL in 24s

 

문서 생성 확인 완료!!

다음 2탄은 content-type 이 x-www-form-urlencoded 일때 문서 생성하는 방법에 대해 다뤄보도록 하겠다.

반응형
반응형

라라벨을 공부하면서 참고하는 서적이 있는데 해당 서적에서는 sail 을 통해 프로젝트 실행환경 구축을 권장한다.

라라벨 공식문서에도 sail을 통한 프로젝트 생성 방법이 안내되어 있는데 아래의 명령어로 실행된다. (macOS기준)

curl -s "https://laravel.build/example-app" | bash

하지만 larave.build 커맨드는 "항상 최신버전의 laravel" 을 사용하도록 되어있다. 파라미터로 버전값을 명시해 주어도 최신 버전의 라라벨이 설치된다.

version=8.7 을 하였으나 해당 커맨드는 적용되지 않는다.

특정 버전으로 라라벨을 실행하고 싶다면 sail이 아닌 composer를 통해 프로젝트를 생성해야 한다.

전체적인 흐름은 이전 글에서 설명해 두었다.

 

[개발환경] php laravel 개발을 시작하는 방법 : 설치부터 실행까지

php를 사용한 웹 어플리케이션을 만드는데에는 먼저 환경세팅이 필요하다.이때 php를 사용할 환경을 구성하는 방법으론 크게 세가지가 있다. 1. xampp를 사용해 apach 웹 서버와 mysql까지 한번에 다

jel-log.tistory.com

 

1. composer 로 프로젝트 생성하기

composer가 이미 설치되어있다는 가정하에 진행된다. composer로 라라벨 프로젝트를 생성해 준다.

composer create-project --prefer-dist laravel/laravel example-app "8.6.11"

create-project : 프로젝트 생성 명령어

--prefer-dist :  프로젝트 생성 옵션

더보기

--prefer-dist ?

 

패키지를 설치할 때 가능한 경우 압축된 아카이브 파일(preferred distribution package) 형태로 다운로드하도록 지시합니다. 이는 일반적으로 깃 리포지토리에서 소스 코드 전체를 클론하는 대신, 아카이브된 릴리즈 파일을 다운로드하여 설치 시간을 줄이고 네트워크 대역폭을 절약할 수 있습니다.

 

2. laravel sail 을 설치하고 실행하기

도커환경에서의 실행을 위해 sail 을 설치해준다.

cd example-app
composer require laravel/sail --dev
php artisan sail:install

 

3. sail 을 통해 도커 실행하기

설치된 sail을 통해 도커를 실행하여 서버가 구동되는것을 확인한다.

./vendor/bin/sail up

 

책에서는 버전을 명시한 설치방법이 없어서 곤란했지만 방법을 찾아서 다행이다!

반응형
반응형

php를 사용한 웹 어플리케이션을 만드는데에는 먼저 환경세팅이 필요하다.

이때 php를 사용할 환경을 구성하는 방법으론 크게 세가지가 있다.

 

1. xampp를 사용해 apach 웹 서버와 mysql까지 한번에 다운로드 받는 방법

2. brew를 통해 php와 mysql, composer를 다운로드 받는 방법 + php 내장 웹서버 사용

 

1번을 이용해 간단한 laravel 따라하기 프로젝트를 진행해 봤으므로 이번에는 로컬환경에서 다운로드 하여 환경을 구성해 보고자 한다. 참고로 xampp 방법으로 진행 할 경우 아래 팁 확인!

 

+) m1칩 기준 xampp 다운로드

xampp를 검색해서 바로 다운로드 하면 설치되지 않는다.. 바로 휴지통으로 이동된다. 그 원인을 알순 없지만 아마도 xampp 에서 기본으로 최신 기기에 맞는 버전만 다운로드 되도록 되어있기 때문이 아닌가 싶다. 그래서 아래 링크로 이동하여 다운로드를 진행해준다.

https://sourceforge.net/projects/xampp/files/XAMPP%20Mac%20OS%20X/7.2.34/

 

XAMPP - Browse /XAMPP Mac OS X/7.2.34 at SourceForge.net

Ganttic is a resource management software that excels in high-level resource planning and managing multiple project portfolios at once. In Ganttic, anything and anyone you need to schedule can be a resource – people, rooms, machinery – you name it! Th

sourceforge.net

 

설치를 한 후로는 apach 웹 서버를 실행시킬 수 있고, 해당 서버가 실행중이어야 php 서버를 정상적으로 이용 가능하다.

그 이유는 클라이언트로부터 html이 아닌 php 파일요청이 들어올 경우 apach에서 php 서버로 호출해주기 때문이다.

 

1. brew 설치

php와 mysql, 그리고 composer는 모두 brew 환경에서 설치해 줄 것이므로 먼저 brew를 설치해 준다.

https://brew.sh/ko/

 

Homebrew

The Missing Package Manager for macOS (or Linux).

brew.sh

참고로 램 8gb 기준.. 한시간 정도 걸리고 팬 소음도 매우 컸다 ㅎㅎ..

2. php, mysql, composer 설치

어차피 세개 다 설치해줄꺼기 때문에 한번에 명령어를 날려준다.

brew install php mysql composer

이때 이전 프로젝트 따라하기로 이미 mariadb가 설치되어 있었어서 mysql 설치과정에서 다음과 같은 에러가 발생했다.

 

당황하지않고.. mariadb를 사용할 일이 더이상 없기 때문에 안내된대로 unlink + 삭제 해주고 mysql을 설치해 준다.

brew unlink mariadb
brew install mysql

 

3. composer를 활용해 laravel 프로젝트 설치하기

python과 django를 이용해본 경험이 있다면 흐름을 이해하는건 큰 어려움이 없을것이라 생각한다.

php의 의존성 관리도구인 composer를 통해 쉽게 laravel 어플리케이션을 생성할 수 있는데 설치하고자 하는 위치에서 아래의 명령어를 입력해준다.

composer create-product larave/laravel {프로젝트명}

 

나의경의 개발 home directory의 MyProjects 라는 폴더 안에서 개인용 프로젝트를 모아두고 관리하고 있기 때문에 MyProjects 위치에서 위의 명령어를 실행해 주었다.

수많은.. 패키지들이 자동으로 주입되고 프로젝트가 생성된다.

+) 특정 버전의 laravel 로 프로젝트 생성하기 

만약 생성하고자 하는 프로젝트의 버전을 명시하고 싶다면 위에서 프로젝트 생성 명령어를 다음과 같이 해준다

composer create-project laravel/laravel=8.* {프로젝트명} --prefer-dist

이때 주의할 것은 정확한 laravel 버전을 입력해 주어야 한다는 것이다. 대충 검색해서 8버전때 아무거나 사용했다!

+) 여러개 php 버전 사용하기 feat. brew

laravel은 버전에 따라 지원하는 php 버전이 다르다. 각 상황에 맞게 php 버전도 관리해 주어야 하는데 내가 사용하고자 하는 laravel 버전 8의 경우 php 7.3 ~ 8.1 까지 지원한다고 한다. 현재 내 컴퓨터에는 8.2와 8.3이 설치되어 있으므로 추가로 8.1버전을 설치해 준다

brew install php@8.1

이때 패키지들을 brew 설치하면 좋은점이 나타난다.. 이제 brew list 로 설치된 php 목록을 보면 다음과 같다

php  # 8.3.7
php@8.1
php@8.2

참고로 첫번째꺼는 버전명시 없이 설치했더니 이름에서 버전이 안적혀있다~

php@8.2를 제일 처음에 설치하고 이후 최신버전인 8.3.7을 설치해서 사용중이었으나 8.1버전을 다시 설치하였고, 앞으로 사용할 php의 버전을 8.1버전으로 하기 위해 brew 설정을 바꿔주었다.

brew unlink php
brew link php@8.1

먼저 unlink하면 뭔가를 해제했다고 뜬다.

 

그러면 환경변수에서도 설정을 변경해 주라는 안내문구가 나오고, 그대로 입력하면 된다.

이후 아까와 같이 현재 설정된 php 버전을 확인하면 변경된 것을 볼 수 있다!

+) artisan ?

docker를 이용한 프로젝트 실습에서 명령어를 날릴때 아래와 같이 입력했었다

vendor/bin/sail artisan {명령어}

라라벨 공식문에서에서는 artisan (아티즌) 이 라라벨에 포함된 커맨드라인 인터페이스(CLI)의 이름 이라고 알려준다!

 

4.php 내장 웹 서버로 서버 연결하기 : php artisan

프로젝트의 홈 디렉토리에서 아래의 명령어를 통해 서버와 연결해 준다.

php artisan serve

 

그럼 localhost 환경에서 서버가 실행된다.

해당 주소로 접속하면 빈 페이지가 뜰줄 알았지만 생각외로 멀쩡한 페이지가 노출된다!

이는 laravel 프로젝트를 설치하면서 다양한 view 도 생성되었기 때문이다.

laravel 프로젝트에서 route가 기록되는 routes > web.php 파일을 보면 아래와 같은 코드가 작성되어 있다.

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

하위 경로 없이 호출하게되면 welcome view를 return 한다는 것! welcome view는 resources > views > welcome.blade.php 에 작성되어 있다.

 

이를 통해 view의 경로가 resources > views 로 자동 매핑되는것을 알아차릴 수 있었다.

 

5. docker-compose 로 프로젝트 실행하기 : sail

4번에서 처럼 php 명령어로도 서버를 실행할수 있음에도 왜 docker로 해야할까? 그 이유는 php/laravel 프로젝트를 전체적으로 실행시키기 위해선 db연결도 필요하고, 이 전체적인 서버 연결을 보다 편리하게 하기 위해서이다

docker를 통해 php서버와 mysql 서버를 동시에 띄우고, 내리고 하면 편리하겠다! docker-compose를 통해 웹서버와 db서버를 동시에 띄울것이므로 docker-compose를 사용한다고 한다.

 

1) docker-desktop

먼저 docker-compose를 사용하기 위해서는 docker-desktop 어플리케이션 다운로드가 필요하다. 인터넷에서 검색 후 다운로드 해 주고, 아이콘을 클릭하여 running 모드로 해준다!

이후 laravel 프로젝트 내에서 docker 실행을 진행해 준다.

2) sail 의존성 추가해주기

Laravel의 기본 Docker 개발 환경과 상호 작용하기 위한 간단한 명령줄 인터페이스이다. 이를 프로젝트에서 사용하려면 먼저 composer 로 의존성을 추가해 주어야 한다.

composer require laravel/sail --dev

 

그러면 몇가지 폴더가 생성되는걸 확인할 수 있다.

 

이후로 sail 을 설치해 주는데 이때 어떤 db를 사용할 것인지 선택지가 뜬다. mysql을 사용하도록 결정!

 

laravel 11버전의 화면

laravel 8버전 화면

 

영상과 캡쳐본에서도 보다싶이 적용되는데 몇초 걸린다.. 이건아마도 컴퓨터 사양탓인듯 하다 ㅎㅎ

설치가 완료되면 명령어를 통해 실행해보라고 안내가 나오면서, 선택한 db를 실행해보라고도 뜬다!

 

3) docker로 프로젝트 실행하기

먼저 docker로 프로젝트를 실행해보도록 한다. 그 전에 언제 ./vendor/bin/sail 을 다 치랴.. 진작에 alias로 해당 단어들을 sail 로 저장해 두었다 ㅎㅎ 그리고 도커 실행!

# 단축키 저장
vi ~/.zshrc
alias sail="./vendor/bin/sail"
# 변경된 환경변수 바로 적용
source ~/.zshrc

# docker 로컬에서 실행
sail up

# docker 백그라운드에서 실행
sail up -d

그러면 80포트로 서버가 실행되었다고 확인된다. 하지만 주의할 것은 아직 db세팅을 안해주었다는것! 실제로 해당 서버페이지를 접속하면 서버 연결을 확인할 수 있다.

오른쪽 아래에 사용중인 php와 laravel 버전을 확인할 수 있다.

+) laravel 11 버전 docker compose 실행시 db 오류

laravel 11버전이었을 때 sail 설치해주고 바로 실행했을때 다음과 같은 에러 페이지가 노출된다.

 

일단 서버가 실행되는것은 확인했고, 정상적인 실행을 위해 db migration을 적용해 테이블이 생성되도록 해준다.

그럼 laravel 프로젝트 설치 중 기본으로 추가된 패키지들에서 지원하는 세가지 테이블이 생성된다.

그리고 나서 다시 localhost:80 으로 접속하면 아래와 같이 정상적인 페이지를 확인할 수 있다. 아래 페이지는 아까 php artisan serve 로 실행했을때 확인한 페이지와 동일하다.

 

즉, php artisan serve를 통해 서버를 실행하게 되면 db 연결 없이 프로젝트를 실행하게 되는것이고, sail을 통해 실행하게 되면 db연결이 추가된다는 것을 알 수 있다. laravel 8버전에서는 안이랬는데 버전이 달라지면서 지원하는 것이 달라졌나보다!

 

이제 여러가지 기능을 추가하면서 view를 만들어 혼자서 하는 웹페이지 개발을 시작해보자!

반응형
반응형

나는 작심 30일 해.. 할수있어..

# 이번목표 (~ 5월 31일)

  • SQL 첫걸음 읽기
  • SQL 레벨업 읽기
  • php & laravel 익힐겸 프로젝트 해보기 (아이디어는 구상 완료)
  • 코틀린 인 액션 읽기 (6월로 넘어갈 수 있음)
  • 코틀린 고급 강의 듣기
더보기

SQL 첫걸음 : ~ 5월 15일

SQL 레벨업 : ~ 5월 18일

php & laravel 프로젝트 : 5월 15일 + 그 이후 꾸준히 ~ 5월 24일까지 / 매주 일요일 3시간씩 할애

코틀린 인 액션 읽기 : ~ 5월 31일

코틀린 고급 강의 : 코틀린 인 액션과 주제가 일치할때

반응형
반응형

laravel 강의는 예전것들이 많이 올해 새로 나온 laravel11 버전에 맞지 않는 경우가 종종 있다. 대부분의 경우 방식은 달라도 자동으로 import 해주거나 하여 개발자가 수작업 할 일이 없지만 middleware 적용에서 에러가 발생했다.

 

[에러 상황]

middleware 에 관한 공부를 하던 중 web.php 에서 route group 에 한번에 middleware를 적용할 수 있음을 배웠다.

 

Route::resource('articles', ArticleController::class)->middleware('auth')->except(['index', 'show']);

 

또한 except 를 통해 middleware 처리를 제외할 조건도 확인하였다.

다음으로는 개별 controller 에서 middleware를 처리해 주는 방법을 학습하였다.

Controller가 실행될때 자동으로 적용되게 하기 위해 생성자로 지정해 주었다.

class ArticleController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth')->except(['index', 'show']);
    }

 

하지만 middleware 부분에 warning이 떴고 메소드를 추가하라는 선택지가 나왔다.

 

laravel에 설정되어 있는 middleware 메소드를 알아서 찾았어야 하는데 해당 메소드를 Class 내에서 찾고, 이것이 없다는 것이다.

 

실제 import를 하려고 하면 해당 메소드를 구현하거나, 이름을 변경하거나의 선택지가 있다. Add @method 해봤지만 동작하지 않았고, 아래와 같은 화면이 노출되었다.

이 외에도 강의와 다르게 라라벨 11버전에서는 몇가지 다른 점이 있었다.

[1] Laravel11 버전의 업데이트 내용

버전 문제일수 있다 생각하여 검색해 보니 실제 Laravel11 버전에서는 middleware 관련하여 몇가지 변경사항이 있었다.

https://rezakhademix.medium.com/laravel-11-no-http-kernel-no-casts-no-console-kernel-721c62adb6ef

 

Laravel 11: No Http Kernel, No $casts, No Console Kernel!

Laravel 11 is scheduled for release in Q1 2024, bringing various improvements and new features to the framework.

rezakhademix.medium.com

 

1. app/http/Middleware 폴더 삭제

더이상 해당 폴더가 지원되지 않는다고 한다. 대신 app.php 파일에 middleware 설정이 작성되어 있다고 한다.

 

2. Kernel.xml 파일 삭제

해당 파일도 더이상 존재하지 않는다고 한다. 해당 파일에서 기존에 수정하던 것은 app.php 파일에서 작성하라고 한다.

 

[에러 해결]

구글링한 결과 다행히 답을 찾을 수 있었다! 검색은 화면에 노출된 에러메세지로 검색하였다.

검색 : Call to undefined method Controller::middleware()

 

어떤 깃헙 repo의 pr 내역에서 해결법을 찾을 수 있었다.

https://github.com/laravel/framework/issues/50566

 

Call to undefined method ::middleware() when calling authorizeResource() · Issue #50566 · laravel/framework

Laravel Version 11 PHP Version 8.3 Database Driver & Version No response Description Calling authorizeResource() in a resource controller throws a "Call to undefined method ::middleware()" exceptio...

github.com

To be able to use authorizeResource() in Laravel 11, your base controller class in app\Http\Controllers\Controller.php should extend \Illuminate\Routing\Controller (because that's where the middleware() method is), just like the Controller.php file found in a fresh Laravel 10 installation.

 

Laravel11 에서는 app\Http\Controllers\Controller.php 파일에 작성된 클래스가 상속받아야 한다는 것이다. 상속 대상 클래스가 middleware 메소드를 구현하고 있다고 한다. 그래서 해당 파일을 아래와 같이 작성하였다.

<?php

namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\Access\AuthorizesRequests;

abstract class Controller extends \Illuminate\Routing\Controller
{
    use AuthorizesRequests;
}

 

이후 기존에 작성한 ArticleController로 돌아가보니 middleware 메소드를 사용하는 부분의 warning 라인이 없어지고 command+클릭으로 이동하니 AuthorizesRequesets 파일로 이동했다!

 

그리고 기능도 정상적으로 동작하였다!

 

[테스트로 기능확인]

middleware를 적용하고 기존에 작성한 테스트코드를 실행해보니 테스트가 실패하였다.

 

인증관련해서 404가 나올줄알았지만 302인 이유는 실제 화면에서는 미인증 유저가 해당 화면에 접근할 경우 로그인 화면으로 "리다이렉트" 시키기 때문이다! 따라서 리다이렉트 코드인 302가 반환되었다. 

 

해당 테스트코드는 미인증 회원의 접근이 잘 막아지는지로 코드를 수정하고, 로그인한 회원의 테스트코드를 작성하였다.

/**
 * @test
 */
public function 로그인한_사용자는_글쓰기_화면을___있다(): void
{
    $user = User::factory()->create();
    $this->actingAs($user)
        ->get(route('articles.create'))
        ->assertStatus(200)
        ->assertSee('글쓰기');
}

테스트 성공!

반응형
반응형

강의를 따라하며 실습을 진행하다가 강사님의 화면에서 IDE를 이용해 db ui를 확인하는 장면을 보았다.

관련해서 언급이 없어서 삽질을 좀 하고... 어찌저찌 연결하긴 했는데 관련 내용 검색을 했을때 정확한 내용을 찾지 못해 내 해결법을 공유한다.

 

[1] database 아이콘 좌측 네비게이션 바로 이동

 

지금 필요한 것은 IDE의 네비게이션 바에 database가 추가되는 것이다.

참고로 나는 좌측 네비게이션 바에 두었지만 좌측하단 혹은 우측에 두어도 상관은 없는듯!!

 

먼저 처음부터 database 아이콘이 보인다면 상관없겠지만 처음에 보이지 않는다면 다음을 따라한다.

 

1) 우측 메뉴툴바에서 View > Tool Windows확인

2) database 클릭

 

그러면 짜잔~ 하고 나타난다. 이제 db를 연결해 줄 차례이다!

 

[2] Laravel 프로젝트 내 databse.app 확인

Laravel은 앱을 생성하면 config나 다른 기타 설정 파일들이 함께 들어오더라! 약간 Django 처럼~~??

먼저 db 연결을 위해 어떤 정보가 있는지 확인해자

1) 연결할 DB종류 선택

나의 프로젝트는 mysql을 사용중이다! 따라서 Data Source > MySQL 을 선택해 준다.

 

2) 필요한 정보 확인

 

Host UserName Password Database URL Port 정보가 필요하다. 모두 database.php 파일에서 확인가능하다 ㅎㅎ

 

3) Config > database.php 확인

해당 파일을들어가면 아래와 같은 코드가 작성되어 있다.

.env 파일에서 값을 찾고 없을 경우 default값을 참조하도록 되어있는데 이때 env 파일에 있는 값도 있지만 없는 값도 있어서 잘 확인해 주어야 한다.!

 

해당 정보들에서 위에 입력에 필요한 정보들을 확인해 둔다.

[3] database 연결하기

이제 연결할 차례이다 참고로 이때 docker를 실행중에 있어야 한다!

Host 가 localhost 이기 때문~

 

참고로 정보를 입력한 뒤 아래의 Test Connection 을 누르면 Failed 뜬다.. 그치만 연결 된다 ㅋㅋㅋ

 

아까도 말했듯이 docker가 실행중이 아니라면 DB 연결이 실패한다!!! 아래와 같은 에러메세지가 뜨고 db 안은 텅 비어있다.

따라서 docker를 꼭 실행해 주도록 한다. 이때 Laravel 프로젝트로 실행하므로 나는 아래와 같은 명령어로 도커를 background에서 실행한다.

verdor/bin/sail up -d

 

맨날 vendor/bin/sail 입력하는게 귀찮아서 alias 로 등록했다 ㅋㅋ

// alias 등록하기
> vi ~/.zshrc

alias sail="vendor/bin/sail"

// 변경된 사항 적용하기
> source ~/.zshrc

 

그럼 아까 실행했던 명령어를 아래와 같이 입력할 수 있다.

 

Docker Desktop 켜주고.. 실행하기... ㅎㅎ...

프로젝트 하나 실행하는데 터미널을 무쟈게 많이 띄우긴 곤란하니 docker는 백그라운드에서 실행! 그리고 프론트 쪽 소스도 적용하기 위해서 npm run dev도 실행해 준다.

 

이렇게 실행하고 나면 아까 연결해 두었던 db를 새로고침!

데이터양마다 다른진 모르겠지만.. 1초도 안걸린다!

Refresh 를 하고 나면 그동안 프로젝트로 생성한 table과 그 안의 document들을 확인할 수 있다!

 

참고로 처음에 db를 연결할때 database 를 Laravel 로 하였기 때문에 해당 db가 연결된 것이다.

 

끝~~!!

반응형
반응형

코틀린 강의를 들으면서 한가지 기능에 대해 가장 쉽고, 빠르고, 코틀린만의 문법규칙을 사용하지 않고 작성하여 기능을 확인한 뒤 점진적으로 리팩토링 하며 코틀린 문법에 익숙해지려 한다.

 

강의를 들으면서 3단계에 걸쳐 동일한 기능을 리팩토링 하였고 그 과정을 기억해보려 한다.

[기능] 기능 설명

카테고리가 있는 책들의 현재 대출 현황에 대한 갯수를 보여준다.

{
    "COMPUTER": 1,
    "SCIENCE": 2,
    "SOCIATY": 5
}

카테고리 종류는 더 많지만 대출중인 유효 갯수가 있는 카테고리만 반환한다.

> bookRepository

현재 도서관이 보유중이 책 목록을 조회할 수 있다.

 

> BookStatResponse

카테고리별 대출중인 책의 갯수를 담는 DTO 이다.

 

[1] for loop 로 조회하기

- serivce

@Transactional(readOnly = true)
fun getBookStatistics_v1(): List<BookStatResponse> {
    val results = mutableListOf<BookStatResponse>()
    val books = bookRepository.findAll()
    for (book in books) {
        val targetDto = results.firstOrNull { dto -> book.type == dto.type }
        if (targetDto == null) {
            results.add(BookStatResponse(book.type, 1))
        } else {
            targetDto.plusOne()
        }
    }
    return results
}

 

- BookStatResponse

data class BookStatResponse(
        val type: BookType,
        var count: Int,
) {
    fun plusOne() {
        count++
    }
}

 

> 문제점

- 코드가 너무 길다

- BookStatResponse의 count가 가변 필드로 되어 의도치 않은 수정이 일어날 수 있다.

 

[2] ?. 와 ?: 사용하기

- service

@Transactional(readOnly = true)
fun getBookStatistics_v2(): List<BookStatResponse> {
    val results = mutableListOf<BookStatResponse>()
    val books = bookRepository.findAll()
    for (book in books) {
        results.firstOrNull { dto -> book.type == dto.type }?.plusOne()
                ?: results.add(BookStatResponse(book.type, 1))
    }
    return results
}

 

> 문제점

- call chain이 길어져 디버깅이 쉽지 않다.

- BookStatResponse의 count가 가변 필드로 되어 의도치 않은 수정이 일어날 수 있다.

 

[3] group by 사용하기

- service

@Transactional(readOnly = true)
fun getBookStatistics(): List<BookStatResponse> {
    return bookRepository.findAll() //List<Book>
            .groupBy{ book -> book.type }  // Map<BookType, List<Book>>
            .map { (type, books) -> BookStatResponse(type, books.size)} // List<BookStatResponse>
}

 

- BookStatResponse

data class BookStatResponse(
        val type: BookType,
        val count: Int,
)

 

> 코드 설명

1) findAll() 을 사용해 도서관에 등록되어 있는 모든 책을 조회한다 List<Book>

2) groupBy 를 사용해 카테고리 별 Map 으로 만들어준다 Map<BookType, List<Book>>

3) map 을 사용해 BookStatResponse DTO에 카테고리별 갯수를 담아준다 List<BookStatResponse

 

> 해결한 것

- BookStatResponse data class의 count 필드가 불변 필드로 되어 의도치 않은 수정을 막을 수 있다.

- 코드가 간결해 졌다.

 

> 문제점

도서관에 등록된 책의 전체 목록을 조회하여 그 갯수를 세는 과정이므로 db부하 등을 걱정해야 한다.

(전체 데이터 쿼리 => 메모리 로딩 + grouping)

 

[4] JPQL + Spring data JPA 활용하기 (예제 2개)

1] (위 예제와 이어지는 예제) 예제 1 : Book 기능

bookRepository에 커스텀 query function을 작성한다.

//jpql
@Query("select new com.group.libraryapp.dto.book.response.BookStatResponse(b.type, count(b.id)) " +
        "from Book b group by b.type")
fun getStats(): List<BookStatResponse>

 

위 메소드를 사용하면 service에서는 매우 간결해진다.

@Transactional(readOnly = true)
fun getBookStatistics(): List<BookStatResponse> = bookRepository.getStats()

 

해당 쿼리를 통해 조회하면 h2 query 출력은 아래와 같이 나온다

select
    book0_.type as col_0_0_,
    count(book0_.id) as col_1_0_ 
from
    books book0_ 
group by
    book0_.type

 

> 해결한 것

groupBy 쿼리를 사용하여 db로부터 전체 데이터를 메모리에 로드하지 않고 필요한 정보만을 가져오게 되었다.

-> index를 사용해 튜닝 (성능 최적화) 할수있는 여지가 잇다.

> 문제점

- query 구문에서 외부 객체를 사용하기 위해서는 절대 경로를 전부 입력해 주어야 한다...

- 오타와 띄어쓰기를 매우 주의해야 한다..

 

2] 예제 2 : User 기능

유저와 대출히스토리 table이 각각 있는 db구조에서 유저와 해당 유저의 전체 대출 히스토리 기록을 한번에 조회하는 기능이 있다. 이때 대출기록이 없는 유저일지라도 유저목록으로는 반환되야 한다. 

- service

@Transactional(readOnly = true)
fun getUserLoanHistories(): List<UserLoanHistoryResponse> {
    return userRepository.findAllWithHistories().map(UserLoanHistoryResponse::of)
}

 

- repository

@Query("select distinct u from User u left join fetch u.userLoanHistories")
fun findAllWithHistories(): List<User>

 

> 문제점

- 오타 문제

- 조건이 추가된다면 또 다른 jpql 문을 작성해야 한다.

- 어떤것을 참조하는지 타고 들어가 봐야 안다.

 

[5] Querydsl (예제 2개)

0] 코프링에 querydsl 적용하기

1) gradle 추가

먼저 코프링 환경에서 querydsl을 사용하기 위해서 gradle 에 추가해주어야 한다.

// plugin
id "org.jetbrains.kotlin.kapt" version "1.6.21"

// dependecy
implementation("com.querydsl:querydsl-jpa:5.0.0")
kapt("com.querydsl:querydsl-apt:5.0.0:jpa")
kapt("org.springframework.boot:spring-boot-configuration-processor")

참고로 환경은 스프링 부트 3버전 / intelliJ 23.1버전 / kotlin 1.16 버전 이다.

 

설정 변경후 빌드를 하면 build > generated > source > kapt > main > .....{프로젝트 루트} ..... > domain 에 User와 Book이 아닌 QUser QBook 처럼 앞에 Q가 붙은 클래스들이 생성된걸 볼 수 있다.

 

2) 스프링 빈 등록

 

그리고 나서 QuerydslConfig 파일을 만들어 Component로 등록하고 JPAQueryFactory를 Bean으로 생성한다.

import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import javax.persistence.EntityManager

@Configuration
class QuerydslConfig(
        private val em: EntityManager
) {

    @Bean
    fun querydsl(): JPAQueryFactory {
        return JPAQueryFactory(em)

    }
}

 

다음으로 querydsl을 사용할 interface와 구현체 클래스를 작성한다. 이는 기존의 repository 는 jpql + jpa를 사용하기 때문에 분리하기 위함이다.

 

3) querydsl 인터페이스, 구현체 생성

 

- interface : UserRepositoryCustom

interface UserRepositoryCustom { ... }

 

- class : UserRepositoryCustomImpl

class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory
) : UserRepositoryCustom{ ... }

 

 

4) 기존 repository interface에 방금만든 것 상속

그리고 기존의 userRepository interface에서 Custom interface를 상속받도록 한다. 기존에 상속받고 있던 JPARepository는 그대로.

interface UserRepository : JpaRepository<User, Long>, UserRepositoryCustom{ ... }

 

1] 예제 2 : User 기능 => interface로 만들기

querydsl을 활용해 repository를 작성한다.

- UserRepositoryCustom

interface UserRepositoryCustom {
    fun findWithHistories(): List<User>
}

 

- UserRepositoryCustomImpl

import com.group.libraryapp.domain.user.QUser.user
import com.group.libraryapp.domain.user.loanhistory.QUserLoanHistory.userLoanHistory
import com.querydsl.jpa.impl.JPAQueryFactory

class UserRepositoryCustomImpl(
        private val queryFactory: JPAQueryFactory
) : UserRepositoryCustom{
    override fun findWithHistories(): List<User> {
        // querydsl 로 작성
        return queryFactory.select(user).distinct()
                .from(user)
                .leftJoin(userLoanHistory).on(userLoanHistory.user.id.eq(user.id)).fetchJoin()
                .fetch()
    }
}

 

의존성 주입을 위해 생성자에 JPAQueryFactory를 적어주어야 한다. query 구문에서는 기존 Entity class가 아닌 Q클래스를 사용한다.

 

2] (위 예제와 이어지는 예제) 예제 1 : Book 기능 => class로 만들기

BookQuerydslRepository 라는 클래스를 정의한다.

@Component
class BookQuerydslRepository(
        private val queryFactory: JPAQueryFactory,
) {
    fun getStats(): List<BookStatResponse> {
        return queryFactory.select(Projections.constructor(
                BookStatResponse::class.java,
                book.type,
                book.id.count()
        ))
                .from(book)
                .groupBy(book.type)
                .fetch()
    }
}

> Projections.constructor

주어진 DTO의 생성자를 호출한다. (첫번째로 나오는 클래스의 생성자)

그 뒤 인자들은 생성을 위한 초기값으로 사용된다.

select type, count(book.id) from book;

 

위 쿼리문을 해석하면 아래와 같다.

select type, count(book.id)
from book
group by type;

 

반응형

+ Recent posts