전체 글 검색 결과

178개 발견
  1. 미리보기
    2024.05.06 - mr.november11

    켄트 벡의 Tidy First? 리뷰

  2. 미리보기
    2022.01.09 - mr.november11

    NestJS에서 AWS S3로 파일 업로드하기

  3. 미리보기
    2021.04.06 - mr.november11

    [React] yarn build 후 배포한 페이지에서 tailwind.config.js 에 정의한 커스텀 컬러가 적용되지 않을 때 해결 방법

  4. 미리보기
    2020.12.08 - mr.november11

    [React Native] Expo 에서 iOS 시뮬레이터 실행 시 오류 발생 해결 방법

  5. 미리보기
    2020.10.19 - mr.november11

    [spring.io] 레디스로 메시지 통신하기(코틀린 ver.)

  6. 미리보기
    2020.10.18 - mr.november11

    [spring.io] 스프링 JDBC를 활용하여 관계형 데이터베이스 연동하기(코틀린 ver.)

  7. 미리보기
    2020.10.17 - mr.november11

    [spring.io]RESTful 웹 서비스 호출하기(코틀린 ver.)

  8. 미리보기
    2020.10.17 - mr.november11

    [spring.io]RESTful 웹 서비스 만들기(코틀린 ver.)

  9. 미리보기
    2020.10.05 - mr.november11

    [nginx] nginx 페이지 응답 헤더에 버전 정보가 노출될 경우 조치 방법

  10. 미리보기
    2020.07.08 - mr.november11

    스프링 부트에서 도커 컨테이너 이미지 생성하기

켄트 벡의 Tidy First? 리뷰

2024. 5. 6. 14:39 - mr.november11



ChatGPT, LLM, AI와 같은 유행성 서적들이 주를 이루는 IT도서 시장에서 오랜만에 소프트웨어 기본기를 다루는 서적이 출간되어 기쁘다. 특히 저자가 TDD로 유명한 '켄트 벡'이라는 것을 알게 되었을 때 이 책을 반드시 읽어야겠다고 생각했다.

이 책은 두 권으로 구성되어 있다. 첫 번째 권은 켄트 벡이 집필한 'Tiny First?'이며 두 번째 권은 역자가 추가로 기록한 '옮긴이 노트'이다. 'Tiny First?'의 첫인상은 생각보다 책의 분량이 적다는 느낌이었다. 100페이지 조금 넘는 공간에서 복잡한 소프트웨어 설계에 대한 내용을 다 다룰 수 있을까라는 의문이 들었다. 켄트 벡에 따르면 이 책은 연작으로 기획되었으며 최소 3권의 시리즈로 구성될 예정이다. 이 책은 그 시리즈 중 첫 번째 책이다. 책 가격 대비 분량이 적어 조금 아쉽다는 생각이 들었다. 하지만 내용이 군더더기 없이 정제되어 있고, 출퇴근 시간에 대중교통에서 읽기에 적합한 크기라는 장점이 있다.

켄트 벡의 'Tiny First?'는 단순한 코드 정리 방법부터 복잡한 설계 이론에 이르기까지 좋은 설계를 위한 다양한 내용을 다루고 있다. 초보 개발자들은 특히 1장 '코드 정리법'과 2장 '관리'에서 협업과 유지 보수를 위한 코드 정리 방법을 배울 수 있다(이 두 장의 내용에 흥미를 느꼈다면 해당 내용을 좀 더 자세히 다루고 관련 예시를 제공하는 클린 코드와 리팩터링 책을 읽어보길 권한다). 개인적으로 흥미로웠던 내용은 3장 '이론'에서 코드 정리의 가치를 경제 이론을 통해 다룬 부분이었다. 현업에서 개발 업무를 하면서 신규 기능 구현과 코드 정리의 중요성 사이에서 우선순위를 결정해야 하는 상황이 자주 발생한다. 개발자들은 자연스럽게 코드 정리에 더 큰 가치를 두는 경향이 있지만, 이를 사업부나 상위 관리자에게 설득하는 것이 쉽지가 않다. 또한 해당 시점에서 코드 정리가 실제로 더 중요한지에 대한 의문도 항상 남아 있었다. 켄트 벡은 이런 문제에 대해 경제적 직관을 활용하여 해결 방향을 제시한다.

첫 번째 책인 'Tiny First?'에서 소프트웨어 설계에 관련된 지식을 얻을 수 있었다면, 두 번째 책인 '옮긴이 노트'에서는 좋은 개발자로 성장하기 위한 동기 부여를 전달받을 수 있었다. 특히 역자가 켄트 벡과 주고받은 대화를 통해 개발자로서 가져야 할 사명감과 열정을 배울 수 있었다. 이들처럼 노력하고, 실천하며, 즐기는 태도로 꾸준함을 잃지 않으면 나도 언젠가 좋은 개발자가 될 수 있을 것이라는 희망을 가지게 되었다.

"한빛미디어의 도서 지원을 받아 작성한 리뷰입니다."

다른 카테고리의 글 목록

리뷰 카테고리의 포스트를 톺아봅니다

NestJS에서 AWS S3로 파일 업로드하기

2022. 1. 9. 08:55 - mr.november11

1. nest cli로 컨트롤러,서비스 생성하기

  • 파일 업로드 서비스의 이름은 upload 로 생성한다.
nest g mo uploads
nest g co uploads
nest g s uploads

2. 업로드 컨트롤러 구현

Nest는 파일 업로드 처리를 위해 Express의 multer 미들웨어를 제공한다.

multer는 POST 메소드로 multipart/form-data 컨텐츠 타입을 지원한다.

업로드 컨트롤러를 구현하기 앞서 Multer 라이브러리를 설치한다.

$ yarn add --dev @types/multer

이번 예제는 단일 파일 업로드를 처리한다.

@Controller('uploads')
export class UploadsController {
  @Post()
  @UseInterceptors(FileInterceptor('file'))
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    console.log(file);
  }
}

업로드 컨트롤러에서 단일 파일 입력을 받도록 설정한 후 포스트맨으로 API를 테스트한다.

포스트맨으로 테스트 한 결과 console.log(file) 에 다음과 같은 정보가 출력된다.

{
  fieldname: 'file',
  originalname: 'test.png',
  encoding: '7bit',
  mimetype: 'image/png',
  buffer: <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 01 46 00 00 01 3e 08 06 00 00 00 f1 a6 b9 5c 00 00 0c 6d 69 43 43 50 49 43 43 20 50 72 6f 66 69 ... 36041 more bytes>,
  size: 36091
}

이제 컨트롤러의 file을 서비스로 전달하여 S3로 업로드하는 작업을 구현한다.

2. 업로드 서비스 구현

먼저 업로드 컨트롤러에서 업로드 서비스의 uploadFile 함수를 호출하도록 코드를 수정한다.

@Controller('uploads')
export class UploadsController {
  constructor(private readonly uploadService: UploadsService) {}
  @Post()
  @UseInterceptors(FileInterceptor('file'))
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    return this.uploadService.uploadFile(file);
  }
}

업로드 서비스에서는 AWS S3로 파일을 업로드하기 위해 aws-sdk 라이브러리를 사용한다.

$ yarn add --dev aws-sdk

업로드 서비스에서 aws-sdk를 사용하기 위해 클래스에 S3 멤버 변수를 추가한다.
(실행 환경에 S3 업로드 권한을 가진 Access Key가 설정되어 있어야 한다.)

import { Injectable } from '@nestjs/common';
import * as AWS from 'aws-sdk';
@Injectable()
export class UploadsService {
  s3 = new AWS.S3();
}

실제 파일 업로드를 수행하는 uploadFile 함수를 구현한다.

async uploadFile(file: Express.Multer.File) {
    const AWS_S3_BUCKET = 'nestjs-upload-test-bucket';
    const params = {
      Bucket: AWS_S3_BUCKET,
      Key: String(file.originalname),
      Body: file.buffer,
    };
    try {
      const response = await this.s3.upload(params).promise();
      return response;
    } catch (e) {
      throw new Error('Failed to upload file.');
    }
  }

업로드가 성공했다면 AWS.S3.ManagedUpload.SendData 타입의 결괏값이 반환된다.

export interface SendData {
        /**
         * URL of the uploaded object.
         */
        Location: string;
        /**
         * ETag of the uploaded object.
         */
        ETag: string;
        /**
         * Bucket to which the object was uploaded.
         */
        Bucket: string;
        /**
         * Key to which the object was uploaded.
         */
        Key: string;
    }

응답 객체의 Location은 업로드 된 객체의 URL을 포함한다.

{
    "ETag": "\"fff730c0a3f344034854117aaf88d9ac\"",
    "Location": "https://nestjs-upload-test-bucket.s3.ap-northeast-2.amazonaws.com/test.png",
    "key": "test.png",
    "Key": "test.png",
    "Bucket": "nestjs-upload-test-bucket"
}

업로드 시 별도의 ACL을 설정하지 않았기 때문에 해당 URL로 접속할 경우 AccessDenied 가 발생한다.

<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>BJNMX4V6S5NHCN9Q</RequestId>
<HostId>
gW79WxeQfreLOY+bmPKSxrRBaOUJ/girPZsMev1icrU5foITkTwdF8njH6VnkKJVZRjibxm0P/I=
</HostId>
</Error>

외부에서 접속 가능한 ACL로 파일을 업로드하려면 params 변수에 public-read로 ACL을 설정해야 한다.
이 경우 해당 S3 버킷의 ACL도 퍼블릭 액세스가 허용된 상태여야 한다.

S3 버킷의 퍼블릭 액세스를 허용하는 방법은 다음 문서를 참고한다.

const params = {
      Bucket: AWS_S3_BUCKET,
      Key: String(file.originalname),
      Body: file.buffer,
      ACL: 'public-read',
    };

 

다른 카테고리의 글 목록

AWS 카테고리의 포스트를 톺아봅니다
  • 문제 현상 : yarn build 후 배포한 페이지에서 tailwind.config.js 에 정의한 커스텀 컬러가 적용되지 않음

  • 해결 방법

    1. npx tailwindcss-cli@latest build -o tailwind.css 으로 tailwind 관련 css 파일 생성

       $ npx tailwindcss-cli@latest build -o tailwind.css
      
          tailwindcss 2.0.4
      
          🚀 Building from default CSS... (No input file provided)
      
          ✅ Finished in 3.04 s
          📦 Size: 4.28MB
          💾 Saved to tailwind.css
    2. 생성된 tailwind.css 파일을 public 폴더에 복사

    3. public/index.html 파일에 head 내에서 tailwind.css 을 로드

    <link href="/tailwind.css" rel="stylesheet" />

다른 카테고리의 글 목록

React 카테고리의 포스트를 톺아봅니다

1. 현상 

 

expo에서 iOS 시뮬레이터를 실행할 경우 터미널 상에서 아래와 같이
'System Events에 Apple 이벤트를 보낼 권한이 없습니다.'  

에러가 발생한다. 

 

Expo 개발 웹 UI 상에서는 Error opening is simulator. Check Metro logs for details. 라는 에러 문구가 나온다. 

(node:47980) UnhandledPromiseRejectionWarning: Error: Command failed: osascript -e tell app "System Events" to count processes whose name is "Simulator"
28:69: execution error: System Events에 Apple 이벤트를 보낼 권한이 없습니다. (-1743)

    at ChildProcess.exithandler (child_process.js:295:12)
    at ChildProcess.emit (events.js:223:5)
    at maybeClose (internal/child_process.js:1021:16)
    at Socket.<anonymous> (internal/child_process.js:430:11)
    at Socket.emit (events.js:223:5)
    at Pipe.<anonymous> (net.js:664:12)
(node:47980) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 14)
(node:47980) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

 

2. 해결 방법

Mac OS의 환경 설정에서 Termial -> System Events 항목을 활성화한다. 

다른 카테고리의 글 목록

리뷰/기타 카테고리의 포스트를 톺아봅니다

# [spring.io] 레디스로 메시지 통신하기(코틀린 ver.)

#kotlin #spring

참고 사이트 : Getting Started | Messaging with Redis

레디스 서버 실행하기

spring.io 예제에서는 brew 를 사용하여 reds 서버를 설치하고 실행합니다.
이번 예제에서는 docker 를 사용하여 redis 를 실행합니다.
참고 사이트 : https://hub.docker.com/_/redis/

실행 명령어 : docker run —name some-redis -p 6379:6379 -d redis

Spring Initializr 로 시작하기

메이븐 기준으로 다음 의존성을 추가한 프로젝트를 생성합니다.

  • Spring Data Redis (Access+Driver)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk8</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

Receiver 클래스 생성하기

메시지를 받고 응답하는 Receiver 클래스를 생성합니다.
Receiver는 POJO로 메시지를 수신하는 메소드를 정의합니다.
예제에서는 메시지를 전달 받으면 receiveMessage 함수를 호출할 예정입니다.

class Receiver {
    val LOGGER = LoggerFactory.getLogger(Receiver::class.java)
    val counter = AtomicInteger()

    fun receiveMessage( message: String ) {
        LOGGER.info(“Received < $message >”);
        counter.incrementAndGet();
    }
}

리스너를 등록하고 메시지를 보내기

스프링 데이터 레디스는 레디스와 메시지를 주고받기 위한 모든 컴포넌트를 제공합니다.
- 커넥션 팩토리
- 메시지 리스너 컨테이너
- 레디스 템플릿
예제에서는 레디스 템플릿을 사용하여 메시지를 보내고 Receiver 메시지 리스너로 등록합니다.

@SpringBootApplication
class RedisApplication{

    val LOGGER = LoggerFactory.getLogger(RedisApplication::class.java)

    @Bean
    fun container(
            connectionFactory: RedisConnectionFactory,
            listernerAdapter: MessageListenerAdapter) : RedisMessageListenerContainer {

        val container = RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory)
        container.addMessageListener(listernerAdapter, PatternTopic(“chat”))

        return container
    }

    @Bean
    fun listenerAdapter(receiver: Receiver) : MessageListenerAdapter {
        return MessageListenerAdapter(receiver, “receiveMessage”)
    }

    @Bean
    fun receiver() : Receiver {
        return Receiver()
    }

    @Bean
    fun template(connectionFactory: RedisConnectionFactory) : StringRedisTemplate {
        return StringRedisTemplate(connectionFactory)
    }

}

fun main(args: Array<String>) {

    val ctx = runApplication<RedisApplication>(*args)
    val template : StringRedisTemplate = ctx.getBean(StringRedisTemplate::class.java)

    val receiver : Receiver = ctx.getBean(Receiver::class.java)

    while(receiver.counter.toInt() == 0) {
        *println*(“Sending message…”);
        template.convertAndSend(“chat”, “Hello from Redis!”);
        Thread.sleep(500L);
    }
}

실행 결과

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

INFO 61756 --- [           main] com.example.redis.RedisApplicationKt     : Starting RedisApplicationKt on C02XHD9VJG5J with PID 61756 (/Users/user/study/redis/target/classes started by in /Users/user/study/redis)
INFO 61756 --- [           main] com.example.redis.RedisApplicationKt     : No active profile set, falling back to default profiles: default
INFO 61756 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
INFO 61756 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
INFO 61756 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 8ms. Found 0 Redis repository interfaces.
INFO 61756 --- [           main] com.example.redis.RedisApplicationKt     : Started RedisApplicationKt in 1.298 seconds (JVM running for 1.737)
Sending message...
2020-10-19 20:18:43.770  INFO 61756 --- [    container-2] com.example.redis.Receiver               : Received < Hello from Redis! >

Process finished with exit code 0

다른 카테고리의 글 목록

Spring 카테고리의 포스트를 톺아봅니다

[spring.io] 스프링 JDBC를 활용하여 관계형 데이터베이스 연동하기(코틀린 ver.)

#kotlin #spring

참고 사이트 : https://spring.io/guides/gs/relational-data-access/

스프링의 JdbcTemplate 을 사용하여 관계형 데이터베이스에 저장된 데이터에 접근하는 애플리케이션을 작성합니다.

Spring Initializr 로 시작하기

메이븐 기준으로 다음 의존성을 추가한 프로젝트를 생성합니다.

  • Spring DATA JDBC
  • H2 Database
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk8</artifactId>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

Customer 객체 생성하기

간단한 데이터 접근 로직 테스트를 위해서 서비스에서 사용할 Customer 클래스를 생성합니다.

class Customer(
        val id : Long,
        val firstName : String,
        val lastName : String
) {
    override fun toString(): String {
        return “Customer id=$id, firstName=‘$firstName’, lastName=‘$lastName’]”
    }
}

JDBC 로 H2 데이터 데이터 베이스에 SQL 쿼리 실행하기

생성자에서 JdbcTemplate 객체를 주입받아 사용합니다.
Customer 리스트를 생성한 후 jdbcTemplate.batchUpdate 로 데이터베이스에 적재합니다.
적재 이후에는 SELECT 쿼리를 사용하여 전체 Customer 목록을 조회한 후 log로 출력합니다.

@SpringBootApplication
class JdbcApplication(
        val jdbcTemplate: JdbcTemplate
): CommandLineRunner{

    override fun run(vararg args: String?) {
        val log = LoggerFactory.getLogger(this.javaClass)

        log.info("Creating tables");

        jdbcTemplate.execute("DROP TABLE customers IF EXISTS");
        jdbcTemplate.execute("CREATE TABLE customers(" + "id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))");

        val splitUpNames = listOf<String>("John Woo", "Jeff Dean", "Josh Bloch", "Josh Long")
                .map{ it.split(" ").toTypedArray()}


        splitUpNames.forEach { log.info("Inserting customer record for ${it[0]} / ${it[1]}") }

        jdbcTemplate.batchUpdate("INSERT INTO customers(first_name, last_name) VALUES (?,?)", splitUpNames);

        log.info("Querying for customer records where first_name = 'Josh':");

        jdbcTemplate.query("SELECT id, first_name, last_name FROM customers") {
            rs, rowNum -> println("${rs.getLong("id")}, ${rs.getString("first_name")}, ${rs.getString("last_name")} $rowNum")
        }
    }
}

출력 결과

1, John, Woo 0
2, Jeff, Dean 1
3, Josh, Bloch 2
4, Josh, Long 3

다른 카테고리의 글 목록

Spring 카테고리의 포스트를 톺아봅니다

# [spring.io]RESTful 웹 서비스 호출하기(코틀린 ver.)

#kotlin #spring

참고 사이트 : https://spring.io/guides/gs/consuming-rest/

RESTful 웹 서비스 호출하기

스프링의 RestTemplate을 사용하여 RESTful 웹 서비스를 호출하는 예제입니다.
호출할 테스트 URL 은 https://gturnquist-quoters.cfapps.io/api/random 입니다.

Spring Initializr 로 시작하기

메이븐 기준으로 다음 의존성을 추가한 프로젝트를 생성합니다.

  • Spring Web
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-kotlin</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk8</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

REST 리소스 가져오기

https://gturnquist-quoters.cfapps.io/api/random URL을 호출하면 다음과 같은 JSON 응답을 받아옵니다.

{
   type: "success",
   value: {
      id: 10,
      quote: "Really loving Spring Boot, makes stand alone Spring apps easy."
   }
}

도메인 클래스 생성하기

Quote 클래스 생성

class Quote (
        val type : String,
        val value : Value
){
    override fun toString(): String {
        return "Quote{" +
                "type='" + type + '\'' +
                ", value=" + value +
                '}';

    }
}

Value 클래스 생성

class Value(
        val id: Long,
        val quote : String
) {
    override fun toString(): String {
        return “Value{“ +
                “id=“ + id +
                “, quote=‘” + quote + ‘\’’ +
                ‘}’;
    }
}

애플리케이션 생성

  • 참고 사이트에서는 RestTemplate 의 getForObject 함수를 사용하지만,
    코틀린에서는 getForObject 를 사용할 경우 코드가 조금 복잡해지기 떄문에 getForEntity를 사용합니다.
fun main() {
    val quote: ResponseEntity<Quote> = RestTemplate().*getForEntity*<Quote>(
            “https://gturnquist-quoters.cfapps.io/api/random”)
    *println*(quote.toString())
}

실행 결과

<200,Quote{type='success', value=Value{id=5, quote='Spring Boot solves this problem. It gets rid of XML and wires up common components for me, so I don't have to spend hours scratching my head just to figure out how it's all pieced together.'}},[Content-Type:"application/json;charset=UTF-8", Date:"Sat, 17 Oct 2020 14:36:41 GMT", X-Vcap-Request-Id:"ad5f0439-f4ee-42b7-495c-0c44834274d5", Content-Length:"235", Connection:"keep-alive"]>

다른 카테고리의 글 목록

Spring 카테고리의 포스트를 톺아봅니다

[spring.io]RESTful 웹 서비스 만들기(코틀린 ver.)

참고 사이트 : Getting Started | Building a RESTful Web Service

RESTful 웹 서비스 만들기

이번 가이드에서는 스프링과 코틀린 언어로 “Hello, World”를 출력하는 RESTful 웹 서비스를 만들어볼 예정입니다.

http://localhost:8080/greeting GET 요청에 위와 같은 JSON 인사를 응답하는 웹 서비스를 만듭니다.

{"id":1,"content":"Hello, World!”}

추가로, http://localhost:8080/greeting?name=User 와 같이 문자열 쿼리에 name 파라미터를 전달 받으면 전달 받은 이름을 응답합니다.
(name 파라미터 값이 없을 경우 기본 값은 World입니다.)

{"id":1,"content":"Hello, User!”}

Spring Initializr 로 시작하기

메이븐 기준으로 다음 의존성을 추가한 프로젝트를 생성합니다.

  • Spring Web
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-kotlin</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk8</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

리소스 클래스 생성

서비스는 /greeting 에 대한 GET 요청과 문자열 쿼리의 name 파라미터를 처리합니다. 서비스는 200 OK 코드와 함께 다음과 같은 JSON 을 본문에 넣어 응답합니다.

{
    "id": 1,
    "content": "Hello, World!"
}
  • Greeting 클래스 생성
    class Greeting(
          val id : String,
          val content : String`
    ) 

리소스 컨트롤러 생성

HTTP 요청 처리를 위해 @RestController 어노테이션을 추가합니다.

@RestController
class GreetingController {
    val counter = AtomicLong()

    @GetMapping(“/greeting”)
    fun greeting(@RequestParam(value=“name”, defaultValue = “World”)  name :String ) =
            Greeting(counter.incrementAndGet(), “Hello, $name”)
}

@GetMapping 어노테이션은 /greeting의 HTTP GET 요청을 greeting() 메소드에 매핑합니다.

@RequestParam 어노테이션은 문자열 쿼리 name 의 값을 greeting() 메소드의 name 파라미터에 주입합니다. 문자열 쿼리에 name 값이 없을 경우에는 defaultValue를 주입합니다.

테스트 결과

 curl localhost:8080/greeting                               
{"id":4,"content":"Hello, World"}                                               
curl localhost:8080/greeting?name=User                      
{"id":5,"content":"Hello, User”}

다른 카테고리의 글 목록

Spring 카테고리의 포스트를 톺아봅니다

1. 현상
아래와 같이 nginx 기본 설정에서는 응답해더에서 nginx 버전 정보가 노출된다.

이 경우 특정 nginx 버전의 보안 취약점을 노린 해커의 공격이 유입될 수 있기 때문에 조치가 필요하다. 

 

2. 조치 방법 

nginx.conf 파일 내 아래 설정을 추가한다. 

server_tokens off;

 

3. 조치 결과 

응답 헤더의 Server 정보에 nginx 버전인 1.19.2 사라졌다. 

다른 카테고리의 글 목록

리뷰/기타 카테고리의 포스트를 톺아봅니다

스프링 부트에서 도커 컨테이너 이미지 생성하기

  • 개발 환경
    • Intellij IDE
    • Maven

참고 사이트 : https://spring.io/guides/gs/spring-boot-docker/

예제 코드 생성

@SpringBootApplication
@RestController
public class DockerTestApplication {

    @RequestMapping("/")
    public String sayHello() {
        return "Hello CI";
    } 

    public static void main(String[] args) {
        SpringApplication.run(DockerTestApplication.class, args);
    }

}
  • maven 을 활용하여 jar 파일 생성 및 실행
$ mvn package && java -jar target/*.jar
$ curl localhost:8080                                        
Hello CI

Dockerfile 생성

  • maven package 로 생성된 jar 파일을 컨테이너 이미지 안에 넣고 엔트리 포인트를 지정한다.
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

Docker 이미지 생성

  • 실행 명령어 : docker build -t ci/hello-ci .
$docker build -t ci/hello-ci .                                                 SIGINT(2) ↵
Sending build context to Docker daemon  16.75MB
Step 1/4 : FROM openjdk:8-jdk-alpine
8-jdk-alpine: Pulling from library/openjdk
e7c96db7181b: Pull complete
f910a506b6cb: Pull complete
c2274a1a0e27: Pull complete
Digest: sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3
Status: Downloaded newer image for openjdk:8-jdk-alpine
 ---> a3562aa0b991
Step 2/4 : ARG JAR_FILE=target/*.jar
 ---> Running in a50bdf4f6a76
Removing intermediate container a50bdf4f6a76
 ---> a44d7bc3b35f
Step 3/4 : COPY ${JAR_FILE} app.jar
 ---> ca072fc923ee
Step 4/4 : ENTRYPOINT ["java","-jar","/app.jar"]
 ---> Running in d5fb7258f922
Removing intermediate container d5fb7258f922
 ---> 336f8ce8cdc3
Successfully built 336f8ce8cdc3
Successfully tagged ci/hello-ci:latest

Docker 이미지 실행

  • 실행 명령어 : docker run -p 8080:8080 ci/hello-ci
$ docker run -p 8080:8080 ci/hello-ci                                                



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

2020-07-08 00:46:10.959  INFO 1 --- [           main] c.e.dockertest.DockerTestApplication     : Starting DockerTestApplication v0.0.1-SNAPSHOT on 145d1d9f806a with PID 1 (/app.jar started by root in /)
2020-07-08 00:46:10.968  INFO 1 --- [           main] c.e.dockertest.DockerTestApplication     : No active profile set, falling back to default profiles: default
2020-07-08 00:46:13.175  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-07-08 00:46:13.211  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-07-08 00:46:13.212  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-07-08 00:46:13.399  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-07-08 00:46:13.400  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2305 ms
2020-07-08 00:46:14.013  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-07-08 00:46:14.453  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-07-08 00:46:14.480  INFO 1 --- [           main] c.e.dockertest.DockerTestApplication     : Started DockerTestApplication in 4.277 seconds (JVM running for 5.158)

다른 카테고리의 글 목록

Spring 카테고리의 포스트를 톺아봅니다