BDD


1. BDD(Behavior-Driven Development)란?

BDDTDD에서 파생된 개발 방법론으로 테스트에 대한 집중보다는 명세와 행위에 대해 비중을 두고 있다.

1.1. TDD

tdd
Figure 1. TDD

TDDTest Case(이하 TC)를 작성하고 실패를 확인 후 실제 비즈니스 코드를 작성, TC 성공확인을 하나의 반복주기로 잡고, 이를 반복해서 진행하는 것을 요구한다. 하지만 TC를 작성하는데 의문점이 있다. 코드가 없는데 무엇을 테스트할 것인가? TDD를 보면 무엇을 테스하는지에 대한 명시는 되어있지 않다. 또한, 보통 테스트라 하면 제품이 나오면 그 제품을 사용해보면서 제대로 작동하는지 확인을 해보는 과정인데, 제품이 나오지도 않았는데 테스트를 한다라는 것부터 모순이 생긴다. 이를 개선한 것이 BDD라고 생각을 한다.

1.2. BDD(Behavior-Driven Development)

BDD는 위에서 언급한바와 같이 TDD에서 파생된 개발방법론이고, 코드의 구현과 테스트 보다는 행위(동작, 명세)에 집중하고 있다. BDD에서는 기능의 TC를 작성하는 것이 아닌 명세를 작성하는 것이고, 요구사항 분석 후 기능 설계 및 명세작성, 코드구현의 순서로 진행을 하게된다.

BDD에서 테스트(Test) 라는 단어를 사용하지 않고 명세(Specification)를 사용함으로써 많은 것이 바뀌게 된다. TDD에서는 테스트라는 단어를 사용하게 되어 혼란과 모순을 가지게 되었는데, BDD에서는 이를 명세라는 단어로 사용함으로써, 제품이 생산되기 전에 제품에 대한 명세를 작성하게 되는 것이므로 이에 대한 모순이 사라지게 된다.

2. BDD Template

Title: 스토리에대한 제목을 간략하고 명확하게 작성
User Story
  Who 누가
  Why 왜
  What 무엇을 하는지
Scenario
  Given 어떤 값이 주어졌을 때
  When 어떤 행위를 하게 되면
  Then 어떤 결과를 도출한다

3. BDD Example

할일을 관리할 수 있는 프로그램을 만든다고 가정을 한다. 그렇다면 기획자 혹은 개발자는 고객과 커뮤니케이션을 하며 프로그램에 대한 요구사항을 수집하게 된다.

할일을 관리할 수 있는 시스템을 개발한다.
Todo Item을 등록/수정/삭제 할 수 있다.
Todo/Doing/Done 할 수 있고 Archive할 수 있어야 한다.
Todo Item을 등록할 때 Todo 상태로 시작한다.
상태변경을 할 수 있고 상태 변경은
Todo > Doing, Doing > Done, Done > Doing, Doing > Todo로만 할 수 있다.
Archive는 Todo/Doing/Done 모든 상태에서 가능하다.
목록보기/상세보기 기능을 포함한다.
페이징 기능은 스펙에서 제외하고 현재 스펙에서는 전체 목록을 한번에 조회한다.

수집한 요구항이 위와 같다고 하면 아래 그림과 같은 Use Case DiagramClass Diagram을 그릴 수 있다.

use case diagram
Figure 2. Use Case Diagram
class diagram
Figure 3. Class Diagram

위 예시에서 상태변경을 통해 명세작성 예시를 진행하도록 하겠다.

Title: Todo Item의 상태를 변경한다.

User Story
Who: Todo Management System을 사용하는 사용자가
Why: Todo Item의 상태관리를 위해서
What: 각각의 Todo Item의 상태를 변경할 수 있다.
      상태 변경은 Todo > Doing, Doing > Done,
      Done > Doing, Doing > Todo로만 할 수 있다.

Scenario 1: Todo상태를 Doing상태로 변경하면 상태가 변경된다.
Scenario 2: Doing상태를 Done상태로 변경하면 상태가 변경된다.
Scenario 3: Done상태를 Doing상태로 변경하면 상태가 변경된다.
Scenario 4: Doing상태를 Todo상태로 변경하면 상태가 변경된다.
Scenario 5: Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.
Scenario 6: Done상태를 Todo상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.

일반 글로 위와 같은 명세를 작성할 수 있고 아래와 같이 코드로 명세를 작성할 수 있다.

package kr.pe.nuti.home.api.service.todo;

import kr.pe.nuti.home.api.core.application.Application;
import kr.pe.nuti.home.api.core.application.JpaConfiguration;
import kr.pe.nuti.home.api.core.application.WebConfiguration;
import kr.pe.nuti.home.api.domain.todo.TodoItem;
import kr.pe.nuti.home.api.enumeration.todo.TodoState;
import kr.pe.nuti.home.api.exception.todo.IllegalStateChangeException;
import kr.pe.nuti.home.api.repository.todo.TodoItemRepository;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Optional;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * Title: Todo Item의 상태를 변경한다.
 * User Story:
 * Todo Management System을 사용하는 사용자가
 * Todo Item의 상태관리를 위해서
 * 각각의 Todo Item의 상태를 변경할 수 있다.
 * 상태 변경은 Todo > Doing, Doing > Done,
 * Done > Doing, Doing > Todo로만 할 수 있다.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JpaConfiguration.class, WebConfiguration.class, Application.class})
public class TodoServiceStateChangeTest {

  @Mock
  private TodoItemRepository todoItemRepository;

  @Autowired
  @Spy
  @InjectMocks
  private TodoService service;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
  }

  /**
   * Todo상태를 Doing상태로 변경하면 상태가 변경된다.
   * @throws Exception
   */
  @Test
  public void testStateChangeFromTodoToDoing() throws Exception {
    // given Todo 상태의 Todo Item
    TodoItem savedItem = new TodoItem();
    savedItem.setIdx(1L);
    savedItem.setState(TodoState.TODO);
    TodoItem changedItem = new TodoItem();
    changedItem.setIdx(1L);
    changedItem.setState(TodoState.DOING);
    when(todoItemRepository.save(any(TodoItem.class))).thenReturn(changedItem);
    when(todoItemRepository.findById(1L)).thenReturn(Optional.of(savedItem));

    TodoItem item = new TodoItem();
    item.setIdx(1L);

    // when Todo Item의 상태를 Doing으로 변경한다.
    TodoItem result = service.changeState(item, TodoState.DOING);

    // then Todo Item의 상태가 Doing으로 변경된다.
    Assert.assertThat(result.getState(), is(TodoState.DOING));
    verifyNoMoreInteractions(service);
  }

  /**
   * Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.
   * @throws Exception
   */
  @Test(expected = IllegalStateChangeException.class)
  public void testStateChangeFromTodoToDoneThrownException() throws Exception {
    try {
      // given Todo 상태의 Todo Item
      TodoItem savedItem = new TodoItem();
      savedItem.setIdx(1L);
      savedItem.setState(TodoState.TODO);
      TodoItem changedItem = new TodoItem();
      changedItem.setIdx(1L);
      changedItem.setState(TodoState.DOING);
      when(todoItemRepository.findById(any(Long.class))).thenReturn(Optional.of(savedItem));
      when(todoItemRepository.save(any(TodoItem.class))).thenReturn(changedItem);

      TodoItem item = new TodoItem();
      item.setIdx(1L);

      // when Todo Item의 상태를 Done으로 변경한다.
      service.changeState(item, TodoState.DONE);

      // then Todo Item의 상태가 변경되지 않고 예외사항이 발생한다.
    } catch (Exception e) {
      verifyNoMoreInteractions(service);
      throw e;
    }
  }
}

Spring AOP(Aspect Oriented Programming)


1. 개요

AOP는 스프링의 기반 기술 중 하나로 이해하기 힘든 용어와 개념을 가졌다. AOP는 자바의 Reflection API를 활용해서 구현을 하게되고, 주로 비즈니스 요구사항이 아닌 부분들을 처리하기 위해 사용한다.

2. Reflection API

jvm architecture
Figure 1. JVM Architecture

자바의 Reflection API는 컴파일 레벨에서 실행될 클래스를 정하는 것이 아닌, 런타임에 실행할 클래스 파일을 정하게 할 수 있다. 또한, 런타임에서 클래스의 공개되지 않은 필드에 대한 정보를 볼 수 있고 조작할 수 있다.

주로 JDBC나 MyBatis에서 많이 사용한다. 예시는 아래와 같다.

2.1. Reflection API Example

package kr.pe.nuti.home.api.core.annotation;

import java.lang.annotation.*;

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.METHOD
})
public @interface LogDetail {
}
package kr.pe.nuti.home.api.core.annotation;

import java.lang.annotation.*;

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.TYPE
})
public @interface TrackLog {
}
package kr.pe.nuti.home.api.core.util;

import kr.pe.nuti.home.api.core.annotation.TrackLog;

import java.lang.reflect.Field;

import static kr.pe.nuti.home.api.core.util.BooleanUtil.not;

public final class LogUtil {

  private LogUtil() {
    throw new IllegalAccessError();
  }

  public static String argValues(Object arg, int depth) throws IllegalAccessException {
    if (arg == null) {
      return "null";
    }
    final Class<?> cls = arg.getClass();

    if (cls.isPrimitive() || cls.isAssignableFrom(String.class) || not(cls.isAnnotationPresent(TrackLog.class))) {
      return arg.toString();
    }

    StringBuilder builder = new StringBuilder();
    builder.append(whiteSpace(depth)).append(cls.getName()).append("{\n");

    for (Field field : cls.getDeclaredFields()) {
      field.setAccessible(true);
      Object fieldObj = field.get(arg);
      builder.append(whiteSpace(depth + 1))
          .append(field.getName())
          .append(" : ")
          .append(argValues(fieldObj, depth + 1))
          .append("\n");
    }

    builder.append("}");

    return builder.toString();
  }

  public static String whiteSpace(int depth) {
    final String appender = "  ";
    StringBuilder builder = new StringBuilder();

    for (int i = 0; i < depth; i++) {
      builder.append(appender);
    }

    return builder.toString();
  }
}
package kr.pe.nuti.home.api.core.handler;

import kr.pe.nuti.home.api.core.annotation.LogDetail;
import kr.pe.nuti.home.api.core.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import static kr.pe.nuti.home.api.core.util.BooleanUtil.not;

public class LogDetailMethodInvocationHandler implements InvocationHandler {

  private static final Logger logger = LoggerFactory.getLogger(LogDetailMethodInvocationHandler.class);

  private Object target;

  public LogDetailMethodInvocationHandler(Object target) {
    this.target = target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (not(method.isAnnotationPresent(LogDetail.class))) {
      return method.invoke(target, args);
    }
    String className = method.getDeclaringClass().getName();
    String methodName = method.getName();
    StringBuilder argBuilder = new StringBuilder();

    for (Object arg : args) {
      argBuilder.append(LogUtil.argValues(arg, 0))
          .append("\n");
    }
    String argString = argBuilder.toString();

    logger.info("invoke method {}${}", className, methodName);
    logger.info("method arguments: {}", argString);

    Object result  = method.invoke(target, args);

    logger.info("finish the method {}${}", className, methodName);

    return result;
  }
}

위 예시는 LogDetail이라는 어노테이션을 가진 메소드에 대해서 해당 메소드의 파라미터 정보를 상세하게 로깅하는 것이다. 런타임에서 메소드의 정보를 분석해서 어노테이션 표기 여부에 따라 로그를 남기고 메소드를 실행시키게 된다. 또한, LogUtil.argValues는 Object의 정보를 상세하게 분석해서 Object 내부의 필드정보를 보여줄 수 있도록 되어있다.

Reflection API는 이런식으로 컴파일 타임에 어떤 클래스의 인스턴스가 실행될 지 알 수 없는 경우에 런타임에서 클래스정보를 분석하고 실행할 수 있도록 할 때 사용한다.

3. Proxy Pattern

proxy pattern
Figure 2. Proxy Pattern
  • 클라이언트가 실제 사용하려 하는 기능에 부가적인 기능을 더해서 자신이 핵심 기능인 척 위장하는 것

  • 타겟은 프록시가 있는지 알아서는 안된다.

  • 타겟의 기능을 확장 및 접근 방법을 제어할 수 있는 유용한 방법

  • 특정 Object에 대한 접근을 제어

  • 대상이 되는 Object의 생성에 관여를 하기도 함

    • 생성이 복잡한 경우

    • 당장 생성이 필요하지 않은 경우에 바로 생성하지 않고, 필요한 시기에 생성

  • 원격 Object를 이용하는 경우에 사용

    • RMI

    • EJB

  • 대상이 되는 Object에 대한 접근권한을 제어하기 위해 사용

4. Decorator Pattern

decorator pattern
Figure 3. Decorator Pattern
  • 대상이 되는 Object에 부가적인 기능을 부여하기 위해 사용

  • 컴파일 시점에 어떤 방법과 순서로 연결되어 사용하는지 정해지지 않음

  • InputStream, OutputStream

4.1. 프록시 패턴과의 차이

  • 프록시는 어떤 오브젝트를 사용하기 위해 대리인 역할을 맡은 오브젝트를 사용하는 방법을 총칭

  • 프록시패턴 프록시를 사용하는 방법 중 타겟에 대한 접근 방법을 제어하려는 목적

  • 타겟을 생성하기 복잡하거나 당장 필요하지 않은 경우에 타겟을 바로 생성하지 않고 프록시를 사용

  • 실제 타겟을 사용할 때 타겟을 생성(Lazy)

  • 기능에 대한 접근 권한을 제어하는 목적으로도 사용(읽기/쓰기 권한)

  • 자신이 만들거나 접근할 타겟을 알고있는 경우가 많음

5. Proxy

  • Client와 사용 대상 Object 사이에서 대리 역할을 하는 Object

  • 대상 Object의 핵심 기능에 부가적인 기능을 추가

  • 대상 Object는 Proxy Object의 존재 여부를 모름

  • 대상 Object를 Target 또는 Real Object라고 부름

6. Dynamic Proxy

dynamic proxy
Figure 4. Dynamic Proxy
  • 프록시는 매 Class, Method마다 Proxy를 정의해주어야 한다는 단점이 존재

  • JAVA의 Reflection API를 통해 Runtime에 동적으로 Proxy하도록 함

7. AOP

aop
Figure 5. AOP
  • Advice

    • 타겟이 필요 없는 순수한 부가 기능

    • 스프링에서는 부가기능을 제공하는 Object를 Advice라고 부름

  • Pointcut

    • 부가기능 적용 대상 선정 방법

    • 스프링에서는 메소드 선정 알고리즘을 담은 Object를 Pointcut이라고 부름

  • Advisor

    • Pointcut + Advice

  • Join Point

    • Advice가 적용될 수 있는 위치

  • Aspect

    • 독립적인 모듈화가 불가능한 모듈

    • 그 자체로 핵심 기능을 담고 있지는 않지만, 어플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심 기능에 부가되어 의미를 갖는 특별한 모듈

  • 핵심적인 기능에서 부가적인 기능을 분리해서 Aspect라는 독특한 모듈로 만들어 설계하고 개발하는 방법

  • 객체지향을 좀 더 편하고 객체지향답게 사용할 수 있도록 하는 개념

8. AOP Example

8.1. Expression

execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | “..}, …) [throws 예외 패턴])

ex) public int springbook.learningtest.spring.pointcut.Target.mins(int, int) throws java.lang.RuntimeException

  • public

    • 접근 제한자, 생략 가능

  • int

    • 리턴 값의 타입을 나타내는 패턴

  • springbook.learningtest.spring.pointcut.Target

    • 패키지 및 클래스 이름 패턴

  • minus

    • 메소드 이름 패턴

  • (int, int)

    • 메소드 파리미터 패턴

  • throws java.lang.RuntimeException

    • 예외 이름 패턴

8.2. Example Code

package kr.pe.nuti.home.api.core.annotation;

import java.lang.annotation.*;

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.METHOD
})
public @interface LogDetail {

}
package kr.pe.nuti.home.api.core.aspect;

import kr.pe.nuti.home.api.core.annotation.LogDetail;
import kr.pe.nuti.home.api.core.util.LogUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogDetailAspect {

  private static final Logger logger = LoggerFactory.getLogger(LogDetailAspect.class);

  @Around("execution(* kr.pe.nuti.home.api..*.*(..)) && @annotation(logDetail)")
  public Object aroundTargetObject(ProceedingJoinPoint joinPoint, LogDetail logDetail) throws Throwable {
    Object target = joinPoint.getTarget();
    Object[] args = joinPoint.getArgs();

    String className = target.getClass().getName();
    String methodName = joinPoint.getSignature().getName();
    StringBuilder argBuilder = new StringBuilder();

    for (Object arg : args) {
      argBuilder.append(LogUtil.argValues(arg, 0))
          .append("\n");
    }
    String argString = argBuilder.toString();

    logger.debug("invoke method {}${}", className, methodName);
    logger.debug("method arguments: {}", argString);

    Object result  = joinPoint.proceed(args);

    logger.debug("finish the method {}${}", className, methodName);

    return result;
  }
}
@Transactional
@Override
public TodoItem changeState(@NonNull TodoItem todo, @NonNull TodoState state) throws IllegalStateChangeException {
  TodoItem savedItem = this.getItem(todo.getIdx());

  final boolean possibleToChangeState = TodoState.isPossibleToChangeState(savedItem.getState(), state);
  if (not(possibleToChangeState)) {
    throw new IllegalStateChangeException();
  }

  savedItem.setState(state);

  return todoItemRepository.save(savedItem);
}

Java Exception


1. 개요

자바 계통 언어에서는 오류를 표현하는 2가지 방법이 존재한다. 첫 번째는 Error 클래스인데 Error 클래스를 상속받는 하위 클래스는 시스템 오류를 표현하게 된다. 두 번째는 Exception 클래스로 어플리케이션 레벨에서 개발자가 예외사항을 표현하기 위해 사용한다.

이번 포스팅에서는 ErrorException에 대해서 작성한다.

2. Error

Error 클래스는 시스템 오류를 표현하는 것으로 주로 JVM에서 사용한다. 어플리케이션 레벨에서 사용하는 경우도 있기는 한데 거의 사용되지 않고, try~catch로 처리할 수 없다. 시스템 오류를 표현하는 것이므로 어플리케이션 개발자가 관여하게될 가능성이 매우 낮다. 흔히 볼 수 있는 Error 클래스의 구현체는 OutOfMemoryErrorStackOverflowError가 있다.

3. Exception

Exception 클래스는 어플리케이션을 개발하는 과정에서 로직을 처리하는 중에 예외사항이 발생할 경우 사용하게 된다. Exception은 크게 Checked ExceptionUnchecked Exception으로 구분되는데 이 둘의 차이는 아래 표와 같다.

Checked ExceptionUnchecked Exception

명시적 처리

명시적으로 처리해야 함

명시적인 처리를 강요하지 않음

처리 시점

Compile Time

Application Runtime

대표 Class

Exception 클래스와 하위 클래스 중 RuntimeException과 그 하위 클래스를 제외

RuntimeException

자바에서는 Exception을 처리하는 과정에서 RuntimeException과 그 하위 클래스들을 특별하게 취급하여 Compile Time에서 처리를 강제하지 않고, Runtime에서 처리한다.

주로 개발자에게 예외사항이 발생할 수 있음을 알려주기 위하거나, 비즈니스 요구사항을 표현하기 위해서 CheckedException을 사용하게 된다. 반대로 Unchecked Exception은 반드시 처리하지 않아도 되는 경우나, 굳이 개발자가 알 필요가 없을 경우 사용하게 된다.

Exception을 사용하게 되면 비즈니스 로직에서 true/false 혹은 object/null을 사용하는 것에 비해서 훨씬 다양하게 예외사항을 표현할 수 있고, 비즈니스 요구사항을 코드로 깔끔하게 담을 수 있다. 또한 의미와 용도에 맞는 Exception을 다양하게 사용하게 된다면, 디버깅이나 유지부수에도 큰 이점을 가질 수 있다. Exception은 주로 분석/설계 단계에서 정의를 하게 된다. 실제 비즈니스 로직을 구현하면서 메소드에 throws를 명시적으로 작성함으로써 해당 메소드를 사용하는 개발자에게 예외사항이 발생할 수 있음을 열려준다.

4. 예시

어플리케이션을 개발하는 과정에서 요구사항을 분석하는 과정이 있고, 이 과정에서 User StoryScenario를 정의할 수 있다.

예를 들어 TODO Item을 관리하는 어플리케이션에서 TODO의 상태를 변경하는 기능 요구사항이 있고, 요구사항 분석 결과 아래와 같은 User StoryScenario가 나왔다고 가정한다.

User Story:
TODO Item의 상태를 변경할 수 있다.
Todo/Doing/Done으로 상태를 변경할 수 있고, 상태변경은 Todo > Doing, Doing > Done, Done > Doing, Doing > Todo 로만 할 수 있다.


Scenario 1: Todo에서 Doing으로 상태를 변경한다.
Scenario 2: Doing에서 Done으로 상태를 변경한다.
Scenario 3: Done에서 Doing으로 상태를 변경한다.
Scenario 4: Doing에서 Todo로 상태를 변경한다.
Scenario 5: Todo에서 Done으로 상태를 변경한다.
Scenario 6: Done에서 Todo로 상태를 변경한다.

위와 같이 User StoryScenario를 정의한다고 할 때 Scenario 5Scenario 6은 예외사항이 발생하는 Scenario 이다. 이렇게 분석 과정에서 예외사항이 발생할 수 있음을 미리 파악하고, 이를 설계 및 구현에서 반영을 하게 된다.

위의 예시를 코드로 표현하면 아래와 같다.

public TodoItem changeState(@NonNull TodoItem todo, @NonNull TodoState state) throws IllegalStateChangeException {
  TodoItem savedItem = todoItemRepository.findById(todo.getIdx())
      .orElseThrow(ResourceNotFoundException::new);

  final boolean possibleToChangeState = TodoState.isPossibleToChangeState(savedItem.getState(), state);
  if (not(possibleToChangeState)) {
    throw new IllegalStateChangeException();
  }

  savedItem.setState(state);

  return todoItemRepository.save(savedItem);
}

위의 코드 예시와 같이 비즈니스 요구사항에 의한 예외사항은 Checked Exception으로 처리하여 throws를 통해 명시적으로 알려준다. 반면에 비즈니스 요구사항이 아닌, 해당 TODO Item이 없어서 처리할 수 없는 예외같은 경우는 Unchecked Exception을 통해 처리를 하면 된다.


Dockerfile


1. Dockerfile?

DockerfileImage 설정 파일로 Image를 생성할 내용을 작성한다.

2. Options

OptionDescription

FROM

어떤 이미지를 기반으로 생성할지 설정한다. <이미지 이름>:<태그> 형식으로 설정한다.

MAINTAINER

메인테이너 정보를 작성한다.

ENV

이미지의 환경변수를 설정한다.

RUN

쉘 스크립트 혹은 명령어를 실행한다.

VOLUME

호스트와 공유할 디렉토리를 지정한다.

CMD

컨테이너가 시작됐을 때 실행할 실행 파일 혹은 쉘 스크립트를 지정한다.

ADD

호스트에서 이미지로 복사할 파일을 지정한다.

EXPOSE

호스트와 연결할 포트 번호를 지정한다.

3. Example

FROM centos:centos6.8
MAINTAINER HyeonilJeong <dev.nuti0102@gmail.com>

ENV V_NGINX=1.12.1 \
  V_TOMCAT=8.5.23 \
  DIR_CONTENTS=/opt/project/contents \
  STATIC_DOMAIN=http://static.nuti.pe.kr \
  VM_XMS=256m \
  VM_XMX=1024m \
  VM_XX_NEW_SIZE=384m \
  VM_XX_MAX_PERM_SIZE=128m \
  DB_HOST=db.nuti.pe.kr \
  DB_NAME=blog \
  DB_USERNAME=nuti \
  DB_PASSWORD=db_pw

RUN rpm --import /etc/pki/rpm-gpg/RPM* \
  && yum update -y

ADD repos.d/nginx.repo /etc/yum.repos.d/nginx.repo

RUN yum install -y nginx $V_NGINX \
  && rm -f /etc/nginx/conf.d/*.conf \
  && yum install -y wget \
  && rpm -Uvh  http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm \
  && rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

RUN yum install -y supervisor
RUN yum install -y java-1.8.0-openjdk-devel.x86_64
RUN mkdir -p $DIR_CONTENTS

ADD nginx/nginx.conf /etc/nginx/nginx.conf
ADD nginx/copy.conf /etc/nginx/conf.d/copy.conf
ADD supervisor/supervisord.conf /etc/supervisord.conf
ADD scripts/start.sh /scripts/start.sh
ADD tomcat /tomcat
ADD war/ROOT.war /tomcat/webapps/ROOT.war

RUN chmod +x /scripts/start.sh \
 && chmod +x /tomcat/bin/startup-tomcat.sh

EXPOSE 80 8080

VOLUME $DIR_CONTENTS
VOLUME /tomcat/logs

CMD ["/scripts/start.sh"]

docker-compose


1. docker-compose?

Docker Compose는 하나의 설정으로 여러 컨테이너를 정의하고 실행할 수 있도록 도와주는 툴이다. 하나의 명령어로 여러 컨테이너를 띄워서 어플리케이션을 실행할 수 있다.

  • 사용 환경

    • 개발

    • 테스트

    • 스테이징

2. Options

OptionDescription

image

사용할 이미지의 태그나 ID를 명시

build

Docker파일이 있는 디렉토리의 경로

dockerfile

기본 Dockerfile명이 아닐 경우 여기에 이름을 명시

command

기본 command를 지정

links

컨테이너를 다른 서비스와 연결. 연결하려는 서비스의 이름이나 별칭을 작성하면 컨테이너의 /etc/hosts 파일에 그 내용이 추기

external_links

links와 비슷하지만 compose외부의 서비스와 연결을 위해 사용

extra_hosts

/etc/hosts에 외부 호스트 정보를 추가

ports

호스트포트:컨테이너포트 식으로 호스트와 컨테이너의 포트를 포딩한다.

expose

호스트에는 노출하지 않고 연결된 서비스끼리만 접근 가능한 포트를 명

volumes

볼륨으로 마운트할 디렉토리를 명

volumes_from

다른 서비스나 컨테이너의 볼륨 전체를 마운트

environment

환경 변수를 명시

env_file

환경 변수를 파일에서 추

extends

다른 compose 설정을 현재 파일에 가져와 사용

labels

컨테이너docker label을 이용해서 메타데이터를 추가

container_name

컨테이너 이름을 지정

log driver

컨테이너에서 사용할 로깅 드라이버를 명시

net

네트워킹 모드를 지정. docker-net 옵션과 동일

pid

PID 모드를 호스트 PID모드로 설정. 호스트와 컨테이너 운영체제가 같은 PID 주소 공간을 공유

dns

컨테이너가 사용할 커스텀 DNS서버를 설정

dns_search

DNS검색 도메인을 지정

cap_add

컨테이너의 시스템에 관한 권한을 추가

cap_drop

컨테이너의 시스템에 관한 권한을 삭제

devices

디바이스 매핑 목록. docker--devices와 동일

3. Example

version: '3'
services:
  example:
    build:
      context: ./docker
      dockerfile: Dockerfile
    image: example:latest
    environment:
      STATIC_DOMAIN: "http://static.nuti.pe.kr"
      VM_XMS: "256m"
      VM_XMX: "1024m"
      VM_XX_NEW_SIZE: "384m"
      VM_XX_NEW_PERM_SIZE: "128m"
      DB_HOST: "db.nuti.pe.kr"
      DB_NAME: "blog"
      DB_USERNAME: "nuti"
      DB_PASSWORD: "db_pw"
    volumes:
      - ./logs:/tomcat/logs
    ports:
      - "80:80"
    network_mode: "bridge"

Pagination